区块链安全

math类型ctf-02

题目: https://capturetheether.com/challenges/math/token-whale/

1 pragma solidity ^0.4.21;
2
3 contract TokenWhaleChallenge {
4     address player;
5
6     uint256 public totalSupply;
7     mapping(address => uint256) public balanceOf;
8     mapping(address => mapping(address => uint256)) public allowance;
9
10    string public name = "Simple ERC20 Token";
11    string public symbol = "SET";
12    uint8 public decimals = 18;
13
14    function TokenWhaleChallenge(address _player) public {
15        player = _player;
16        totalSupply = 1000;
17        balanceOf[player] = 1000;
18    }
19
20    function isComplete() public view returns (bool) {
21        return balanceOf[player] >= 1000000;
22    }
23
24    event Transfer(address indexed from, address indexed to, uint256 value);
25
26    function _transfer(address to, uint256 value) internal {
27        balanceOf[msg.sender] -= value;
28        balanceOf[to] += value;
29
30        emit Transfer(msg.sender, to, value);
31    }
32
33    function transfer(address to, uint256 value) public {
34        require(balanceOf[msg.sender] >= value);
35        require(balanceOf[to] + value >= balanceOf[to]);
36
37        _transfer(to, value);
38    }
39
40    event Approval(address indexed owner, address indexed spender, uint256 value);
41
42    function approve(address spender, uint256 value) public {
43        allowance[msg.sender][spender] = value;
44        emit Approval(msg.sender, spender, value);
45    }
46
47    function transferFrom(address from, address to, uint256 value) public {
48        require(balanceOf[from] >= value);
49        require(balanceOf[to] + value >= balanceOf[to]);
50        require(allowance[from][msg.sender] >= value);
51
52        allowance[from][msg.sender] -= value;
53        _transfer(to, value);
54    }
55 }

如上面的代码,是一个 ERC20 标准代币 SET,要使得 player 也就是 msg.sender 的余额大于 1000,那么就要求 balanceof[msg.sender] 的值溢出。

仔细观察存在加、减、乘的风险点,主要有以下几处:

  • 27行可能存在下溢
  • 28行可能存在上溢,但是不是更新 msg.sender 账户余额的
  • 35行可能存在上溢,但不是更新 msg.sender 账户余额的
  • 49行可能存在上溢,但不是更新 msg.sender 账户余额的
  • 52行可能存在下溢,但不是更新 msg.sender 账户余额的

所以实际可用的就是27行的下溢。观察一下如何利用这个溢出点:

  • transfer() 函数调用了 _transfer() 函数,但存在溢出检查,基本上是对于 SafeMath 的实现
  • transferFrom() 函数调用了 _transfer() 函数,且没有检查溢出。

于是思路就是利用 transferFrom() 函数,但是前置步骤是调用 approve 函数配给转账额度。所以逆向推过来:

  1. 最终要实现 27 行的 balanceOf[msg.sender] -= value; 这要求 msg.sender 的 mapping 里面的余额 < value。
  2. 所以这里 _transfer 函数的调用应该是 msg.sender._transfer(to,value)。其中 to 为新的我们可控的 account,value 应该大于 msg.sender 的余额。
  3. 所以要先调用 msg.sender.transferFrom(to,any address,value)。这里的第二个参数其实没有什么影响,因为最终落地到 28 行。相当于白送了这个账户一些 SET 代币。
  4. 所以要先调用 to.approve(msg.sender,value)。

所以总结来说,我们需要引入另一个可控的地址 to:(可控是因为需要用其发起交易)

  1. 先给这个地址转入一些 SET 代币,最终导致 msg.sender 也就是 player 的钱要 < value。value 又要小于 1000 也就是总共的钱。那么假设 value = 500,就给 to 地址转入 800,msg.sender 还剩 200,一会儿 200 < 500 就可以导致下溢。这样又符合从 to 地址转出 500,是在 800 的配额内的。
  2. to.approve(msg.sender,800)
  3. msg.sender.transferFrom(to, this, 500) //随便选了当前合约地址作为接收币的幸运地址
  4. msg.sender._transfer(this,500)
  5. balanceOf[msg.sender] -= 500

其中第1、2、3步是需要操作的。

我的实验如下:

操作完之后,可以看到 balanceOf[msg.sender] 的值为巨额 SET:

于是完成题目: