CertiK的Skyfall团队最近在Aptos、StarCoin和Sui等多个区块链中发现了基于Rust的RPC节点的多个漏洞。由于RPC节点是连接dApp和底层区块链的关键基础设施组件,其稳健性对于无缝操作至关重要。区块链设计者都知道稳定RPC服务的重要性,因此他们采用Rust等内存安全语言来规避可能破坏RPC节点的常见漏洞。
采用内存安全语言(如Rust)有助于RPC节点避免许多基于内存破坏漏洞的攻击。然而,通过最近的审计,我们发现即使是内存安全的Rust实现,如果没有经过精心设计和审查,也很容易受到某些安全威胁的影响,从而破坏RPC服务的可用性。
本文我们将通过实际案例介绍我们对一系列漏洞的发现。
区块链RPC节点作用
区块链的远程过程调用(RPC)服务是Layer 1区块链的核心基础设施组件。它为用户提供重要的API前端,并作为通向后端区块链网络的网关。然而,区块链RPC服务与传统的RPC服务不同,它方便用户交互无需身份验证。服务的持续可用性至关重要,任何服务中断都会严重影响底层区块链的可用性。
审计角度:传统RPC服务器 VS 区块链RPC服务器
对传统RPC服务器的审计主要集中在输入验证、授权/认证、跨站请求伪造/服务器端请求伪造(CSRF/SSRF)、注入漏洞(如SQL注入、命令注入)和信息泄露等方面进检查。
然而,区块链RPC服务器的情况有所不同。只要交易是签名的,就不需要在RPC层对发起请求的客户端进行身份验证。作为区块链的前端,RPC服务的一个主要目标是保证其可用性。如果它失效,用户就无法与区块链交互,从而阻碍查询链上数据、提交交易或发布合约功能。
因此,区块链RPC服务器最脆弱的方面是“可用性”。如果服务器宕机,用户就失去了与区块链交互的能力。更严重的是,一些攻击会在链上扩散,影响大量节点,甚至导致整个网络瘫痪。
为何新区块链会采用内存安全的RPC
一些著名的Layer 1区块链,如Aptos和Sui,使用内存安全编程语言Rust实现其RPC服务。得益于其强大的安全性和编译时严格的检查,Rust几乎可以使程序免受内存破坏漏洞的影响,如堆栈溢出、和空指针解引用和释放后重引用等漏洞。
Custodia Bank CEO:行业不良行为和监管机构不作为导致加密货币崩溃:7月2日消息,Custodia Bank首席执行官Caitlin Long表示,加密货币的崩溃是可以预见的。自2018年以来,比特币和加密货币中的高杠杆迹象就一直存在,她希望这一教训得到重视,但监管机构仍应受到指责,因为他们没有更快地打击不良行为,没有批准该行业的优秀公司和产品。
Long表示,灰度比特币信托(GBTC)是美国证券交易委员会(SEC)批准的为数不多的基金之一,投资者可以通过他们的经纪账户来获得比特币敞口,而不需要购买、存储或保护他们的比特币,最终“带来了大量对冲基金的资金,然而却对行业造成了严重破坏,真正摧毁了价值。”
Long进一步解释道,在这种情况下,由于供需失衡,GBTC的交易价格远高于比特币的市场价格,就像一个引入了大量杠杆的对冲基金。散户投资者纷纷买入,但也只是维持到SEC批准竞品之前。一旦竞争逐渐进入,像GBTC这样的封闭基金就会发生转变,开始以低于其资产净值的价格进行交易。(CoinDesk)[2022/7/2 1:46:17]
为了进一步确保代码库的安全,开发人员需严格遵循最佳实践,例如不引入不安全代码。在源代码中使用#![forbid(unsafe_code)]确保阻拦过滤不安全的代码。
区块链开发者执行Rust编程实践的例子
为了防止整数溢出,开发人员通常使用checked_add、checked_sub、saturating_add、saturating_sub等函数,而不是简单的加法和减法(+、-)。通过设置适当的超时、请求大小限制和请求项数限制来缓解资源耗尽。
Layer 1区块链中内存安全RPC威胁
尽管不存在传统意义上内存不安全的漏洞,但RPC节点会暴露在攻击者容易操纵的输入中。在内存安全RPC实现中,有几种情况会导致拒绝服务。例如,内存放大可能会耗尽服务的内存,而逻辑问题可能会引入无限循环。此外,竞态条件也可能构成威胁,并发操作可能会出现意外的事件序列,从而使系统处于未定义的状态。此外,管理不当的依赖关系和第三方库可能会给系统带来未知漏洞。
Swan Bitcoin首席执行官:Celsius流动性危机或是加密借贷领域更广泛崩溃的开始:6月24日消息,Swan Bitcoin 首席执行官 Cory Klippsten 在接受采访时表示,涉及Celsius 网络的流动性危机可能只是加密借贷领域更广泛崩溃的开始。 Klippsten表示,他们的贷款账簿是不透明的,他们的活动是不透明的,你的风险补偿不足。因此,不管你是一个了不起的CeFi借贷平台,把这些零售存款从后端借出并给他们带来收益,或者一个糟糕的平台,它们都会被拖累。
此前报道,Celsius 本月早些时候以“极端的市场条件”为由停止了提款,不久之后,Babel Finance 和三箭资本在内的其他加密公司都遇到了流动性问题。[2022/6/24 1:27:46]
在这篇文章中,我们的目的是让人们关注可以触发 Rust 运行时保护的更直接的方式,从而导致服务自行中止。
显式的Rust Panic:一种直接终止RPC 服务的方法
开发人员可以有意或无意地引入显式panic代码。这些代码主要用于处理意外或异常情况。一些常见的例子包括:
assert!():当必须满足一个条件时使用该macro。如果断言的条件失败,程序将 panic,表明代码中存在严重错误。
panic!():当程序遇到无法恢复的错误且无法继续执行时调用该函数。
unreachable!():当一段代码不应该被执行时使用该macro。如果该macro被调用,则表示存在严重的逻辑错误。
unimplemented!()和todo!():这些宏是尚未实现功能的占位符。如果达到该值,程序将崩溃。
unwrap():该方法用于Option或Result类型,当遇到Err变量或None时会导致程序宕机。
漏洞一:触发Move Verifier中的assert!
Aptos区块链采用Move字节码验证器,通过对字节码的抽象解释进行引用安全分析。execute()函数是TransferFunctions trait实现的一部分,模拟基本块中字节码指令的执行。
Cardano创始人:狗狗币泡沫必将崩溃,会给散户带来巨大损失:Cardano创始人Charles Hoskinson警告称,随着DOGE价格飙升,泡沫无疑正在形成,很快许多人将损失大量资金。他表示:“在加密行业里,DOGE一直是一个圈子里的笑话,一个有趣而轻松的东西。它似乎一直存在,但在很大程度上,我们从未认真对待过它。最近,由于马斯克的努力带货和鲸鱼对市场的操纵,DOGE价格已经变得非常高。让我们明确一点——这是一个泡沫。DOGE的上涨是不可持续的,它将崩溃,大量散户资金将很快损失。我认为,在泡沫破裂后,它将成为监管者和立法者介入加密行业、损害整个行业的催化剂。这不是一件好事。DOGE没有一个稳定的开发团队。也没有独创的技术。在必然来临的泡沫破裂之后,国会将展开调查。美国SEC将四处奔走。各种各样的监管机构会到处说,‘这证明加密货币无法控制自己,我们需要进来救你。’”[2021/4/17 20:31:14]
函数execute_inner()的任务是解释当前字节码指令并相应地更新状态。如果我们已经执行到基本块中的最后一条指令,如index == last_index所示,函数将调用assert!(self.stack.is_empty())以确保栈为空。此行为背后的意图是保证所有操作都是平衡的,这也意味着每次入栈都有相应的出栈。
在正常的执行流程中,栈在抽象解释过程中始终是平衡的。堆栈平衡检查器(Stack Balance Checker)保证了这一点,它在解释之前对字节码进行了验证。然而,一旦我们将视角扩展到抽象解释器的范围,就会发现堆栈平衡假设并不总是有效的。
AbstractInterpreter中analyze_function漏洞的补丁程序
火币大学方军:不能因为比特币在全球金融崩溃时大跌,就否定它的避险资产地位:3月24日21:00,在火币大学直播大课上,火币大学顾问合伙人方军以“金融风暴下重新认识区块链与区块链创业机遇”为主题进行直播,方军表示,避险资产并不会在全球金融危机时大涨,避险资产只是在所有资产配置中的一项,并且是相对收益较高的一项。但这并不意味着万无一失,金融市场有自己的规律和法则,万无一失的资产是违背金融市场规律的,所以,不能因为比特币在全球金融崩溃时大跌,就否定它的避险资产地位。详情点击原文链接。[2020/3/25]
抽象解释器的核心是在基本块级别中模拟字节码。在其最初的实现中,在execute_block过程中,遇到错误会提示分析过程记录错误,并继续执行控制流图中的下一个块。这可能会造成一种情况:执行块中的错误会导致堆栈不平衡。如果在这种情况下继续执行,就会在堆栈不为空的情况下进行assert!检查,从而引发panic。
这就使得攻击者有机可趁。攻击者可通过在execute_block()中设计特定的字节码来触发错误,随后execute()有可能在堆栈不为空的情况下执行assert,从而导致assert检查失败。这将进一步导致panic并终止RPC服务,从而影响其可用性。
为防止出现这种情况,已实施的修复中,确保了在execute_block函数首次出现错误时会停止整个分析过程,进而避免了因错误导致堆栈不平衡而继续分析时可能发生的后续崩溃风险。这一修改消除了可能引起panic的情况,并有助于提高抽象解释器的健壮性和安全性。
漏洞二:触发StarCoin中的panic!
Starcoin区块链有自己的Move实现分叉。在这个Move repo中,Struct类型的构造函数中存在一个panic! 如果提供的StructDefinition拥有 Native 字段信息,就会显式触发这个panic!。
动态 | 比特币的崩溃并不是数字货币的终结:据The FT View报道,今年11月,比特币价格自2017年10月以来首次低于5000美元,加密货币批判者认为这是法币的胜利,然而加密货币推动了银行和政府得出结论——货币必须变得更加数字化。关于比特币是作为交易媒介还是投机资产的矛盾一直存在,它或许已经无法成为未来的货币,但由比特币引发的关于数字货币潜力的争论是有价值的。[2018/11/23]
规范化例程中初始化结构体的显式panic
这种潜在风险存在于重新发布模块的过程中。如果被发布的模块已经存在于数据存储中,则需要对现有模块和攻击者控制的输入模块进行模块规范化处理。在这个过程中,"normalized::Module::new "函数会从攻击者控制的输入模块中构建模块结构,从而触发 "panic!"。
规范化例程的前提条件
通过从客户端提交特制的有效载荷,可以触发该panic。因此,恶意行为者可以破坏RPC服务的可用性。
结构初始化panic补丁
Starcoin的补丁引入了一个新的行为来处理Native情况。现在,它不会引起panic,而是返回一个空的ec。这减少了用户提交数据引起panic的可能性。
隐式 Rust Panic: 一种容易被忽视的终止RPC服务的方法
显式 panic 在源代码中很容易识别,而隐式panic则更可能被开发人员忽略。隐式panic通常发生在使用标准或第三方库提供的API时。开发人员需要彻底阅读和理解API文档,否则他们的Rust程序可能会意外停止。
BTreeMap 中的隐式 panic
让我们以Rust STD中的BTreeMap为例。BTreeMap是一种常用的数据结构,它以排序的二叉树形式组织键值对。BTreeMap提供了两种通过键值检索值的方法:get(&self, key: &Q)和index(&self, key: &Q)。
方法get(&self, key: &Q)使用键检索值并返回一个Option。Option可以是Some(&V),如果key存在,则返回值的引用,如果在BTreeMap中没有找到key,则返回None。
另一方面,index(&self, key: &Q)直接返回键对应的值的引用。然而,它有一个很大的风险:如果键不存在于BTreeMap中,它会触发隐式panic。如果处理不当,程序可能会意外崩溃,从而成为一个潜在漏洞。
事实上,index(&self, key: &Q)方法是std::ops::Index trait的底层实现。该特质为不可变上下文中的索引操作(即 containerfmjd[index])提供了方便的语法糖。开发者可以直接使用 btree_map[key],调用 index(&self, key: &Q) 方法。然而,他们可能会忽略这样一个事实:如果找不到key,这种用法可能会触发panic,从而对程序的稳定性造成隐性威胁。
漏洞三:在Sui RPC中触发隐式panic
Sui模块发布例程允许用户通过RPC提交模块有效载荷。在将请求转发给后端验证网络进行字节码验证之前,RPC处理程序使用SuiCommand::Publish函数直接反汇编接收到的模块。
在这个反汇编过程中,提交模块中的code_unit部分被用来构建一个VMControlFlowGraph。该构建过程包括创建基本块,这些块存储在一个名为 "'blocks' "的BTreeMap中。该过程包括创建和操作该Map,在某些条件下,隐式panic会在这里触发。
下面是一段简化的代码:
创建VMControlFlowGraph时的隐式panic
在该代码中,通过遍历代码并为每个代码单元创建一个新的基本块来创建一个新的VMControlFlowGraph。基本块存储在一个名为block的BTreeMap中。
在对堆栈进行迭代的循环中,使用block[&block]对块图进行索引,堆栈已经用ENTRY_BLOCK_ID进行了初始化。这里的假设是,在block映射中至少存在一个ENTRY_BLOCK_ID。
然而,这一假设并不总是成立的。例如,如果提交的代码是空的,那么在“创建基本块”过程之后,“块映射”仍然是空的。当代码稍后尝试使用&blocks[&block].successors中的for succ遍历块映射时,如果未找到key,可能会引起隐式panic。这是因为blocks[&block]表达式本质上是对index()方法的调用,如前所述,如果键不存在于BTreeMap中,index()方法将导致panic。
拥有远程访问权限的攻击者可以通过提交带有空code_unit字段的畸形模块有效载荷来利用该函数的漏洞。这个简单的RPC请求会导致整个JSON-RPC进程崩溃。如果攻击者以最小的代价持续发送此类畸形有效载荷,就会导致服务持续中断。在区块链网络中,这意味着网络可能无法确认新的交易,从而导致拒绝服务(DoS)情况。网络功能和用户对系统的信任将受到严重影响。
Sui的修复:从RPC发布例程中移除反汇编功能
值得注意的是,Move Bytecode Verifier中的CodeUnitVerifier负责确保code_unit部分绝不为空。然而,操作顺序使RPC处理程序暴露于潜在的漏洞中。这是因为验证过程是在Validator节点上进行的,而该节点是在RPC处理输入模块之后的一个阶段。
针对这一问题,Sui通过移除模块发布RPC例程中的反汇编功能来解决该漏洞。这是防止RPC服务处理潜在危险、未经验证的字节码的有效方法。
此外,值得注意的是,与对象查询相关的其他RPC方法也包含反汇编功能,但它们不容易受到使用空代码单元的攻击。这是因为它们总是在查询和反汇编现有的已发布模块。已发布的模块必须已经过验证,因此,在构建VMControlFlowGraph时,非空代码单元的假设始终成立。
对开发人员的建议
在了解了显式和隐式panic对区块链中RPC服务稳定性的威胁后,开发人员必须掌握预防或降低这些风险的策略。这些策略可以降低服务意外中断的可能性,提高系统的弹性。因此CertiK的专家团队提出以下建议,并作为Rust编程的最佳实践为大家列出。
Rust Panic Abstraction: 尽可能考虑使用Rust的catch_unwind函数来捕获panic,并将其转换为错误信息。这可以防止整个程序崩溃,并允许开发人员以可控的方式处理错误。
谨慎使用API:隐式panic通常是由于滥用标准或第三方库提供的API而发生的。因此,充分理解API并学会适当处理潜在错误至关重要。开发人员要始终假设API可能会失效,并为这种情况做好准备。
适当的错误处理:使用Result和Option类型进行错误处理,而非求助于panic。前者提供了一种更可控的方式来处理错误和特殊情况。
添加文档和注释:确保代码文档齐全,并在关键部分(包括可能发生panic的部分)添加注释。这将帮助其他开发人员了解潜在风险并有效处理。
总结
基于Rust的RPC节点在Aptos、StarCoin和Sui等区块链系统中扮演着重要的角色。由于它们用于连接DApp和底层区块链,因此它们的可靠性对于区块链系统的平稳运行至关重要。尽管这些系统使用的是内存安全语言Rust,但仍然存在设计不当的风险。CertiK的研究团队通过现实世界中的例子探讨了这些风险,也足以证明了内存安全编程中需要谨慎和细致的设计。
CertiK中文社区
企业专栏
阅读更多
金色财经
金色荐读
Block unicorn
区块链骑士
金色财经 善欧巴
Foresight News
深潮TechFlow
郑重声明: 本文版权归原作者所有, 转载文章仅为传播更多信息之目的, 如作者信息标记有误, 请第一时间联系我们修改或删除, 多谢。