> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# JS 逆向中的 WebAssembly（WASM）特征识别与逆向
- URL: https://blog.vercanti.com/js-ni-xiang-zhong-de-webassembly-wasm-te-zheng-shi-bie-yu-ni-xiang/
- Published: 2026-08-28T14:35:11.000Z
- Updated: 2026-08-28T14:58:14.000Z
- Description: 逆向遇到 WebAssembly（WASM）时，第一选择是把 .wasm 当成一个编译好的 .so/.dll，在 Node 里加载并黑盒调用它的导出函数，而不是逐行把字节码翻译回 JS。WASM 是一种独立的二进制字节码格式（Binary Bytecode Format），由 C/C++/Rust 等语言编译而来，浏览器原生执行，加载快、体积小，也因此成了把签名、加密、风控逻辑藏起来的常见手段——抖音 a_bogus/bdms 这类把算法塞进 WASM 甚至字节码虚拟机（Bytecode VM）的方案，目的就是对抗"扣代码"。 什么时候该黑盒调用而非硬扣
- Author: yellowdog
- Tags: js逆向, 技巧步骤

> 官方文档：
> 
> - WebAssembly 规范主站：<https://webassembly.org/>
> - MDN WebAssembly JavaScript 接口：<https://developer.mozilla.org/en-US/docs/WebAssembly>
> - Emscripten 工具链文档：<https://emscripten.org/>
> - WABT（WebAssembly Binary Toolkit）：<https://github.com/WebAssembly/wabt>
> 
> 适用版本：WebAssembly Core Spec 3.0（含异常处理 Tag、bulk-memory）、Emscripten 6.x、WABT/Binaryen/wasm-tools 当前发行版。  
> 核实日期：2026-06-07。

## 概述

逆向遇到 WebAssembly（WASM）时，第一选择是把 `.wasm` 当成一个编译好的 `.so`/`.dll`，在 Node 里加载并黑盒调用它的导出函数，而不是逐行把字节码翻译回 JS。WASM 是一种独立的二进制字节码格式（Binary Bytecode Format），由 C/C++/Rust 等语言编译而来，浏览器原生执行，加载快、体积小，也因此成了把签名、加密、风控逻辑藏起来的常见手段——抖音 `a_bogus`/`bdms` 这类把算法塞进 WASM 甚至字节码虚拟机（Bytecode VM）的方案，目的就是对抗"扣代码"。

什么时候该黑盒调用而非硬扣算法：当 WASM 内部经过编译器优化、自定义 VM 或混淆，逐行还原成本极高且极易出错时。只要能在受控环境里复现"相同输入产生相同输出"，就完全满足签名/加密需求，无需理解算法内部。反汇编一个 VM 型 WASM 只会看到解释器循环，看不到算法本身，这正是它的反逆向设计——把整个 VM 连同字节码一起当黑盒调用，才是正解。

什么时候才需要真正读懂逻辑：当目标是判别用了哪种已知算法（MD5/AES/SM3 等）、需要在纯算法层面复现、或要修改逻辑时。此时通过魔法常量（Magic Constant）和导出函数名快速判定算法族，再决定是否值得手翻。本文按"识别 → 工具 → 调试 → 黑盒复现 → 算法还原"的顺序展开，目标读者是有 JS 逆向基础但没系统接触过 WASM 的工程师。

## WASM 二进制结构

### 模块头（Module Preamble）

每个 `.wasm` 文件以 8 字节固定头开始，这是最强也最简单的识别特征。

| 偏移   | 字节                  | 含义                                         |
| ---- | ------------------- | ------------------------------------------ |
| 0x00 | 0x00 0x61 0x73 0x6D | 魔数（Magic Number），即 ASCII \\0asm（\\0、a、s、m） |
| 0x04 | 0x01 0x00 0x00 0x00 | 版本（Version），小端 u32 = 1（自 MVP 至今仍为 1）       |

判别要点：任何文件前 8 字节是 `00 61 73 6D 01 00 00 00`，100% 是 WASM 模块。`0x61 0x73 0x6D` 三个字节正好是 `asm` 的 ASCII。十六进制速记：`\0asm` \= `00 61 73 6d`，后跟版本 `01 00 00 00`。

### Section 编码格式

紧跟模块头之后是一系列段（Section），每个段的编码格式如下：

```text
section ::= section_id : byte    # 1 字节段 ID
            size       : u32     # LEB128 变长，段内容字节数
            content    : ...     # size 字节

```

`size` 用 LEB128（无符号变长整数编码，Little Endian Base 128）表示，这是 WASM 二进制里随处可见的整数编码方式——读 wat 或 hex 时遇到的多字节整数大多是 LEB128。

### Section 速查表

WebAssembly 3.0 规范定义的段 ID 如下。除自定义段（ID 0）外，每个标准段至多出现一次，且必须按规范规定的固定顺序排列——该顺序并非简单的 ID 升序（Tag 段紧跟 Memory 段、DataCount 段位于 Code 段之前，见表后规范顺序）；自定义段可插在任意两个标准段之间，且可多次出现。

| ID | 段名称             | 作用                                                      | 逆向关注点                                                           |
| -- | --------------- | ------------------------------------------------------- | --------------------------------------------------------------- |
| 0  | Custom（自定义）     | 调试信息/第三方扩展，被 WASM 语义忽略；可出现在任意位置、可多次                     | name 子段含函数名/局部变量名（调试符号）；sourceMappingURL 子段指向 source map；逆向第一目标 |
| 1  | Type（类型）        | 函数签名（递归类型）声明列表                                          | 看导出函数的参数/返回值类型                                                  |
| 2  | Import（导入）      | 从宿主（JS）导入 function/table/memory/global                  | 看模块依赖 JS 提供哪些环境函数，是补垫片的线索                                       |
| 3  | Function（函数）    | 函数到类型索引的映射（仅声明，函数体在 Code 段）                             | 与 Code 段配合阅读                                                    |
| 4  | Table（表）        | 表定义，通常存函数引用，用于间接调用                                      | 间接调用 / dynCall 相关                                               |
| 5  | Memory（内存）      | 线性内存（Linear Memory）定义，初始/最大页数，1 页 = 64 KiB              | 内存大小，构造 importObject 时参考                                        |
| 6  | Global（全局）      | 全局变量定义                                                  | 常量、状态                                                           |
| 7  | Export（导出）      | 导出 function/table/memory/global 给 JS                    | 逆向核心：导出名如 encrypt/sign/\_md5 直接暴露逻辑                             |
| 8  | Start（启动）       | 可选的启动函数，实例化后自动执行                                        | 初始化逻辑                                                           |
| 9  | Element（元素）     | 表初始化数据段                                                 | 填充函数表                                                           |
| 10 | Code（代码）        | 函数体（局部变量 + 指令字节码）                                       | 算法逻辑所在，反编译主战场                                                   |
| 11 | Data（数据）        | 初始化线性内存的字节                                              | 藏 S-Box / 魔法常量 / 字符串的地方                                         |
| 12 | DataCount（数据计数） | 可选 u32，data 段数量；启用 bulk-memory 时出现，必须在 Code 段之前（单遍验证需要） | bulk memory 特征                                                  |
| 13 | Tag（标签）         | 异常处理 tag 定义（exception handling 提案，已并入 3.0）              | 较新模块才有                                                          |

规范要求的固定段顺序（注意不是 ID 升序）：Type(1) → Import(2) → Function(3) → Table(4) → Memory(5) → Tag(13) → Global(6) → Export(7) → Start(8) → Element(9) → DataCount(12) → Code(10) → Data(11)。Tag 段插在 Memory 与 Global 之间，DataCount 段排在 Code 段之前（单遍验证需要先知道 data 段数量）。

说明：很多教学材料只列 0–11 共 12 类段（custom 到 datacount）。规范当前实为 0–13 共 14 个段 ID，其中 Tag(13) 是异常处理扩展段，较新工具链产物才出现。Function 段（声明）与 Code 段（函数体）拆分是为了支持并行编译。

## 在网页中识别用到了 WASM

判断一个页面是否用了 WASM，从网络层、JS API、内联编码三个维度入手，任一命中即可确认。

### 网络层特征

| 特征      | 表现                                               | 说明                                                                  |
| ------- | ------------------------------------------------ | ------------------------------------------------------------------- |
| 资源请求    | DevTools Network 出现 .wasm 后缀请求（如 app.wasm），有时无后缀 | 用 Type 列或文件名过滤                                                      |
| MIME 类型 | 响应头 Content-Type: application/wasm               | instantiateStreaming/compileStreaming 强制要求服务器返回该 MIME，否则流式编译报错，是强信号 |
| 响应体头部   | 二进制响应以 00 61 73 6D 01 00 00 00 开头                | 直接抓包看前 8 字节                                                         |

### JS API 调用特征

页面 JS 中出现以下符号即说明用到了 WASM，全文搜索这些关键字定位入口。

```javascript
// app.js —— WebAssembly 命名空间下的各类符号
// 编译类
WebAssembly.compile(bytes);
WebAssembly.compileStreaming(fetch("x.wasm"));
WebAssembly.validate(bytes);             // 返回 true 即合法 WASM

// 实例化类（最常见）
WebAssembly.instantiate(bytesOrModule, importObject);
WebAssembly.instantiateStreaming(fetch("x.wasm"), importObject);

// 构造器 / 命名空间
new WebAssembly.Module(bytes);
new WebAssembly.Instance(module, importObject);
new WebAssembly.Memory({ initial: 256, maximum: 32768 });
new WebAssembly.Table({ initial: 8, element: "anyfunc" });
new WebAssembly.Global({ value: "i32", mutable: true }, 0);

// 错误类型（出现在 catch / 报错栈里）
WebAssembly.CompileError;   // 解码 / 校验阶段
WebAssembly.LinkError;      // 实例化链接阶段
WebAssembly.RuntimeError;   // 运行期 trap

```

`WebAssembly` 是命名空间对象（类似 `Math`/`Intl`），不是构造器：

```javascript
// 正确：调用命名空间上的方法
const { instance } = await WebAssembly.instantiate(bytes, importObject);

// 错误：WebAssembly 不是构造器，不能 new
const w = new WebAssembly();   // 错误：WebAssembly 是命名空间对象，不可实例化

```

最典型的加载链（流式）与兼容写法（手动 fetch → arrayBuffer → instantiate，老代码常见）：

```javascript
// 流式：要求服务器返回 application/wasm
WebAssembly.instantiateStreaming(fetch("simple.wasm"), importObject)
  .then(obj => obj.instance.exports.exported_func());

// 兼容写法：先取 ArrayBuffer 再实例化（适配旧浏览器 / MIME 不对的服务器）
fetch("module.wasm")
  .then(r => r.arrayBuffer())
  .then(bytes => WebAssembly.instantiate(bytes, importObject))
  .then(results => {
    const exports = results.instance.exports;   // 访问导出
  });

```

### Base64 内联 WASM 的识别

为减少请求或反爬，WASM 常被 base64 内联进 JS。关键判别常量：base64 字符串以 `AGFzbQ` 开头，正是 `00 61 73 6D`（`\0asm`）的 base64 编码；完整头 `00 61 73 6D 01 00 00 00` 的 base64 是 `AGFzbQEAAAA`。在 JS 里全文搜 `AGFzbQ` 能直接定位内联 WASM。

```javascript
// glue.js —— 内联 base64 WASM 的典型解码流程
// 特征：一段超长 base64 字符串，以 AGFzbQ 开头
var wasmBinary = "AGFzbQEAAAA...";        // "AGFzbQ" = "\0asm" 的 base64
var binary = atob(wasmBinary);
var bytes = new Uint8Array(binary.length);
for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
WebAssembly.instantiate(bytes, importObject);

```

也可能是 `data:application/wasm;base64,AGFzbQ...` 形式的 data URI，或十六进制数组 `[0,97,115,109,1,0,0,0,...]`（开头是十进制的 `0 97 115 109`）。

### 其他线索

| 线索                          | 含义                                          |
| --------------------------- | ------------------------------------------- |
| Module 全局对象                 | Emscripten 默认配置对象                           |
| .js 胶水文件 + 同名 .wasm 成对出现    | 如 a.out.js \+ a.out.wasm、xxx.js \+ xxx.wasm |
| Sources 面板出现 wasm:// 协议虚拟文件 | DevTools 把二进制反汇编为 wat 直接展示                  |

## Emscripten 产物特征识别

Emscripten 是 C/C++ 编译到 WASM 的主流工具链，生成胶水 JS（Glue Code） + `.wasm` 两部分。胶水 JS 里有一批高度固定、硬编码的运行时符号，看到它们基本可断定"这是 Emscripten 编译的 WASM"。这是实战中最常遇到的形态，也是本文重点。

![Emscripten WASM 胶水代码中的 HEAP 视图与 ccall 调用](assets/wasm%E7%89%B9%E5%BE%81/file-20260309222007394.png)

图解读：调试器里出现 `HEAPU8`/`HEAP32` 等视图、以及 `t.ccall("cmd_clientDecrypt", "number", [...])` 这种调用，就是 Emscripten 编译 WASM 的铁证，`cmd_clientDecrypt` 是 WASM 导出的解密函数。

### HEAP 类型化数组视图（最强铁证）

Emscripten 把 WASM 线性内存（一块 `ArrayBuffer`）用多种类型化数组（TypedArray）视图暴露，命名极其固定。这 8 个名字是硬编码的，手写或别的工具链不会恰好用这套命名，全文搜 `HEAPU8` 命中即 Emscripten。

| 视图名     | 类型           | 元素宽度 | 说明             |
| ------- | ------------ | ---- | -------------- |
| HEAP8   | Int8Array    | 1 字节 | 8 位有符号         |
| HEAPU8  | Uint8Array   | 1 字节 | 8 位无符号，操作字节最常用 |
| HEAP16  | Int16Array   | 2 字节 | 16 位有符号        |
| HEAPU16 | Uint16Array  | 2 字节 | 16 位无符号        |
| HEAP32  | Int32Array   | 4 字节 | 32 位有符号        |
| HEAPU32 | Uint32Array  | 4 字节 | 32 位无符号        |
| HEAPF32 | Float32Array | 4 字节 | 32 位浮点         |
| HEAPF64 | Float64Array | 8 字节 | 64 位浮点         |

逆向用途：读写 WASM 内存就靠它们。读地址 `ptr` 处一个字节 = `HEAPU8[ptr]`；读 32 位整数 = `HEAP32[ptr >> 2]`（`>> 2` 是因为 `Int32Array` 索引以 4 字节为单位）。

### 内存与栈管理符号

| 符号           | 签名                      | 说明                         |
| ------------ | ----------------------- | -------------------------- |
| \_malloc     | \_malloc(size) -> ptr   | 在 WASM 堆分配内存，返回字节偏移（指针）    |
| \_free       | \_free(ptr)             | 释放                         |
| stackAlloc   | stackAlloc(size) -> ptr | 在 WASM 栈上分配临时缓冲            |
| stackSave    | stackSave() -> sp       | 保存栈指针                      |
| stackRestore | stackRestore(sp)        | 回退栈指针，一次性释放 stackAlloc 的分配 |

下划线前缀 `_` 代表对应的 C 函数（C 符号 `malloc` 编译为 JS 端 `_malloc`）。新版 Emscripten 默认不再导出 `_malloc`/`_free`，需 `-sEXPORTED_FUNCTIONS=['_malloc','_free']`；但 `stackAlloc`/`stackSave`/`stackRestore` 几乎总在胶水里。

### 字符串转换辅助函数（铁证）

JS 字符串与 WASM 内存（UTF-8）互转的工具，命名固定。

| 函数                | 签名                                             | 说明                                                                   |
| ----------------- | ---------------------------------------------- | -------------------------------------------------------------------- |
| UTF8ToString      | UTF8ToString(ptr, maxBytesToRead?, ignoreNul?) | 从堆里读 NUL 结尾的 UTF8 串转 JS String                                       |
| UTF8ArrayToString | UTF8ArrayToString(heap, ptr)                   | 从指定 heap 数组读 UTF8 串                                                  |
| stringToUTF8      | stringToUTF8(str, outPtr, maxBytesToWrite)     | JS String 写入堆（含 NUL）；返回实际写入字节数（不含 NUL），可据此推进指针，常与 lengthBytesUTF8 配合 |
| stringToNewUTF8   | stringToNewUTF8(str) -> ptr                    | 分配并写入，返回指针需 \_free                                                   |
| lengthBytesUTF8   | lengthBytesUTF8(str) -> number                 | 计算 UTF8 字节数（不含 NUL，分配前用）                                             |

逆向用途：当 C 函数参数/返回是字符串，必经这些函数。Hook `UTF8ToString` 能拿到 WASM 返回的明文字符串；Hook `stringToUTF8` 能截获传入 WASM 的明文（如密码、待加密原文）。

### 调用桥接 ccall / cwrap

```javascript
// ccall: 调一次 C 函数
// 参数依次为：函数名、返回类型、参数类型数组、实参数组
Module.ccall("func_name", "number", ["string", "number"], ["hello", 42]);

// cwrap: 返回一个可复用的 JS 包装函数
var func = Module.cwrap("func_name", "number", ["string", "number"]);
func("hello", 42);

```

`ccall`/`cwrap` 需通过 `-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap']` 导出后才能在 `Module.` 上访问。逆向时它们是"JS 调 WASM"的入口，Hook `Module.ccall` 的第一个参数能枚举被调用的所有 C 函数名。

### WASM 导出对象 / asm 别名（关键演变）

历史上 Emscripten 把 WASM 实例的 exports 命名为 `asm`（沿用 asm.js 时代命名），自 Emscripten 3.1.43（2023 年，issue #19792）起改名为 `wasmExports`。

```javascript
// 老版本：exports 别名为 asm
var asm = output.instance.exports;
asm["_main"]();
asm["functionName"];

// 新版本（Emscripten 3.1.43+）：改名 wasmExports
var wasmExports = wasmInstance.exports;
wasmExports["functionName"];

```

判别：胶水里出现 `var asm =` 或 `wasmExports =`，且后续大量 `asm["..."]` / `wasmExports["..."]` 形式的下标访问。这是真正的 WASM 函数表，所有导出函数都挂在这里。

### 动态调用与构造器

| 符号                              | 说明                                                                      |
| ------------------------------- | ----------------------------------------------------------------------- |
| dynCall\_<sig>                  | 通过函数表做间接调用，<sig> 是签名串：vi\=void(i32)、iii\=i32(i32,i32) 等                 |
| dynCall('vi', funcPtr, \[arg\]) | 通用动态调用入口                                                                |
| \_\_wasm\_call\_ctors           | 调用全局构造器（C++ 全局对象初始化），常用 createExportWrapper("\_\_wasm\_call\_ctors") 包装 |

`dynCall_xxx` 和 `__wasm_call_ctors` 都是 Emscripten 专有命名。

### Emscripten 铁证清单

任意命中 ≥2 项基本可确认是 Emscripten 产物：

```text
[ ] HEAP8 / HEAPU8 / HEAP16 / HEAPU16 / HEAP32 / HEAPU32 / HEAPF32 / HEAPF64
[ ] var asm = ... 或 wasmExports = ...   且大量 asm["xxx"] / wasmExports["xxx"]
[ ] _malloc / _free / stackAlloc / stackSave / stackRestore
[ ] ccall / cwrap
[ ] UTF8ToString / UTF8ArrayToString / stringToUTF8 / lengthBytesUTF8
[ ] dynCall_ 开头的函数 / __wasm_call_ctors
[ ] Module 全局对象 + EXPORTED_RUNTIME_METHODS / EXPORTED_FUNCTIONS 痕迹
[ ] 成对的 xxx.js（胶水） + xxx.wasm
[ ] 字符串 "memory grew"、"wasm" 或 emscripten 报错文案

```

这些符号是 Emscripten 运行时模板写死的，不属于 WASM 标准 API（标准只有 `WebAssembly.*`）。它们的存在说明 `.wasm` 不是手写/Rust wasm-bindgen/AssemblyScript 产物，而是 Emscripten（C/C++）产物。

### 区分其他工具链

| 工具链                 | 旁证符号                                                        |
| ------------------- | ----------------------------------------------------------- |
| Emscripten（C/C++）   | HEAP\* 视图、ccall/cwrap、\_malloc/UTF8ToString                 |
| Rust + wasm-bindgen | 胶水里有 wasm\_bindgen、\_\_wbindgen\_ 前缀、\_\_wbg\_ 前缀函数         |
| AssemblyScript      | 导出常含 \_\_new/\_\_pin/\_\_collect/\_\_rtti\_base，无 HEAP\* 命名 |
| Go（TinyGo/标准 Go）    | 有 go.importObject、runtime.wasmExit、syscall/js               |

### asm.js 与 WASM 的区分

asm.js 是 WASM 的前身（一种 JS 严格子集），WASM 标准化后取代了它。Emscripten 早年用 `asm` 命名 WASM exports 就是这段历史的遗留。实战区分：

| 维度 | asm.js                    | WebAssembly   |
| -- | ------------------------- | ------------- |
| 本质 | JavaScript 严格子集（文本 JS 源码） | 独立的二进制字节码格式   |
| 标志 | 函数体顶部 "use asm"; 指令       | 文件头 \\0asm 魔数 |
| 体积 | 冗长，按 JS 解析慢               | 紧凑，解析快        |
| 现状 | 已被取代，仅作旧浏览器 fallback      | 主流            |

看到 `.wasm` 文件 / `\0asm` 头 / `WebAssembly.*` API 就是 WASM；看到 JS 函数体里有 `"use asm";` 加大量 `x = x | 0`（强制整型）、`+x`（强制浮点）就是 asm.js。注意 asm.js 也用 `HEAP32` 命名（与 Emscripten 同源），区别在于 asm.js 是纯文本 JS、有 `"use asm"`、无 `.wasm` 文件。

## WASM 逆向工具链

核心目标：把不可读的 `.wasm` 二进制转为可读 `.wat`（文本格式）→ 类 C 伪代码 → 在动态调试器里跑起来。

### WABT（WebAssembly Binary Toolkit）

官方仓库 `WebAssembly/wabt`。定位是 1:1 高保真往返，不做优化、不改指令，最贴合规范，是看 wat、看段、反汇编、剥符号的首选。

```bash
# 安装
brew install wabt            # macOS / Linux (Homebrew)
sudo apt install wabt        # Debian / Ubuntu
# Windows / 通用：GitHub Releases 下载预编译二进制

```

| 工具             | 用途                         | 典型命令                                   |
| -------------- | -------------------------- | -------------------------------------- |
| wasm2wat       | 二进制转文本（.wat），逆向第一步         | wasm2wat app.wasm -o app.wat           |
| wat2wasm       | 文本转二进制（改完 wat 后回编验证）       | wat2wasm app.wat -o app.wasm           |
| wasm-decompile | 反编译为类 C 伪代码，可读性远高于 wat     | wasm-decompile app.wasm -o app.dcmp    |
| wasm-objdump   | 看段/反汇编/十六进制，类 UNIX objdump | wasm-objdump -x app.wasm               |
| wasm-strip     | 删除自定义段（如 name 段）减小体积       | wasm-strip app.wasm                    |
| wasm-validate  | 校验二进制是否合法                  | wasm-validate app.wasm                 |
| wasm-interp    | 栈式解释器，直接跑 wasm             | wasm-interp app.wasm --run-all-exports |
| wasm2c         | wasm 转 C 源码 + 头文件          | wasm2c app.wasm -o app.c               |

`wasm-objdump` 选项表（逆向最常用）：

| 选项  | 长选项                | 含义                                                   |
| --- | ------------------ | ---------------------------------------------------- |
| \-x | \--details         | 显示各段详情（imports/exports/functions/globals/memory 等结构） |
| \-d | \--disassemble     | 反汇编函数体                                               |
| \-h | \--headers         | 打印段头（每段的偏移和大小）                                       |
| \-s | \--full-contents   | 打印原始段内容（十六进制），找 Data 段常量用                            |
| \-j | \--section=SECTION | 只看某一段                                                |
| \-r | \--reloc           | 反汇编时内联显示重定位                                          |
| 无   | \--section-offsets | 用段偏移而非文件偏移                                           |

常用组合：`wasm-objdump -x app.wasm` 先看全貌结构，再 `-d` 反汇编关键函数。`wasm-decompile` 的伪代码（V8 团队出品）比 wat 更接近源码语义，是无源码场景下最快的读懂逻辑手段。

### Binaryen

官方仓库 `WebAssembly/binaryen`。优化器，逆向价值在 `wasm2js`（转回 JS 调试）和 `wasm-dis`（反汇编）。

```bash
npm install binaryen         # Node 版命令行工具
brew install binaryen        # macOS

```

| 工具         | 用途                             | 典型命令                                   |
| ---------- | ------------------------------ | -------------------------------------- |
| wasm-opt   | wasm 转 wasm 优化器，跑各种 pass       | wasm-opt input.wasm -O3 -o out.wasm    |
| wasm2js    | wasm 转 JS，便于在 Node 里跑/调试/打断点   | wasm2js app.wasm -o app.mjs            |
| wasm-dis   | 二进制转文本（经 Binaryen IR，命名更可读）    | wasm-dis input.wasm -o output.wat      |
| wasm-as    | 文本转二进制（汇编）                     | wasm-as in.wat -o out.wasm             |
| wasm-merge | 合并多个 wasm，自动连接 import 与 export | wasm-merge a.wasm a b.wasm b -o m.wasm |

注意：`wasm-dis` 与 WABT 的 `wasm2wat` 区别——前者经过 IR 不保证 1:1 往返，但命名更可读；要高保真往返用 WABT。

### bytecodealliance/wasm-tools（Rust）

官方仓库 `bytecodealliance/wasm-tools`。Rust 写的低层 wasm 操控 CLI + 库，对 Component Model / WIT 支持最好，独有符号反修饰能力。

```bash
cargo install --locked wasm-tools     # 源码编译
cargo binstall wasm-tools             # 预编译产物

```

| 子命令       | 用途                                   |
| --------- | ------------------------------------ |
| print     | 二进制转文本（约等于 wasm2wat）                 |
| parse     | 文本转二进制（约等于 wat2wasm）                 |
| dump      | 打印二进制格式的详细调试信息（按字节注解）                |
| objdump   | 打印段头调试信息                             |
| demangle  | 反修饰 name 段里的 Rust/C++ 符号名（WABT 无此能力） |
| strip     | 删除自定义段                               |
| validate  | 校验 wasm                              |
| addr2line | 用 DWARF 把 wasm 偏移翻译成 文件:行号           |
| component | 操作组件（new/wit/embed/unbundle）         |
| metadata  | 查看/添加 name 与 producer 元数据            |

官方示例管道：

```bash
# 先反修饰符号名，再剥自定义段，最后看段头
wasm-tools demangle foo.wasm | wasm-tools strip | wasm-tools objdump

```

### 运行时（动态分析）

| 运行时      | 安装                                                                             | 黑盒调用命令                                        |
| -------- | ------------------------------------------------------------------------------ | --------------------------------------------- |
| wasmtime | cargo install wasmtime-cli / curl https://wasmtime.dev/install.sh -sSf \| bash | wasmtime run --invoke main app.wasm arg1 arg2 |
| wasmer   | cargo install wasmer-cli / curl https://get.wasmer.io -sSfL \| sh              | wasmer run ./app.wasm                         |

两者默认安全沙箱（secure by default），不给文件/网络权限，需显式授权（wasmtime 用 `--dir`，wasmer 用 `--dir`/`--net`/`--enable-all`）。`wasmtime explore foo.wasm` 生成 HTML 并排展示 wat 与机器码，看编译映射很有用。

### 反编译器 / 专业分析器

| 工具                           | 类型 | 说明                                                           |
| ---------------------------- | -- | ------------------------------------------------------------ |
| JEB Decompiler（PNF Software） | 商业 | 业界最佳 WASM 反编译，输出类 C 代码，JEB 5.36 已支持 WebAssembly 3.0；有在线 demo |
| Ghidra WASM 插件               | 开源 | nneonneo/ghidra-wasm-plugin，反汇编 + 反编译 + 交叉引用，可脚本化            |
| wasm-decompile               | 开源 | WABT 自带，伪代码反编译，无需大型框架，最轻量                                    |

### 选型表

| 需求                       | 推荐工具                                                            |
| ------------------------ | --------------------------------------------------------------- |
| 看二进制对应的 wat              | wasm2wat（高保真）/ wasm-dis（可读）/ wasm-tools print / Chrome DevTools |
| 看类 C 伪代码（快速理解逻辑）         | wasm-decompile（轻量）                                              |
| 最强反编译（无源码逆向）             | JEB（商业）/ Ghidra WASM 插件（免费）                                     |
| 看段结构 / imports / exports | wasm-objdump -x / wasm-tools objdump                            |
| 反汇编函数体                   | wasm-objdump -d                                                 |
| 看原始字节（hex）               | wasm-objdump -s / wasm-tools dump                               |
| 反修饰 Rust/C++ 符号名         | wasm-tools demangle                                             |
| 把 wasm 转成 JS 来调试         | wasm2js                                                         |
| 直接运行 / 黑盒探测导出函数          | wasmtime run --invoke / wasmer run / wasm-interp                |
| 源码级断点调试（带 DWARF）         | Chrome DevTools（\-g 编译）                                         |
| 偏移转源码行号                  | wasm-tools addr2line                                            |

实战推荐顺序：`wasm-decompile`（懂逻辑）→ `wasm2wat` / `wasm-objdump -x -d`（看细节）→ 需要时上 JEB/Ghidra 做深度反编译 → 动态阶段用 Chrome DevTools（带 `-g`）或 `wasmtime --invoke` 验证。

## 动态调试

### Chrome DevTools 调试 wat 与查看线性内存

页面加载 `.wasm` 时，即便没有调试信息，Sources 面板也会把二进制即时反汇编为 WAT 文本展示，整个模块合并显示在单个文件里，使用 `wasm://` 前缀标识。

| 操作   | 方法                                                            |
| ---- | ------------------------------------------------------------- |
| 下断点  | 在反汇编视图里点代码左侧的地址列（offset 列，形如 0x0042），刷新页面触发                   |
| 单步   | 命中后用 Step over / into / out 逐条 wasm 指令执行                      |
| 看调用栈 | 右侧 Debugger 面板的 Call Stack；无 DWARF 时帧名为 $funcN 或 name 段名字     |
| 看作用域 | Scope 区展示 Local（局部变量）、Global（全局）、Stack（操作数栈）、Module（含 memory） |

查看线性内存用 Memory Inspector（菜单 More Tools 打开，或在 Scope 里右键 `WebAssembly.Memory`/`ArrayBuffer` 选 Reveal in Memory Inspector）。它把内存按 Int8/Uint8、Int16/Uint16、Int32/Uint32、Pointer 32/64-bit、Float 32/64-bit 多类型解释，工具栏可切大小端（WASM 线性内存是小端），地址输入框输入 hex 地址（如 `0x00000008`）跳转。

### DWARF 与 source map

若编译时带 `-g`，Emscripten 会在 wasm 内嵌入 DWARF 调试信息。

```bash
# 内嵌 DWARF，可在 DevTools 调原始 C/C++ 源码
emcc -g source.cc -o app.html

# 分离调试信息（减小线上体积）
emcc -g source.cc -o app.html -gseparate-dwarf=debug.wasm -s SEPARATE_DWARF_URL=path/URL

# 生成轻量 source map（把 wat 行映回源码行）
emcc -gsource-map source.cc -o app.html

```

安装 Chrome 扩展 C/C++ DevTools Support (DWARF)（扩展 ID `pdcpmagijalfljmkmjngeonclgbbannb`），Chrome 114+ 无需开实验开关即可直接调原始 C/C++ 源码、源码行下断点、悬停看变量。

### JS 层 Hook importObject 与 instantiate

抓取实例、exports、memory 的核心 Hook，必须在目标 wasm 加载之前注入（油猴 `@run-at document-start`，或扩展 content script `"run_at": "document_start"` 注入主世界）。

```javascript
// hook.js —— 注入到页面主世界，必须早于 wasm 加载
(function () {
  const origInstantiate = WebAssembly.instantiate;
  const origInstantiateStreaming = WebAssembly.instantiateStreaming;

  window.__wasm = { instances: [], memory: null, exports: null };

  function captureResult(result) {
    // result 可能是 WebAssembly.Instance，或 { module, instance }
    const instance = result.instance || result;
    if (instance && instance.exports) {
      window.__wasm.exports = instance.exports;
      window.__wasm.instances.push(instance);
      if (instance.exports.memory instanceof WebAssembly.Memory) {
        window.__wasm.memory = instance.exports.memory;
      }
      console.log("[wasm-hook] exports:", Object.keys(instance.exports));
    }
    return result;
  }

  // 包裹 importObject，拦截 env 里所有导入函数并打印入参/返回
  function wrapImportObject(importObject) {
    if (!importObject) return importObject;
    const out = {};
    for (const ns of Object.keys(importObject)) {      // 通常是 "env"、"wasi_snapshot_preview1"
      const mod = importObject[ns];
      out[ns] = {};
      for (const name of Object.keys(mod)) {
        const val = mod[name];
        if (typeof val === "function") {
          out[ns][name] = function (...args) {
            console.log(`[import] ${ns}.${name}(`, args, ")");
            const ret = val.apply(this, args);
            return ret;
          };
        } else {
          out[ns][name] = val;                          // Memory/Table/Global 原样传递
        }
      }
    }
    return out;
  }

  WebAssembly.instantiate = function (bufferOrModule, importObject, ...rest) {
    return origInstantiate
      .call(this, bufferOrModule, wrapImportObject(importObject), ...rest)
      .then(captureResult);
  };

  // 注意：streaming 走 Response 流，无法改字节，只能包 importObject 和抓 exports
  WebAssembly.instantiateStreaming = function (source, importObject, ...rest) {
    return origInstantiateStreaming
      .call(this, source, wrapImportObject(importObject), ...rest)
      .then(captureResult);
  };
})();

```

读 WASM 内存里的 C 字符串（注意每次重建视图，见常见陷阱）：

```javascript
// 从 window.__wasm.memory 读以 NUL 结尾的字符串
function wasmReadCString(ptr, maxLen = 4096) {
  // memory.buffer 是 ArrayBuffer；grow 后旧 buffer 会 detach，所以每次重新取
  const u8 = new Uint8Array(window.__wasm.memory.buffer);
  let end = ptr;
  while (end < ptr + maxLen && u8[end] !== 0) end++;
  return new TextDecoder("utf-8").decode(u8.subarray(ptr, end));
}

```

## 实战：抽取 wasm 与在 Node 黑盒调用

### 从网站抽取 .wasm

方法 A 最简单：F12 → Network 面板，过滤 `wasm`，找到目标请求右键 Save as，落盘为 `sign.wasm`。

方法 B 最可靠（适用于运行时解密/拼接的 wasm）：在页面最早期 Hook 编译入口，从字节落盘。关键是 `instantiateStreaming` 走 Response 流，必须 `clone()` 后才能既 dump 又给 WASM 用（流只能消费一次）。

```javascript
// dump-wasm.js —— @run-at document-start 注入，截获进入编译的字节并下载
(function () {
  function dump(bytes, tag) {
    const buf = bytes instanceof ArrayBuffer ? bytes
              : ArrayBuffer.isView(bytes) ? bytes.buffer : null;
    if (!buf) return;
    const a = document.createElement("a");
    a.href = URL.createObjectURL(new Blob([buf], { type: "application/wasm" }));
    a.download = tag + "_" + buf.byteLength + ".wasm";
    a.click();
  }

  const _inst = WebAssembly.instantiate;
  WebAssembly.instantiate = function (bytes, ...rest) {
    if (!(bytes instanceof WebAssembly.Module)) dump(bytes, "instantiate");
    return _inst.call(this, bytes, ...rest);
  };

  const _instS = WebAssembly.instantiateStreaming;
  if (_instS) {
    WebAssembly.instantiateStreaming = async function (src, ...rest) {
      const resp = await src;
      resp.clone().arrayBuffer().then(b => dump(b, "stream"));   // 必须 clone
      return _instS.call(this, resp, ...rest);
    };
  }
})();

```

落盘后先看头 4 字节是否为 `00 61 73 6d`（`\0asm`）确认是裸 wasm，是则可直接 `wasm2wat`。

### 判断 wasm 形态决定复现路径

| 形态                       | 特征                                                                                  | 复现方式             |
| ------------------------ | ----------------------------------------------------------------------------------- | ---------------- |
| Emscripten 模块            | 同目录有胶水 JS（Module/onRuntimeInitialized/ccall/\_malloc）；import 段大量 env.emscripten\_\* | 直接复用胶水 JS，最省事    |
| 裸 SIDE\_MODULE / 手写 wasm | import 少而干净（仅 env.memory、几个 env.\_\_\*）；无胶水 JS                                      | 手工补 import 垫片    |
| 字节码 VM（抖音 bdms 风格）       | 导出极少，内部是解释器循环，算法是被解释执行的字节码数据                                                        | 仍黑盒调用导出，别反推字节码语义 |

用 `wasm-objdump -x sign.wasm` 看 import/export 段：`(import "env" "...")` 列出必须在 importObject 里补齐的垫片，`(export "...")` 列出能调用的函数。

### Node 手工加载裸 wasm + import 垫片

`WebAssembly.instantiate(bufferSource, importObject)` 返回 Promise 解析为 `{ module, instance }`；`importObject` 每个声明 import 都必须有对应属性，否则抛 `LinkError`——报错会精确告诉你缺哪个，这是补垫片的导航。`new WebAssembly.Memory({initial, maximum})` 单位是页（1 页 = 64 KiB）。

```javascript
// load_wasm.js  (Node 18+，WebAssembly 是全局对象)
const fs = require("fs");

async function main() {
  const bytes = fs.readFileSync("./sign.wasm");

  // initial/maximum 看 wasm2wat 里的 (memory ...) / (table ...)
  const memory = new WebAssembly.Memory({ initial: 256, maximum: 32768 });
  const table = new WebAssembly.Table({ initial: 8, element: "anyfunc" });

  const importObject = buildImports(memory, table);
  const { instance } = await WebAssembly.instantiate(bytes, importObject);
  console.log("exports:", Object.keys(instance.exports));
}

// 按 LinkError 报缺什么就补什么，不要一次性全填
function buildImports(memory, table) {
  let HEAPU8 = new Uint8Array(memory.buffer);
  const refresh = () => { HEAPU8 = new Uint8Array(memory.buffer); };
  const env = {
    memory, table, __indirect_function_table: table,
    __memory_base: 0, __table_base: 0,
    __stack_pointer: new WebAssembly.Global({ value: "i32", mutable: true }, 5 * 1024 * 1024),
    abort: (w) => { throw new Error("wasm abort: " + w); },
    __assert_fail: () => { throw new Error("assert_fail"); },
    // 内存拷贝类用 copyWithin 实现；旧版符号名 emscripten_memcpy_big，
    // Emscripten 3.1.43+（含当前 6.x）改名为 emscripten_memcpy_js，两个都补上最稳妥
    emscripten_memcpy_big: (dest, src, num) => { refresh(); HEAPU8.copyWithin(dest, src, src + num); return dest; },
    emscripten_memcpy_js: (dest, src, num) => { refresh(); HEAPU8.copyWithin(dest, src, src + num); return dest; },
    emscripten_resize_heap: () => false,
    // 时间/随机务必给确定值，便于复现
    emscripten_get_now: () => Date.now(),
    emscripten_random: () => Math.random(),
    // 无关 syscall 直接返回 0
    setTempRet0: () => {}, getTempRet0: () => 0,
    __syscall_openat: () => -1,
  };
  return {
    env,
    wasi_snapshot_preview1: { fd_write: () => 0, fd_close: () => 0, proc_exit: (c) => { throw new Error("proc_exit " + c); } },
    "GOT.mem": {}, "GOT.func": {}, global: { NaN: NaN, Infinity: Infinity },
  };
}
main().catch(console.error);

```

补垫片的实战节奏：先只给 `memory`/`table`/`abort` 跑，看 `LinkError` 报缺哪个 `env.xxx`，回去补一个再跑，循环到不报错为止。绝大多数缺失要么是内存拷贝（用 `copyWithin`），要么是无关 syscall（返回 0 / throw）。

### Emscripten 模块复现：直接用胶水 JS

如果抽 `.wasm` 时同时拿到胶水 JS，不要自己补 import——直接复用它生成的运行时。需要三件套：`locateFile`（告诉运行时去哪找 `.wasm`）、`onRuntimeInitialized`（实例化完成回调，必须等它触发后才能调导出）、`Module` 预置对象。

```javascript
// run_emscripten.js  —— 老式（非 MODULARIZE）胶水 JS
const path = require("path");

// 在 require 胶水 JS 之前预置全局 Module 配置
global.Module = {
  locateFile: (file) => path.join(__dirname, file),    // 让它找到本地 sign.wasm
  onRuntimeInitialized: () => {
    callSign();                                          // 在回调里调用，保证已就绪
  },
};
require("./glue.js");   // 执行胶水 JS，它读 Module、实例化 wasm、触发回调

```

### 调用导出函数的完整骨架

典型 C 签名 `char* sign(const char* input)`：入参是字符串指针、返回结果字符串指针。流程：`_malloc` 写入参 → 调用 → `UTF8ToString` 读返回 → `_free`。

```javascript
function callSign() {
  // 方式 1：cwrap（推荐，包一次反复用），'string' 入参自动 malloc+写堆+调用+free
  const sign = Module.cwrap("sign", "string", ["string"]);
  console.log(sign("hello_payload"));

  // 方式 2：ccall（一次性调用）
  const out = Module.ccall("sign", "string", ["string"], ["hello_payload"]);
  console.log(out);

  // 方式 3：手动管内存（传二进制 / 自己解析返回指针时）
  const input = "hello_payload";
  const nBytes = Module.lengthBytesUTF8(input) + 1;     // +1 给 \0
  const inPtr = Module._malloc(nBytes);                 // 在 wasm 堆上分配
  Module.stringToUTF8(input, inPtr, nBytes);            // 写入 UTF8（带终止符）

  const retPtr = Module._sign(inPtr);                   // 调底层导出（带下划线），返回 char* 指针
  const result = Module.UTF8ToString(retPtr);           // 从指针读字符串（按 \0 截断）
  console.log("sign result:", result);

  Module._free(inPtr);
  // 返回指针是否要 free 取决于 C 端：内部 malloc 的要 _free(retPtr)，静态缓冲区则不要
}

```

二进制入参/出参不用 `'string'`，用 `HEAPU8.set` 写、`HEAPU8.slice` 读：

```javascript
function callBinary(inputBytes /* Uint8Array */) {
  const inPtr = Module._malloc(inputBytes.length);
  Module.HEAPU8.set(inputBytes, inPtr);                 // 写入参
  const outLen = 32;
  const outPtr = Module._malloc(outLen);
  Module._hash(inPtr, inputBytes.length, outPtr);       // void hash(in, len, out)
  // grow 后视图会失效，读之前重取 Module.HEAPU8
  const out = Module.HEAPU8.slice(outPtr, outPtr + outLen);   // 拷出，脱离 wasm 堆
  Module._free(inPtr);
  Module._free(outPtr);
  return out;
}

```

### wasm2js 转 JS 后在 Node 调试

把不可读的二进制转成可断点的 JS，每条 wasm 指令变成具名 JS 语句，比 wat 易读、易插桩。

```bash
wasm2js secret.wasm -o secret.mjs --debug     # 转换（输入可为 .wasm 或 .wat）
node --inspect-brk run.mjs                     # 首行暂停，等 attach 后再继续
# 打开 chrome://inspect -> Open dedicated DevTools for Node -> Sources 里给函数下断点

```

局限：浮点/trap 语义在纯 JS 下是模拟的（`--pedantic` 可逼近但不完全等价），大模块转出体积大、性能低，仅用于分析而非生产。

## WASM 内加密算法的判别与还原

两条路，强烈优先黑盒调用：不管它内部是 MD5/AES/自研，只要能"喂相同输入得相同输出"就够了。验证方法：在浏览器对同一输入观察真实输出，与 Node 复现结果逐字节对比，一致即收工。抖音 a\_bogus/bdms 推荐走这条。

实在要看算法时才反编译。加密算法即便编译进 WASM，其魔法常量/S-Box 通常原样躺在 Data 段（ID 11）或字节码立即数里，可直接搜出来。先 `wasm2wat module.wasm -o module.wat` 或 `wasm-objdump -s` 转 hex，再搜常量。

### 哈希算法初始向量/常量

字节序两头都搜（大端原值 + 小端反转，如 `67452301` / `01234567`）。

| 算法        | 标志常量                                                         | 说明                                  |
| --------- | ------------------------------------------------------------ | ----------------------------------- |
| MD5       | 0x67452301、0xefcdab89、0x98badcfe、0x10325476                  | 4 个初始 IV（A/B/C/D），0x67452301 是最经典指纹 |
| SHA-1     | 前 4 个同 MD5，多出第 5 个 0xC3D2E1F0                                | 0xC3D2E1F0 是区分 SHA-1 与 MD5 的关键      |
| SHA-256   | 0x6a09e667、0xbb67ae85 等 8 个 IV；常量表 K 以 0x428a2f98 开头         | 0x428a2f98 辨识度极高，优先搜                |
| SHA-512   | 0x6a09e667f3bcc908 等 64 位 IV                                 | 64 位版本                              |
| SM3（国密哈希） | IV 0x7380166f 开头；常量 T 0x79cc4519（0≤j≤15）、0x7a879d8a（16≤j≤63） | 0x7380166f 和 0x79cc4519 是 SM3 铁证    |

### 对称加密 S-Box / 常量

| 算法        | 标志                                                             | 说明                                  |
| --------- | -------------------------------------------------------------- | ----------------------------------- |
| AES       | 正向 S-Box 以 63 7c 77 7b f2 6b 6f c5 开头；逆 S-Box 以 52 09 6a d5 开头 | 256 字节 S-Box 在 Data 段最常见，搜 637c777b |
| SM4（国密对称） | S-Box 以 d6 90 e9 fe cc e1 3d b7 开头；系统参数 FK 首项 0xa3b1bac6       | 搜 d690e9fe 或 0xa3b1bac6             |
| DES/3DES  | IP/FP 置换表、S1–S8 盒（值域 0–15）                                     | 特征是大量小数值表                           |
| Base64    | 字符表 ABCD...wxyz0123456789+/（URL 变体末尾 \-\_）                     | Data 段里直接可见，最易识别                    |
| RC4       | 无固定常量，但有 for i in 0..256: S\[i\]=i 初始化模式                       | 看代码逻辑                               |

### 导出名与字符串线索

`name` 自定义段（若未 strip）或 Export 段可能直接暴露：`encrypt`/`decrypt`/`sign`/`verify`/`hash`/`md5`/`sha256`/`sm3`/`sm4`/`aes`/`getSign`/`makeToken`，C++ 修饰名如 `__Z7encryptPKc`。Data 段明文常泄露算法描述（`"AES/CBC/PKCS5Padding"`）、硬编码 key/iv、错误信息。

### 判别流程清单

```text
1. 确认是 WASM：前 8 字节 00 61 73 6D 01 00 00 00 / 搜 AGFzbQ
2. wasm2wat module.wasm -o module.wat（或浏览器 DevTools 看反汇编）
3. 在 wat / hex 里搜魔法常量：
   [ ] 67452301        -> MD5 或 SHA-1
   [ ] C3D2E1F0        -> 确认是 SHA-1（而非 MD5）
   [ ] 6a09e667 / 428a2f98 -> SHA-256
   [ ] 7380166f / 79cc4519 -> SM3
   [ ] 637c777b        -> AES S-Box
   [ ] d690e9fe / a3b1bac6 -> SM4
   [ ] ABCDEFGH...+/   -> Base64 字符表
4. 搜 Export 段 / name 段函数名：encrypt/sign/md5/sm3 ...
5. 搜 Data 段明文字符串：算法描述、硬编码 key/iv
6. 字节序两头都搜（大端原值 + 小端反转）
7. 定位到 Code 段对应函数，结合 Hook 验证输入输出

```

## 最佳实践

1. **优先黑盒调用，不要硬扣算法。** WASM 经过编译优化和混淆，逐行翻译成本极高且易错；只要在 Node 复现"相同输入产生相同输出"就满足签名需求。把 `.wasm` 当 `.so` 加载即可。  
```javascript  
const sign = Module.cwrap("sign", "string", ["string"]);  
console.log(sign("payload"));   // 与浏览器输出逐字节对比，一致即收工  
```
2. **先看导出与导入再动手。** 用 `wasm-objdump -x` 先弄清模块暴露了哪些函数、依赖哪些环境，决定复现路径和补垫片清单，避免盲目反汇编 VM 循环。  
```bash  
wasm-objdump -x sign.wasm     # Export 段告诉你能调什么，Import 段告诉你要补什么  
```
3. **每次访问内存都从 buffer 重建视图，不要缓存。** `memory.grow()`（如 `-sALLOW_MEMORY_GROWTH`）会让旧 `ArrayBuffer` detach，缓存的 TypedArray 失效。  
```javascript  
function readByte(ptr) {  
  return new Uint8Array(window.__wasm.memory.buffer)[ptr];   // 每次重建  
}  
```
4. **Hook 必须在 wasm 加载前注入。** `instantiate`/`instantiateStreaming` 一旦执行就抓不到了，用 `@run-at document-start` 或 content script `"run_at": "document_start"` 注入主世界。streaming 走 Response 流，dump 字节前必须 `clone()`。  
```javascript  
resp.clone().arrayBuffer().then(b => dump(b));   // clone 后流才能既 dump 又给 WASM  
```
5. **复现时给环境依赖确定值。** 算法若读 `Date.now`/`Math.random`/`navigator` 指纹，输出会随环境变化。在垫片里固定这些值，必要时配合补环境（见 [补环境](https://blog.vercanti.com/bu-huan-jing/)）。  
```javascript  
env.emscripten_get_now = () => 1700000000000;   // 固定时间确保可复现  
```
6. **用魔法常量快速判定算法族再决定要不要手翻。** 搜到 `637c777b`（AES）或 `6a09e667`（SHA-256）就知道是标准算法，可直接用现成库验证，不必逐行还原。  
```bash  
wasm2wat app.wasm -o app.wat   # 然后在 app.wat 里搜 637c777b / 6a09e667 / 7380166f  
```

## 常见陷阱

### 陷阱一：memory.grow 后旧视图失效

- **现象**：调用一次导出函数后再读内存，`HEAPU8.length` 变 0 或读到全 0，报 `RuntimeError: memory access out of bounds`。
- **原因**：模块用 `-sALLOW_MEMORY_GROWTH`，内部 `memory.grow()` 让旧 `ArrayBuffer` detach（`byteLength` 归零），所有缓存的 TypedArray 视图失效。
- **解决**：每次访问前从 `memory.buffer` / `Module.HEAPU8` 重新建视图，绝不缓存。  
```javascript  
// 错误：缓存视图，grow 后失效  
const HEAPU8 = new Uint8Array(memory.buffer);  
function read(ptr) { return HEAPU8[ptr]; }   // 错误：grow 后 HEAPU8 已 detach  
// 正确：每次重建  
function read(ptr) { return new Uint8Array(memory.buffer)[ptr]; }  
```

### 陷阱二：LinkError 缺少 import

- **现象**：`WebAssembly.instantiate` 抛 `LinkError: Import #N "env" "emscripten_memcpy_big": function import requires a callable`（Emscripten 3.1.43+ 产物里该符号名为 `emscripten_memcpy_js`）。
- **原因**：手工加载裸 wasm 时，importObject 没提供模块声明的某个导入符号。
- **解决**：报错精确指出缺哪个，按需补一个再跑，循环到不报错。内存拷贝类用 `copyWithin`，无关 syscall 返回 0 或 throw。  
```javascript  
// 按 LinkError 提示补：内存拷贝用 copyWithin，杂项 syscall 返回 0  
// 旧版符号名 emscripten_memcpy_big；Emscripten 3.1.43+（含 6.x）为 emscripten_memcpy_js  
const memcpy = (dest, src, num) => {  
  new Uint8Array(memory.buffer).copyWithin(dest, src, src + num);  
  return dest;  
};  
env.emscripten_memcpy_big = memcpy;  
env.emscripten_memcpy_js = memcpy;  
env.__syscall_fcntl64 = () => 0;  
```

### 陷阱三：Module.ccall / Module.\_xxx 不存在

- **现象**：`TypeError: Module.ccall is not a function`，或 `Module._sign is not a function`。
- **原因**：编译时未导出运行时方法（`ccall` 等需 `-sEXPORTED_RUNTIME_METHODS`）或函数（`_sign` 需 `-sEXPORTED_FUNCTIONS`）；也可能是导出名带不带下划线弄反了。
- **解决**：用 `wasm-objdump -x` 确认真实导出名；若运行时方法没导出，改走手动 `_malloc + Module._fn + UTF8ToString` 路径。  
```javascript  
// ccall 不存在时，直接调底层导出（注意带下划线前缀）  
const inPtr = Module._malloc(16);  
Module.stringToUTF8("data", inPtr, 16);  
const retPtr = Module._sign(inPtr);          // 导出名以 wasm-objdump -x 确认  
console.log(Module.UTF8ToString(retPtr));  
Module._free(inPtr);  
```

### 陷阱四：instantiateStreaming 在 Node 报 source must be Response

- **现象**：Node 里跑 Emscripten 胶水，streaming 路径报 `source must be Response`。
- **原因**：Node 内置 undici 的 `Response` 与外部 npm 装的 `undici` 实例不互认。
- **解决**：用 `Module.wasmBinary` 注入字节，走非 streaming 路径。  
```javascript  
const fs = require("fs");  
global.Module = { wasmBinary: fs.readFileSync("./sign.wasm") };   // 跳过 fetch/streaming  
require("./glue.js");  
```

## 参见

- [加密算法识别](https://blog.vercanti.com/jia-mi-suan-fa-shi-bie/)
- [补环境](https://blog.vercanti.com/bu-huan-jing/)
- [unidbg基础](https://blog.vercanti.com/unidbg-ji-chu/)
- [Webpack逆向还原](https://blog.vercanti.com/webpack-ni-xiang-huan-yuan/)
- [js逆向调试技巧](https://blog.vercanti.com/js-ni-xiang-diao-shi-ji-qiao/)
- [混淆还原](https://blog.vercanti.com/hun-yao-huan-yuan/)
- [Node.js运行环境](https://blog.vercanti.com/node-js-yun-xing-huan-jing/)
- [魔法数字速查](https://blog.vercanti.com/jia-mi-suan-fa-mo-fa-shu-zi-su-cha/)
- [JS逆向入门指南](https://blog.vercanti.com/js-ni-xiang-ru-men-zhi-nan/)