Skip to content

JS Runtime / Browser Reversing ​

JS Runtime 逆向不是把 bundle 从头读完,而是先把请求、签名、加密、状态和 WASM 的关键边界打出来,再决定是动态复现、AST 静态还原,还是直接劫持 runtime oracle。

输入信号 ​

信号立即动作命中样本失败样本
bundle 里有 sign/token/nonce/timestamphook fetch/XHR 与签名函数请求参数与签名输入同步出现sign 在服务端生成
Webpack/Vite chunk 分裂提取 chunk map、source map、动态 import找到 API path 和 crypto 模块chunk 加载前 hook 太晚
CryptoJS/WebCryptohook encrypt/decrypt/sign/importKeykey/iv/plain/cipher 出现调用在 Worker/WASM 内
debugger/console 检测hook timer/toString/Function调试不中断,逻辑继续检测在闭包里自校验
WASM 模块dump bytes、exports、memory导出函数和输入输出可复现indirect table 混淆未定位
Worker/Service Workerhook Worker 构造和 postMessageworker 脚本 URL/消息可见CSP/COOP 影响调试

工作流 ​

text
抓 HTML 与所有 JS chunk
  → 导航前注入 runtime hook
  → 触发关键业务动作并保存请求/响应/console
  → 定位签名/crypto/WASM/worker 模块
  → AST 还原或直接复现算法
  → 用 Python/Node 重放 API,记录成功和失败样本

0. Bundle 路由矩阵 ​

入口目标工具/动作
main.*.js / app.*.js路由、API client、状态管理搜 baseURL, axios, fetch, /api/
vendor.*.jscrypto/lib/framework gadget搜 CryptoJS, subtle, protobuf, wasm
source map还原源码路径和函数名下载 .map,按 sourcesContent 检索
dynamic chunk登录后/点击后逻辑hook script append/import
Worker签名、加密、WASMhook new Worker、postMessage

1. 导航前总 hook ​

javascript
(() => {
  const enc = v => {
    if (v instanceof ArrayBuffer) return Array.from(new Uint8Array(v)).map(b=>b.toString(16).padStart(2,"0")).join("");
    if (ArrayBuffer.isView(v)) return Array.from(new Uint8Array(v.buffer, v.byteOffset, v.byteLength)).map(b=>b.toString(16).padStart(2,"0")).join("");
    try { return typeof v === "string" ? v : JSON.stringify(v); } catch { return String(v); }
  };
  const oldFetch = window.fetch;
  window.fetch = async function(input, init = {}) {
    const url = String(input && input.url || input);
    console.log("[fetch:req]", url, enc(init.headers || {}), enc(init.body || ""));
    const r = await oldFetch.apply(this, arguments);
    r.clone().text().then(t => console.log("[fetch:resp]", url, r.status, t.slice(0, 1200))).catch(()=>{});
    return r;
  };
  const open = XMLHttpRequest.prototype.open;
  const send = XMLHttpRequest.prototype.send;
  XMLHttpRequest.prototype.open = function(method, url) {
    this.__rt_method = method; this.__rt_url = url;
    return open.apply(this, arguments);
  };
  XMLHttpRequest.prototype.send = function(body) {
    console.log("[xhr:req]", this.__rt_method, this.__rt_url, enc(body || ""));
    this.addEventListener("load", () => console.log("[xhr:resp]", this.__rt_url, this.status, String(this.responseText).slice(0, 1200)));
    return send.apply(this, arguments);
  };
  const oldWorker = window.Worker;
  window.Worker = function(url, opts) {
    console.log("[worker:new]", url, opts || "");
    return new oldWorker(url, opts);
  };
})();

2. crypto / sign 打点 ​

javascript
(() => {
  const show = x => {
    try { return JSON.stringify(x); } catch { return String(x); }
  };
  if (window.crypto && crypto.subtle) {
    for (const n of ["importKey","generateKey","deriveKey","encrypt","decrypt","sign","verify"]) {
      const o = crypto.subtle[n].bind(crypto.subtle);
      crypto.subtle[n] = async (...a) => {
        console.log("[subtle:req]", n, a.map(show));
        const r = await o(...a);
        console.log("[subtle:ret]", n, r && r.algorithm, r && r.usages);
        return r;
      };
    }
  }
  const names = ["sign","getSign","makeSign","encrypt","decrypt","encode","decode"];
  for (const k of names) {
    if (typeof window[k] === "function") {
      const o = window[k];
      window[k] = function(...a) {
        console.log("[fn:req]", k, a.map(show));
        const r = o.apply(this, a);
        console.log("[fn:ret]", k, show(r));
        return r;
      };
    }
  }
})();

成功样本:同一次业务请求里同时记录到 params/raw/timestamp/nonce/key/iv/sign。失败样本:只有密文或签名结果,没有输入;需要 hook 更早或找 Worker/WASM。

3. AST 去混淆脚手架 ​

javascript
#!/usr/bin/env node
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generate = require("@babel/generator").default;
const t = require("@babel/types");

const code = fs.readFileSync(process.argv[2], "utf8");
const ast = parser.parse(code, {sourceType: "unambiguous"});

traverse(ast, {
  BinaryExpression(path) {
    const ev = path.evaluate();
    if (ev.confident && ["string","number","boolean"].includes(typeof ev.value)) {
      path.replaceWith(t.valueToNode(ev.value));
    }
  },
  MemberExpression(path) {
    const prop = path.node.property;
    if (path.node.computed && t.isStringLiteral(prop)) {
      path.node.computed = false;
      path.node.property = t.identifier(prop.value);
    }
  },
  IfStatement(path) {
    const ev = path.get("test").evaluate();
    if (ev.confident && typeof ev.value === "boolean") {
      path.replaceWith(ev.value ? path.node.consequent : (path.node.alternate || t.emptyStatement()));
    }
  }
});

fs.writeFileSync(process.argv[3] || "deobfuscated.js", generate(ast, {comments:false}).code);

4. Webpack chunk 抽取 ​

javascript
(() => {
  const scripts = [...document.scripts].map(s => s.src).filter(Boolean);
  console.log("[scripts]", scripts);
  const oldAppend = Element.prototype.appendChild;
  Element.prototype.appendChild = function(n) {
    if (n && n.tagName === "SCRIPT") console.log("[script:append]", n.src || n.textContent.slice(0, 120));
    return oldAppend.call(this, n);
  };
  for (const k of Object.keys(window)) {
    if (/webpackJsonp|webpackChunk|__LOADABLE_LOADED_CHUNKS__/.test(k)) console.log("[webpack-global]", k, window[k]);
  }
})();

5. WASM 打点 ​

javascript
(() => {
  const oldInstantiate = WebAssembly.instantiate;
  WebAssembly.instantiate = async function(src, imports) {
    const bytes = src instanceof ArrayBuffer ? new Uint8Array(src) : null;
    if (bytes) {
      console.log("[wasm:bytes]", bytes.length);
      window.__wasm_dump = Array.from(bytes);
    }
    const r = await oldInstantiate.apply(this, arguments);
    const inst = r.instance || r;
    console.log("[wasm:exports]", Object.keys(inst.exports));
    for (const [name, fn] of Object.entries(inst.exports)) {
      if (typeof fn === "function") {
        inst.exports[name] = new Proxy(fn, {
          apply(target, self, args) {
            console.log("[wasm:req]", name, args);
            const out = Reflect.apply(target, self, args);
            console.log("[wasm:ret]", name, out);
            return out;
          }
        });
      }
    }
    return r;
  };
})();

6. 签名复现骨架 ​

python
#!/usr/bin/env python3
import argparse
import hashlib
import hmac
import json
import time

def canonical(params):
    return "&".join(f"{k}={params[k]}" for k in sorted(params))

def sign(params, secret, algo="sha256"):
    raw = canonical(params)
    digest = hmac.new(secret.encode(), raw.encode(), getattr(hashlib, algo)).hexdigest()
    return raw, digest

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--params", required=True, help="JSON params from hook")
    ap.add_argument("--secret", required=True)
    ap.add_argument("--algo", default="sha256")
    args = ap.parse_args()
    params = json.loads(args.params)
    params.setdefault("timestamp", str(int(time.time())))
    raw, sig = sign(params, args.secret, args.algo)
    print(json.dumps({"raw": raw, "sign": sig, "params": {**params, "sign": sig}}, ensure_ascii=False))

if __name__ == "__main__":
    main()

判定:复现脚本生成的 sign/token 与浏览器 hook 结果一致,才进入 API 重放;不一致时回到 canonicalization、编码、timestamp、nonce、salt。

攻击链 ​

text
bundle/chunk 枚举
  → runtime hook 捕获请求和 crypto 输入
  → AST 还原签名/加密模块
  → Python/Node 复现算法
  → 重放 API 或生成 payload
  → 转支付、JWT、签名、WebCrypto、WASM 下一跳

Evidence ​

项记录内容
bundle 来源HTML、script URL、chunk map、source map、hash
runtime 日志fetch/XHR/WS、Worker、crypto、WASM 的输入输出
静态还原AST 脚本、还原前后片段、关键函数名
复现结果canonical string、secret/key、timestamp/nonce、签名值
成功样本复现签名后 API 接受、flag/订单/token/权限数据返回
失败样本hook 太晚、签名不一致、nonce 过期、服务端二次校验
下一跳crypto 转 web-crypto-abuse;支付转 12-payment;签名转 13-signature

MCP 工具映射 ​

步骤MCP 工具说明
JS 行为探测jshook导航前注入 fetch/XHR/crypto/WASM hook
端点与 bundlehttp_probe下载 HTML、JS、source map、API 样本
知识路由kb_router按 JS runtime、CryptoJS、WASM、签名信号搜索

GPL-3.0 · 仅供授权环境下的学习与防御性研究使用