防止重入漏洞 - 终极指南

本文深入探讨了以太坊智能合约中一种臭名昭著且具有破坏性的漏洞——重入漏洞。文章解释了重入漏洞的原理、类型(包括单函数重入、跨函数重入和只读重入),并通过具体的合约例子进行了详细分析。此外,还提供了多种缓解重入攻击的技术手段,如在外部调用前更新状态、使用重入锁、遵循Checks-Effects-Interactions模式等。

1. 介绍

重入是以太坊智能合约历史上最臭名昭著和最具破坏性的漏洞之一。

它利用了一个简单而强大的缺陷:在保护你自己的合约状态之前,信任外部调用

什么是重入?

从技术角度讲,当发生以下情况时,就会发生重入:

  1. 合约调用外部的、不受信任的合约(或任何可以接收以太币的地址)。
  2. 在完成自身的状态变更之前,调用合约无意中将控制权交还给外部合约。
  3. 现在处于控制状态的外部合约,回调到原始函数中——有时多次——利用了这样一个事实,即状态尚未更新。

这使得攻击者可以重复敏感操作(如提取资金)一遍又一遍,耗尽远远超出允许范围的资产。

🛑 DAO 黑客攻击——以太坊历史上的转折点

这种漏洞最臭名昭著的例子是 2016 年的 DAO 黑客攻击

  • DAO 是一个建立在以太坊上的去中心化风险投资基金。
  • 它持有来自数千名投资者的约 1.5 亿美元的 ETH
  • 攻击者利用了其提款逻辑中的一个重入漏洞
  • 攻击者在余额更新之前反复提取资金
  • 结果:仅在几个小时内,就有超过 6000 万美元的 ETH 被盗。

该事件是如此灾难性,以至于它分裂了以太坊网络

  • 以太坊 (ETH)——被盗资金通过硬分叉恢复。
  • 以太坊经典 (ETC)——链在没有干预的情况下继续存在,保留了原始状态。

有关攻击如何发生的详细分析,请参阅:

2.攻击者的合约可以 “回调你”

  • 当你发送以太币时,接收者的合约可以有一个 receive()fallback() 函数,立即再次调用你的函数。
  • 这让他们可以在你的状态改变之前偷偷地进行多次提款。

3.没有 “一次一个” 的锁

  • 如果没有像 Checks-Effects-Interactions(检查-生效-交互) 或 ReentrancyGuard(重入保护) 这样的保护措施,你的合约允许同一个函数一次运行多次,从而使攻击成为可能。

如果你在锁定你的保险箱(更新余额)之前交出控制权(发送以太币),并且不在门口留下警卫,那么攻击者可以在你注意到之前一次又一次地走进来。

3. 重入的类型

重入攻击有不同的类型,具体取决于攻击者如何以及在何处重新进入你的合约。 了解这些类型有助于你设计更好的防御措施。

3.1 单函数重入

经典且最常见的形式。

  • 攻击者调用一个有漏洞的函数——例如,withdraw() 函数。
  • 此函数在更新攻击者的余额之前,将以太币发送到攻击者的合约。
  • 当攻击者收到以太币时,他们的 fallbackreceive() 函数会在 余额更新之前 自动再次调用同一个易受攻击的函数。
  • 这个循环重复,允许攻击者提取比他们拥有的更多的资金。

示例场景:

攻击者的 fallback 在 call 期间再次调用 withdraw(),在余额减少之前。

3.2 跨函数重入

这是一种更微妙和高级的重入形式。

  • 攻击者不是重新进入同一个函数,而是调用一个不同的函数,该函数改变了相同的易受攻击的状态变量。
  • 这可以绕过仅保护单个函数或假设只有一个入口点的简单保护。
  • 例如,攻击者可能首先调用 withdraw(),然后通过不同的函数(如 transfer()claimReward())重新进入,从而利用共享的余额状态。

为什么它很棘手:

  • 如果每个函数都受到单独保护,你的合约可能看起来很安全,但函数之间共享的状态仍然可能被意外操纵。
  • 跨函数重入需要仔细跟踪合约中的所有状态依赖关系。

跨函数重入在这里是如何工作的:

  • 攻击者调用 withdraw(),它发送以太币,但在发送_之后_更新余额。
  • 在他们的 fallback 内部,攻击者调用 transfer(),它也会在更新之前操作相同的 balances
  • 这使得攻击者可以通过在不同的函数之间反弹来提取资金,这些函数在任何余额更新发生之前操纵共享状态。

3.3 只读重入

在 Solidity 0.5.x 左右引入,这是一种更细微和不太明显的攻击媒介。

  • 攻击者在外部调用期间重新进入一个 view(视图) 或 pure(纯函数) 函数(不修改状态)。
  • 虽然这些函数不改变状态,但它们可能会读取正在更新的状态变量——读取陈旧或不一致的数据
  • 这可能会导致你的合约或依赖于这些读取的链下 oracle(预言机) 做出错误的决定

例子:

  • 在调用发送以太币期间,攻击者调用一个 view 函数,该函数在余额更新_之前_读取余额。
  • 依赖于该 view 函数的 oracle 或其他合约会看到不正确的信息,从而导致不正确的奖励或状态更新。

4. 易受攻击的合约示例

4.1 单函数重入(深入)

说明:

  • withdraw() 函数允许用户提取他们存入的资金。
  • 然而,它首先发送以太币,然后更新用户的余额
  • 如果用户是一个恶意合约,它的 fallback 函数可以在余额更新之前再次调用 withdraw(),提取比他们存入的更多的以太币。
  • 这种漏洞的出现是因为合约在保护其内部状态之前,信任外部调用

攻击是如何工作的:

  1. 攻击者调用 withdraw(1 ether)
  2. 合约将 1 个以太币发送到攻击者的 fallback 函数。
  3. fallback 再次调用 withdraw(1 ether),在余额减少之前重新进入同一个函数。
  4. 这种情况重复发生,直到合约为空或 gas 用完。

图表由 Cyfrin Updraft 提供。 如需更多有价值的资源,请访问他们的网站

4.2 跨函数重入

重入漏洞的出现是因为合约在更新用户的余额之前,通过 call 将以太币发送给调用者。 在此示例中,攻击者可以首先调用 withdrawPartial 以触发一个 fallback 函数,该函数递归调用 withdrawFull,从而利用不一致的状态并耗尽资金。

攻击描述:

  • 攻击者将 1 ETH 存入易受攻击的银行。
  • 调用 withdrawPartial(1 ether),该函数在更新余额之前将以太币发送到攻击者的 fallback 函数。
  • fallback 函数在余额更新之前递归调用 withdrawFull(),允许资金被多次耗尽。
  • 攻击者合约收集耗尽的以太币,并可以在以后提取它。

你可以在我们的 GitHub 存储库中找到本次讨论中的所有 Solidity 代码示例,以供参考

4.3 在 Remix 上尝试

4.4 Foundry 设置

单击此处可在 GitHub 上查看 Foundry 设置和工具

5. 重入缓解技术

在外部调用之前更新状态

  • 始终在发送以太币或调用外部合约之前更改余额或关键状态变量。
  • ✅ 示例:

使用重入保护(互斥锁)

  • 添加一个简单的锁,或使用 OpenZeppelin 的 ReentrancyGuard 来防止嵌套(递归)调用。

遵循检查-生效-交互模式

  • 检查: 验证输入和条件。
  • 生效: 更新状态。
  • 交互: 最后进行外部调用。

使用 transfersend 进行以太币转账 (如果 gas 约束允许)

这些方法转发有限的 gas,因此复杂的 fallback 执行更加困难(但要注意 opcode(操作码) 中的 gas 变化)。

拉取而不是推送付款

  • 让用户提取自己的资金,而不是在操作期间自动发送以太币。

结论

重入漏洞可能会对智能合约造成严重损害,但只要小心谨慎并遵循最佳实践,你就可以有效地保护你的项目。 通过应用诸如检查-生效-交互之类的原则,在进行外部调用之前更新状态以及利用重入保护,你可以构建强大的防御来抵御这些攻击。

请记住,安全性是一个持续的旅程——始终彻底测试、审查你的代码,并随时了解智能合约开发中的最新技术。

🚀 准备好深入了解了吗? 此博客中的所有示例代码、测试和详细设置都可以在我的 GitHub 存储库中找到。 立即开始探索、学习和构建安全合约!

top10-smartcontract-vulnerabilities/05-Reentrancy at main ·…

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

0 条评论

请先 登录 后评论
blockmagnates
blockmagnates
The New Crypto Publication on The Block