本文深入探讨了Solidity智能合约中的重入攻击,详细解释了重入攻击的机制、类型及防护策略,并提供了真实案例如DAO黑客事件和Curve Finance攻击的分析,强调了安全审计的重要性。
什么是Solidity智能合约中的重入攻击?了解区块链重入攻击是如何工作的,以及如何保护你的智能合约免受其影响。
重入攻击 是利用智能合约中的一个漏洞,在上一个执行完成之前重复调用一个函数, 这可能允许攻击者操控合约的状态。
本文将回答以下问题: 什么是Solidity重入攻击?将深入讨论重入攻击的机制、不同类型、缓解策略以及实际的智能合约示例。
那么让我们开始吧。
在Solidity智能合约的上下文中,重入攻击是指执行流被转移到一个外部合约,通常是通过外部调用(例如“fallback”函数或“onERC721Received”),允许函数(或另一个函数)被递归调用。 这使得外部合约可以重新进入合约,从而在执行完成之前操纵状态。
重入是一个状态同步问题,发生在状态在进行外部调用之前没有更新。这意味着当函数被重新进入时,状态与第一次调用时相同。
例如,如果该函数执行以下操作:
由于状态是在执行外部调用后更新的,如果调用者是一个合约,并且在其fallback函数中重新调用同一个函数,他们仍然会通过步骤(1),使他们能够耗尽合约的资金。这段代码执行顺序,如下文所述,并不是避免重入攻击的正确模式。
这可能导致意外行为,允许攻击者操控合约的状态并可能耗尽其资金。
重入攻击导致合约资金被耗尽
这是最简单的重入攻击类型,发生在合约中的单个函数被重新进入时。这通常发生在函数修改合约状态,然后在没有首先更新其内部状态变量的情况下调用外部合约或发送Ether时。
跨函数重入发生在一个函数在更新状态之前执行外部调用,而外部合约调用另一个依赖于该状态的函数。这可能导致合约不同部分之间的意外交互,使攻击者可以利用一个函数的漏洞来操控另一个函数的状态。
跨合约重入涉及多个合约之间的函数交互,其中状态是共享的。与之前一样,如果第一个合约中的共享状态在外部调用之前没有被更新,依赖于共享状态的合约可能会被重新进入。
跨链重入虽然较少见,但涉及在不同区块链网络上部署的智能合约之间的交互。此场景可能发生在互操作性协议或去中心化交易所(DEX)中,后者促进跨多个区块链的交易。虽然机制与跨合约重入类似,但由于不同区块链生态系统之间的交互,复杂性增加。
欲了解有关跨链重入的更多信息,包括一个示例,请访问 Mateocesaroni撰写的以下博客。
也称为“只读外部调用重入”,指的是一种特定类型的重入漏洞,其中对另一个合约进行外部调用,但被调用合约的函数不会修改其状态。相反,被调用的函数读取来自调用合约的数据,然后重新进入调用合约,可能导致意外行为。尽管被调用的函数不修改状态,但它仍然可以影响调用合约的控制流或行为,从而带来安全风险。
有关只读重入的示例,请访问SunWeb3Sec的 DeFiVulnLabs常见智能合约漏洞库。
有关重入攻击代码示例,请访问Cyfrin Updraft的示例 漏洞库。
确保在与外部合约交互或发送Ether之前进行状态更改。根据之前描述的示例,将步骤修改为遵循检查-效果-交互模式:
让我们看看这在实践中是什么样子的:
mapping (address => uint) public balance;
function withdraw(uint amount) public {
// 1. 检查
require(balance[msg.sender] >= amount);
// 2. 效果
balance[msg.sender] -= amount;
// 3. 交互
msg.sender.call{value: amount}("");
// 注意:状态变更后始终应发出事件
emit Withdrawal(msg.sender, amount);
}
使用检查-效果-交互模式来防止重入攻击
isWithdrawing = true
。ReentrancyGuard
。该修饰符确保攻击者无法同时运行多个函数。// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract ReentracyProtected is ReentrancyGuard {
mapping(address => uint) public balances;
function withdraw() external nonReentrant {
uint balance = balances[msg.sender];
require(balance > 0, "余额不足");
balances[msg.sender] = 0;
(bool success, ) = address(msg.sender).call{ value: balance }("");
require(success, "提现失败");
}
}
以下代码片段显示了在HypercertMinter::splitValue
中发现的一个漏洞,允许一个代币拆分成多个部分。
该函数调用SemiFungible1155::_splitValue
,在写入减少的值到存储之前调用_mintBatch()
。因此,没有遵循检查-效果-交互模式,因此该函数易受重入攻击。
_mintBatch()
在_account
是合约时调用_account.onERC1155BatchReceived()
;因此攻击者可以将执行流转移到攻击合约,并多次调用HypercertMinter::splitValue
以分割相同的tokenId
,从而铸造大量的部分。
/// @dev 将`account`持有的`_tokenID`的单位拆分到`_values`
/// @dev `_values`必须总和等于在`_tokenID`持有的总`units`
function _splitValue(address _account, uint256 _tokenID, uint256[] calldata _values) internal {
// ... //
uint256 valueLeft = tokenValues[_tokenID];
// ... //
for (uint256 i; i < len;){
valueLeft -= values[i];
tokenValues[toIDs[i]] = values[i];
unchecked {
++i;
}
}
//
// @audit CRITICAL 由于未遵循检查-效果-交互模式的重入攻击
//
// ERC1155._mintBatch()将在`_account`是合约时调用`_account.onERC1155BatchReceived()`。
// AttackContract.onERC1155BatchReceived()可以通过多次重新进入`_splitValue()`来劫持执行流,
// 从而铸造相同`tokenID`的大量部分,因为减少的`valueLeft`尚未写入存储,
// 在调用ERC1155._mintBatch()之前。
_mintBatch(_account, toIDs, amounts, "");
tokenValues[_tokenID] = valueLeft;
emit BatchValueTransfer(typeIDs, fromIDs, toIDs, values);
}
此漏洞来自于 Pashov对Hypercerts的审计。
有关此漏洞及其他重入审计发现示例的更多详细信息,请参考 Dacian的重入深入文章。
本文探讨了智能合约中的重入攻击,详细说明了其机制、类型、缓解策略以及诸如The DAO攻击和Curve Finance事件等实际示例,强调了安全措施和审计在防止此类攻击中的重要性。
进行协议审计可以显著降低发生此类攻击的概率。
- 原文链接: cyfrin.io/blog/what-is-a...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!