目录

智能合约中的重入攻击

当智能合约功能通过对未知或敌对参与者编写的合约进行外部调用而暂时放弃交易的控制流时,就会发生重入攻击。这允许后一个合约对主要智能合约函数进行递归调用以耗尽其资金。

这次攻击的步骤如下:

  1. 攻击者请求易受攻击的合约X将资金转移到恶意合约Y
  2. 合约X确定攻击者是否拥有所需资金,然后将资金转移到合约Y
  3. 然后攻击者执行回调函数。一旦合约Y收到资金,它就会执行一个回调函数,在更新账户余额之前调用合约X。
  4. 这个递归过程一直持续到所有资金都用完并转移为止。

重入攻击最好用一个例子来说明。考虑这个名为EOABank的简单银行合约:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.9;

contract EOABank {

    mapping(address => uint) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdrawAll() public {
        (bool success, ) = msg.sender.call{ value: balances[msg.sender] }("");
        require(success, "Transaction failed!");
        balances[msg.sender] = 0;
    }
}

这个合约的作用很简单。它通过deposit()函数接受付款,并允许使用withdrawAll()函数提取所有资金。

这个EOABank合约如何被攻击?

考虑一下这个攻击者合同:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
contract Attacker {

    EOABank public exploitBank;

    constructor(address _thebankAddress) {
        exploitBank = EOABank(_thebankAddress);
    }

    // fallback
    receive() external payable {
        if (address(exploitBank).balance >= 1 ether) {
            exploitBank.withdrawAll();
        }
    }

    function attack() external payable {
        require(msg.value >= 1 ether);
        // send 1 Ether
        exploitBank.deposit{value: 1 ether}();
        // immediately withdraw
        exploitBank.withdrawAll();
    }

}

该合约在部署时需要EOABank合约的地址。它还用自己的功能覆盖receive()函数(通常是每个智能合约中的回退函数)。attack()函数是EOABank被攻击的地方。

让我们更详细地了解这次攻击的步骤:

https://hicoldcat.oss-cn-hangzhou.aliyuncs.com/img/20230420234255.png

  1. 攻击者合约在EOABank存入 1 个以太币。
  2. 资金立即被提取。
  3. 取款触发receive()函数。这发生在balances被设置为 0 之前。
  4. receive()函数允许执行任意代码。攻击者利用这一点,再次调用withdrawAll()
  5. 这里的循环在receive()withdrawAll()之间开始,直到所有的资金被耗尽。
  6. 循环。
  7. 只有在所有资金都用完后,balances才设置为 0。

EOABank 的安全实施是什么样的?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
contract EOABank {

    mapping(address => uint) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdrawAll() public {
        // save balances in new variable
        uint256 bal = balances[msg.sender];
        // reset balances
        balances[msg.sender] = 0;
        // transfer
        (bool success, ) = msg.sender.call{ value: bal }("");
        require(success, "Transaction failed!");
    }
}

注意区别。首先,balances值保存在一个新变量bal中。其次,在转移价值之前将余额设置为 0。这样,EOABank就可以免受重入攻击。

原文:https://krz.hashnode.dev/re-entrancy-attacks-in-smart-contracts

https://hicoldcat.oss-cn-hangzhou.aliyuncs.com/img/profile.jpg