介绍Movetool:一个Move字节码反汇编工具

  • zellic
  • 发布于 2024-07-19 18:56
  • 阅读 19

本文介绍了Move语言的二进制格式和汇编语言,强调了智能合约审计人员需要了解此类低级编程的必要性。文章详细讨论了Move虚拟机模型、主要的类型规则以及Move的验证器,提供了实例和工具来简化Move汇编的编写过程。最后,文中展示了如何在Move沙箱中部署和测试模块,包括添加逻辑炸弹后门的示例。

这是针对真正程序员的 Move 教程。每个人都知道真正的程序员只用汇编语言来编写代码。在这种情况下,是 Move 字节码。

说回正题,智能合约审核员熟悉 Move 二进制格式和 Move 汇编语言是非常有用的,因为最近基于 Move 的链,如 Sui 和 Aptos,变得非常流行。在这篇博客中,我们将介绍 Move 二进制格式和 Move 汇编语言,以及一个可以简化编写 Move 汇编代码的工具。

在 Move 语言中,主要有两种类型的程序:模块和脚本。模块是永久部署的程序,旨在链上运行;脚本是临时程序,旨在由用户从链外运行。基于 Move 的链可能还有自己的自定义类型。总体而言,本讨论适用于这两种情况。然而,也有一些特定类型的细节。我们将讨论最一般的程序类型,即模块。

Move 二进制格式

Move 二进制格式是一个非常简单的格式。它以魔数和版本开始;接着是表Handle的列表,包含文件中表的位置和长度;最后是表本身,以及一个自引用索引(稍后解释)。可以把表想象成一系列原始二进制数据,不同于 ELF 节头。虽然可以轻松编写自定义工具解析它,但使用 Move 的 crate 来实现这个目的是更简单的↗

这些表包含可能通过(通常是 16 位)索引从其他表引用元素的条目列表。在反序列化时,检查表以确保每个字节在文件的表区域中恰好出现一次。文件中包含表的部分的每个字节必须属于恰好一个表,并且所有表必须是连续的。对于模块,描述该文件中的模块的模块Handle(所有在该文件中引用的模块都有模块Handle)的索引(自引用索引)位于文件的表之后。对此类值,将其放在文件开头(在文件头中)是有意义的,但它在文件的末尾。甚至 Move 开发者也认为这很不寻常。

// 加载模块索引(自引用 ID) - 位于二进制的末尾。为什么?

表之间的关系以及表的目的非常复杂(且繁琐),因此我们不会在这里完全描述它们。请参见 源代码↗ 获取完整列表和一些内联文档。在 Move 基于链使用的不同 Move 版本之间,二进制格式得到了相当好的保留。

Move 虚拟机模型

Move 虚拟机是一个基于栈的机器。与传统的机器架构不同,Move 虚拟机不是在字节级别操作,而是在对象级别操作。机器级对象是可以存储的最小数据单元。对象包括复杂数据类型,如结构体,以及原始数据类型,如 u256

进入函数时,有若干寄存器(“局部变量”)可用,栈的大小是有限的(由父链配置)。所有需要操作数的指令都要求先将操作数放到栈上。栈可以包含任何数据类型,但寄存器是有类型的,这意味着它们只能包含该类型的对象。每个函数声明可用的寄存器的数量和类型。进入函数时,前 x 个寄存器包含传给该函数的 x 个参数。因此,前 x 个寄存器的类型由函数的参数类型定义。

指令 栈之前 栈之后
调用 arg1, arg2, arg3, …, argn return_value
弹出 val
LdTrue True
注意:栈顶在右侧。

Move 字节码是高级的,Move 函数不是单态化的。Move 函数除了常规参数外,还接受类型参数。在使用类型参数时,必须确保类型参数符合函数的不变条件。

至于 Move 汇编,当前 源代码↗ 是最好的文档来源。

Move 验证器

在这个水平上工作的主要动机之一是审核验证器。编译器不会生成能破坏验证器的代码,除非编码错误。撇开这一点,也有必要理解验证器,以便编写能在 Move 链上运行的汇编代码。与大多数汇编语言不同,Move 字节码有一套非常严格的规则,必须遵循这些规则才能通过验证器。我们将在此详细解释这些规则。

在这一点上,有必要澄清我们正在使用核心 Move。然而,验证器在不同的 Move 链之间大致保持不变。有相当多的函数用作进入 Move 验证器的入口点。调用者可以传入参数,以确定施加的限制。不同的链倾向于使用不同的入口点和选项。在撰写本文时,核心 Move 中的所有此类选项是数值限制,仅用于防止资源枯竭攻击。总体而言,很难意外达到这些限制,因此我们将忽略它们。

验证器工作的最小单元是基本块。基本块是 Move 汇编代码的基本单元。它是一个不包含任何跳转或跳转目的地的连续 Move 字节码部分。然而,调用指令并不被视为跳转,尽管控制权转移。

验证器的主要要求如下:

  • 通常,模块本身存储的任何信息都不得重复。例如,这意味着标识符必须唯一,同名的函数只能定义一次,等等。
  • 任何被引用的内容必须存在。
  • 循环(例如,在数据类型中)通常是不允许的。递归函数是明确允许的。递归类型必须改用间接引用,例如在另一数据结构(例如,表)中的索引,用于其存放真实数据的地方。
  • 所有对数据的操作必须遵循类型规则。操作数必须是正确的类型,并具有正确的能力。
  • 每个基本块的末尾,栈必须为空。这意味着推送到栈的任何项目都必须在基本块结束之前被消费。每个基本块开始时栈总是为空。
  • 基本块的控制流图必须是 可约的↗。简化而言,这意味着每个循环必须有恰好一个入口。进入循环的所有边都必须通向入口。然而,循环仍然可以互相嵌套。可以将其视为无 goto 规则。
  • 引用必须遵循引用规则(见下文)。
  • 任何使用的 全局资源↗ 必须正确获取。
  • 任何被引用的局部变量在引用时必须确保被设置。这意味着可以将代码放入 SSA 形式↗。例如,可以有两个分支将局部变量设置为不同的值,但对局部变量的所有引用必须在所有代码路径中被设置之前。
  • 在函数结束时,局部变量中任何剩余的值都必须被丢弃。

引用规则

真正的引用规则是极为复杂的,并涉及到借用图的广泛讨论。我们在这里提供一个简化的版本。有关完整引用规则,请参见 源代码↗

  • 引用可以被复制,但引用的最旧副本被视为活动。当引用被销毁时,活动引用可能会改变。
  • 借对字段的引用被视为源借用的子引用。
  • 当引用被销毁时,指向该对象的下一个最活跃的借用继承其子引用。
  • 传入函数的任何可变引用必须是活动的,并且没有子引用。不可变引用可以在任何时候传入函数。

Movetool

了解了所有这些,编写 Move 汇编时,仍然需要手动将 Move 二进制格式输入到 Rust 代码中,并调用序列化器。为了使这个过程更容易,我们创建了 movetool↗。此工具允许将 Move 二进制模块反编译为自定义汇编格式,并将源代码组装回二进制模块。它目前存在一些小问题,但也能完成任务。

编写 Move 汇编的主要动机之一是审核 Move 验证器。为此,我们还包含了一种直接调用验证器并在工具中打印结果的方法。

示例:创建一个无效代码的二进制模块

Move 编译器通常不会生成无效果的代码。我们创建的模块将无法使用 Move 编译器创建。我们将编写一个简单的函数,添加两个数字并返回结果,还计算这两个数字的异或但丢弃该结果。

Move 模块涉及大量的样板代码,因此我们将从创建一个新的 Move 项目并进行修改开始。

move new addxor

我们在 sources/addxor.move 中编写以下代码。

module addxor::addxor {
    public fun add(a: u64, b: u64): u64 {
        a + b
    }
}

然后我们必须在 Move.toml 中设置 addxor 的地址,如下所示,以便项目能够成功构建。

[addresses]
addxor = "0x1337"

然后我们构建并反汇编它。

move build
movetool dis < build/addxor/bytecode_modules/addxor.mv > addxor.mvasm

我们在 addxor.mvasm 中得到以下输出。

.type module
.version 6
.self_module_handle_idx 0
.table module_handles
; address_idx identifier_idx
0 1
.endtable
.table struct_handles
; abiltiies,cdsk module_idx identifier_idx
.endtable
.table function_handles
; module_idx identifier_idx parameters_sig_idx return_sig_idx type_parameters...,cdsk
0 0 0 1
.endtable
.table field_handles
; struct_def_idx member_count
.endtable
.table friend_decls
; address_idx identifier_idx
.endtable
.table struct_def_instantiations
; struct_def_idx type_params_signature_idx
.endtable
.table function_instantiations
; function_handle_idx type_params_signature_idx
.endtable
.table field_instantiations
; field_handle_idx type_params_signature_idx
.endtable
.table signatures
; arrays of types, for specifics see source code
[u64, u64]
[u64]
[]
.endtable
.table identifiers
; literal string identifiers
add
addxor
.endtable
.table address_identifiers
; addresses in hex
00000000000000000000000000000000
00000000000000000000000000000000
.endtable
.table constant_pool
; type encoded_value_in_hex
.endtable
.table metadata
; key value
.endtable
.table struct_defs
; structs can be native or declared
.endtable
.table function_defs
; function_handle visibility_public_private_friend is_entry
.func 0 public false
; indices of struct definitions acquired by this function
.acquires
.locals 2
moveloc 0
moveloc 1
add
ret
.endfunc
.endtable

反汇编的模块大致遵循二进制模块的布局。我们关心的表是函数定义表,包含实际的字节码。从那里,我们可以看到参数的值被移动到栈上并相加。当返回指令运行时,返回值位于栈顶。

我们现在将添加代码来计算异或并丢弃结果。为此,我们必须复制参数,因为需要将参数保留以便进行加法计算。将以下代码添加到 .locals 2 行后。

copyloc 0
copyloc 1
xor
pop

然后我们组装代码并验证它。

movetool asm < addxor.mvasm > addxor.mv
movetool verify < addxor.mv

接下来,我们将其部署到 Move 沙箱中。首先,通过部署原始合约来设置 Move 沙箱。

move sandbox publish

Move 沙箱将其合约部署到“区块链”的“storage/”中,addxor.mv 存储在 storage/0x00000000000000000000000000001337/modules/addxor.mv 中。我们可以通过将二进制模块复制到相应位置来替换该合约。

cp addxor.mv storage/0x00000000000000000000000000001337/modules/addxor.mv

我们可以编写如下脚本并将其放入 sources/calladdxor.move 中以测试该程序。由于某种原因,std::debug 在 Move 沙箱中不起作用,因此我们将使用一个 abort 来检查返回值。

script {
    use addxor::addxor;
    fun main() {
        let x = addxor::add(1, 2);
        assert!(x != 3, 1);
    }
}

我们按以下方式运行它,发现确实会中止。

move sandbox run sources/calladdxor.move
执行在交易脚本中中止,代码为 1

示例:在后门中添加逻辑炸弹

现在让我们在 addxor 模块中添加一个后门,如果第一个参数是 1337 则中止。我们将函数的代码更改如下。

copyloc 0
copyloc 1
xor
pop
copyloc 0
ld64 1337
eq
br_true 12
moveloc 0
moveloc 1
add
ret
ld64 1338
abort

我们编译它并像之前一样将其加载到 Move 沙箱中。

movetool asm < addxor.mvasm > addxor.mv
movetool verify < addxor.mv
cp addxor.mv storage/0x00000000000000000000000000001337/modules/addxor.mv

然后,我们更改脚本,通过将第一个参数改为 1337 来触发后门。运行脚本将给出以下结果。

执行在模块 00000000000000000000000000001337::addxor 中中止,代码为 1338。

显然,我们使用 Move 汇编添加的后门被触发了。

继续前进

Move 的威胁格局不断演变。越来越多基于 Move 的链正在发布,每条链都有其独特的原生对象和潜在的新虚拟机功能。我们已经提供了关于 Move 的二进制格式、机器模型和验证器的介绍,以及一个让你开始的工具——但还有很多需要研究和探索的内容。是时候行动了!

关于我们

Zellic 专注于确保新兴技术的安全。我们的安全研究人员在最具价值的目标中发现了漏洞,从财富 500 强到去中心化金融巨头。

开发者、创始人和投资者信任我们的安全评估,快速、自信地发布,没有关键漏洞。凭借我们在现实世界攻防安全研究中的经验,我们发现了别人遗漏的内容。

与我们联系↗ 获取比其他地方更好的审计服务。真正的审计,而非走过场。

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

0 条评论

请先 登录 后评论
zellic
zellic
Security reviews and research that keep winners winning. https://www.zellic.io/