Solidity 中的重入攻击是什么?
作者 Alchemy
**重入攻击(reentrancy attack)**是指在Solidity中反复从智能合约中提取资金并转移到未经授权的合约,直到资金被耗尽。这种攻击发生在区块链的执行周期中,此时不良行为者发现了可利用的智能合约漏洞。重入攻击已经从DAO和区块链协议中卷走了数百万美元。
本文将解释重入攻击的运作机制、两种类型的重入攻击,以及Solidity 开发者可以采取的预防措施,以保护 Ethereum 和 Solana 区块链上的智能合约免受漏洞影响。
什么是重入攻击?
当智能合约函数通过对某个可能由未知或恶意行为者编写的合约进行外部调用而暂时放弃对交易的控制流时,就会发生重入攻击。这使得后者的合约可以递归调用回主要的智能合约函数,从而耗尽其资金。
Ethereum 区块链上智能合约的执行周期是先检查余额,然后发送资金,之后再更新余额。当智能合约处于托管状态时,不良行为者可以发起另一次调用来提取资金。这个循环会一直重复,直到所有资金都被实际耗尽。
重入攻击是如何运作的?
重入攻击创建了一个递归过程,在两个智能合约——即存在漏洞的合约和恶意合约——之间转移资金。 重入攻击的步骤如下:
- 不良行为者对存在漏洞的合约“X”发起调用,将资金转移到恶意合约“Y”。
- 合约 X 判断攻击者是否拥有所需资金,然后开始将资金转移给合约 Y。
- 一旦合约 Y 收到资金,它会执行一个 回调 函数,在余额更新之前回调合约 X。
- 这个递归过程会持续进行,直到所有资金都被耗尽并转移。
下图展示了该攻击场景:

重入攻击有哪些不同类型?
重入攻击有两种类型:单函数重入攻击和跨函数重入攻击。
1. 单函数重入攻击
当存在漏洞的函数与攻击者试图递归调用的函数是同一个函数时,就会发生单函数重入攻击。相比跨函数重入攻击,单函数重入攻击更简单,也更容易防范。
2. 跨函数攻击
只有当存在漏洞的函数与另一个对攻击者有利的函数共享状态时,跨函数重入攻击才能实施。跨函数攻击更难检测,也更难防范。
3. 跨合约攻击
当一个合约的状态在完全更新之前被另一个合约调用时,就会发生跨合约重入攻击。跨合约重入攻击通常发生在多个合约手动共享同一变量,且其中某些合约以不安全的方式更新该共享变量时。
Solidity 重入攻击实例
以下几起著名的重入攻击进一步说明了不良行为者是如何利用区块链协议漏洞的:The DAO hack、Lendf.me 以及 Cream Finance。
1. DAO hack(2016 年)
Ethereum 的 DAO 被黑客攻击,损失约 6000 万美元的 Ether。Ethereum 的 DAO 被设计为一个投资基金,网络成员可以直接对投资决策进行投票。
DAO 筹集了大约 1.5 亿美元,但专家和社区参与者对存储资金的智能合约的安全性表示担忧。由于源代码中存在递归调用漏洞,这些资金被锁定在一个易受重入攻击的智能合约中。在开发团队修复该问题之前,黑客发起了攻击并耗尽了合约中的资金。
2. Lendf.me 协议(2020 年)
2020 年 4 月,一名不良行为者利用重入攻击从 Lendf.me 协议(一个在 Ethereum 网络上进行借贷操作的去中心化金融协议)中窃取了 2500 万美元。
该协议的开发者忽略了这样一个事实:ERC-777 代币包含一个回调函数,用于在资金发送或接收时通知用户。黑客利用了这一漏洞,将一个恶意智能合约设为接收方,从而耗尽了 Lendf.me 协议 99.5% 的资金。
3. Cream Finance 攻击事件(2021 年)
2021 年 10 月,一名不良行为者利用该协议“闪电贷”功能中的重入攻击漏洞,窃取了价值超过 1.3 亿美元的 ERC-20 代币和 CREAM 流动性协议(LP)代币。此次漏洞的根本原因在于 AMP 与 CREAM finance 协议之间存在错误的集成方式。
如何防范重入攻击
建立严格的区块链安全框架,对于预防和减轻重入攻击可能造成的损害至关重要。 以下反重入最佳实践将帮助开发者和更广泛的 web3 社区保护其资金安全:检查-效果-交互(CEI)模式、重入锁、拉取支付以及 gas 限制。
检查、效果与交互(CEI)
CEI 流程是一种防止重入的基础方法。检查(Checks)指的是对条件真实性的判断,效果(effects)指的是交互产生的状态变更,交互(interactions)指的是函数或合约之间的交易。
对于开发者来说,需要重点考虑将执行效果置于交互之前所带来的潜在安全风险和漏洞。
重入锁(Reentrancy guard 或 mutex)
重入锁(reentrancy guard 或 mutex)可以作为函数或函数修饰符(function modifier)来创建。围绕易受重入攻击的函数调用设置一个布尔锁。这意味着“locked”的初始状态为 false,但在易受攻击的函数开始执行之前会立即将其设为 true,并在执行结束后迅速将其重新设回 false。
拉取支付(Pull payment)
实现更安全的端到端交易的一种方法是使用拉取支付(pull payment)方式。拉取支付流程要求使用中间托管来发送资金,从而避免与可能存在恶意的合约直接接触。
通过中间托管发送资金,智能合约的资金可以免受重入攻击的影响。但如果该托管合约同时管理多个账户的资金,它本身也可能面临重入风险。在适当情况下,应当同时实施 CEI 模式和重入锁。
Gas 限制
Gas 限制并不是防止攻击者的最佳方法,因为 gas 成本取决于 Ethereum 的操作码(opcode),而操作码是可能发生变化的。相比之下,智能合约代码则是不可变的。
理解 send、transfer 和 call 函数之间的区别非常重要。Send 和 transfer 在功能上基本相同,但如果交易失败,transfer 会回滚,而 send 不会。
与 send 和 transfer 不同,call 函数没有 gas 限制,会将其 gas 全部转发用于执行多合约交易。遗憾的是,这也意味着重入攻击是可能发生的。
通过 Alchemy University 深入了解重入攻击
Ethereum 上智能合约的执行周期并非万无一失。不良行为者通过实施重入攻击来利用区块链漏洞,从存在漏洞的智能合约中转移并耗尽资金。采取预防措施将有助于 Solidity 开发者实现更安全的执行周期,并保护其区块链协议。
想要了解 Solidity 智能合约开发的最佳实践,欢迎加入 Alchemy 免费的为期 7 周的 Ethereum 开发者训练营。这门课程最初是一门售价 3000 美元的认证课程,如今 Alchemy University 的训练营已成为免费学习 Solidity的首选之地。
如果开发者是编程新手,Alchemy 的为期 3 周的 JavaScript 速成课程是开始 Ethereum 训练营之前非常好的先修课程。
相关概览

构建区块链应用
Alchemy 将最强大的 Web3 开发者产品和工具与资源、社区及专业支持结合在一起。


