Skip to main content

Re-entrancy

重入攻击是智能合约中的经典攻击。以太坊 The DAO 项目遭受的重入攻击直接导致了以太坊(ETH)和以太坊经典(ETC)的硬分叉。

相关题目练习: Re-Entrancy

原理

重入攻击一般是利用fallback+递归调用来反复提取合约中的资金。要注意递归调用次数,防止一笔交易 gas 费耗尽导致交易失败

例如以下题目: 默认合约有一定数量的 ETH,要求把合约中的以太全部提走

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Reentrance {

using SafeMath for uint256;
mapping(address => uint) public balances;

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

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

function withdraw(uint _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转账再执行balance减操作。并且call方法没有限制 gas 大小。所以想要攻击,我们可以部署一个新的合约,在fallback接收方法中递归调用withdraw即可 执行入下

withdraw->fallback            // 此时balances并未发生变化
fallback->withdraw->fallback // 如此递归循环,直到合约中余额提取完毕

这里有几个必要条件:

  1. 目标合约通过call调用,并且没有限制 gas 大小,则可进行。如果用transfersend则不行。因为这两个方法都限制了 2300gas。如果call方法调用也限制了 gas 大小,比如call{value: _amount,gas: 2300}("")也无法实现攻击
  2. call方法先执行,status 后修改才可以,比如上诉案例,如果把balance状态修改放到call方法前面,也无法实现攻击
  3. 执行重入攻击前,需要确认目标合约有足够的以太币来向我们多次转账。如果目标合约没有 payable 的 fallback 函数,则需要新建一个合约,通过 selfdestruct 自毁强制转账。