题目
pragma solidity ^ 0.4 .24;
contract Hash {
bytes32 question = "";
struct Game {
bytes32 slogan;
address owner;
uint96 times;
address player;
bool ready;
}
Game game;
modifier onlyOwner() {
require(msg.sender == game.owner, "Illegal user!");
_;
}
modifier onlyPlayer() {
require(msg.sender == game.player, "Illegal user!");
_;
}
constructor(string hash) public {
question = keccak(hash);
game.slogan = "Welcome to imagin's Hash World!";
game.owner = msg.sender;
}
function getFlag() view public returns(bool) {
return (game.times == 43856731668828204536206669571);
}
function play() public {
require(game.player == address(0), "Have been played!");
game.player = address(msg.sender);
}
function guessHash(string answer) public onlyPlayer payable returns(string) {
game.player = msg.sender;
require(msg.value > 0.1 ether, "Get yourself rich first.");
require(game.ready);
game.ready = false;
require(isMan(), "You should be real man.");
if (keccak(answer) == question) {
game.times++;
return "Congratulations, you're right~";
} else {
game.times--;
return "Oops, you lost your chance (-1s).";
}
}
function changeSlogen(bytes32 slogan) public {
Game a;
a.slogan = slogan;
game = a;
}
function nextHash(string hash) public onlyOwner {
question = keccak256(hash);
game.ready = true;
}
function keccak(string str) internal pure returns(bytes32) {
return keccak256(str);
}
function getSlogan() view public returns(bytes32) {
return game.slogan;
}
function isMan() internal view returns(bool) {
uint size;
assembly { size: = extcodesize(caller) }
return (size == 0);
}
// 方便调试,原题目以下函数不存在
function getquestion() view public returns(bytes32) {
return question;
}
function getTime() view public returns(uint96) {
return game.times;
}
function getOwner() view public returns(address) {
return game.owner;
}
}
分析
注意到,题目提供了
- 3 个公开传参方法分别为
changeSlogen
、guessHash
、nextHash
- 2 个公开无参方法分别为
play
、getFlag
首先我们看 getFlag 方法,最终目标是让它返回 true,而对应的目标 times 为
43856731668828204536206669571(10) == 8db5702bac41153c09c73703(16)
这个 times(次数) 是通过 guessHash 进行调整的,但是根本不可能在有限时间内通过循环操作来达到,尤其是这个函数还有onlyPlayer payable修饰,另外内部还有 isMan 的合约调用检测,只能手工来操作实现
我们抛开题目,先来看下guessHash的代码逻辑,可以看到通过一堆检测后,其主要内容就是实现对answer和question的等值判断,之后对 times 进行 +- 操作
function guessHash(string answer) public onlyPlayer payable returns(string) {
game.player = msg.sender;
require(msg.value > 0.1 ether, "Get yourself rich first.");
require(game.ready);
game.ready = false;
require(isMan(), "You should be real man.");
if (keccak(answer) == question) {
game.times++;
return "Congratulations, you're right~";
} else {
game.times--;
return "Oops, you lost your chance (-1s).";
}
}
粗略想想,应该有两种解题方向
- 爆破 times
- 控制 times 然后通过操作 guessHash 进行调整
上面提到无法通过合约爆破形式使 times 暴增,而且题目实际上同时几个人在做,如果中间有人参一脚修改了 times同时攻击者有没有发觉,则会导致前功尽弃,难以控制
那我们只能看第二种方法能否实现
变量覆盖
原理
- Understanding Ethereum Smart Contract Storage
- 详解Solidity合约数据存储布局
- Solidity中存储方式错误使用所导致的变量覆盖
- 智能合约审计系列————3、变量覆盖&不一致性检查
分析
我们向 changeSlogen 传入0x0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff
来观察下结构体值的变化,可以看到所有值全部被修改了
尤其注意此时 times 为 80596284442678810400085(10) == 11112222333344445555(16)
调用前 | 调用后 |
---|---|
![]() |
![]() |
根据占位符,我们可以快速确定如何填充数据,通过修改 times 然后进行 +- 操作就可以完成解题了
解题
- 执行 changeSlogen 传入
0x8db5702bac41153c09c737040000000000000000000000000000000000000000
,修改 times 为 43856731668828204536206669572(10) - 执行 play 成为当前玩家
- 执行 guessHash 传入
任意值
导致 times - 1 - 执行 getFlag
题目地址:0x299DfDB000C6c0131D4cEe84348e6B5Fb656Fff8