一道题回顾智能合约 Coverage

注意到,题目提供了

本页目录
  1. 题目
  2. 分析
  3. 变量覆盖
  4. 原理
  5. 分析
  6. 解题

题目

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).";
}
}

粗略想想,应该有两种解题方向

  1. 爆破 times
  2. 控制 times 然后通过操作 guessHash 进行调整

上面提到无法通过合约爆破形式使 times 暴增,而且题目实际上同时几个人在做,如果中间有人参一脚修改了 times同时攻击者有没有发觉,则会导致前功尽弃,难以控制

那我们只能看第二种方法能否实现

变量覆盖

原理

  1. Understanding Ethereum Smart Contract Storage
  2. 详解Solidity合约数据存储布局
  3. Solidity中存储方式错误使用所导致的变量覆盖
  4. 智能合约审计系列————3、变量覆盖&不一致性检查

分析

我们向 changeSlogen 传入0x0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff来观察下结构体值的变化,可以看到所有值全部被修改了

尤其注意此时 times 为 80596284442678810400085(10) == 11112222333344445555(16)

调用前调用后
-w668-w667

根据占位符,我们可以快速确定如何填充数据,通过修改 times 然后进行 +- 操作就可以完成解题了

解题

  1. 执行 changeSlogen 传入 0x8db5702bac41153c09c737040000000000000000000000000000000000000000,修改 times 为 43856731668828204536206669572(10)
  2. 执行 play 成为当前玩家
  3. 执行 guessHash 传入 任意值 导致 times - 1
  4. 执行 getFlag

题目地址:0x299DfDB000C6c0131D4cEe84348e6B5Fb656Fff8

-w682