对 Aptos Move 虚拟机首个关键零日漏洞的分析

本文分析了Aptos Move虚拟机的一个关键安全漏洞,详细介绍了Move语言的概念、结构及其在安全方面的内涵。文章深入探讨了漏洞的根本原因、可导致拒绝服务攻击的细节,及其潜在影响,同时还涵盖了漏洞修复的过程。由于内容结构清晰、逻辑合理且包含丰富的细节,文章让读者更好地理解了Move语言及其安全性。

Aptos Move VM 首个重大漏洞分析

1. 前言

Move 编程语言由于在多方面表现出对以太坊的 Solidity 语言的强大优势,近年来逐渐受到关注。Move 在许多知名项目中被使用,如 AptosSui。最近,Numen Web3 安全漏洞检测产品发现了 Aptos 公链虚拟机(VM)的一个重大安全漏洞。我们发现,该语言中的一个漏洞可能导致 Aptos 节点崩溃并引发拒绝服务(DoS)。在本文中,我们希望通过对该漏洞的解释,使你能更好地了解 Move 语言及其安全性。作为 Move 语言安全研究的领导者,我们将继续为其生态安全做出持续贡献。

2. Move 语言的重要概念

模块和脚本

Move 有两种不同类型的程序:模块和脚本。模块是定义结构类型和对这些类型进行操作的函数的库。结构类型定义了 Move 的全局存储模式,模块函数则定义了更新存储的规则。模块本身也存储在全局存储中。脚本作为可执行文件的入口点,类似于传统语言中的主函数。脚本通常调用已发布模块的函数以更新全局存储。脚本是临时的代码片段,不会发布到全局存储。一个 Move 源文件(或编译单元)可以包含多个模块和脚本。然而,发布模块或执行脚本使用的是不同的虚拟机(VM)操作。

对于熟悉操作系统的人来说,Move 模块类似于在系统可执行文件运行时加载的动态库模块,而脚本类似于主程序。用户可以编写自己的脚本以访问全局存储,包括调用模块的代码。

全局存储

Move 程序的目的是以树的形式读写全局存储。程序不能访问文件系统、网络或任何此树之外的数据。

在伪代码中,全局存储看起来像这样:

从结构上看,全局存储是一个森林,由以帐户地址为根的树组成。每个地址可以存储资源数据和模块代码。如上面的伪代码所示,每个地址最多可以存储一种类型的资源值和一种名称的模块。

MOVE 虚拟机原理

movevm 和 evm 虚拟机是相同的,它需要将源代码编译成字节码,然后在虚拟机中执行。下面的图表展示了这个过程。

  1. 通过函数 execute_script 加载字节码

  2. 执行 load_script 函数,该函数主要用于反序列化字节码,并验证字节码是否合法,如果验证失败,将返回失败

  3. 验证成功后,真正的字节码代码随后被执行

  4. 执行字节码,访问或修改全局存储的状态,包括资源、模块

注意:与 Move 相关的其他许多功能,本文将不一一介绍,我们将继续从安全的角度分析 Move 语言的特性。

3. 漏洞描述

此漏洞主要涉及验证模块。在讨论具体漏洞之前,将介绍验证模块的功能以及 StackUsageVerifier::verify。

验证模块

我们知道,在字节码代码的真实执行之前,会对字节码进行验证,而验证可以细分为多个子过程。

它们是:

BoundsChecker,主要用于检查模块和脚本的边界安全性,包括检查签名、常量等的边界。

DuplicationChecker,实现一个检查器,用来验证 CompiledModule 中每个矢量是否包含不同的值。

SignatureChecker,检查签名在功能参数、本地变量和结构成员时字段结构是否正确。

InstructionConsistency,验证指令的一致性。

Constants,用于验证常量是否为原始类型,以及常量数据是否正确地序列化为其类型。

CodeUnitVerifier,用于通过 stack_usage_verifier.rs 和 abstract_interpreter.rs 分别验证函数体代码的正确性。

_scriptsignature,验证脚本或入口函数的签名是否有效。

漏洞发生在验证过程中 CodeUnitVerifier::verify_script(config, script)? ; 函数中。可以看出,这里有许多验证子过程。

这些是堆栈安全检查、类型安全检查、本地变量安全检查和引用安全检查。漏洞出现在堆栈安全验证过程中。

堆栈安全验证(StackUsageVerifier::verify)

该模块用于验证函数的字节码指令序列中的基本块是否以平衡的方式使用。每个基本块,除了以 Ret (返回调用者) 操作码结束的基本块,必须确保它在离开时保持与开始时相同的堆栈高度。此外,对于任何基本块,堆栈高度不得低于块开始时的堆栈高度。

循环遍历所有块,以验证上述条件是否满足:

该循环遍历以验证所有基本块的合法性。

漏洞细节

如前所述,由于 movevm 是一个堆栈虚拟机,在验证指令的合法性时,显然首先需要确保指令字节码是正确的,其次需要确保在块调用后堆栈内存是合法的,即在堆栈操作后堆栈是平衡的。verify_block 函数用于实现第二个目的。

verify_block 代码中可以看出,它将循环遍历所有指令,并通过添加或减去 num_popsnum_pushes 验证指令块对堆栈的影响是否合法。首先,通过 stack_size_increment < num_pops 来判断堆栈空间是否合法。如果 num_pops 大于 stack_size_increment,则意味着字节码弹出数大于堆栈本身的大小,会返回错误且字节码检查失败。接着,通过 stack_size_increment -= num_pops; stack_size_increment += num_pushes;,这两条指令在每次执行指令后修改对堆栈高度的影响。最后,当循环结束时,stack_size_increment 需要等于 0,即经过该块的操作后,堆栈需要保持平衡。

看起来这里没有问题,但由于在执行 16 行代码时,并没有判断是否存在整数溢出,导致可通过构造大型 num_pushesstack_size_increment 间接控制整数溢出。那么我们该如何构造如此巨大的推送数字?

看似没有问题,但是由于执行的 16 行代码并未判断是否存在整数溢出,导致 stack_size_increment 可以通过构造过大的 num_pushes 间接控制,造成整数溢出漏洞。

在这里我们首先需要介绍 move 字节码文件格式。

Move 字节码文件格式

类似于 Windows PE 文件或 Linux ELF 文件,move 字节码文件以 .mv 结尾,并且文件本身具有一定的格式。

首先是魔数,值为 A11CEB0B,其次是版本信息,以及表的数量,之后是表头,可以有多个表。表的种类是表的类型,总共有 0x10 种(如图右侧所示),欲了解更多细节,请查阅 Move 语言文档。接下来是表的偏移量和长度。最后是表的内容,最后是特定数据,有两种类型,对于模块,它是模块特定数据,对于脚本类型,它是脚本特定数据。

构造恶意文件格式

在此,我们与 Aptos 进行脚本交互,因此我们构造如下文件格式以引发 stack_size_increment 溢出:

首先,让我们解释一下该字节码文件的格式:

+0x00–0x03: 是魔数 0xA11CEB0B

+0x04–0x07: 是文件格式版本,其版本为 4

+0x08–0x08: 是表计数,值为 1

+0x09–0x09: 是表种类,其类型为 SIGNATURES

+0x0a–0x0a: 是表偏移量,值为 0

+0x0b–0x0b: 是表长度,值为 0x10

+0x0c–0x18: 是 SIGNATURES Token 的数据

从 0x22 开始是脚本的主函数代码部分。

通过 move-disassembler 工具,我们可以看到指令的反汇编代码如下:

其中,指令 0、1 和 2 对应的代码分别是红框、绿框和黄框中的数据。

LdU64 与漏洞本身无关,我们不会详述,但若你感兴趣,可以查看相关代码。这里我们重点解释 VecUnpack 指令。VecUnpack 的作用是在代码中遇到矢量对象时,将所有数据推送到堆栈。

/// 销毁矢量并将静态已知数量的元素卸载到堆栈上。如果矢量的长度不是 N,则会中止。
///
/// 堆栈转换:
/// 
/// ```..., vec[e1, e2, ..., eN] -> ..., e1, e2, ..., eN```
VecUnpack(SignatureIndex, u64),

在这个构造的文件中,我们对 VecUnpack 的调用进行了两次,其矢量数量分别为 3315214543476364830,18394158839224997406。

当函数 instruction_effect 被执行时,以下第二行代码实际上被执行:

在第一次执行 instruction_effect 函数时,返回 (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 团队,他们迅速修复了漏洞。你可以参考下面的图示,查看修复的截图。

相关代码链接如下:

[serializer] 修复序列化中的问题 · move-language/move@566ace5\ \ 通过在 GitHub 上创建账户为 move-language/move 开发做出贡献。\ \ github.com

  • 原文链接: medium.com/numen-cyber-l...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
numencyberlabs
numencyberlabs
江湖只有他的大名,没有他的介绍。