Time:2023-08-02 Click:108
原文作者:Jaleel,BlockBeats
原文编辑:Jack,BlockBeats
随着一场漏洞利用事件的发生,DeFi 行业陷入了一场混乱。Curve Finance,这个 DeFi 行业的巨头,成为了严重「攻击」的目标,诸如 alETH/msETH/pETH 的多个稳定币池岌岌可危。据不完全统计,该漏洞利用事件已造成 Alchemix、JPEG'd、MetronomeDAO、deBridge、Ellipsis 和 CRV/ETH 池累计损失 5200 万美元,整个市场的信心被严重撼动。
Vyper 0.2.15、 0.2.16 和 0.3.0 版本的重入锁失效,Vyper 官方文档安装界面推荐的也是一个错误的版本。其他使用 Vyper 编译器的项目方也赶紧进行了自查,试图确保自己不会成为下一个受害者。随着漏洞利用事件的源头被逐渐揭露,市场逐渐认识到,这次的危机并不仅意味着一次普通的黑客漏洞利用事件,更是展露出整个底层堆栈对整个 DeFi 行业潜在的巨大风险。
和以往相比,前段时间的黑客事件数量越来越少,这与市场的繁荣程度脱不开关系。DeFi summer 和 NFT summer 时期,每周都有新的十亿美元协议推出,相比之下如今的市场十分萎缩。与此同时的,黑客们找到漏洞利用或者制造大笔的攻击事件的市场机会也在逐渐萎缩,这意味着黑客需要更新的、未开发的切入口来探索。
回归「第一性原理」的黑客们在更底层的编译器上找到了一个完美的切入口,去馋食 DeFi 市场上巨大可口的「蛋糕」,更底层的编译器成为了黑客们更「聪明」的选择。就这次事件及其暴露出的相关问题,BlockBeats 采访了智能合约开发者 Box(@BoxMrChen)和 BTX 研究员 Derek(@begas_btxcap)。
Curve 遭遇的漏洞利用事件是最古老,也许是最常见的以太坊智能合约攻击形式之一,重入攻击。重入攻击允许攻击者反复调用智能合约的某一函数,而不等该函数的前一个调用完成。这样,他们就可以不断利用漏洞提款,直至受害合约资金耗尽。
举个简单的例子来说明重入攻击:一家银行总共拥有 10 万现金。但这家银行有一个很大的漏洞,每当人们取钱时,银行工作人员并不立即更新账户余额,而是等到一天结束时才进行核对和更新。这时有人发现了这个漏洞,他在银行开了一个账户,先存入 1000 元,然后取出 1000 元,过 5 分钟再取出 1000 元。由于银行没有实时更新余额,系统会在进行核对和更新前认为他账户还有 1000 元。通过反复操作,最终该用户取出了银行里全部的 10 万美元现金。到这天结束时银行才发现被利用了这个漏洞。
重入攻击的复杂性在于它利用了合约之间相互调用的特性,以及合约自身的逻辑漏洞,通过故意触发异常和回退功能来实现欺诈行为。攻击者可以反复利用合约的逻辑漏洞,窃取资金。防止重入攻击的解决方案也很普遍,提前设置一段针对性的特殊代码内容进行防护,用这样的保护机制来确保资金安全,这被称为重入锁。
Solidity 为智能合约编程设定了一个「CEI 原则」(Check Effects Interactions),能很好地保护函数免受重入攻击。CEI 原则的内容包括:
1. 函数组件的调用顺序应该是:首先是检查,其次是对状态变量的影响,最后是与外部实体的交互。
2. 在与外部实体交互之前,应该先更新所有状态变量。这被称为「乐观记账」,即在交互真正发生之前就将影响写入。
3. 检查应在函数开始处进行,以确保调用实体有调用该函数的权限。
4. 状态变量应在任何外部调用之前更新,以防重入攻击。
5. 即使是可信的外部实体,也应该遵循 CEI 模式,因为它们可能会将控制流转移给恶意的第三方。
根据文档的说法,CEI 原则有助于限制合约的攻击面,特别是防止重入攻击。CEI 原则可以很容易地应用,主要就是按功能代码的顺序,不需要改变任何逻辑。众所周知的导致以太坊分叉的 The DAO 漏洞利用,也是无视「CEI 原则」,而被攻击者实现了重入攻击,造成了毁灭性后果。
但被攻击的 Curve 池并没有遵循这个 CEI 原则,原因是 Curve 采用的是 Vyper 编译器。作为编译器的 Vyper 代码漏洞,导致重入锁失效,使得黑客的重入攻击成功实现了。
大多数人都知道 Solidity,但 Solidity 并不是创建智能合约的唯一语言,目前替代 Solidity 的流行方案就是 Vyper。虽然 Vyper 的功能和流行程度不如 Solidity,但对于熟悉 Python 的开发人员来说是理想的选择,因为 Vyper 能将类 Python 的代码转译成以太坊智能合约编程语言。
根据 github 的信息显示,Vyper 的 github 代码库贡献第一的开发者,也是 Curve 开发者。这也不难解释 Curve 为什么采用的是 Vyper 而不是 Solidity。
那在这次攻击事件里,Vyper 的问题究竟出在哪里?重入锁为什么会失效呢?是因为没有进行测试吗?BlockBeats 采访了智能合约开发者 Box 826.eth(@BoxMrChen),据他透露,Vyper 重入锁是做过用例测试的。但失效的原因是,测试用例是结果导向,也就是测试用例也是错的。
简而言之,Vyper 重入锁失效最大的原因在于,编写测试用例的人是根据结果来编写的测试用例,而没有思考过为什么 slot 会莫名其妙跳过 1 。
在 Box 后续分享出来的这几段 Vyper 代码中,可以明显看出问题。当锁名称第二次出现的时候,storage_slot 的数量会被覆盖,也就是说,在 ret 中,第一次获取锁的 slot 为 0 ,但是再次有函数使用锁后,锁的 slot 被加一。编译后使用错误的 slot,导致重入锁无法生效。
左为被攻击代码,右为修复后代码
「预计错误的测试结果,当然验证不出错误。举个简单的例子,现在我们在做计算题, 1 1 = 2 ,但给定的标准答案错了,说 1 1 = 3 。而这时做题的同学答错了,回答了 1 1 = 3 ,但却和提前给定的标准答案相同,程序就自然没办法判定出测试结果出错。」Box 在和 BlockBeats 的采访中这么说道。
在有所记录的史上第一次重入攻击事件中,WETH Attack 韦斯攻击的攻击者,正是为了让开发者重视重入攻击而故意制造攻击的白客,目的是使更多项目免受重入攻击的可能性。在智能合约的情境下,开发者应该采用不同的触发机制,例如调用某个状态改变函数来实现保护。这就要求开发人员在设计合约时充分考虑可能的攻击场景,采取适当的预防措施。
为了深入了解 Vyper 编辑器,BlockBeats 采访了 BTX 研究员 Derek(@begas_btxcap),他表示,对于熟悉 Python 的开发人员来说,Vyper 是比 Solidity 更理想的选择,UI 界面更舒服,上手更快。但显然,一些版本的 Vyper 编辑器代码并没有经过可靠的第三方审计。甚至有的审计工作,可能是开发者自己完成的。「传统 IT 行业就不会发生这种事,因为一个新的语言出来后,会有无数的审计公司往死里找你的漏洞。」
更不用说,能让一个错误堂而皇之的存在两年之久。
Vyper 贡献者 fubuloubu 也表示:编译器并没有像大家想象的那样受到审查或审计。大多数编译器都会进行重大且频繁的更改,这使得审核变得不利。即使有完整的代码库审核,在此之后添加的版本越多,它也会过时。审核编译器并不是一个很好的方式,因为审核最终用户使用该工具生成的最终产品(即原始 EVM 代码)更有意义。
所有这些都指向最后一个问题:激励。也就是说没有人有动力去寻找编译器中的关键漏洞,尤其是旧版本。fubuloubu 此前提出过一个提案,提议将通过添加由用户共同赞助的赏金计划来帮助改进 Vyper,但没有被通过。
黑客们正在回归「第一性原理」
对协议和项目的开发人员而言,这是合约安全开发实践的又一次鲜活案例。但最重要的是,Curve 事件给了我们所有人一个警示,底层编译器的安全性问题被严重忽略了,回归「第一性原理」的黑客们在更底层的编译器上找到了一个完美的切入口。
事后,Aave 及 Lens 的创始人 Stani(@StaniKulechov)也在社交媒体发布了长文表达自己的感想:这次 Curve 被攻击事件意味着,DeFi 风险一直涉及整个底层堆栈、编程语言、EVM 等。这警示了我们应更加谨慎和敏感,尤其是将来使用定制的 EVM 和应用程序链时。
对于编译器的漏洞,仅通过对合约源码逻辑的审计,是很难发现的。仅仅是研究版本和版本之间的差异也是一个大工程。需要结合特定编译器版本与特定的代码模式共同分析,才能确定智能合约是否受编译器漏洞的影响。
「目前只有两个编译器最佳,Vyper 的代码库更小,更容易阅读,对其历史进行分析的更改也更少,这可能就是黑客从这里下手的原因,Solidity 的代码库要更大一些。」fubuloubu 甚至怀疑国家支持的黑客可能参与这起 Curve 攻击事件:「找到该漏洞需要几周到几个月的时间,考虑到投入的资源,这也许是由一个小团体或团队进行的。」
作为加密行业使用最广泛的编译语言,Solidity 的安全更是被用户所牵挂,毕竟如果这次是 Solidity 编译器出现了重入锁失效的问题,那么整个 DeFi 行业的历史或许都要被改写了。
根据 Solidity 开发团队定期发布的安全预警,在多个不同版本的 Solidity 编译器中也都曾存在过安全漏洞。
最近一次的编译器错误记录是 6 月 26 日,在调查与使用具有副作用的三元表达式的 abi.decode 作为类型参数相关的安全报告时,在 Solidity 编译器的旧代码生成管道中发现了一个错误。旧代码生成器没有评估复杂的表达式,如赋值、函数调用或条件,其 .selector 正在被访问。这会导致此类表达式未执行的副作用,因此使用旧管道编译的合约的行为可能不正确。
我们也可以看到 Solidity 的Github 仓库 中放置着一个文件,上面列出了 Solidity 编译器上一些已知的安全相关的 bug。该列表可以追溯到 0.3.0 版本,只在此版本之前存在的 bug 没有被列入。这里,还有另外一个 bugs_by_version.json 文件。该文件可用于查询特定的某个编译器版本会受哪些 bug 影响。
幸运的是,也正是因为 Solidity 语言的广泛应用,以及以太坊基金会在背后的辅助,许多已存在的问题已经被项目和协议在部署的过程中被指出。因此 Solidity 比 Vyper 更快几步完成了修改和完善,从这个角度看,这也是 Solidity 更规范和更安全的原因之一。
为了帮助 Solidity 开发者进行更好的测试,防止发生同样的事。UnitasProtocol 联合创始人 SunSec(@ 1 nf 0 s 3 cpt)在 Curve 被攻击事件后,发布了一份 DeFiVulnLabs Solidity 安全测试指南,支持 47 种漏洞,其中包括漏洞描述、场景、防御、漏洞代码、缓解措施以及如何测试。
在这起 Curve 事件上,Box 认为所有开发者得到的启示是:不要贪图追随技术潮流选择不成熟的方案;不要在不写测试用例的情况下就认可自己的代码(Vyper 出问题的几个版本上,甚至连测试用例都是错误的);永远不要自己批准自己的代码;有些财富,可能要数年才会被发现;不可升级是对自己的傲慢和对其他人的藐视。
通常开发人员也不会想到这里面有什么坑,随手就选一个版本编译,可能会忽略版本之间的区别所带来的风险。即使是小版本升级也可能引入重大变化,这一点在开发去中心化应用程序时尤其重要。
这起 Curve 事件对开发者的警示有:使用较新版本的编译器语言。保持最新的代码库、应用程序和操作系统非常重要,同时也要全方位搭建自身的安全防御机制。尽管新版本也可能引入新的安全问题,但已知的安全问题通常较旧版本要少。当然也要及时关注社区和官方的版本更新公告。了解每一个版本带来的变化,按需更新自己的代码库和运行环境。采取这些措施或许可以大大减少编译器错误导致的安全事件。
此外,要完善代码的单元测试用例。大部分编译器级别的错误会导致代码执行结果不一致, 这很难仅通过代码 review 发现, 但在测试中就可以暴露出来。提高代码覆盖率有助于避免这类问题。以及要尽量避免使用内联汇编、多维数组编解码等复杂语言特性,除非有明确需求。历史上大多数 Solidity 语言漏洞都与这些高级功能相关。在没有特殊需求的情况下,开发人员应该避免为了炫技而使用实验性语言特性。
对协议层以及安全人员而言,在进行代码审计时,也不能忽视编译器版本可能带来的风险。可以预见的是,黑客们已然打开了新思路,在未来一段时间里,更底层的漏洞被利用事件会越来越多。同时,作为更底层的基础设施,底层堆栈、编程语言、EVM 等更需要被好好审计。未来审计公司的市场会越来越大,白客赏金的市场也会越来越大。Vyper 团队也计划在事情正式结束梳理后,开启审查漏洞赏金的计划。
当然我们也不用对底层基础设施存在的风险过度恐慌。目前大多数编译器 Bug 仅在特定代码模式下触发,还需要根据项目情况具体评估实际影响。定期升级编译器版本、充分的单元测试都能帮助预防风险。
参考内容:
1.https://www.quicknode.com/guides/ethereum-development/smart-contracts/a-broad-overview-of-reentrancy-attacks-in-Solidity-contracts
2.https://github.com/pcaversaccio/reentrancy-attacks/issues/1
3.https://nipunp.medium.com/5-8-21-rari-capital-exploit-timeline-analysis-8beda31cbc1a
4.https://docs.Soliditylang.org/en/v0.8.21/