SuperAPI AI Directory

re-wasm

General & other

View repository

Updated 10-15 21:35

About this skill

WebAssembly reverse engineering (WABT / wasmtime / Chrome DevTools)

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 / .wasm samples 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 wasm2wat read 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 wabt or GitHub WebAssembly/wabt release'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/wasm3 release, 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); Fedora dnf install chromium;Arch pacman -S chromium;macOS brew install --cask google-chrome; Windows official installer
  • Verify: launch the browser F12 open 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 binaryen or GitHub WebAssembly/binaryen release
  • Verify: wasm-opt --version

Operation steps

Execute in order, recording evidence at each step (path + sha256, see [[re-triage]]).

  1. 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) + version 01 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 JS Uint8Array([0,97,115,109,...]) array literal
    • Confirm from the JS side:WebAssembly.instantiate / WebAssembly.Module / .wasm the appearance of fetch means a wasm payload (JS glue goes to [[re-script-deob]])
  2. 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
  3. 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 wasm2wat WAT produced by [it] as the authoritative source for close reading
    • Entry point location:(export "checkPassword" ...) → JS side instance.exports.checkPassword; imported functions → go to the JS glue to find the implementation (pitfall 2)
    • Strings:wasm-objdump -s sample.wasm dump the raw contents of all sections (hex dump, including the Data segment; the Data section can also be read from the -x output); in the WAT look for (data (i32.const 偏移) "字符串"), and map the i32.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
  4. Browser side (DevTools Sources / JS call surface):

    • Open the sample page (start a local server), DevTools → Sources → expand on the left the .wasm file: 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) 's importObject defines 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 Node WebAssembly.instantiate to 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
  5. 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 -d static instructions + --invoke black-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)

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 run observe 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.wasm dump the raw contents of all sections (hex dump, with the Data section among them; you can also read the Data section from the -x output) (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 -x list 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,select noise, 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 -O3 multiple 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 side instance.exports.f(1n, 2n) pass BigInt; wasmtime --invoke parses 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 import wasi_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 -x look at the Import section to confirm which WASI functions are used → wasmtime run --dir . --env K=v explicitly authorize → strace -f wasmtime run observe the actual system calls ([[re-tracing]]); annotate the conclusion with the runtime environment (browser vs wasi)