作者:Immunefi 编译:CoinTime 237
重入攻击在以太坊中已经有很长的历史,并且是导致 DAO黑客事件的漏洞类别之一,该事件是2016年早期以太坊网络遭受的最大攻击之一。自那时以来,许多标准已被引入以减轻这类漏洞,例如限制由外部保护程序转发的gas、重入保护程序以及遵循Checks-Effects-Interactions(CEI)模式。
许多人现在甚至质疑重入的相关性作为一个重要的漏洞,因为攻击向量及其模式已广为人知。然而,看看最近的黑客攻击和事件,情况却截然不同。
自Paweł Kuryłowicz撰写关于Solidity中重入攻击是否仍然存在问题的文章以来已经有一些时间了。在2021年,Pawel提出了以下问题:
好吧,但是这种重入攻击是一个重大问题吗?
答案是:
是的,它是一个重大问题。
到目前为止,重入攻击仍然是一个重大问题,而且在可预见的未来可能仍将如此。
重入如何影响生态系统?
自Pawel发布他的文章以来,已经有数亿美元因重入攻击而遭受损失,其中最著名的是Fei的Rari Fuse Pools事件,据报道损失超过8000万美元。尽管重入是最常被引用的智能合约漏洞之一,也是许多安全研究人员首次接触智能合约安全的方式,但仍有许多项目会因为这种漏洞而受到攻击。您可以在pcaversaccio的存储库"A Historical Collection of Reentrancy Attacks"中查看受影响的项目。
什么是重入?
重入是一个状态同步问题。当向另一个智能合约进行外部调用时,执行流控制被传递。调用合约必须确保所有全局共享状态在传递控制之前完全同步。由于EVM是单线程机器,如果一个函数在传递执行控制之前没有完全同步状态,则该函数可以使用与第一次调用时相同的状态重入。这可能会导致该函数重复执行本来只打算执行一次的操作。
如果我们对 WETH 合约进行一个简单的修改,将原生资产以太包装成 ERC20 兼容的代币,我们就可以更好地理解可能导致重入的反模式。存款功能接收以太币并增加存储在映射中的用户余额balanceOf。
当用户想将他们的 WETH 转换回以太币时,他们调用withdraw. 当该withdraw函数使用低级调用将以太币转移给用户时,执行流程将转移到接收者。在此示例中,在更新余额之前进行外部调用。如果调用方是 EOA,则转账成功完成并在撤回函数内继续执行。但是,如果调用者是智能合约,则调用默认的 payable 函数,可以控制它做任何我们喜欢的事情。
在我们执行默认支付功能期间,WETH 合约不知道它已经发送了以太币,因为映射balanceOf尚未被修改!如果我们回调该withdraw函数,检查我们是否有足够的 WETH 余额来提取以太币的 require 语句将会通过。我们刚刚破解了 WETH 合约并获得了无限的以太币!
* 一旦所有可重入调用都解决,balanceOf映射仍然根据调用函数的次数减少。在 Solidity 版本 >= 0.8.0 中,这将导致整个功能恢复,因为默认情况下发生下溢/溢出检查。然而,任何低于此的 Solidity 版本都将导致余额不足,攻击者将额外获得非常大的余额。
你能做些什么来防止重入?
fallback重入的第一个案例发生在以太币的传输中,因为代码执行在本机资产传输期间被转移到接收函数。这些函数send和transfer被引入地址类型以传输以太币,但限制转发给接收者的gas量以限制可以执行的逻辑。这减轻了潜在的 gas griefing 风险,并防止了重入,因为内部调用会在能够执行必要的逻辑之前耗尽 gas。然而,这个解决方案也有缺点。使用transfer或send将破坏与智能合约的可组合性,智能合约可能在回退函数中出现一些必要的逻辑,例如代理,它将它们的逻辑委托给实现合约。
由于操作码的 gas 成本可能会发生变化,这可能会破坏依赖于这些调用期间传递的有限 gas 量的现有合约,因此建议不要使用和send。ConsenSys 在他们的文章Stop Using Solidity's transfer() Nowtransfer中详细介绍了这个问题,但是因为 gas 成本可能会发生变化并且有更有效的方法来减轻重入风险,如果遵循最佳实践则不应使用。sendtransfer
防止重入的最推荐和最简单的方法是实施检查-效果-交互 ( CEI ) 模式。那些执行外部调用的函数应该确保所有外部交互发生在任何检查或状态更改之后。这也就是传统并发编程中俗称的尾调用模式。如果我们要在withdrawWETH 函数中修复前面的示例,我们将首先有检查用户是否有足够的 WETH 余额的 require 语句(检查),对存储进行更改以更新用户余额(效果),最后使外部调用用户转移资金(交互)。
最后,如果协议的无权限操作可能会引入未知风险,则reentrancyGuard可以使用 a 来确保无法在同一调用框架内多次调用该函数。OpenZeppelin 提供了一个用于实现ReentrancyGuards 的库。但是,执行 SLOAD 和 SSTORE 以检查函数是否已被调用的额外 gas 成本将增加 gas 成本,如果遵循推荐的模式,则可能没有必要。此外,这种类型的重入守卫不会防止跨合约重入。
* EIP-1153旨在通过为每次交易后丢弃的数据引入新的操作码来降低此成本
什么可以触发重入?
如果没有遵循正确的 CEI 模式,任何外部调用都可能导致重入。Slither是一个开源静态分析框架,可以帮助审计人员和漏洞猎手找到潜在的可重入漏洞入口点。但是,以下标准是执行流程可以转移到任意合约的方式的几个示例:
低级别调用(.call())
transferERC223 代币
transferAndCallERC667 代币
transferERC777 代币
safe*ERC1155代币的传递函数
*AndCallERC1363代币的功能
safe*ERC721 代币safeTransfer等功能safeMint
transfer某些 ERC20 代币可能已经为接收者实现了自定义回调函数
重入有哪些不同类型?
1、单函数重入
这是最简单的重入类型,导致了 6000 万美元的 The DAO Hack 和以太坊网络的硬分叉,导致创建了单独的区块链、未改变的“以太坊经典”以及我们今天所知的改变历史的以太坊网络.
当合约在完成状态更改之前进行外部调用,并且在外部调用中重入相同的函数时,就会发生单函数重入。
2、跨函数重入
攻击者还可以使用共享相同状态的两个不同函数进行类似的攻击。如果第一个函数在共享数据更新之前进行外部调用,则攻击者可能会以未更改的状态进入第二个函数。
如果两个函数都有一个守卫, OpenZeppelin 的重入守卫nonReentrant可能会阻止这个问题,因为它们共享相同的存储值作为检查该函数是否已被调用的值。nonReentrant这也可以防止在同一个调用框架内调用带有修饰符的函数。
3、跨合约重入
重入不限于调用同一合约中的函数。共享相同状态的多个合约也容易受到重入的影响。同样,CEI 模式可以防止任何重入风险。但是,如果共享状态在外部调用之前没有更新,重入可能会导致严重的漏洞。您可以在Phuwanai Thummavet 的这个例子中阅读更多关于跨合约重入的信息。
4、只读重入
通常,审计员和漏洞猎手在寻找重入时只关心修改状态的入口点。但是,当协议依赖于读取另一个协议的状态时,可能会发生只读重入。最值得注意的是,通过在移除流动性的过程中重入视图函数,Curvesget_virtual_price很容易受到此类攻击。get_virtual_price在许多情况下,这会影响依赖于另一个定价机制的协议,因此项目在集成交易所价格预言机或其他流动性管理协议时应该非常小心。在Curve LP Oracle Manipulation: Chain Security 的Post Mortem中阅读更多关于野外只读重入的信息。此外,您可以在 SunWeb3Sec 的DeFiVulnLabs中找到只读重入的示例常见的智能合约漏洞存储库。
5、跨链重入
跨链重入是最新类型的重入攻击,随着跨链消息传递协议的兴起,它最近才开始受到关注。在野外没有跨链重入攻击的先例。然而,随着跨链互操作性和多链未来统一愿景的兴起,任何在链之间桥接资产或利用跨链消息传递的协议都必须理解和审查这种范式。可以在此处查看专门为演示跨链可重入性而创建的示例。
重入的未来?
在EIP-1153TSTORE中引入新的瞬态存储操作码为改进智能合约中的重入保护提供了机会。这些操作码允许将数据存储在合同功能完成后重置的临时位置,使攻击者无法重入功能。通常,可重入守卫是使用存储实现的。话虽这么说,但操作码的 gas 成本很高。OpenZeppelin 的重入守卫可能会改为使用更省油的瞬态存储操作码。TLOADSSTORESLOAD
随着这些新操作码的添加,还有在编译器级别默认禁用重入的举措。这将为防止重入攻击提供额外的保护层,并有助于确保开发人员了解与重入代码相关的风险。Vyper和Solidity编程语言都在考虑实现此功能,这将使开发人员更容易编写安全合约,并可能导致开发人员在考虑在其智能合约中进行外部调用时的范式转变。
在此之前,重入攻击仍然是智能合约领域的一个严重问题。
因此,开发人员必须在编码实践中保持警惕并采用最佳安全实践来最大程度地降低重入攻击的风险。此外,审计员和安全研究人员在识别漏洞和向开发人员提供反馈方面发挥着至关重要的作用。通过共同努力,区块链社区可以继续提高智能合约的安全性,并通过漏洞奖励和审计防止重入攻击造成进一步的危害。
所有评论