Wasm逆向分析

Published: 2023年11月11日

In Reverse.

首先,这个东西虽然叫ASM但实际上只是个非常非常简单的计算机,它本身只能做纯运算,和外部的一切交互都得在模块初始化时给引入,从风控的角度看它没太大逆向的必要,不过前提还是先了解清楚它!所以下面先详细介绍它的语法,文件格式,指令集,再说怎么逆向~

语法

首先,它通常作为一个编译目标,即我们不会直接写WASM的汇编,而是用go/rust/c/cpp甚至JS开发,再由编译器(如emscripten / assemblyscript /golang/...)将其转换为wasm指令,当然了它执行的字节码有对应的汇编表示,即Wat(WebAssembly Text Format),Wat采用S表达式(S-expression)形式,代码里就暗含了语法树结构,这种格式更容易理解,不过它在汇编时会先被转换为flat-wat形式,即我们反汇编时看到的样子。

一些概念

1.Module: 每个wasm文件有且仅有一个模块,它实现一组特定的功能,模块是wasm的代码单元

2.import:wasm本身纯计算,要调外部的功能必须由import指定

3.export:wasm本身实现的功能或变量/内存可被导出给其他模块或外部环境使用

4.instance:模块需要先被实例化再使用,实例化时需要传入import,并且它会导出export

一个然破

(module
  (; 这是一个块注释,如此处所示。 ;)

  ;; =================================
  ;; 1. 导入 (Imports)
  ;; =================================
  ;; 从 "env" 模块导入功能。
  (import "env" "log" (func $log (param i32 i32))) ;; 导入日志函数,接收 (指针, 长度)
  (import "env" "config" (global $config i32))      ;; 导入一个只读的 i32 全局配置变量

  ;; =================================
  ;; 2. 内存与数据段 (Memory & Data Segment)
  ;; =================================
  (memory (export "memory") 1) ;; 定义并导出一个1页(64KB)的内存
  (data (i32.const 0) "Module Initialized.") ;; 在内存地址0处预置一个字符串

  ;; =================================
  ;; 3. 全局变量 (Globals)
  ;; =================================
  ;; 定义并导出一个可变的全局变量,用于任务计数
  (global $task_count (export "task_count") (mut i32) (i32.const 0))

  ;; =================================
  ;; 4. 表 (Table for Function Pointers)
  ;; =================================
  (table (export "op_table") 2 funcref) ;; 定义并导出一个大小为2的函数引用表
  (elem (i32.const 0) $add_op $div_op)   ;; 初始化表元素,索引0是$add_op,索引1是$div_op

  ;; =================================
  ;; 5. 类型定义 (Type Definitions)
  ;; =================================
  ;; 为函数签名定义一个类型,用于 call_indirect 和函数声明
  (type $binary_op (func (param i32 i32) (result i32)))

  ;; =================================
  ;; 6. 函数 (Functions)
  ;; =================================

  ;; 内部操作函数1:加法
  (func $add_op (type $binary_op) ;; 使用已定义的类型
    local.get 0 ;; 获取第一个参数
    local.get 1 ;; 获取第二个参数
    i32.add     ;; 栈机操作:弹出两个值,相加,然后压入结果
  )

  ;; 内部操作函数2:有符号除法
  (func $div_op (param $a i32) (param $b i32) (result i32)
    (i32.div_s (local.get $a) (local.get $b)) ;; 使用折叠格式 (folded form)
  )

  ;; 主处理函数,将导出
  (func (export "process_task")
    (param $a i32) (param $b i32) (param $op_index i32) (param $out_ptr i32)
    (result i32)

    ;; 定义一个局部变量并观察其零初始化
    (local $temp_val i32)

    ;; 调用导入的 log 函数,打印初始化信息
    ;; (i32.const 0) 是 "Module Initialized." 的地址
    ;; (i32.const) 是其长度
    (call $log (i32.const 0) (i32.const))

    ;; 更新全局任务计数器
    (global.set $task_count
      (i32.add
        (global.get $task_count)
        (i32.const 1)
      )
    )

    ;; 使用 block 来返回值
    (block $result_block (result i32)
      ;; 控制语句:检查除数是否为0
      (if (i32.and (i32.eq (local.get $op_index) (i32.const 1)) (i32.eq (local.get $b) (i32.const 0)))
        (then
          ;; 如果是除法且除数为0,返回错误码 -1
          (i32.const -1)
          (br $result_block) ;; 跳转出 block,此时栈顶的 -1 成为 block 的返回值
        )
      )

      ;; 使用 loop 模拟一些工作
      (local.set $temp_val (i32.const 5))
      (loop $work_loop
        (local.set $temp_val (i32.sub (local.get $temp_val) (i32.const 1)))
        ;; 当 $temp_val > 0 时继续循环
        (br_if $work_loop (i32.gt_s (local.get $temp_val) (i32.const 0)))
      )

      ;; 间接调用 (函数指针)
      (call_indirect (type $binary_op)
        (local.get $a)
        (local.get $b)
        (local.get $op_index) ;; 根据索引动态选择函数
      )
      ;; 此时计算结果在栈顶,它将成为 block 的返回值
    )

    ;; 此时 block 的返回值在栈顶
    ;; 将结果写入由 $out_ptr 指定的内存地址
    (i32.store (local.get $out_ptr) (i32.add (global.get $config))) ;; 加上导入的配置值

    ;; drop 掉结果,因为函数签名要求返回一个 i32,而我们已经 store 了
    ;; drop

    ;; 返回状态码 0 表示成功
    (i32.const 0)
  )
)

它在被编译后,可在任何支持的环境里运行,但这里主要关心浏览器:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WASM Showcase</title>
    <style>
        body { font-family: monospace; padding: 1em; }
        .output { border: 1px solid #ccc; padding: 1em; margin-top: 1em; white-space: pre-wrap; }
    </style>
</head>
<body>
    <h1>WASM Showcase</h1>
    <button id="addBtn">执行加法 (100 +)</button>
    <button id="divBtn">执行除法 (100 /)</button>
    <button id="divZeroBtn">执行除零 (100 / 0)</button>
    <div id="output" class="output">点击按钮开始...</div>

    <script>
        const outputLog = document.getElementById('output');

        async function main() {
            const memory = new WebAssembly.Memory({ initial: 1 });
            const importObject = {
                env: {
                    log: (ptr, len) => {
                        const buffer = new Uint8Array(memory.buffer, ptr, len);
                        const text = new TextDecoder('utf8').decode(buffer);
                        console.log(`[Log from WASM]: ${text}`);
                        outputLog.textContent += `[Log from WASM]: ${text}\n`;
                    },
                    config: new WebAssembly.Global({ value: "i32", mutable: false },), // 导入一个值为10的常量全局变量
                    memory: memory,
                }
            };

            const { instance } = await WebAssembly.instantiateStreaming(fetch('showcase.wasm'), importObject);
            const wasm = instance.exports;

            outputLog.textContent = 'WASM 模块已准备就绪。\n';
            outputLog.textContent += `导出的内存大小: ${wasm.memory.buffer.byteLength} bytes\n`;
            outputLog.textContent += `导出的函数表大小: ${wasm.op_table.length}\n\n`;

            const RESULT_POINTER =4; // 选一个内存地址用于存放结果

            function runTask(a, b, opIndex, opName) {
                outputLog.textContent += `--- Running Task: ${opName} ---\n`;

                const status = wasm.process_task(a, b, opIndex, RESULT_POINTER);

                if (status === 0) {
                    // 从内存中读取结果
                    const resultArray = new Int32Array(memory.buffer, RESULT_POINTER, 1);
                    outputLog.textContent += `计算结果 (已存入内存): ${resultArray[0]}\n`;
                } else {
                    outputLog.textContent += `WASM 返回错误码: ${status}\n`;
                }

                // 读取导出的全局变量
                outputLog.textContent += `任务总数 (全局变量): ${wasm.task_count.value}\n\n`;
            }

            document.getElementById('addBtn').addEventListener('click', () => runTask(100,, 0, "ADD"));
            document.getElementById('divBtn').addEventListener('click', () => runTask(100,, 1, "DIV"));
            document.getElementById('divZeroBtn').addEventListener('click', () => runTask(100, 0, 1, "DIV by Zero"));
        }

        main().catch(console.error);
    </script>
</body>
</html>

在浏览器中,实例化wasm模块处最关键,当前浏览器存在四种方式去初始化,所以需要在这些位置获取:

WebAssembly.compile(bufferSource, compileOptions) // 将 WebAssembly 二进制代码编译为一个 WebAssembly.Module 对象
WebAssembly.compileStreaming(source, compileOptions) // 直接将流式传输的底层源码编译为一个 WebAssembly.Module 对象
WebAssembly.instantiate(bufferSource/module, importObject, compileOptions) // 编译和实例化 WebAssembly 代码,含两种重载
WebAssembly.instantiateStreaming(source, importObject, compileOptions) // 直接从流式底层源编译并实例化 WebAssembly 模块

更多基础语法可参考[5]。

文件结构

wasm的文件结构十分简单,里面由节组成,由于要实现流式加载,它里面的内置节存在固定的顺序,下面简单说明下,详细的可以找一个010模板文件自己看。

文件头

Field Name Type Description
magic_cookie [uint32] 0x6d736100 (\0asm)
version [uint32] 现在是0x1

节名称 (Section Name) Opcode (ID) 内容与用途 (Content and Purpose) 关联关系 (Relationships)
Custom Section 0x00 自定义节:存放任意元数据,如调试信息。WASM引擎会忽略其内容 -
Type Section 0x01 类型节:定义模块中用到的所有函数签名 被函数节、导入节、代码节(call_indirect)引用
Import Section 0x02 导入节:声明从外部环境导入的函数、表、内存和全局变量 依赖类型节(用于函数签名)。定义了函数、表、内存、全局变量等索引空间的起始部分
Function Section 0x03 函数节:声明模块内部函数,并关联到其在类型节中的签名索引 依赖类型节,与代码节一一对应,定义函数的签名部分
Table Section 0x04 表节:定义模块内部的(通常用于函数指针) 被元素节(用于初始化)和代码节(call_indirect)引用
Memory Section 0x05 内存节:定义模块内部的线性内存 被数据节(用于初始化)和代码节(load/store指令)引用
Global Section 0x06 全局变量节:定义模块内部的全局变量及其初始值 被代码节(global.get/set)和导出节引用。其初始值可能依赖导入的全局变量
Export Section 0x07 导出节:声明模块向外部环境导出的函数、表、内存和全局变量 依赖函数节、表节、内存节、全局变量节(以及导入节),它导出的是这些索引空间中的成员
Start Section 0x08 起始函数节:指定一个在模块实例化时自动执行的函数 依赖函数节(指定要执行的函数索引)
Element Section 0x09 元素节:用于在实例化时初始化表的内容(即用函数引用填充表) 依赖表节(指定要初始化的表)和函数节(提供用于填充的函数索引)
Code Section 0x0a 代码节:包含模块中每个函数的实际可执行指令(函数体) 与函数节一一对应,提供函数的实现。依赖几乎所有节,它包含的指令会引用类型、函数、表、内存、全局变量等
Data Section 0x0b 数据节:用于在实例化时初始化线性内存的内容 依赖内存节(指定要初始化的内存)

指令集

先说下wasm的虚拟机是栈式虚拟机(虽然不纯,有本地变量),它有两个栈,操作数栈和控制流栈,前者很好懂,后者等哈看控制流指令就能更清晰的理解了,再说说类型,它只有4种值类型:i32, i64, f32, f64,其它的就是上面提到的函数引用funcref和签名类型了。

指令描述

Instruction Mnemonic Field (指令助记符)

即汇编的操作码,它的命名规则为:

  • 类型前缀 (Type Prefix): 很多指令是针对特定数据类型的。助记符会以类型开头,如 i32.add (32位整数加法), f64.load (从内存加载64位浮点数)。
  • 类型后缀 (Type Suffix): 类型转换指令会用后缀指明输入类型,如 i32.trunc_f64_s (将一个有符号64位浮点数f64截断成32位整数i32)。
  • 有/无符号后缀 (Signed/Unsigned Suffix): _s 代表有符号 (signed) 整数运算,_u 代表无符号 (unsigned) 整数运算。例如 i32.div_s (有符号除法) 和 i32.div_u (无符号除法)。

Instruction Opcode Field (指令操作码)

没啥说的,就是数值形式的操作码

Instruction Immediates Field (指令立即数)

跟随在 Opcode 后面的、作为指令一部分的“硬编码”参数。这些值不是从栈上动态获取的,而是指令自带的。如br 1的br 是跳转指令,1 就是一个立即数,表示要跳转到外层第1个 block 或 loop。

Instruction Signature Field (指令签名)

它描述了指令与操作数栈 (Operand Stack) 的交互方式。WASM 是一个基于栈的虚拟机,大部分指令都是从栈顶取出数据,运算后再将结果推回栈顶。它的格式为(operands) : (returns),即 (消耗的值) : (产生的值)。如i32.add 的签名是 (i32, i32) : (i32)。意思是:它会从栈顶弹出两个 i32 类型的值,将它们相加,然后把结果(一个 i32 值)推回到栈顶。

Instruction Families Field (指令家族)

为了更好地组织和理解,指令被按功能分成了不同的“家族”。主要家族举例:

  • B (Branch), Q (Control-Flow Barrier), L (Call): 控制流家族。负责 if/else, loop 循环,函数调用 (call),返回 (return) 等改变程序执行顺序的操作。
  • G, S, U, T, R (Integer): 整数运算家族。负责各种整数的加减乘除、位移、求余等。
  • F, E (Floating-Point): 浮点数运算家族。负责浮点数的运算,严格遵循 IEEE 754 标准,保证了跨平台计算结果的一致性。
  • M (Memory), Z (Memory Size): 内存访问家族。负责从一块被称为 Linear Memory 的连续内存中读取 (load) 和写入 (store) 数据。这是 WASM 与外部世界(如 JavaScript)交换复杂数据的主要方式。
  • C (Comparison): 比较家族。负责比较两个值的大小或是否相等,结果是一个布尔值(0或1)。

指令详请

控制流指令

指令名称 操作码 立即数 签名 家族 说明
block 0x02 $signature: [block signature type] () : () 推送一个控制流条目到栈上,用于创建一个代码块。
loop 0x03 $signature: [block signature type] () : () 绑定一个标签到当前位置并推送一个控制流条目,用于创建可跳转的循环。
br 0x0c $depth: [varuint32] ($T[$block_arity]) : ($T[$block_arity]) [B] [Q] 无条件跳转到由$depth指定的嵌套层级的标签。
br_if 0x0d $depth: [varuint32] ($T[$block_arity], $condition: i32) : ($T[$block_arity]) [B] 如果栈顶的$condition为true,则跳转到由$depth指定的嵌套层级的标签。
br_table 0x0e $table: [array] of [varuint32], $default: [varuint32] ($T[$block_arity], $index: i32) : ($T[$block_arity]) [B] [Q] 根据$index的值在跳转表$table中选择一个目标进行跳转,若越界则使用$default目标。
if 0x04 $signature: [block signature type] ($condition: i32) : () [B] 开始一个if块。如果$condition为false,则直接跳转到对应的else或end。
else 0x05 ($T[$any]) : ($T[$any]) [B] 标记if块的'else'分支的开始。
end 0x0b ($T[$any]) : ($T[$any]) 标记一个block, loop, ifelse代码块的结束。
return 0x0f ($T[$block_arity]) : ($T[$block_arity]) [B] [Q] 从当前函数返回。
unreachable 0x00 () : () [Q] 执行到此指令时,总是会触发一个陷阱(Trap),中止执行。

基本指令

指令名称 操作码 立即数 签名 家族 说明
nop 0x01 () : () 什么也不做。
drop 0x1a ($T[1]) : () 从栈顶丢弃一个值。
i32.const 0x41 $value: [varsint32] () : (i32) 将立即数$value作为i32常量推到栈顶。
i64.const 0x42 $value: [varsint64] () : (i64) 将立即数$value作为i64常量推到栈顶。
f32.const 0x43 $value: [float32] () : (f32) 将立即数$value作为f32常量推到栈顶。
f64.const 0x44 $value: [float64] () : (f64) 将立即数$value作为f64常量推到栈顶。
local.get 0x20 $id: [varuint32] () : ($T[1]) 获取索引为$id的局部变量的值,并将其推到栈顶。
local.set 0x21 $id: [varuint32] ($T[1]) : () 从栈顶弹出一个值,并设置到索引为$id的局部变量。
local.tee 0x22 $id: [varuint32] ($T[1]) : ($T[1]) local.set类似,但设置后不弹出值,而是将其保留在栈顶。
global.get 0x23 $id: [varuint32] () : ($T[1]) 获取索引为$id的全局变量的值,并将其推到栈顶。
global.set 0x24 $id: [varuint32] ($T[1]) : () 从栈顶弹出一个值,并设置到索引为$id的全局变量。
select 0x1b ($T[1], $T[1], $condition: i32) : ($T[1]) 根据$condition的值(非0为true),从栈顶的两个值中选择一个返回。
call 0x10 $callee: [varuint32] ($T[$args]) : ($T[$returns]) [L] 调用索引为$callee的函数。
call_indirect 0x11 $signature: [varuint32], $reserved: [varuint1] ($T[$args], $callee: i32) : ($T[$returns]) [L] 间接调用函数。从栈顶获取函数在表中的索引$callee进行调用。

整数算数指令

指令名称 操作码 立即数 签名 家族 说明
i32.add 0x6a (i32, i32) : (i32) [G] 32位整数加法。
i64.add 0x7c (i64, i64) : (i64) [G] 64位整数加法。
i32.sub 0x6b (i32, i32) : (i32) [G] 32位整数减法。
i64.sub 0x7d (i64, i64) : (i64) [G] 64位整数减法。
i32.mul 0x6c (i32, i32) : (i32) [G] 32位整数乘法。
i64.mul 0x7e (i64, i64) : (i64) [G] 64位整数乘法。
i32.div_s 0x6d (i32, i32) : (i32) [S] 32位有符号整数除法。
i64.div_s 0x7f (i64, i64) : (i64) [S] 64位有符号整数除法。
i32.div_u 0x6e (i32, i32) : (i32) [U] 32位无符号整数除法。
i64.div_u 0x80 (i64, i64) : (i64) [U] 64位无符号整数除法。
i32.rem_s 0x6f (i32, i32) : (i32) [S] [R] 32位有符号整数取余。
i64.rem_s 0x81 (i64, i64) : (i64) [S] [R] 64位有符号整数取余。
i32.rem_u 0x70 (i32, i32) : (i32) [U] [R] 32位无符号整数取余。
i64.rem_u 0x82 (i64, i64) : (i64) [U] [R] 64位无符号整数取余。
i32.and 0x71 (i32, i32) : (i32) [G] 32位整数按位与。
i64.and 0x83 (i64, i64) : (i64) [G] 64位整数按位与。
i32.or 0x72 (i32, i32) : (i32) [G] 32位整数按位或。
i64.or 0x84 (i64, i64) : (i64) [G] 64位整数按位或。
i32.xor 0x73 (i32, i32) : (i32) [G] 32位整数按位异或。
i64.xor 0x85 (i64, i64) : (i64) [G] 64位整数按位异或。
i32.shl 0x74 (i32, i32) : (i32) [T], [G] 32位整数逻辑左移。
i64.shl 0x86 (i64, i64) : (i64) [T], [G] 64位整数逻辑左移。
i32.shr_s 0x75 (i32, i32) : (i32) [T], [S] 32位整数算术右移。
i64.shr_s 0x87 (i64, i64) : (i64) [T], [S] 64位整数算术右移。
i32.shr_u 0x76 (i32, i32) : (i32) [T], [U] 32位整数逻辑右移。
i64.shr_u 0x88 (i64, i64) : (i64) [T], [U] 64位整数逻辑右移。
i32.rotl 0x77 (i32, i32) : (i32) [T], [G] 32位整数循环左移。
i64.rotl 0x89 (i64, i64) : (i64) [T], [G] 64位整数循环左移。
i32.rotr 0x78 (i32, i32) : (i32) [T], [G] 32位整数循环右移。
i64.rotr 0x8a (i64, i64) : (i64) [T], [G] 64位整数循环右移。
i32.clz 0x67 (i32) : (i32) [G] 32位整数计算前导零。
i64.clz 0x79 (i64) : (i64) [G] 64位整数计算前导零。
i32.ctz 0x68 (i32) : (i32) [G] 32位整数计算末尾零。
i64.ctz 0x7a (i64) : (i64) [G] 64位整数计算末尾零。
i32.popcnt 0x69 (i32) : (i32) [G] 32位整数计算置1的位数。
i64.popcnt 0x7b (i64) : (i64) [G] 64位整数计算置1的位数。
i32.eqz 0x45 (i32) : (i32) [G] 判断32位整数是否等于零。
i64.eqz 0x50 (i64) : (i32) [G] 判断64位整数是否等于零。

浮点数算数指令

指令名称 操作码 立即数 签名 家族 说明
f32.add 0x92 (f32, f32) : (f32) [F] 32位浮点数加法。
f64.add 0xa0 (f64, f64) : (f64) [F] 64位浮点数加法。
f32.sub 0x93 (f32, f32) : (f32) [F] 32位浮点数减法。
f64.sub 0xa1 (f64, f64) : (f64) [F] 64位浮点数减法。
f32.mul 0x94 (f32, f32) : (f32) [F] 32位浮点数乘法。
f64.mul 0xa2 (f64, f64) : (f64) [F] 64位浮点数乘法。
f32.div 0x95 (f32, f32) : (f32) [F] 32位浮点数除法。
f64.div 0xa3 (f64, f64) : (f64) [F] 64位浮点数除法。
f32.sqrt 0x91 (f32) : (f32) [F] 32位浮点数开平方根。
f64.sqrt 0x9f (f64) : (f64) [F] 64位浮点数开平方根。
f32.min 0x96 (f32, f32) : (f32) [F] 32位浮点数取较小值。
f64.min 0xa4 (f64, f64) : (f64) [F] 64位浮点数取较小值。
f32.max 0x97 (f32, f32) : (f32) [F] 32位浮点数取较大值。
f64.max 0xa5 (f64, f64) : (f64) [F] 64位浮点数取较大值。
f32.ceil 0x8d (f32) : (f32) [F] 32位浮点数向上取整。
f64.ceil 0x9b (f64) : (f64) [F] 64位浮点数向上取整。
f32.floor 0x8e (f32) : (f32) [F] 32位浮点数向下取整。
f64.floor 0x9c (f64) : (f64) [F] 64位浮点数向下取整。
f32.trunc 0x8f (f32) : (f32) [F] 32位浮点数向零取整。
f64.trunc 0x9d (f64) : (f64) [F] 64位浮点数向零取整。
f32.nearest 0x90 (f32) : (f32) [F] 32位浮点数取最近整数(中间值取偶)。
f64.nearest 0x9e (f64) : (f64) [F] 64位浮点数取最近整数(中间值取偶)。
f32.abs 0x8b (f32) : (f32) [E] 32位浮点数取绝对值。
f64.abs 0x99 (f64) : (f64) [E] 64位浮点数取绝对值。
f32.neg 0x8c (f32) : (f32) [E] 32位浮点数取负。
f64.neg 0x9a (f64) : (f64) [E] 64位浮点数取负。
f32.copysign 0x98 (f32, f32) : (f32) [E] 复制32位浮点数符号。
f64.copysign 0xa6 (f64, f64) : (f64) [E] 复制64位浮点数符号。

整数比较指令

指令名称 操作码 立即数 签名 家族 说明
i32.eq 0x46 (i32, i32) : (i32) [C], [G] 判断两个32位整数是否相等。
i64.eq 0x51 (i64, i64) : (i32) [C], [G] 判断两个64位整数是否相等。
i32.ne 0x47 (i32, i32) : (i32) [C], [G] 判断两个32位整数是否不相等。
i64.ne 0x52 (i64, i64) : (i32) [C], [G] 判断两个64位整数是否不相等。
i32.lt_s 0x48 (i32, i32) : (i32) [C], [S] 32位有符号整数比较:是否小于。
i64.lt_s 0x53 (i64, i64) : (i32) [C], [S] 64位有符号整数比较:是否小于。
i32.lt_u 0x49 (i32, i32) : (i32) [C], [U] 32位无符号整数比较:是否小于。
i64.lt_u 0x54 (i64, i64) : (i32) [C], [U] 64位无符号整数比较:是否小于。
i32.le_s 0x4c (i32, i32) : (i32) [C], [S] 32位有符号整数比较:是否小于等于。
i64.le_s 0x57 (i64, i64) : (i32) [C], [S] 64位有符号整数比较:是否小于等于。
i32.le_u 0x4d (i32, i32) : (i32) [C], [U] 32位无符号整数比较:是否小于等于。
i64.le_u 0x58 (i64, i64) : (i32) [C], [U] 64位无符号整数比较:是否小于等于。
i32.gt_s 0x4a (i32, i32) : (i32) [C], [S] 32位有符号整数比较:是否大于。
i64.gt_s 0x55 (i64, i64) : (i32) [C], [S] 64位有符号整数比较:是否大于。
i32.gt_u 0x4b (i32, i32) : (i32) [C], [U] 32位无符号整数比较:是否大于。
i64.gt_u 0x56 (i64, i64) : (i32) [C], [U] 64位无符号整数比较:是否大于。
i32.ge_s 0x4e (i32, i32) : (i32) [C], [S] 32位有符号整数比较:是否大于等于。
i64.ge_s 0x59 (i64, i64) : (i32) [C], [S] 64位有符号整数比较:是否大于等于。
i32.ge_u 0x4f (i32, i32) : (i32) [C], [U] 32位无符号整数比较:是否大于等于。
i64.ge_u 0x5a (i64, i64) : (i32) [C], [U] 64位无符号整数比较:是否大于等于。

浮点数比较指令

指令名称 操作码 立即数 签名 家族 说明
f32.eq 0x5b (f32, f32) : (i32) [C], [F] 判断两个32位浮点数是否相等。
f64.eq 0x61 (f64, f64) : (i32) [C], [F] 判断两个64位浮点数是否相等。
f32.ne 0x5c (f32, f32) : (i32) [C], [F] 判断两个32位浮点数是否不相等。
f64.ne 0x62 (f64, f64) : (i32) [C], [F] 判断两个64位浮点数是否不相等。
f32.lt 0x5d (f32, f32) : (i32) [C], [F] 32位浮点数比较:是否小于。
f64.lt 0x63 (f64, f64) : (i32) [C], [F] 64位浮点数比较:是否小于。
f32.le 0x5f (f32, f32) : (i32) [C], [F] 32位浮点数比较:是否小于等于。
f64.le 0x65 (f64, f64) : (i32) [C], [F] 64位浮点数比较:是否小于等于。
f32.gt 0x5e (f32, f32) : (i32) [C], [F] 32位浮点数比较:是否大于。
f64.gt 0x64 (f64, f64) : (i32) [C], [F] 64位浮点数比较:是否大于。
f32.ge 0x60 (f32, f32) : (i32) [C], [F] 32位浮点数比较:是否大于等于。
f64.ge 0x66 (f64, f64) : (i32) [C], [F] 64位浮点数比较:是否大于等于。

转换指令

指令名称 操作码 立即数 签名 家族 说明
i32.wrap_i64 0xa7 (i64) : (i32) [G] 将64位整数截断(wrap)为32位整数。
i64.extend_i32_s 0xac (i32) : (i64) [S] 将32位有符号整数符号扩展为64位整数。
i64.extend_i32_u 0xad (i32) : (i64) [U] 将32位无符号整数零扩展为64位整数。
i32.trunc_f32_s 0xa8 (f32) : (i32) [F], [S] 将32位浮点数转为32位有符号整数(截断)。
i32.trunc_f64_s 0xaa (f64) : (i32) [F], [S] 将64位浮点数转为32位有符号整数(截断)。
i64.trunc_f32_s 0xae (f32) : (i64) [F], [S] 将32位浮点数转为64位有符号整数(截断)。
i64.trunc_f64_s 0xb0 (f64) : (i64) [F], [S] 将64位浮点数转为64位有符号整数(截断)。
i32.trunc_f32_u 0xa9 (f32) : (i32) [F], [U] 将32位浮点数转为32位无符号整数(截断)。
i32.trunc_f64_u 0xab (f64) : (i32) [F], [U] 将64位浮点数转为32位无符号整数(截断)。
i64.trunc_f32_u 0xaf (f32) : (i64) [F], [U] 将32位浮点数转为64位无符号整数(截断)。
i64.trunc_f64_u 0xb1 (f64) : (i64) [F], [U] 将64位浮点数转为64位无符号整数(截断)。
f32.demote_f64 0xb6 (f64) : (f32) [F] 将64位浮点数降级(demote)为32位浮点数。
f64.promote_f32 0xbb (f32) : (f64) [F] 将32位浮点数升级(promote)为64位浮点数。
f32.convert_i32_s 0xb2 (i32) : (f32) [F], [S] 将32位有符号整数转为32位浮点数。
f32.convert_i64_s 0xb4 (i64) : (f32) [F], [S] 将64位有符号整数转为32位浮点数。
f64.convert_i32_s 0xb7 (i32) : (f64) [F], [S] 将32位有符号整数转为64位浮点数。
f64.convert_i64_s 0xb9 (i64) : (f64) [F], [S] 将64位有符号整数转为64位浮点数。
f32.convert_i32_u 0xb3 (i32) : (f32) [F], [U] 将32位无符号整数转为32位浮点数。
f32.convert_i64_u 0xb5 (i64) : (f32) [F], [U] 将64位无符号整数转为32位浮点数。
f64.convert_i32_u 0xb8 (i32) : (f64) [F], [U] 将32位无符号整数转为64位浮点数。
f64.convert_i64_u 0xba (i64) : (f64) [F], [U] 将64位无符号整数转为64位浮点数。
i32.reinterpret_f32 0xbc (f32) : (i32) 按位将32位浮点数重新解释为32位整数。
i64.reinterpret_f64 0xbd (f64) : (i64) 按位将64位浮点数重新解释为64位整数。
f32.reinterpret_i32 0xbe (i32) : (f32) 按位将32位整数重新解释为32位浮点数。
f64.reinterpret_i64 0xbf (i64) : (f64) 按位将64位整数重新解释为64位浮点数。
i32.extend8_s 0xc0 (i32) : (i32) [S] 将i32的低8位作为有符号数,符号扩展到32位。
i32.extend16_s 0xc1 (i32) : (i32) [S] 将i32的低16位作为有符号数,符号扩展到32位。
i64.extend8_s 0xc2 (i64) : (i64) [S] 将i64的低8位作为有符号数,符号扩展到64位。
i64.extend16_s 0xc3 (i64) : (i64) [S] 将i64的低16位作为有符号数,符号扩展到64位。
i64.extend32_s 0xc4 (i64) : (i64) [S] 将i64的低32位作为有符号数,符号扩展到64位。

访存指令

指令名称 操作码 立即数 签名 家族 说明
i32.load 0x28 $flags: [memflags], $offset: [varuPTR] ($base: iPTR) : (i32) [M], [G] 从内存加载一个32位整数。
i64.load 0x29 $flags: [memflags], $offset: [varuPTR] ($base: iPTR) : (i64) [M], [G] 从内存加载一个64位整数。
f32.load 0x2a $flags: [memflags], $offset: [varuPTR] ($base: iPTR) : (f32) [M], [E] 从内存加载一个32位浮点数。
f64.load 0x2b $flags: [memflags], $offset: [varuPTR] ($base: iPTR) : (f64) [M], [E] 从内存加载一个64位浮点数。
i32.store 0x36 $flags: [memflags], $offset: [varuPTR] ($base: iPTR, $value: i32) : () [M], [G] 向内存存储一个32位整数。
i64.store 0x37 $flags: [memflags], $offset: [varuPTR] ($base: iPTR, $value: i64) : () [M], [G] 向内存存储一个64位整数。
f32.store 0x38 $flags: [memflags], $offset: [varuPTR] ($base: iPTR, $value: f32) : () [M], [F] 向内存存储一个32位浮点数。
f64.store 0x39 $flags: [memflags], $offset: [varuPTR] ($base: iPTR, $value: f64) : () [M], [F] 向内存存储一个64位浮点数。
i32.load8_s 0x2c $flags: [memflags], $offset: [varuPTR] ($base: iPTR) : (i32) [M], [S] 从内存加载8位有符号整数,符号扩展为32位。
i32.load16_s 0x2e $flags: [memflags], $offset: [varuPTR] ($base: iPTR) : (i32) [M], [S] 从内存加载16位有符号整数,符号扩展为32位。
i64.load8_s 0x30 $flags: [memflags], $offset: [varuPTR] ($base: iPTR) : (i64) [M], [S] 从内存加载8位有符号整数,符号扩展为64位。
i64.load16_s 0x32 $flags: [memflags], $offset: [varuPTR] ($base: iPTR) : (i64) [M], [S] 从内存加载16位有符号整数,符号扩展为64位。
i64.load32_s 0x34 $flags: [memflags], $offset: [varuPTR] ($base: iPTR) : (i64) [M], [S] 从内存加载32位有符号整数,符号扩展为64位。
i32.load8_u 0x2d $flags: [memflags], $offset: [varuPTR] ($base: iPTR) : (i32) [M], [U] 从内存加载8位无符号整数,零扩展为32位。
i32.load16_u 0x2f $flags: [memflags], $offset: [varuPTR] ($base: iPTR) : (i32) [M], [U] 从内存加载16位无符号整数,零扩展为32位。
i64.load8_u 0x31 $flags: [memflags], $offset: [varuPTR] ($base: iPTR) : (i64) [M], [U] 从内存加载8位无符号整数,零扩展为64位。
i64.load16_u 0x33 $flags: [memflags], $offset: [varuPTR] ($base: iPTR) : (i64) [M], [U] 从内存加载16位无符号整数,零扩展为64位。
i64.load32_u 0x35 $flags: [memflags], $offset: [varuPTR] ($base: iPTR) : (i64) [M], [U] 从内存加载32位无符号整数,零扩展为64位。
i32.store8 0x3a $flags: [memflags], $offset: [varuPTR] ($base: iPTR, $value: i32) : () [M], [G] 将32位整数截断为8位并存入内存。
i32.store16 0x3b $flags: [memflags], $offset: [varuPTR] ($base: iPTR, $value: i32) : () [M], [G] 将32位整数截断为16位并存入内存。
i64.store8 0x3c $flags: [memflags], $offset: [varuPTR] ($base: iPTR, $value: i64) : () [M], [G] 将64位整数截断为8位并存入内存。
i64.store16 0x3d $flags: [memflags], $offset: [varuPTR] ($base: iPTR, $value: i64) : () [M], [G] 将64位整数截断为16位并存入内存。
i64.store32 0x3e $flags: [memflags], $offset: [varuPTR] ($base: iPTR, $value: i64) : () [M], [G] 将64位整数截断为32位并存入内存。

其它内存相关指令

指令名称 操作码 立即数 签名 家族 说明
memory.grow 0x40 $reserved: [varuint1] ($delta: iPTR) : (iPTR) [Z] $delta指定的页数增加内存大小。成功则返回旧大小,失败返回-1。
memory.size 0x3f $reserved: [varuint1] () : (iPTR) [Z] 获取当前内存的大小(以页为单位)。

逆向

静态

基本所有逆向工具的原生或通过插件支持wasm的反汇编和反编译,下面列出一些:

1.WABT:这是官方的二进制工具集,含多个工具,例如wasm2wat能做反汇编,wasm-decompile能做反编译,wasm2c能直接转换为C和相应头文件,而且基本不会有问题!

2.JEB原生支持WASM分析

3.IDA:之前IDA不支持Wasm,有两种方法可以分析,第一种是将其编译为elf,即利用和;而第二种方法是用第三方插件,如wasm2idaidawasm,不过ida8.5开始支持wasm了

4.Ghidra通过ghidra-wasm-plugin支持wasm

5.radare2也支持,但俺不咋用它

动态

1.chrome本身对Wasm的调试支持很有限,不过类似ida的脚本语言,咱可以直接在DevTools中用JS定义一些脚本,去辅助分析

2.它还可以被编译为C等运行,或者python等都存在它的解释器,可用于运行它,便于黑盒使用

3.可使用Cetus,它利用wail做插桩,实现了很多类似于cheat engine的功能

4.可以反编译再回编译,给它添加dwarf信息,调试起来会更爽

参考

[0] WebAssembly Specifications

[1] What’s in that.wasm? Introducing:wasm-decompile

[2] WebAssembly-learn

[3] WebAssembly Instructions

[4] WebAssembly Reference Manual

[5] WASM汇编入门教程

[6] Hacking WebAssembly Games with Binary Instrumentation

[6] Standing on the Shoulders of Giants: De-Obfuscating WebAssembly Using LLVM