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 // 如此递归循环,直到合约中余额提取完毕
这里有几个必要条件:
- 目标合约通过
call
调用,并且没有限制 gas 大小,则可进行。如果用transfer
或send
则不行。因为这两个方法都限制了 2300gas。如果call
方法调用也限制了 gas 大小,比如call{value: _amount,gas: 2300}("")
也无法实现攻击 call
方法先执行,status 后修改才可以,比如上诉案例,如果把balance
状态修改放到call
方法前面,也无法实现攻击- 执行重入攻击前,需要确认目标合约有足够的以太币来向我们多次转账。如果目标合约没有 payable 的 fallback 函数,则需要新建一个合约,通过
selfdestruct
自毁强制转账。