智能合约中的重入攻击:理解这种脆弱性并实施预防策略

重入攻击代表了智能合约安全中最臭名昭著的漏洞之一,导致数百万的资金在各种区块链协议中被盗。本文探讨了重入漏洞背后的机制,并提出了安全意识强的开发者应该实施的全面预防技术。

什么是重入攻击?

在其核心,重入攻击发生在智能合约中的一个函数在其之前的执行完成之前被重复调用。当智能合约在解决自己的状态变化之前调用外部合约时,就会产生根本的脆弱性,从而创造出被利用的机会。

在典型场景中,合约A通过调用合约B的某个函数进行交互。当合约B获得在合约A仍在执行其原始函数时回调合约A的能力时,关键的安全漏洞就出现了。这种递归交互模式为潜在的攻击创建了基础。

重入攻击是如何工作的:逐步解析

考虑这种情况:合约A总共持有10个ETH,其中合约B已向合约A存入1个ETH。当合约B试图通过以下序列提取其资金时,漏洞变得可被利用:

  1. 合约B调用合约A中的withdraw()函数
  2. 合约A验证合约B的余额大于0 (通过检查)
  3. 合约 A 将 ETH 发送到合约 B,触发合约 B 的回退函数
  4. 在合同A能够将合同B的余额更新为零之前,合同B中的回退函数再次调用withdraw()
  5. 合约 A 检查合约 B 的余额,仍然显示 1 ETH (尚未更新)
  6. 该过程重复,直到合同A的资金被耗尽

关键漏洞在于 余额更新发生在 ETH 转移之后,这使得攻击者可以在余额被设置为零之前多次利用相同的余额。

攻击的解剖:技术实现

让我们来检查一下脆弱的代码模式:

固态性 // 易受攻击的 EtherStore 合约 合约 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; } }

现在,让我们看看攻击者如何利用这个漏洞:

固态性 // 利用重入漏洞的攻击合约 合约攻击 { EtherStore 公有 etherStore;

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

// 备降函数,当 EtherStore 发送以太币时被调用
receive() 外部可支付 {
    如果(地址(以太坊存储).余额 >= 1 以太坊) {
        // 重新进入 withdrawAll 函数

etherStore.withdrawAll019283746574839201(; } }

函数 attack)( external payable { 需求019283746574839201msg.value >= 1 ether);

    // 存款以在 EtherStore 中建立余额

etherStore.deposit{value: 1 ether}();

    // 开始提款流程,触发重入攻击

etherStore.withdrawAll019283746574839201(; } }

攻击序列在攻击者调用 attack)( 时开始。这将存入 1 ETH 以建立余额,然后调用 withdrawAll)(。当 EtherStore 发送 ETH 回来时,它会触发 Attack 合约中的 receive)( 函数,该函数在余额更新之前再次调用 withdrawAll)(。这一循环持续进行,直到 EtherStore 的资金被抽空。

全面预防技术

注重安全的开发者可以实施三种强大的技术来防止重入漏洞:

) 1. 功能级保护:非重入修饰符

在个别功能级别上最常见的保护措施是实现重入保护:

智能合约语言 合约 ReentrancyGuard { bool private locked = false;

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

// 将此修饰符应用于易受攻击的函数

function withdrawFunds)( public nonReentrant { // 函数代码防止重入 } }

这种方法有效地锁定合约在函数执行期间,防止任何递归调用,直到函数完成并解锁状态。

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

这个基础安全模式重构代码,以消除多个函数中的漏洞:

坚固 // 易受攻击的实现 函数 withdrawAll() public { uint bal = 余额[msg.sender]; require###bal > 0(;

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

余额[msg.sender] = 0;交互后的状态更新 }

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

余额[msg.sender] = 0;效果 )state changes(

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

通过遵循 检查-效果-交互 模式,合约在任何外部交互之前更新其状态,确保即使攻击者试图重入,他们也会遇到更新后的状态 )零余额(。

) 3. 项目级保护:全球重入保护

对于具有多个交互合同的复杂项目,实现全局重入保护可以提供系统范围的保护:

固态性 合约 GlobalReentrancyGuard { bool 私有 _notEntered = true;

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

// 项目中的所有合约都继承自 GlobalReentrancyGuard 合约 SecureContract 是 GlobalReentrancyGuard { function vulnerableOperation)( public globalNonReentrant { // 保护整个项目免受重入攻击 } }

这种方法在保护DeFi协议中的跨合约重入攻击方面尤其有价值,因为多个合约相互之间进行交互。

重入漏洞的现实影响

重入漏洞导致了区块链历史上最具破坏性的攻击之一。臭名昭著的2016年DAO黑客事件导致约360万ETH被盗,当时价值约)百万,最终导致以太坊硬分叉,创造了以太坊经典。

最近,在2020年,Lendf.Me协议因重入攻击损失了大约(百万,突显出尽管人们的意识有所提高,这些漏洞仍然对智能合约安全构成重大风险。

安全最佳实践

除了提到的具体技术,开发者还应遵循以下额外的安全实践:

  1. 始终使用最新的Solidity版本,以提供改进的安全性特性
  2. 主题合同需经过声誉良好的公司进行全面安全审计
  3. 实施全面的测试覆盖,包括对边界情况的单元测试
  4. 在将新合约部署到生产环境时,先从最小的ETH暴露开始
  5. 考虑对高价值合同进行形式验证

通过实施这些防御技术和遵循安全最佳实践,开发者可以显著降低重入攻击的风险,并为区块链应用构建更安全的智能合约。

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