一、WASM 是什么?
WebAssembly (WASM) 是一种运行在浏览器中的“类二进制代码”,它可以使用如 C/C++/Rust 编写,编译为 .wasm
文件,被 JS 调用执行。其主要优势:
运行速度快(接近原生)
加密逻辑隐蔽(不容易直接查看源码)
可与 JavaScript 双向交互
二、WASM + JS 混合逆向整体思路图
[网页源码]
│
└──> [JS逻辑调度层]
│
├── import .wasm 文件
├── 调用 wasm 导出函数
└── 处理参数/结果(加密/解密)
│
↓
[.wasm 文件](真正加密/校验逻辑)
我们分析的目标:搞懂 JS 调用了哪些 wasm 函数、传了什么参数、返回了什么值,是否涉及加密、签名或校验逻辑。
三、常见混合场景举例
场景 | 描述 |
---|---|
滑动验证码验证 | wasm 处理行为轨迹 hash 签名 |
参数加密处理 | JS + wasm 混合加密最终请求参数 |
游戏逻辑校验 | wasm 实现核心计算(如坐标验证) |
防爬参数计算 | 加密算法藏在 wasm,JS 仅为中转 |
四、实战分析流程
从初始页面到还原算法,我们以一个真实例子为线索展开(如极验、某电商参数加密):
步骤 1:识别 WASM 文件
方法:
打开 Chrome DevTools →
Network
面板 → 过滤.wasm
或
Sources
→WASM
区看到模块名(如main.wasm
)搜索 JS 关键字:
WebAssembly.instantiateStreaming()
WebAssembly.instantiate()
WebAssembly.compile()
有时候 wasm 会 base64 编码后再 decode 导入,也要留心。
步骤 2:找 JS 调用 wasm 的代码
这步是关键中的关键!
搜索
.exports.
,如:
instance.exports.encrypt(a, b);
- 或是通过中转函数:
let wasmFunc = Module.cwrap("encrypt", "string", ["string"]);
let result = wasmFunc("abc123");
这时我们要Hook这些调用点,打印 wasm 函数名、参数与返回值。
步骤 3:在浏览器中 Hook wasm 调用(调试级别)
方法 1(常规断点):
// 设置断点在 JS 中的中转调用点
instance.exports.encrypt()
方法 2(高级)用 Chrome 的 DevTools → WebAssembly
面板:
查看所有导出函数
添加 breakpoint 调试
甚至可以单步进入 wasm 指令层(类似汇编)
方法 3(控制台打印):
let oldFunc = instance.exports.encrypt; // 保存原始的 WebAssembly 导出函数 encrypt,防止被覆盖后无法调用原始逻辑
instance.exports.encrypt = function(a, b) { // 用自定义函数覆盖 encrypt,实现 hook 操作
console.log("[WASM Call] a =", a, "b =", b); // 打印输入参数,用于观察调用时传入的值
let ret = oldFunc(a, b); // 调用原始 encrypt 函数,获取加密结果
console.log("result =", ret); // 打印加密结果,用于调试返回值
return ret; // 返回原始函数的结果,保持原有功能不变
}
作用:拦截 WASM 的 encrypt
函数调用,在调用前后打印参数与结果,方便调试或逆向分析。
步骤 4:反编译 .wasm 文件
对于核心算法的还原,需要反编译 .wasm 二进制文件。
方式一:使用 wasm2wat 转为人类可读的 WAT 格式
安装 wabt 工具:
wasm2wat main.wasm -o main.wat
转为 .wat
后,可以看到类似汇编风格的中间代码:
(func $encrypt (param $p0 i32) (result i32)
...
i32.const 32
call $malloc
...
return
)
- 可以看到函数名
$encrypt
、变量、常量、调用关系
方式二:用 Ghidra 分析 .wasm(图形化反汇编)
安装 Ghidra
导入
.wasm
文件自动识别函数结构
可重命名函数并跟踪调用链
还能 patch 重编译
步骤 5:动态还原算法流程
通过 JS 确认入口函数和参数含义
在浏览器中下断点,观察加密前的原始参数
通过反编译观察关键计算逻辑
将 WASM 算法还原为 JS/py 代码(脱离浏览器使用)
五、最终目标:还原加密算法 / 重写 wasm 逻辑
例如一个混合加密的场景(简化):
let wasm = await WebAssembly.instantiate(...); // 异步实例化一个 WebAssembly 模块,返回 { module, instance } 对象
let base = "abc123"; // 定义一个输入字符串,可能是要加密或参与加密的原始数据
let sig = wasm.instance.exports.encrypt(base.length, getPointer(base));
// 调用 WASM 中导出的 encrypt 函数:传入两个参数:
// - base.length 是字符串的长度(即 6),通常传给加密函数让其知道要处理多少字节
// - getPointer(base) 是一个辅助函数,用于将 JS 字符串写入 WASM 内存,并返回它在 WASM 内存中的偏移地址(指针)
// 返回值 sig 是 encrypt 的执行结果,通常是加密后的值(可能是数字、指针、或者通过内存读取后的数据)
我们最终目标就是:
在 Python 或 Node.js 中模拟
encrypt()
的逻辑不再依赖 wasm 文件或 JS 页面
getPointer(base) 通常是这样定义的,用于将 JS 的字符串数据写入 WebAssembly 的内存:
function getPointer(str) {
const encoder = new TextEncoder(); // 创建一个 UTF-8 编码器
const bytes = encoder.encode(str); // 将字符串编码成 Uint8Array(二进制字节)
const ptr = wasm.instance.exports.malloc(bytes.length); // 调用 WASM 导出的 malloc 函数申请内存空间
const mem = new Uint8Array(wasm.instance.exports.memory.buffer, ptr, bytes.length);
// 从 WASM 的 memory 中获取内存视图,范围是 [ptr, ptr+length)
mem.set(bytes); // 将字符串字节写入 WASM 内存
return ptr; // 返回在 WASM 内存中的起始地址(用于作为 encrypt 参数)
}
小技巧与实战建议
常见 WASM 导出函数名:
_encrypt
,_decode
,_crc
,_check_sig
,verify
,hashX
函数名有时会被混淆,但参数顺序/数量往往可推断含义
JS 与 WASM 通信通常通过
Linear Memory
,需分析getUint8Array
/getStringFromPointer
类函数可以用 Frida 等工具实现 Hook 并动态替换返回值绕过 wasm 加密