Solidity中的重入攻击 — 理解与防范

  • zealynx
  • 发布于 2023-03-01 13:28
  • 阅读 5

文章详细介绍了Solidity智能合约中的重入攻击原理及其运作方式。它通过代码示例深入分析了攻击流程,并提供了三种经过验证的最新防御方法:noReentrant修饰符、Checks-Effects-Interactions模式以及GlobalReentrancyGuard,旨在全面保护项目中的智能合约安全。

了解什么是重入攻击,它是如何一步一步工作的,以及保护你项目中所有智能合约的最新三种经过验证的方法。

什么是重入攻击?

首先,我将帮助你简单地理解什么是重入攻击以及如何预防它。然后,我将深入探讨代码示例,以展示漏洞所在,攻击者的代码会是什么样子,最重要的是,我将向你展示最新的经过验证的方法,不仅可以保护一个,还可以保护你项目中所有的智能合约。

剧透:如果你已经听说过 nonReentrant() 修饰符,请继续阅读,因为你即将发现 globalNonReentrant() 修饰符和 Checks-Effects-Interactions 模式。

Contract A and Contract B interaction

在上图中,我们有 ContractA 和 ContractB。现在,正如你所知,智能合约可以与其他智能合约交互,就像本例中 ContractA 可以调用 ContractB。因此,重入攻击最基本的想法是,当 ContractA 仍在执行时,ContractB 能够回调 ContractA。

那么,攻击者如何利用这一点呢?

上面我们看到 ContractA 有 10 Ether,ContractB 在 ContractA 中存储了 1 Ether。在这种情况下,ContractB 将能够使用 ContractA 的提款函数,并将 Ether 发送回自己,因为它通过了其余额大于 0 的检查,然后其总余额将被修改为 0。

Reentrancy attack flow

现在让我们看看 ContractB 如何利用重入攻击来利用提款函数并窃取 ContractA 的所有 Ether。基本上,攻击者需要两个函数:attack()fallback()

在 Solidity 中,fallback 函数是一个外部函数,既没有名称,也没有参数或返回值。任何人都可以通过以下方式调用 fallback 函数:调用合约内部不存在的函数;调用函数时未传入所需数据;向合约发送不带任何数据的 Ether。

重入攻击的工作方式(让我们一步一步跟随箭头)是攻击者调用 attack() 函数,该函数内部调用 ContractA 的 withdraw() 函数。在函数内部,它将验证 ContractB 的余额是否大于 0,如果是,它将继续执行。

Fallback trigger sending Ether

由于 ContractB 的余额大于 0,它将 1 Ether 发送回去,并触发 fallback 函数。请注意,此时 ContractA 有 9 Ether,而 ContractB 已经有 1 Ether。

Fallback re-enters withdraw

接下来,当 fallback 函数执行时,它会再次触发 ContractA 的提款函数,再次检查 ContractB 的余额是否大于 0。如果你再次查看上图,你会注意到它的余额仍然是 1 Ether。

Balance drain loop

这意味着检查通过,它会向 ContractB 发送另一个 Ether,这会触发 fallback 函数。请注意,由于“balance=0”所在的行从未被执行,这将一直持续到 ContractA 中的所有 Ether 都被耗尽。

在 Solidity 代码中识别重入攻击

现在让我们看看一个智能合约,我们可以在其中通过 Solidity 代码识别重入攻击。

EtherStore contract with deposit and withdrawAll

在 EtherStore 合约中,我们有一个 deposit() 函数,用于存储和更新发送者的余额,然后是 withdrawAll() 函数,它将一次性提取所有存储的余额。请注意 withdrawAll() 的实现,它首先使用 require 检查余额是否大于 0,然后立即发送 Ether,再次,将发送者余额更新为 0 的操作留到最后。

Attack contract exploiting EtherStore

这里我们有 Attack 合约,它将使用重入攻击来耗尽 EtherStore 合约。让我们分析它的代码:

  • 在其构造函数中,攻击者将传入 EtherStore 地址,以便创建实例并因此能够使用其函数。
  • 我们看到 fallback() 函数,当 EtherStore 向此合约发送 Ether 时,该函数将被调用。在其中,只要余额等于或大于 1,它就会调用 EtherStore 的提款函数。
  • attack() 函数内部,我们有将利用 EtherStore 的逻辑。正如我们所见,首先,我们将通过确保我们有足够的 Ether 来发起攻击,然后存入 1 Ether,以便在 EtherStore 中拥有大于 0 的余额,从而在开始提款之前通过检查。

我在上面的 ContractA 和 ContractB 示例中一步一步地解释了代码将如何运行,所以现在,让我们总结一下。首先,攻击者将调用 attack(),该函数内部将调用 EtherStore 的 withdrawAll(),然后将 Ether 发送给 Attack 合约的 fallback 函数。在那里,它将开始重入攻击并耗尽 EtherStore 的余额。

如何保护你的合约免受重入攻击

我将向你展示三种预防技术,以全面保护它们。我将涵盖如何防止单函数重入、跨函数重入和跨合约重入。

技术 1: noReentrant 修饰符

noReentrant modifier

保护单个函数的第一个技术是使用一个名为 noReentrant 的修饰符。

修饰符是一种特殊的函数类型,你用它来修改其他函数的行为。修饰符允许你在不重写整个函数的情况下,向函数添加额外的条件或功能。

我们在这里所做的是在函数执行期间锁定合约。这样,它将无法重入单个函数,因为它需要通过函数代码,然后将锁定状态变量更改为 false,以便再次通过 require 中的检查。

技术 2: Checks-Effects-Interactions 模式

Checks-Effects-Interactions pattern applied to EtherStore

第二种技术是利用 Checks-Effects-Interactions 模式,它将保护我们的合约免受跨函数重入攻击。你能发现上面更新的 EtherStore 合约有什么变化吗?

要深入了解 Checks-Effects-Interactions 模式,我建议阅读 Solidity 模式文档

Vulnerable code — balance updated after sending Ether

Fixed code — balance updated before sending Ether

上面我们看到了左图中的脆弱代码和右图中的修复代码之间的比较,左图中余额在发送 Ether 后才更新,这正如上面所见,可能永远无法达到;而在右图中,所做的是将 balances[msg.sender] = 0(或效果)移动到 require(bal > 0)检查)之后,但在发送 Ether(交互)之前。

通过这种方式,我们将确保即使另一个函数正在访问 withdrawAll(),此合约也将受到保护,免受攻击者侵害,因为余额将在发送 Ether 之前始终更新。

技术 3: 用于跨合约保护的 GlobalReentrancyGuard

GlobalReentrancyGuard contract

GMX_IO创建的模式

我将向你展示的第三种技术是创建 GlobalReentrancyGuard 合约,以防止跨合约重入攻击。重要的是要理解,这适用于有多个合约相互交互的项目。

这里的想法与我在第一种技术中解释的 noReentrant 修饰符相同,它进入修饰符,更新变量以锁定合约,并且直到代码执行完毕才解锁。这里最大的区别在于我们使用存储在单独合约中的变量,该变量用作检查函数是否被进入的地方。

Cross-contract reentrancy example — ScheduledTransfer

Cross-contract reentrancy example — AttackTransfer

Cross-contract reentrancy example — GlobalReentrancyGuard applied

我在这里创建了一个没有实际代码,仅包含函数名称的示例作为参考,以帮助理解其思想,因为根据我的经验,可视化情况比单纯用文字描述更有帮助。

在这里,攻击者将调用 ScheduledTransfer 合约中的函数,该函数在满足条件后会将指定的 Ether 发送给 AttackTransfer 合约,后者因此会进入 fallback 函数,从而从 ScheduledTransfer 合约的角度“取消”交易,但仍收到 Ether。这样,它将开始一个循环,直到耗尽 ScheduledTransfer 中的所有 Ether。

嗯,使用我上面提到的 GlobalReentrancyGuard 将避免这种攻击场景。

现实世界的重入攻击

如果你想了解一些重入攻击的真实案例:

参考资料

联系我们

在 Zealynx,我们专注于智能合约安全审计和漏洞预防。无论你需要重入攻击审查还是全面的协议审计,我们的团队都随时准备帮助你交付安全的代码联系我们 开始对话。

想通过更多像这样的深入分析保持领先吗?订阅我们的新闻通讯,确保你不会错过未来的见解。


常见问题:Solidity 中的重入攻击

  1. 用简单的话来说,什么是重入攻击?

    重入攻击发生在恶意合约在第一次调用完成执行之前回调受害合约时。这使得攻击者可以反复提取资金,因为余额尚未更新,从而耗尽合约的全部余额。

  2. 单函数重入和跨合约重入有什么区别?

    单函数重入通过 fallback 函数递归调用自身来利用一个函数。跨合约重入利用同一个项目中多个合约之间的交互,其中一个合约的状态更新依赖于另一个可以通过外部调用重新进入的合约。

  3. Checks-Effects-Interactions 模式能完全防止重入攻击吗?

    CEI 通过在进行外部调用之前更新状态来防止同一合约内的单函数和跨函数重入。但是,它不能防止多个合约共享状态的跨合约重入。为此,你需要一个 GlobalReentrancyGuard。

  4. 到 2026 年,重入攻击仍然是真正的威胁吗?

    是的。尽管重入攻击已经众所周知十年,但其变体仍在不断造成损失。只读重入、跨合约重入以及通过 ERC-777 代币回调的重入仍然经常在审计竞赛和生产代码中发现。

  5. GlobalReentrancyGuard 是如何工作的?

    它在单独的合约中存储一个锁定变量,项目中的所有合约在执行敏感函数之前都会检查该变量。当系统中的任何函数获取锁时,在锁释放之前,任何合约中的其他函数都不能重新进入,从而防止跨合约重入。


词汇表

术语 定义
Reentrancy 一种漏洞,其中外部调用允许恶意合约在状态更新之前回调调用合约,从而实现重复提款。
Fallback Function 一个特殊的 Solidity 函数,没有名称,当合约接收到 Ether 或调用函数不存在时自动执行。
Checks-Effects-Interactions 一种 Solidity 设计模式,通过操作排序来防止重入攻击:首先验证条件,其次更新状态,最后进行外部调用。
nonReentrant Modifier 一种基于互斥锁的修饰符,在函数执行期间锁定合约,防止在第一次调用完成之前再次调用同一函数。
Cross-Program Invocation 智能合约之间的一种交互,其中一个合约调用另一个合约中的函数,如果状态共享,则可能产生重入攻击向量。

查看完整词汇表 →

  • 原文链接: zealynx.io/blogs/reentra...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
zealynx
zealynx
江湖只有他的大名,没有他的介绍。