When to use / when not to use
- Use: restoring logic from .wasm modules (web frontend encryption/validation, browser extensions, games, wasm payloads loaded by malicious web pages/supply chain JS)
- Use:
WebAssembly.instantiate/WebAssembly.Module/.wasmsamples where fetch appears—analyze wasm and JS glue together - Do not use: non-wasm (native goes to [[re-binary-core]], .NET goes to [[re-dotnet]], Java goes to [[re-java]], pure JS/scripts go to [[re-script-deob]])
- Do not use: when the wasm module itself is reasonably readable and you only need to read the logic—just
wasm2watread the WAT, no need for the full process - Note: dynamic execution defaults to a sandbox ([[re-analyze/platform-tips]] highest principle); wasm modules are relatively isolated, but the host JS glue calls sensitive APIs such as DOM/network—restore the JS side first ([[re-script-deob]])
Tool preparation
Refer to [[re-analyze/platform-tips]]—wasm parsing/decompilation are static steps, no sandbox needed; dynamic execution (wasmtime/wasm3/browser loading) follows the highest principle and goes into a sandbox (at least network-isolated).
WABT (wasm2wat / wasm-decompile / wasm-objdump, the main force for parsing and decompilation)
- Debian/Ubuntu:
apt install wabt;Fedora:dnf install wabt;Arch:pacman -S wabt - macOS:
brew install wabt;Windows:choco install wabtor GitHubWebAssembly/wabtrelease's bin directory - Verify:
wasm2wat --version && wasm-decompile --version && wasm-objdump --version
wasmtime (WASI CLI runtime, --invoke to directly call exported functions)
- Official installer (all platforms, installs to
~/.wasmtime/bin):curl https://wasmtime.dev/install.sh -sSf | bash - Debian/Ubuntu: no official package—use the official install script/binary (install.wasmtime.dev) or build from source; Fedora: no CLI package in the official repository—use the official install script or a third-party repository (GetPageSpeed/COPR); Arch:
pacman -S wasmtime;macOS:brew install wasmtime;Windows:winget install BytecodeAlliance.Wasmtime - Verify:
wasmtime --version
wasm3 (lightweight interpreter, can run without WASI dependencies, for embedded/quick verification scenarios)
- Arch: no wasm3 in the official repository—AUR (e.g. the wasm3 package) or build from source; macOS:
brew install wasm3 - Debian/Ubuntu/Fedora have no official package → GitHub
wasm3/wasm3release, download the single-file binary for the corresponding platform, or build from source:git clone --depth 1 https://github.com/wasm3/wasm3 && cmake -B build -S wasm3 && cmake --build build - Verify:
wasm3 --version
Chrome DevTools (browser-side dynamic analysis)
- Chrome/Chromium: Debian/Ubuntu
apt install chromium(Chrome uses the official deb); Fedoradnf install chromium;Archpacman -S chromium;macOSbrew install --cask google-chrome; Windows official installer - Verify: launch the browser
F12open DevTools → the Sources panel can expand the .wasm file - Start a local server for the browser to load:
python3 -m http.server 8000
binaryen (wasm-opt optimization/obfuscation restoration, wasm-dis disassembly)
- Debian/Ubuntu:
apt install binaryen;Fedora:dnf install binaryen;Arch:pacman -S binaryen - macOS:
brew install binaryen;Windows:choco install binaryenor GitHubWebAssembly/binaryenrelease - Verify:
wasm-opt --version
Operation steps
Execute in order, recording evidence at each step (path + sha256, see [[re-triage]]).
-
Identify the wasm module:
file sample.wasm # "WebAssembly (wasm) binary module version 0x1 (MVP)" xxd sample.wasm | head -1 # 魔数 00 61 73 6D(\0asm)+ 版本 01 00 00 00- Magic number
\0asm(00 61 73 6D) + version01 00 00 00= standard wasm; if it does not match → not wasm (may be encrypted/obfuscated/corrupted, see pitfall 3) - Often appears in embedded form; first restore it to a .wasm file:
atob(...)base64 string, in JSUint8Array([0,97,115,109,...])array literal - Confirm from the JS side:
WebAssembly.instantiate/WebAssembly.Module/.wasmthe appearance of fetch means a wasm payload (JS glue goes to [[re-script-deob]])
- Magic number
-
section parsing (wasm-objdump / wasm2wat):
wasm-objdump -x sample.wasm # 全部 section:Type/Import/Function/Table/Memory/Global/Export/Code/Data wasm-objdump -h sample.wasm # 只列 section 摘要 wasm2wat sample.wasm -o sample.wat # 二进制 → WAT 文本(有扩展指令时加 --enable-all) wasm-objdump -d sample.wasm # 反汇编全部函数体(WAT 风格指令)- Reverse-engineering significance of each section: Type (function signatures, aids type restoration), Import (Host JS/WASI-injected functions — call boundary)、Export(Externally exposed function names callable from JS)、Data(String constant area), Code (instruction body)
- First read Import + Export to establish the call surface, then enter Code
-
Decompilation (wasm-decompile):
wasm-decompile sample.wasm -o sample.c # 类 C 伪码(可读性优先)- Cross-check with WAT: wasm-decompile has good readability but loses some type/boundary information; for key functions use the
wasm2watWAT produced by [it] as the authoritative source for close reading - Entry point location:
(export "checkPassword" ...)→ JS sideinstance.exports.checkPassword; imported functions → go to the JS glue to find the implementation (pitfall 2) - Strings:
wasm-objdump -s sample.wasmdump the raw contents of all sections (hex dump, including the Data segment; the Data section can also be read from the-xoutput); in the WAT look for(data (i32.const 偏移) "字符串"), and map thei32.const 偏移in the code back to strings (pitfall 1) - When there are many functions or suspected obfuscation, first do a rough read with wasm-decompile, pick out the target functions, then do a close read
- Cross-check with WAT: wasm-decompile has good readability but loses some type/boundary information; for key functions use the
-
Browser side (DevTools Sources / JS call surface):
- Open the sample page (start a local server), DevTools → Sources → expand on the left the
.wasmfile: Chrome automatically provides a decompiled pseudocode view, where you can set breakpoints and single-step; the Scope panel shows wasm local variables, and the Call Stack simultaneously shows JS↔wasm frames - Call surface: the JS side's
WebAssembly.instantiate(bytes, importObject)'simportObjectdefines the wasm's import implementations;instance.exports.xxx(...)are the wasm's exported functions — set a breakpoint at the JS call site and single-step into the wasm frame to observe the input parameters/return value - Headless environment: Chrome headless + DevTools Protocol (
--headless --remote-debugging-port=9222), or NodeWebAssembly.instantiateto equivalently reproduce the call - Dynamic value modification: in the wasm pseudocode view in DevTools, modify parameters and replay the call, and compare the outputs black-box
- Open the sample page (start a local server), DevTools → Sources → expand on the left the
-
Dynamic (wasmtime + instrumentation/black-box differential):
wasmtime run sample.wasm # WASI 程序(需 _start 导出) wasmtime run --invoke checkPassword sample.wasm '"test"' # 直调导出函数(参数按 JSON 解析) wasmtime run --dir . --env FLAG=xxx sample.wasm # 显式授权 WASI 文件/环境能力 wasm3 sample.wasm checkPassword test # 无 WASI 模块用 wasm3(参数原样)- Instrumentation:
wasm-objdump -dstatic instructions +--invokeblack-box differential — feed different inputs to the same exported function, observe output changes to locate branch/validation logic - WASI system calls:
strace -f wasmtime run sample.wasm(pitfall 5, go through [[re-tracing]]) - When BigInt parameters are needed or you need to trace into the JS side, write node glue:
const { instance } = await WebAssembly.instantiate(bytes, imports); console.log(instance.exports.f(1n, 2n)); // i64 必须 BigInt(坑 4) - All dynamic execution defaults to sandboxed ([[re-analyze/platform-tips]] highest principle)
- Instrumentation:
Cross-domain federation
- [[re-managed]]: gateway workflow step ① (identify runtime) ② (decompile) always invokes this skill; wasm host programs (JS/Node) belong to the managed code domain
- [[re-script-deob]]: JS glue code (instantiate/import object/fetch wasm/atob embedded) should first be deobfuscated and restored — the implementations of imports are basically all on the JS side, so the two skills must be used together
- [[re-tracing]]: WASI program dynamic side —
strace -f wasmtime runobserve the system call boundary - [[re-emulation]]: optional — emulate execution of a WASI module when no suitable runtime is available (wasm is often compiled via Emscripten; before emulating, first understand the host dependencies according to the WASI mapping)
- [[re-firmware]]: wasm modules in embedded/router web interfaces
- Base [[re-binary-core]]: initial survey ([[re-triage]]); mixed wasm and native samples
- [[re-malware]]: the complete chain of malicious web pages/supply chain JS loading wasm payloads
- [[re-deobfuscate]]: wasm obfuscation (wasm-obfuscator, etc.) falls under obfuscation countermeasures; the approach to deep restoration is the same
Common pitfalls and traps
- Strings live in the data section, not in the code: Symptom — wasm2wat/decompilation shows only
i32.const 偏移numbers, and grep finds no plaintext anywhere; Cause — wasm has no string type, so string constants are stored in the Data section and referenced by code via memory addresses; Countermeasure —wasm-objdump -s sample.wasmdump the raw contents of all sections (hex dump, with the Data section among them; you can also read the Data section from the-xoutput) (in WAT look at(data (i32.const 偏移) "…")), and map offsets back to reference sites - imports are the JS boundary, not library calls: Symptom — the key validation/decryption logic cannot be found in the wasm file; Cause — wasm imports are injected by the host JS (
WebAssembly.instantiate(bytes, { env: {...} })), and the sensitive logic is on the JS side; Countermeasure —wasm-objdump -xlist the Import section → find the corresponding implementation in the JS glue code ([[re-script-deob]]) — wasm only proves "what was called", not "what it did" - For wasm obfuscation (wasm-obfuscator, etc.), deobfuscate first, then analyze: Symptom — wasm2wat output has large stretches of control-flow flattening, dead code,
selectnoise, strings chopped up/encrypted, and all function names erased; Cause — the obfuscator performs control-flow flattening + string encryption + name erasure; Countermeasure — restore the flattening by optimizing pass by pass with binaryen:wasm-opt --flatten --simplify-locals --vacuum --dce -O3multiple iterations; string encryption requires locating the decryption logic (the approach is the same as [[re-deobfuscate]]; note that the decryption function may be an import — go back to pitfall 2) - BigInt calling convention: Symptom — JS calling a wasm exported function reports
TypeError: Cannot convert a BigInt value to a number, or i64 arguments are silently truncated; Cause — i64 arguments/return values at the JS↔wasm boundary can only use BigInt (Number has only 2^53 precision); Countermeasure — on the JS sideinstance.exports.f(1n, 2n)pass BigInt; wasmtime--invokeparses arguments as JSON and does not support BigInt — when needed, write node glue; to replicate the algorithm use Python int / JS BigInt, not Number - WASI system call boundary: Symptom — running a WASI program with wasmtime reports
unknown import: wasi_snapshot_preview1.…, or browser and runtime behavior differ; Cause — a WASI program obtains file/clock/randomness/network capabilities through importwasi_snapshot_preview1, and if the runtime has not authorized that capability it fails; in the browser the host imports are JS rather than WASI, so the same module can behave differently at the two ends; Countermeasure —wasm-objdump -xlook at the Import section to confirm which WASI functions are used →wasmtime run --dir . --env K=vexplicitly authorize →strace -f wasmtime runobserve the actual system calls ([[re-tracing]]); annotate the conclusion with the runtime environment (browser vs wasi)