Curve遭遇的漏洞利用或许为黑客们开辟了新的思路。

Curve的漏洞或许为黑客们开辟了新的思路。

Curve事件:对DeFi安全的警示

随着一场漏洞利用事件的发生,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行业潜在的巨大风险。

Curve事件是怎么发生的?

Curve遭遇的漏洞利用事件是最古老、也许是最常见的以太坊智能合约攻击形式之一——重入攻击。重入攻击允许攻击者反复调用智能合约的某一函数,而不等该函数的前一个调用完成。这样,攻击者就可以不断利用漏洞提款,直至受害合约资金耗尽。

举个例子来说明重入攻击的原理:假设一家银行总共拥有10万现金,但这家银行有一个漏洞,每当人们取钱时,银行工作人员并不立即更新账户余额,而是等到一天结束时才进行核对和更新。这时有人发现了这个漏洞,他在银行开了一个账户,先存入1000元,然后取出1000元,在过5分钟再取出1000元。由于银行没有实时更新余额,系统会在进行核对和更新前认为他账户还有1000元。通过反复操作,最终该用户取出了银行里全部的10万美元现金。直到一天结束时银行才发现被利用了这个漏洞。

重入攻击之所以复杂,是因为它利用了合约之间相互调用的特性,以及合约自身的逻辑漏洞,通过故意触发异常和回退功能来实现欺诈行为。攻击者可以反复利用合约的逻辑漏洞,窃取资金。为了防止重入攻击,可以提前设置一段针对性的特殊代码内容进行防护,用这样的保护机制来确保资金安全,这被称为重入锁。

Solidity为智能合约编程设定了一个”CEI原则”(Check Effects Interactions),能够很好地保护函数免受重入攻击。CEI原则包括:

  1. 函数组件的调用顺序应该是:首先是检查,其次是对状态变量的影响,最后是与外部实体的交互。
  2. 在与外部实体交互之前,应该先更新所有状态变量。这被称为”乐观记账”,即在交互真正发生之前就将影响写入。
  3. 检查应在函数开始处进行,以确保调用实体有调用该函数的权限。
  4. 状态变量应在任何外部调用之前更新,以防重入攻击。
  5. 即使是可信的外部实体,也应该遵循CEI模式,因为它们可能会将控制流转移给恶意的第三方。

根据文档的说法,CEI原则有助于限制合约的攻击面,特别是防止重入攻击。在开发智能合约时,可以很容易地应用CEI原则,主要就是按照功能代码的顺序进行编写,不需要改变任何逻辑。正是因为无视CEI原则,以太坊的The DAO漏洞利用事件才给整个行业带来了巨大的破坏。

但被攻击的Curve池并没有遵循CEI原则,原因是Curve采用的是Vyper编译器。作为编译器的Vyper代码存在漏洞,导致重入锁失效,使得黑客的重入攻击成功实现了。

Vyper重入锁为什么会失效?

在这次攻击事件中,Vyper的问题究竟出在哪里?重入锁为什么会失效呢?是因为没有进行测试吗?笔者采访了智能合约开发者Box和BTX研究员Derek。

根据Box的透露,Vyper重入锁是经过用例测试的,但失效的原因是测试用例是结果导向,也就是测试用例也是错的。换句话说,重入锁失效的最大原因是编写测试用例的人是按照结果编写的测试用例,而没有思考过为什么slot(槽位)会莫名其妙跳过1。

从Box分享的Vyper代码中可以明显看出问题。当锁名称第二次出现时,storage_slot的数量会被覆盖,也就是说,在返回结果中,第一次获取锁的slot是0,但是再次有函数使用锁后,锁的slot被加一。编译后使用错误的slot,导致重入锁无法生效。

Box在与采访中表示:“测试的结果错误,当然验证不出错误。举个简单的例子,现在我们做计算题,1 + 1 = 2,但给定的标准答案错了,说1 + 1 = 3。而这时做题的同学答错了,回答了1 + 1 = 3,但却和提前给定的标准答案相同,程序就自然没办法判定出测试结果出错。”

悬而未决的“达摩克利斯之剑”

在有记录以来的第一次重入攻击事件中,攻击者通过韦斯攻击(WETH Attack)故意制造攻击,以引起开发者对重入攻击的重视,为的是避免更多项目受到重入攻击的可能性。在智能合约的情境下,开发者应该采用不同的触发机制,例如调用某个状态改变函数来实现保护。这就要求开发人员在设计合约时充分考虑可能的攻击场景,采取适当的预防措施。

为了更好地了解Vyper编译器,笔者采访了BTX研究员Derek,他表示,对于熟悉Python的开发人员来说,Vyper是比Solidity更理想的选择,UI界面更舒服,上手更快。但显然,一些版本的Vyper编译器代码并没有经过可靠的第三方审计。甚至有的审计工作,可能是开发者自己完成的。Derek表示:“传统IT行业不会发生这种事,因为一个新的语言出来后,会有无数的审计公司往死里找你的漏洞。”

但目前Vyper编译器并没有像大家想象的那样接受过严谨的审核或审计,这使得很多编译器遇到重大且频繁的更改,不利于审核工作。即使有完整的代码库审核,随后添加的版本也使得审核变得困难。因此,审核编译器并不是一个很好的方式,更好的方式是审核最终用户使用该工具生成的最终产品(即原始EVM代码)。此外,缺乏发现编译器关键漏洞的动力也是一个问题。Derek之前提出的通过添加由用户共同赞助的赏金计划来改善Vyper的提案并未通过。

黑客们正在回归“第一性原理”

这次Curve事件给开发者们敲响了警钟。要保护协议和项目,合约安全开发实践是至关重要的。但最重要的是,我们所有人都应该意识到,底层编译器的安全问题被严重忽略了。回归“第一性原理”的黑客们在更底层的编译器上找到了一个完美的切入口。

Aave及Lens的创始人Stani在社交媒体上表达了他对Curve被攻击事件的感想,称这是对整个DeFi行业的不幸挫折。他指出,尽管DeFi是一个可以做出贡献的开放空间,但要做到绝对正确是很困难的,而且风险很高。在Curve的案例中,他们在协议级别上做得对。

对于编译器的漏洞而言,仅通过对合约源码逻辑的审计是很难发现的。需要结合特定编译器版本与特定的代码模式共同分析,才能确定智能合约是否受编译器漏洞的影响。

“目前只有两个编译器最佳,Vyper的代码库更小,更容易阅读,对其历史进行分析的更改也更少,这可能就是黑客从这里下手的原因,Solidity的代码库要大一些。”一些人甚至怀疑国家支持的黑客可能参与了这起Curve攻击事件。

作为加密行业使用最广泛的编译语言,Solidity的安全更是备受关注。根据Solidity开发团队定期发布的安全预警,多个不同版本的Solidity编译器都曾存在过安全漏洞。

幸运的是,正是因为Solidity语言的广泛应用,以太坊基金会在背后提供了支持,许多已知的问题在项目和协议部署过程中就被指出。因此,Solidity比Vyper更快地进行了修改和改进,这也使得Solidity更规范、更安全。

为了帮助Solidity开发者进行更好的测试和防止出现相同的问题,UnitasProtocol的联合创始人SunSec在Curve被攻击事件后发布了一份Solidity安全测试指南,支持47种漏洞,其中包括漏洞描述、场景、防御、漏洞代码、缓解措施以及如何测试。

如何尽可能避免底层攻击?

在这次Curve事件中,我们可以得到的启示是:不要贪图追随技术潮流选择不成熟的方案;不要在不写测试用例的情况下就认可自己的代码(Vyper的几个出问题的版本上,甚至连测试用例都是错误的);永远不要自己批准自己的代码;有些财富,可能要数年才会被发现;不可升级是对自己的傲慢和对其他人的藐视。

同时,开发人员也应该注意,随手选择一个版本编译可能会忽略版本之间的区别所带来的风险。即使是小版本的升级也可能引入重大变化,在开发去中心化应用程序时尤为重要。

为了尽可能地避免底层攻击,开发者们可以采取以下措施:

  • 使用较新版本的编译器语言。
  • 保持最新的代码库、应用程序和操作系统以及全方位搭建自身的安全防御机制。
  • 及时关注社区和官方的版本更新公告,了解每一个版本带来的变化,按需更新自己的代码库和运行环境。
  • 完善代码的单元测试用例,提高代码覆盖率,以避免执行结果不一致的问题。
  • 尽量避免使用复杂的语言特性,除非有明确需求。开发者应该避免为了炫技而使用实验性语言特性。
  • 对代码审计人员来说,在进行审计时,不能忽视编译器版本可能带来的风险。

可以预见的是,黑客们已经开启了新的思路,在未来的一段时间里,更底层的漏洞被利用的事件会越来越多。作为更底层的基础设施,底层堆栈、编程语言、EVM等更需要被审计。未来,审计公司的市场会越来越大,赏金计划也会越来越丰厚。Vyper团队也计划在事件正式结束后,开启审计漏洞赏金计划。

当然,我们也不必对底层基础设施的风险过度恐慌。目前绝大多数编译器bug仅在特定的代码模式下才会触发,因此需要根据项目情况具体评估实际影响。定期升级编译器版本、充分的单元测试都能够帮助预防风险。