第 28 章 WebAssembly 逆向专项
WASM(WebAssembly)正在成为前端、游戏、云函数的新主流"二进制格式"。它既是 Web 技术又是二进制格式——逆向方法介于"读 JS"和"逆原生"之间。本章讲清 WASM 的结构、工具链和逆向流程。
📍 知识点地图 | 主题:WASM逆向 | 前置:第3章 | 后续:— | 核心概念:栈机、wasm2wat、wasm-decompile、name段
28.1 WASM 是什么
定义: 浏览器可执行的紧凑二进制格式(.wasm),由 C/C++/Rust/Go 等编译而来
特点:
- 不是汇编,是"结构化指令集"(有明确的块结构,不是线性指令流)
- 类型系统简单: i32/i64/f32/f64
- 无寄存器,用"栈机"模型(指令操作值栈)
- 导出/导入函数机制(与 JS 互操作)
出现场景:
- 高性能前端(游戏/图像处理/加密)
- 反爬虫/风控(把 JS 校验逻辑编译成 WASM 藏起来)
- 云函数/边缘计算
- CTF 题目(WASM 逆向题逐渐变多)
28.2 为什么 WASM 逆向与原生不同(核心知识点)
| 对比 | 原生二进制 | WASM |
|---|---|---|
| 指令 | 线性汇编 | 结构化(块/循环/分支标签) |
| 寄存器 | 真实寄存器 | 值栈 + 局部变量 |
| 内存 | 进程内存 | 显式线性内存(memory.grow) |
| 函数 | 地址调用 | 索引/导出名调用 |
| 导入 | 动态链接 | 显式 import 段(与 JS 接口) |
| 调试信息 | DWARF(可选) | name 段(保留函数名!) |
| 反编译 | 伪 C 质量参差 | 可还原成接近原 C(结构化优势) |
关键认知:WASM 保留了函数块结构,反编译质量通常远好于同复杂度的原生二进制。
28.3 WASM 文件结构
.wasm 二进制 = 一系列 section:
type 函数签名表
import 导入(JS 函数/内存/表)
function 函数索引
table/export 导出(函数/内存/全局)
code 函数体(栈机指令)
name 函数名(编译时可选保留 ← 金矿!)
data 数据段(字符串/常量)
custom 自定义段(sourceMappingURL 等)
第一件事永远是检查 name 段——很多 WASM 未 strip,函数名直接可见,等于"半开源"。
28.4 工具链
| 工具 | 用途 |
|---|---|
| wabt(wasm2wat/wasm2c) | 官方工具:WASM ↔ WAT 文本互转 |
| Binaryen(wasm-opt) | 优化/去混淆 |
| Ghidra | WASM 反编译(内置支持,输出 C) |
| wasm-decompile(Binaryen 内) | 输出"类 C"伪代码(最高效) |
| wasmtime / wasmer | 运行 WASM(脱离浏览器) |
| wasm-objdump | 查看段结构 |
28.5 WASM 逆向标准流程
Step 1: 侦察
wasm-objdump -h target.wasm # 段概览
wasm-objdump -x target.wasm # 详细(imports/exports/name)
strings target.wasm # 字符串(含函数名线索)
→ 检查 name 段!很多题到这就结束了
Step 2: 转 WAT 文本
wasm2wat target.wasm -o target.wat # 可读的指令文本
# 或直接用 Binaryen:
wasm-decompile target.wasm -o target.dcmp # 输出类 C 伪代码
Step 3: 找入口
从 exports 找到导出的 main/校验函数
→ 从 import 看它依赖 JS 提供什么(加密函数?环境变量?)
Step 4: 还原逻辑
优先 wasm-decompile 输出(最接近 C)
复杂函数 → Ghidra 加载 .wasm 反编译(支持现代 WASM 特性)
→ 结合 strings/常量表还原算法
Step 5: 验证
用 wasmtime 直接运行(可传参/打印输出)
或重放算法(Node/Python 复现)
28.6 WAT 文本阅读基础(能读懂即可)
;; 一个函数: 输入两个 i32,返回 i32
(func $add (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.add
)
;; 比较分支
(func $check (param $x i32) (result i32)
local.get $x
i32.const 42
i32.eq ;; x == 42 压入栈 0/1
if (result i32) ;; 结构化分支!
i32.const 1
else
i32.const 0
end
)
阅读要点:
栈机: 指令消费栈顶值、压入结果(local.get 压值,i32.add 弹两压一)
结构化: if/loop/block 有明确的 end,控制流一目了然(比线性汇编好读)
28.7 常见 WASM 逆向场景
场景 1:风控/反爬 WASM(最常见)
形态: 网站把签名/加密逻辑编译成 WASM,JS 调用它生成参数
分析:
1. 找 import 段 → 看它向 JS 要了什么(随机数? 时间? 密钥?)
2. 找导出函数 → 用 wasmtime 直接调用它复现参数
3. 还原算法 → 用 Node/Python 重写(比浏览器内跑更可控)
4. 验证: 生成的参数能被服务端接受
场景 2:CTF WASM 题
形态: 直接给 .wasm,输入 flag 校验
常见套路:
- name 段保留 → 函数名直读(最简单的题)
- name 段被删 → 从字符串/常量/导出名推断
- 逻辑藏在线性内存数据段(数组/表)
- 用 JS 调 WASM(.js 里封装校验逻辑)→ 先看 JS!
解法:
1. wasm-decompile 还原伪代码(90% 的题到这步就出答案)
2. 复杂算法 → Ghidra 反编译或 wasm2c + 传统逆向
3. 直接调用: wasmtime 跑起来观察行为
场景 3:游戏/高性能前端
形态: C/Rust 编译的游戏逻辑、图像处理
分析: 和原生逆向类似但更友好(结构保留)
注意: 大 WASM 的 data 段可能包含资源/字符串表
28.8 WASM 混淆与对抗
| 混淆手段 | 特征 | 应对 |
|---|---|---|
| strip name 段 | 函数名消失 | 从字符串/导出/常量推断 |
| 控制流扁平化 | 大量 block/br 嵌套 | wasm-opt 优化 / 手工还原 |
| 常量加密 | 常量藏在线性内存 | 运行观察 / 动态解密 |
| import 隐藏 | 关键逻辑依赖 JS | 先逆 JS 部分 |
| 多模块 | 拆分多个 wasm | 逐个分析 + 找交互接口 |
28.9 决策树
拿到 .wasm
├─ name 段在? → 函数名直读,直接看主逻辑
├─ 有 .js 配套? → 先逆 JS(调用关系是文档)
├─ 逻辑复杂度低 → wasm-decompile 还原伪代码
├─ 逻辑复杂/混淆 → Ghidra 反编译 + wasm2c
├─ 需要运行验证 → wasmtime/wasmer 直接跑
└─ 目标是复现签名 → 还原算法后 Node/Python 重写
动手练习
- 用 C 写一个 flag 校验程序编译成 WASM(
emcc或clang --target=wasm32),用 wasm-decompile 还原并解出 flag。 - 找一个真实网站的风控 WASM(浏览器 Network 面板搜 .wasm),分析它的 import/export 结构。
- 练习 WAT 阅读:把一段简单的 add/check 逻辑从 wasm2wat 输出翻译成 C 语言。
- 用 Ghidra 打开一个 .wasm,体验它的反编译输出质量。
深入阅读
- 仓库:
skills/reverse-engineering/tools.md(WASM 分析:decompile/C 转换/常见模式) - 仓库:
skills/reverse-engineering/languages.md(WASM 与字节码) - 仓库:
skills/reverse-engineering/field-notes.md(WASM patching 案例:WAT 翻转比较) - 仓库:
CTF-Sandbox-Orchestrator/competition-web-runtime/(Web 运行时 CTF 子技能)