智能合约中的重入攻击:全面防范指南

重入攻击是智能合约安全中最重要的漏洞之一。本文提供了对重入攻击的详细技术分析,通过代码示例演示了它们的机制,并提出了三种经过实战检验的防范技术,以保护您的智能合约。

理解重入性:基本概念

在其核心,重入攻击发生在一个合约(ContractB)在第一个函数调用完成之前回调到调用合约(ContractA)。这种漏洞创建了一个可被利用的函数调用递归循环,可以用来抽取资金或操纵合约状态。

关键洞察: 当合约在更新其内部状态之前执行外部调用时,重入漏洞就会存在。

考虑这个场景:

  • 合约 A 有 10 ETH 的余额
  • 合约 B 在合约 A 中存入了 1 ETH
  • ContractA 有一个易受攻击的提取功能

当ContractB利用此漏洞时,它可以执行一系列递归调用,在任何余额更新发生之前,耗尽ContractA的资金。

重入攻击的解剖

攻击模式通常由两个基本组成部分组成:

  1. 一种攻击()功能,用于启动该漏洞
  2. 一个在接收ETH时执行的回退()函数,创建递归循环

攻击按以下顺序执行:

  1. 攻击者在他们的恶意合约中调用 attack()
  2. 恶意合约在易受攻击的合约中调用 withdraw()
  3. 脆弱的合约向攻击者发送ETH,触发回退函数
  4. 在回退函数中,攻击者递归调用 withdraw() 再次
  5. 这个循环会一直重复,直到脆弱的合约被抽干资金

历史意义: 2016年臭名昭著的DAO黑客事件,导致大约$60 百万的ETH损失,是一起高调的重入攻击,最终导致以太坊硬分叉。

漏洞代码分析

让我们来看看一个脆弱的智能合约实现:

坚固 合约 EtherStore { mapping(address => uint) 公共余额;

函数 deposit() public payable { balances[msg.sender] += msg.value; }

函数 withdrawAll() public { uint bal = 余额[msg.sender]; require(bal > 0);

(bool发送,) = msg.sender.call{value: bal}(“”); require(sent, "发送以太坊失败");

余额[msg.sender] = 0; } }

此代码中的关键漏洞在于合约在更新发送者的余额之前发送ETH (msg.sender.call{value: bal}(""))。这个顺序导致了重入漏洞。

利用漏洞

攻击者会部署这样的合约来利用漏洞 EtherStore:

坚固 合约攻击 { EtherStore 公有 etherStore;

constructor(address _etherStoreAddress) { etherStore = EtherStore(_etherStoreAddress); }

// 备用函数 - 当 EtherStore 发送以太币时调用

fallback() 外部应付 { 如果 (address019283746574839201etherStore).balance >= 1 ether( { etherStore.withdrawAll019283746574839201(; } }

函数 attack)) external payable { 需求019283746574839201msg.value >= 1 ether(; etherStore.deposit{value: 1 ether})(; etherStore.withdrawAll019283746574839201); } }

攻击流程:

  1. 攻击者调用 attack(),存入 1 ETH
  2. 攻击者在 EtherStore 上调用 withdrawAll()
  3. EtherStore 发送 1 ETH 回去,触发回退函数
  4. 在回退函数中,攻击者递归地再次调用 withdrawAll()
  5. 因为余额尚未更新,这个循环将持续,直到 EtherStore 被耗尽

三种防御重入攻击的技术

( 1. 函数级保护:noReentrant修饰符

此修饰符创建了一种锁机制,防止在函数仍在执行时再次进入该函数:

坚固 合约 ReentrancyGuard { bool private locked = false;

修饰符 noReentrant)( { require)!locked, “可重入调用”(; 锁定 = true; _; 锁定 = false; } }

合约 SecureEtherStore 是 ReentrancyGuard { mapping)address => uint### 公共余额;

function withdrawAll() public noReentrant { uint bal = 余额[msg.sender]; require(bal > 0);

(bool发送,) = msg.sender.call{value: bal}(“”); require(sent, "发送以太坊失败");

余额[msg.sender] = 0; } }

技术分析: 修饰符设置了一个状态变量 (locked) 以防止重入。如果在已执行时调用带有此修饰符的函数,交易将会回滚。

( 2. 跨功能保护:检查-效果-交互模式

该模式通过确保状态变化在外部交互之前发生来解决跨功能重入问题:

坚固 // 脆弱 函数 withdrawAll)( public { uint bal = 余额[msg.sender]; require)bal > 0(;

)bool发送,### = msg.sender.call{value: bal}(“”); require(sent, "发送以太坊失败");

balances[msg.sender] = 0; // 状态在外部调用后更新

}

// 安全 函数 withdrawAll() public { uint bal = 余额[msg.sender]; require(bal > 0);

balances[msg.sender] = 0; // 状态在外部调用之前更新

(bool发送,) = msg.sender.call{value: bal}(“”); require(sent, "发送以太币失败"); }

技术分析: 通过在进行外部调用之前更新状态变量,合约确保即使外部调用允许重入,状态也已经被正确修改,从而防止被利用。

( 3.跨合约保护:GlobalReentrancyGuard

对于具有多个相互作用的合约的项目,实现全球重入保护可以提供系统范围的保护:

坚固 合约 GlobalReentrancyGuard { bool 私人_notEntered;

constructor)( { _notEntered = 真; }

修饰符 globalNonReentrant)( { require)_notEntered, “ReentrancyGuard: reentrant call”###; _notEntered = false; _; _notEntered = 真; } }

合约 ContractA 是 GlobalReentrancyGuard { function transferFunds() public globalNonReentrant { // 安全实现 } }

合约 ContractB 是 GlobalReentrancyGuard { function withdrawFunds() public globalNonReentrant { // 安全实现 } }

**技术分析:**这种方法使用共享的合约状态来防止在同一生态系统中多个合约之间的重入,从而在系统级别提供保护,而不仅仅是在函数级别。

安全最佳实践实施矩阵

| 预防技术 | 保护级别 | 燃气费用 | 实施复杂性 | 最佳用途 | |----------------------|------------------|----------|---------------------------|----------| | noReentrant修饰符 | 函数级别 | 低-中 | 简单 | 单一易受攻击的函数 | | 检查-效果-交互 | 合约级别 | 低 | 中 | 一般合约安全 | | 全局重入保护 | 系统级 | 中等 | 复杂 | 多合约系统 |

技术实施考虑事项

在实现重入保护时,开发者应考虑:

  1. Gas优化: 重入保护增加了函数执行的开销。考虑在高频操作中的性能影响。

  2. 边缘情况: 一些合法的用例需要可重入行为。确保你的保护机制不会破坏预期功能。

  3. 审计要求: 即使有保护机制,专业安全审计仍然是检测更复杂漏洞的关键。

  4. 保护范围: 不同的重入保护机制针对不同的攻击向量。了解您的合约特定的脆弱性对于选择适当的保护至关重要。

通过理解重入攻击的机制并实施适当的保护机制,开发者可以显著增强其智能合约的安全性,以抵御区块链生态系统中最常见和危险的漏洞之一。

IN2.87%
查看原文
此页面可能包含第三方内容,仅供参考(非陈述/保证),不应被视为 Gate 认可其观点表述,也不得被视为财务或专业建议。详见声明
  • 赞赏
  • 评论
  • 转发
  • 分享
评论
0/400
暂无评论
交易,随时随地
qrCode
扫码下载 Gate App
社群列表
简体中文
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • Bahasa Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)