一文了解丨重入攻击(Re-Entrancy Attack):智能合约的安全威胁

 2023-10-03 16:47:47发布 2023-10-03 16:47:57更新

重入攻击(Re-Entrancy Attack)是智能合约中最知名的安全问题,攻击者可以在一次函数调用期间多次进入或“重入”智能合约的函数。如果合约的状态没有正确地被更新或者检查,那么攻击者可能会多次提取资金,或者执行其他有害的操作。

什么是重入攻击

重入攻击通过一个叫做“fallback”的函数执行。Fallback函数是solidity中一个特殊的结构,在某些特殊的场景下会被触发。

fallback()功能有下面这些特点:

  1. 他们是不被命名的;
  2. 他们是被外部调用,不能被自己合约内的函数调用;
  3. 一个合约中只有0个或者1个fallback函数,不会更多;
  4. 他们会在别的合约调用一个本合约不存在的函数时调用;
  5. 当ETH被发送给这个合约时,如果该交易没有calldata同时没有receive()函数时,fallback函数会被触发,并且fallback函数必须标记为payable以便可以接受ETH;
  6. fallback函数可以包含自己的逻辑。

上面的第五个和第六个特性,导致fallback函数被重入攻击

著名的重入攻击事件

2016年,The DAO合约被重入攻击,黑客盗走了合约中的 3,600,000 枚 ETH,并导致以太坊分叉为 ETH 链和 ETC(以太经典)链。

2019年,合成资产平台 Synthetix 遭受重入攻击,被盗 3,700,000 枚 sETH。

2020年,借贷平台 Lendf.me 遭受重入攻击,被盗 $25,000,000。

2021年,借贷平台 CREAM FINANCE 遭受重入攻击,被盗 $18,800,000。

2022年,算法稳定币项目 Fei 遭受重入攻击,被盗 $80,000,000。

距离 The DAO 被重入攻击已经6年了,但每年还是会有几次因重入漏洞而损失千万美元的项目,因此理解这个漏洞非常重要。

The DAO被攻击事件

在2015年之前,还在早期的以太坊社区讨论DAO,2016年,一个名叫“The DAO”的DAO建立了。

The DAO是一个去中心化的,由社区控制的投资基金。它通过销售自己的社区通证募集了价值1亿 5000 万的美元的 ether(大概有354万 ETH)。

人们通过存储 ETH 来购买 The DAO 的社区通证,这些存储在 The DAO 中的 ETH 就变成了投资基金。The DAO 会代表持有社区通证的投资者来进行投资。

在The DAO 开始不到三个月的时间里,就被一个“黑帽”黑客攻击了。在接下来的几周,这个黑客从The DAO中被偷走了价值1亿5000万美元的ETH。这种攻击方式也就是“重入攻击”。

这次攻击对 DAO 进行了非常严重的破坏,使其失去了投资者的信任,同时也严重影响了以太坊的信誉。

行业内的人都看到了资金在 The DAO 中被偷走,并就如何处理这次事件进行了激烈的讨论。

一部分人认为,密码学保证了区块链的不可篡改,如果强行修改,即使是为了正确的原因,也属于篡改。另一方面,有人觉得人们在 The DAO 中资产正在缓慢地偷走,这会破坏公众的信心。为了阻止严重的后果,大家有责任去阻止资产被盗。

在这些讨论进行的时候,一个“白帽”黑客组织进行反击,他们属于要干预的阵营,他们使用黑客的同样手段进行重入攻击。

尝试比黑客更快地把The DAO的资金转走,他们想要拯救这笔资金,然后返还给投资者。大量的资金被返回给了投资者,这样很多投资者就能够通过这个“逃生舱”取回他们的投资。

因为黑客将大量的资金盗走的行为还在继续,以太坊核心团队面临一个艰难的决策。一种阻止黑客的方式是分叉以太坊,这样就可以修改历史,让这个事件没有发生过。

在这个例子中,通过分叉以太坊,黑客在攻击中获得的 ETH 只会存在于以前的旧的网络中。如果用户都接受了新的分叉而把旧的网络废除的话,黑客偷走的 ETH 将不再值钱。

虽然这次分叉将会让黑客攻击发生的那些区块不再有效,但是这个极端的操作将会完全违背以太坊的原则:这种干预正是以太坊自身想要避免的一种中心化的,单方面的行为。

那些投票给分叉的人也同意同时有两条以太坊区块链,这个意愿占到了总投票的 85%,然后分叉就发生了。这也就是为什么现在有两个以太坊链 – 以太经典和我们今天在用的以太坊。

它们都有原生 ETH 通证,当时这些通证在市场上的价格差别很大。

代码示例

银行合约(被攻击合约)

银行合约(被攻击合约)

银行合约(被攻击合约)

攻击合约

攻击合约

攻击合约

attack()

先给自己账户充值ETH,才可以提取金额,调用bank合约的withdraw方法获得ETH。

fallback()

先判断bank合约中是否还有ETH,如果有,则调用withdraw方法,这里会无限次调用withdraw函数,直到合约里面没有钱。

重入攻击的逻辑

  1. 先部署Bank合约,调用deposit函数并充值;
  2. 部署Attack合约;
  3. 在attack()函数中调用了Bank合约的withdraw(),此次转向Bank合约,在withdraw()方法中的msg.sender.call()这一行,call()函数会找Attack合约中的receive(),若没有则看是否有fallback(),在我们这个攻击合约中,有fallback(),则call()函数会调用fallback();
  4. 在本来的fallback()函数中是写的正确的逻辑,不会导致重入攻击,但我们这个攻击合约中的fallback()中的逻辑会导致一直withdraw,直到将全部的钱转走。

防止重入攻击的方法

一次成功的重入攻击后果可能是毁灭性的,可能会耗尽受害者合约中的所有资金,因此,意识到潜在的漏洞并实现有效的保障措施是很重要的,以下四种方式可以防止重入攻击。

检查、影响、交互(CEI)

检查、影响、交互(Checks, Effects, and Interactions,CEI)模式是一种简单而有效的防止重入的方法。检查指的是判断是否符合条件(验证真实性)。影响指的是由交互产生的状态修改。最后,交互指的是函数或合约之间的调用。

下面是一个错误的示范(因为交互在影响之前)

这里是攻击者的receive函数:

攻击者的receive函数收到提款后,本应该只返回success,但却检查contract_A是否包含更多的资金。如果是,contract_B会再次调用提款函数,递归直到contract_A所有资金用完。

下面是一个使用CEI模式的提款函数的例子:

通过在向contract_A转移资金之前将用户在contract_B的账户余额清零,当contract_B发起重入攻击时,提取函数中的条件将为假,执行将被回退。

正如这个案例所强调的那样,一行代码的位置可能引起有重大漏洞与重入性安全之间的巨大区别。

重入保护互斥锁

重入防护互斥锁(mutex)可以被构造成一个函数或函数修改器,但其逻辑很简单:

一个布尔锁被放置在易受重入影响的函数调用周围。locked的初始状态为假(unlocked),在易受攻击的函数执行开始前,它被立即设置为真(locked),然后在其终止后被设置回假(unlocked)。

下面是一个使用上面的提款函数的例子:

虽然这个提款函数没有遵循CEI模式,但简单的布尔 locked 变量可以防止重入,因此同样可以防止重入攻击。
因为在重入时,第一个require语句条件将为false,会回退交易。

Pull(拉) 方式支付

最后这种技术被Open Zeppelin推荐为最佳实践。然而,它在自动化方面有一个小小的折衷。Pull方式支付是通过中间托管账号发送资金来避免直接和潜在的危险合约交互来实现安全。

在这里,合约资金被发送到一个中间托管账号。

而在托管账户的资金,则由接收方来提取:

通过中间托管账号发送资金,合约资金受到保护,不会受到重入攻击。如果托管人持有多个账户的资金,可能会受到重入攻击,所以在适用的情况下,应该实现CEI模式和(或)重入保护互斥锁。

Gas Limit限制

通过Gaslimit 限制也可以防止重入攻击,但这不应该被视为一种安全策略,因为Gas成本取决于以太坊的操作码,而操作码gas是可以改变的。另一方面,智能合约代码是不可改变的。不过, send, transfer, 和 call 这些函数之间的区别是值得了解的。

send和 transfer函数本质上是相同的,但如果交易失败,transfer会回退,而send则不会,而是会返回 false。

关于重入问题, send和 transfer都有2300个单位的Gas限制。使用这些函数应该可以防止重入性攻击的发生,因为他们没有足够的Gas来递归调用到原函数来利用资金。

与 send和 transfer不同,call没有Gas限制,为了执行复杂的多合约交易(当然,也包括重入攻击),会转发其所有剩余Gas。

推荐阅读