https://medium.com/@numencyberlabs/analysis-of-the-first-critical-0-day-vulnerability-of-aptos-move-vm-8c1fd6c2b98e
一、前言
Move 编程语言最近越来越受欢迎,因为它比以太坊的 Solidity 语言具有强大的优势。 Move 在很多知名项目中都有使用,例如 Aptos 和 Sui。最近,Numen Web3 安全性 漏洞检测产品在Aptos公链的虚拟机(VM)中发现了一个严重级别的安全漏洞。我们发现该语言中的漏洞可能导致 Aptos 节点崩溃并导致拒绝服务。在本文中,我们希望通过对该漏洞的解释,让您对Move语言及其安全性有更好的了解。作为Move语言安全研究的领先者,我们将继续为其生态安全做出持续贡献。
2. Move语言的重要概念
模块和脚本
Move 有两种不同类型的程序:模块和脚本。模块是定义结构类型和操作这些类型的函数的库。结构类型定义了 Move 的全局存储模式,模块函数定义了更新存储的规则。模块本身也存储在全局存储中。脚本是可执行文件的入口点,类似于传统语言中的 main 函数。脚本通常调用已发布模块的函数来更新全局存储。脚本是不在全局存储中发布的临时代码片段。一个 Move 源文件(或编译单元)可能包含多个模块和脚本。然而,发布模块或执行脚本使用单独的虚拟机 (VM) 操作。
对于熟悉操作系统的人来说,Move模块类似于系统可执行文件运行时加载的动态库模块,脚本类似于主程序。用户可以编写自己的脚本来访问全局存储,包括调用模块的代码。
全局存储
Move程序的目的是以树的形式读写全局存储。该程序无法访问文件系统、网络或该树之外的任何数据。
在伪代码中,全局存储如下所示:
在结构上,全局存储是一个森林,由以帐户地址为根的树组成。每个地址可以存储资源数据和模块代码。如上面的伪代码所示,每个地址最多可以存储一个给定类型的资源值和一个给定名称的模块。
MOVE虚拟机原理
movevm和evm虚拟机一样,都是把源代码编译成字节码,然后在虚拟机中执行。下图显示了该过程。
1.通过函数execute_script加载字节码
2、执行load_script函数,该函数主要用于反序列化字节码,并校验字节码是否合法,校验失败则返回失败
3.验证成功后,再执行真正的字节码代码
4.执行字节码,访问或修改全局存储的状态,包括资源、模块
注意:Move相关的特性还有很多,这里就不一一介绍了,我们会继续从安全的角度来分析Move语言的特性。
三、漏洞描述
该漏洞主要涉及验证模块。在说具体漏洞之前,先介绍一下验证模块和StackUsageVerifier::verify的功能。
验证模块
我们知道,在字节码代码真正执行之前,都会有对字节码的校验,而校验又可以细分为多个子流程分别进行。
他们是:
边界检查器 ,主要用于检查模块和脚本的边界安全。这包括检查签名、常量等的边界。
重复检查器 ,一个实现检查器以验证 CompiledModule 中的每个向量是否包含不同值的模块
签名检查器 ,当签名用于函数参数、局部变量和结构成员时,检查字段结构是否正确
指令一致性 , 验证指令一致性
常量 用于验证常量是否为原始类型以及常量的数据是否正确序列化为它们的类型
代码单元验证器 ,分别通过stack_usage_verifier.rs和abstract_interpreter.rs来验证函数体代码的正确性
脚本签名 , 验证脚本或入口函数是否为有效签名
该漏洞发生在验证过程中
CodeUnitVerifier::verify_script(配置,脚本)? ;
功能。可以看到这里有很多验证子流程。
它们是堆栈安全校验和、类型安全校验和、局部变量安全校验和和引用安全校验和。该漏洞出现在堆栈安全验证过程中。
堆栈安全验证(StackUsageVerifier::verify)
该模块用于验证一个函数的字节码指令序列中的基本块是否被均衡使用。每个基本块,除了那些以 Ret(返回调用者)操作码结尾的块,必须确保它离开块时堆栈高度与开始时相同。此外,对于任何基本块,堆栈高度不得低于块开头的堆栈高度。
循环遍历所有块,验证是否满足上述条件:
循环遍历以验证所有基本块的合法性。
漏洞详情
前面介绍过,由于movevm是一个栈虚拟机,在验证指令的合法性时,显然首先要保证指令字节码是正确的,其次要保证栈内存是正确的。块调用后合法,即堆栈在堆栈操作后平衡。验证块
函数用于实现第二个目的。
我们可以从验证块
代码,它会循环遍历该块代码块中的所有指令,然后通过加减法来验证指令块对栈的影响是否合法num_pops
,推数
.首先,通过stack_size_increment < num_pops
判断栈空间是否合法。如果num_pops
大于堆栈大小增量
,这意味着字节码弹出的数量大于堆栈本身的大小,并且返回错误并且字节码校验和失败。然后,通过stack_size_increment -= num_pops; stack_size_increment += num_pushes;
,这两条指令修改了每条指令执行后对栈高的影响。最后,当循环结束时,堆栈大小增量
需要等于0,即保留本块中的操作后,需要平衡堆栈。
这里好像没什么问题,但是因为在16行代码的执行中,没有判断是否存在整数溢出,导致整数溢出漏洞,可以通过构造一个大的num_pushes,stack_size_increment来间接控制.那么我们如何构建如此庞大数量的推送呢?
貌似没有问题,但是由于这里执行了第16行代码,所以并没有判断是否有整数溢出。结果,堆栈大小增量
可以通过构建一个超大的间接控制推数
,导致整数溢出漏洞。
这里首先需要介绍一下move字节码文件格式。
移动字节码文件格式
像 Windows PE 文件,或 linux ELF 文件,移动以 .mv 结尾的字节码文件,并且文件本身具有一定的格式。
首先是magic,值为A11CEB0B,其次是版本信息,以及表的个数,之后是表头,可以有很多个表。 table kind是表的类型,一共有0x10种(如图右侧所示),更多细节不妨查看move语言文档,接下来是表的偏移量,以及表的长度桌子。之后是表格内容,最后是Specific Data,有两种,对于module,就是Module Specific Data,对于script type,就是Script Specific Data。
构造恶意文件格式
这里我们在脚本中与Aptos进行交互,所以我们构造如下所示的文件格式,导致stack_size_increment溢出:
首先解释一下这个字节码文件的格式:
+0x00–0x03:是魔法字 0xA11CEB0B
+0x04–0x7: 是文件格式版本,它的版本是4
+0x8–0x8:为表计数,值为1
+0x9–0x9:是table kind,它的类型是SIGNATURES
+0xa-0xa:是表偏移量,值为0
+0xb-0xb: 表长,值为0x10
+0xc-0x18:是SIGNATURES Token的数据
从0x22开始,是脚本主要功能代码的代码部分。
通过move-disassembler工具,我们可以看到该指令的反汇编代码如下:
其中0、1、2这3条指令对应的代码分别是红框、绿框、黄框内的数据。
LdU64 与漏洞本身没有关系。这里就不多说了,有兴趣的可以去看看代码。这里重点讲解VecUnpack指令。 VecUnpack的作用是在代码中遇到vector对象时,将所有数据入栈。
在这个构造文件中,我们构造了两次VecUnpack,其向量的数量分别为3315214543476364830,18394158839224997406。
当函数指令效果
执行完毕,下面第二行代码才真正执行:
执行后指令效果
函数,它第一次返回 (1,3315214543476364830)。此时stack_size_increment为0,num_pops为1,num_pushes为3315214543476364830。第二次返回为(1,18394158839224997406)。再次执行时stack_size_increment += num_pushes ;
stack_size_increment 已经是 0x2e020210021e161d (3315214543476364829)。
num_pushes为0xff452e02021e161e(18394158839224997406),两者相加时大于u64的最大值,导致数据截断,stack_size_increment的值变为0x12d473012043c2c3b,导致整数溢出,从而导致Aptos节点崩溃,这反过来会导致节点停止运行。由于rust语言的安全特性,不会像C/C++那样对代码安全造成进一步的影响。
4. 漏洞影响
由于该漏洞出现在Move执行模块中,对于链上节点而言,如果执行字节码代码,将引发DoS攻击。严重时可导致 Aptos 网络完全停止,造成不可估量的损失,严重影响节点的稳定性。
5.官方修复
当我们发现这个漏洞后,就向Aptos官方团队反映,他们很快就修复了这个漏洞。您可以参考下图获取修复的屏幕截图。
相关代码链接如下: