JS 逆向实战:用 REA 还原 webpack 打包后的源码(含 JS 逆向 MCP 用法)
本页目录
线上的 JavaScript bundle 往往是一整行、变量全被改名的代码。想回答“哪个模块发了这个请求”“2.3 版到底改了什么”,得先把模块、import 关系和调用关系找回来。REA(morluto 维护的开源项目)只读本地文件,从不执行它们:有 source map 就读 source map;没有的话,从语法里恢复 webpack 的 chunk 和模块工厂函数;再配上你自己提供的 Wakaru 可执行文件,还能把 bundle 拆成一个个派生模块。每一步结果都是 Evidence,你和 agent 都能逐条核对。
适合谁读: 排查自家线上构建产物的前端开发者、想确认“到底发布了什么”的维护者,以及想看清打包器如何改写代码的学习者。需要会用 npm 和终端。
你会学到:
- 构建一个能和源码逐条对照的小 bundle
- 通过 REA 的应用图读取 source map
- 没有 source map 时恢复 chunk 和模块边界
- 用
recover-javascript-sources调用 Wakaru 解包(仅 Linux x64) - 用
trace-javascript-semantics追踪数据流 - 用
compare-source-to-bundle给 bundle 和源码树打分对照 - 在 Claude Code 等 AI agent 里通过 MCP 跑完整个 JS 逆向流程
范围说明。 本文只处理已经在你磁盘上的文件。从线上页面抓取脚本、把单个压缩位置映射回源码、把网络请求和发起它的代码对上,请看 用 REA 追踪网站 API 调用。Electron 的 ASAR、preload 和 IPC 请看 Electron 应用分析。只分析你自己的代码、开源代码或已获授权的代码,参见 REA 的 DISCLAIMER.md。
什么是“JS 逆向 MCP”,REA 在其中的位置
搜“js逆向 mcp”的人通常想要的是:让 Claude Code、Cursor、Codex 这类 agent 直接调用工具去读、拆、追踪 JS 代码,而不是把几 MB 的压缩代码粘进对话框。MCP(Model Context Protocol)就是 agent 调用外部工具的协议。
常见的搭配有两类:
- 驱动浏览器的 MCP(比如 Chrome DevTools 方向的方案):适合看运行时,页面实际加载了什么、发了什么请求。
- REA:除了浏览器捕获(见网站 API 那篇),它的强项是对本地 JS 文件做离线静态分析,产出可复核的 Evidence。本文讲的就是这一部分。
两者可以同时接入同一个 agent,并不冲突。第 8 步给出了具体的 MCP 工具名和提示词。
先选路线
| 你手里有什么 | REA 能给你什么 | 对应章节 |
|---|---|---|
bundle + .map 文件 |
原始源文件名和摘要,并和生成文件关联起来 | 第 2 步 |
| webpack / Rspack bundle,没有 map | chunk 注册、模块工厂、import、接口地址 | 第 3 步 |
| 任意单个 bundle,没有 map,Linux x64 主机 | Wakaru 拆出的派生模块,再进入同一张图 | 第 4 步 |
| 某个版本的原始源码树 | 文件到 bundle 的打分映射 | 第 6 步 |
这两种工具都还原不了原始变量名。Wakaru 会把语法改写得更易读,但 REA 把结果标记为 derived(派生)Evidence:原始命名和运行时等价性仍然是未知的。
前置条件
- Node.js 和 npm,以及 REA 6.4.0 或更新版本。用
npm install --global rea-agents安装 CLI,或者在每条命令前加npx -y rea-agents@latest。要在 Claude Code / Cursor / Codex 里用,运行npx rea-agents setup后重启 agent(见 安装与接入)。 - 不需要原生分析引擎。 JS 静态分析用不到 Ghidra、Hopper 或 IDA。
- 仅第 4 步需要: Linux x64 主机(虚拟机或容器都行)、Wakaru ^1.13.0(REA 用 1.14.0 验证过),以及 util-linux 的
prlimit。在其他平台上 REA 会直接拒绝启动恢复。 - 仅第 6 步需要: Linux 或 macOS。在原生 Windows 上导入历史源码会返回
unsupported_host。 - 用来切分大 JSON 结果的
jq。
第 1 步:构建一个能对照的 bundle
先拿自己有源码的 bundle 练手,REA 的每个结论都能对着源码检查。自己写一个小项目就够了:
mkdir bundle-lab && cd bundle-lab
npm init -y
npm install --save-dev webpack webpack-cli
mkdir src
src/api.js:
export async function loadTodos() {
const response = await fetch("/api/v1/todos");
if (!response.ok) throw new Error("HTTP " + response.status);
return (await response.json()).items;
}
src/export.js:
export function toCsv(todos) {
return todos.map((t) => [t.id, JSON.stringify(t.title)].join(",")).join("\n");
}
src/index.js:
import { loadTodos } from "./api.js";
document.querySelector("#export").addEventListener("click", async () => {
const todos = await loadTodos();
const { toCsv } = await import("./export.js");
console.log(toCsv(todos));
});
webpack.config.js:
module.exports = {
entry: "./src/index.js",
output: { filename: "[name].js", chunkFilename: "[name].chunk.js", clean: true },
// Production mode normally concatenates ES modules into one scope.
// Turning it off keeps per-module factories so you can see them.
optimization: { concatenateModules: false },
};
构建两份,一份带 source map,一份不带:
npx webpack --mode production --devtool source-map --output-path "$PWD/dist-maps"
npx webpack --mode production --no-devtool --output-path "$PWD/dist-plain"
为什么要构建两份: 同一份代码,有 map 和没 map 的对比,最能看清 REA 每一层各补了什么。动态 import() 会让 webpack 为 export.js 单独生成一个 chunk,正好可以观察 chunk 拆分。真实的生产构建通常开着 concatenateModules(模块合并),模块边界在打包时就被合掉了,能恢复出的边界会更少。
第 2 步:带 source map 读取 bundle
分析目录,并把 Evidence 存成文件:
rea analyze-javascript-application "$PWD/dist-maps" --json > maps-evidence.json
进度信息走 stderr,stdout 是干净的 JSON。不要整份读完,先投影出摘要:
jq -c '{source: {kind: "inline", evidence: .}, view: {kind: "summary"}}' \
maps-evidence.json > maps-summary.json
rea inspect-analysis-view maps-summary.json
用同样的方式,配合 view: {kind: "page", collection: "modules", offset: 0, limit: 32} 分页浏览模块。完整写法见 Electron 那篇。
有了 source map,图里会多出这些信息:
-
REA 解析每条本地
sourceMappingURL声明及其指向的 map,生成 source map 节点和原始源文件节点,用maps_to边连起来。 -
记录原始源文件名(如
webpack://bundle-lab/./src/api.js)和可选的内容摘要。图里不保存sourcesContent原文,需要原文时自己去读 map。 -
想看单个条目,用 item 视图,填入在模块分页里看到的精确路径或引用:
jq -c '{source: {kind: "inline", evidence: .}, view: {kind: "item", collection: "modules", selector: {path: "PASTE_EXACT_PATH_FROM_PAGE"}}}' \ maps-evidence.json > one-module.json rea inspect-analysis-view one-module.json
map 损坏或读不了时,会被记为状态为 unavailable 的 unknown 范围,而不会被当成“源码不存在”的证据。
第 3 步:没有 map 时恢复模块边界
再分析不带 map 的那份:
rea analyze-javascript-application "$PWD/dist-plain" --json > plain-evidence.json
即使没有 map,REA 仍会把每个文件当作纯文本解析(不执行),并恢复:
- webpack 和 Rspack 的 chunk 注册与模块工厂(只要它们以 AST 字面量形式出现)。在这个项目里,主 bundle 的模块表和独立的
export.jschunk 预期会各自成为节点。 - 静态
import、动态import()、CommonJSrequire、worker 和 service worker。 - 接口地址、路由、存储和第三方库特征。
/api/v1/todos这个字面量会成为一条 endpoint 观测。6.4.0 起还能识别 tRPC 过程调用和具名 GraphQL 操作。tRPC 的路由名和过程名以属性名的形式保留下来,压缩不会改掉它们,所以即使代码被压缩过也读得出来。
对比两份摘要:模块数量和 import 边应该和源码基本吻合,文件名和变量名则对不上。下一步就是处理这个问题。
关于打包器。 webpack 和 Rspack 会保留可见的模块表。Rollup、Vite、esbuild 的生产构建通常把所有模块提升(hoist)到共享作用域里,字节里已经没有模块边界了,REA 和 Wakaru 都没法可靠地把它找回来。
第 4 步:用 Wakaru 拆出模块(Linux x64)
recover-javascript-sources 会调用你提供的 Wakaru 可执行文件,并在校验后发布结果。REA 不负责安装 Wakaru。6.4.0 的适配器只支持 Linux x64,在其他平台上会拒绝启动。
安装 Wakaru,REA 的 恢复文档 给了两种方式:
curl -fL https://github.com/pionxzh/wakaru/releases/download/v1.14.0/wakaru-linux-x64.tar.gz | tar -xz wakaru
export REA_WAKARU_COMMAND="$PWD/wakaru"
或者:
npm install [email protected]
export REA_WAKARU_COMMAND="$(realpath node_modules/@wakaru/cli-linux-x64/wakaru)"
环境变量要指向平台二进制,不要指向 node_modules/.bin 里的 Node.js 包装脚本。REA 每次都会用 util-linux 的 prlimit(默认 /usr/bin/prlimit)限制资源;如果它在别的位置,把 REA_JAVASCRIPT_PRLIMIT_COMMAND 设成它的绝对路径。
对主 bundle 执行恢复:
rea recover-javascript-sources "$PWD/dist-plain/main.js" "$PWD/recovered-main" \
--extraction-mode structural --rewrite-level standard --json > recovery.json
- 输出目录必须事先不存在,两个路径都必须是绝对路径。
--extraction-mode可选structural(默认,对应 Wakaru--unpack=strict)、heuristic(--unpack=auto)、inspection(--unpack=inspect,可能产出不可执行的片段)。--rewrite-level可选minimal、standard(默认)、aggressive。级别越高改写越多,但无论哪一级,运行时等价性都是未知的。
发布出来的目录包含:inputs/bundle.js(输入的精确快照)、modules/(派生模块,以及 Wakaru 生成的 map)、reports/stdout.json 和 reports/provenance.json(引擎自己的报告)、manifest.json。每个模块都记录了它在原始输入中的 UTF-8 字节范围,任何恢复出来的函数都能对回原始字节。
然后把恢复出来的模块再送回应用图:
rea analyze-javascript-application "$PWD/recovered-main/modules" --json > recovered-evidence.json
恢复结果里也直接带有一个 analysis_input 对象,就是为这一步准备的。
需要知道的限制: 单个 worker,1 GiB 地址空间上限,120 秒超时,输入最多 64 MiB,输出最多 128 MiB 或 10000 个条目。超出任何一项都会干净地失败并回滚输出。结构化拆分可能退化成单个文件,REA 自己的测试样本里,esbuild 作用域提升后的 bundle 就仍是一个格式未知的脚本。6.4.0 中这个命令只接受一个输入文件。仓库文档里那个把多个 code-split chunk 一起恢复的 --additional-path 选项只在 main 分支上,npm 6.4.0 里没有。
第 5 步:在 bundle 内部追踪数据流
模块边界告诉你代码在哪,trace-javascript-semantics 告诉你值怎么流动。它查询的是 analyze-javascript-application 在结构图之外另存的一张语义关系图。这张图覆盖词法绑定、属性读写、闭包、能唯一确定目标的本地调用、Promise 链、定时器、事件发射器、请求构造和响应消费等关系。
从保存的 Evidence 构造输入:
node --input-type=module -e '
import { readFileSync, writeFileSync } from "node:fs";
const application = JSON.parse(readFileSync("plain-evidence.json", "utf8"));
writeFileSync("semantic-input.json", JSON.stringify({
application,
query: {
seed: { kind: "endpoint", value: "/api/v1/todos" },
direction: "forward-influence"
}
}));'
rea trace-javascript-semantics ./semantic-input.json --json
种子(seed)类型有 literal、endpoint、property、event、boundary-field、function(按指纹)、semantic-node、application-node。方向(direction)有 backward-provenance(这个值从哪来)、forward-influence(它影响了什么)、callers 和 ownership。这里从 endpoint 出发正向追踪,应该能从 fetch 请求走到读取 .items 的代码。
结果怎么看:status 取值为 found、no-match、ambiguous、partial、unsupported。resolved 只表示静态目标唯一,不代表这条路径真的执行过。含糊的动态边默认排除,需要时设置 include_ambiguous_dynamic_edges: true。找不到某条关系,也不能证明对应的行为不存在。
第 6 步:用源码验证 bundle
手里有源码树时(你自己的、开源项目的发布 tag、或某个旧版本),可以量化 bundle 和它的对应程度。先把源码树导入成历史参照:
rea import-reference-source "$PWD" --json > reference.json
导入会记录文件哈希和 import 关系,遵守 .gitignore,并默认跳过 node_modules/、dist/ 等常见生成目录。然后对比:
node --input-type=module -e '
import { readFileSync, writeFileSync } from "node:fs";
const reference = JSON.parse(readFileSync("reference.json", "utf8"));
const application = JSON.parse(readFileSync("maps-evidence.json", "utf8"));
writeFileSync("stb-input.json", JSON.stringify({ reference, application }));'
rea compare-source-to-bundle ./stb-input.json --json
每个源文件都会得到一个状态:unchanged、modified、removed、split、merged、duplicated 或 unknown,并附上依据的信号和权重:源文件摘要完全一致(100)、source map 原始路径(70)、当前路径完全一致(60)、路径后缀(40)、文件名(20)、语言扩展名(5)。分别拿 maps-evidence.json 和 plain-evidence.json 各跑一次,就能看出映射有多依赖 source map。判定 removed 需要两边的清单都完整,覆盖不完整时,“不存在”只会记为 unknown。
如果要比较 bundle 自身的两个构建版本(而不是源码对 bundle),用 compare-application-versions,见 Evidence、快照与版本对比。
第 7 步:处理大 bundle
6.4.0 起,每个 JS 源文件都在独立进程里分析,各有自己的资源预算:
rea analyze-javascript-application "$PWD/dist-plain" \
--max-heap-mb 4096 --analysis-timeout-ms 600000 --json > plain-evidence.json
默认值是每个源文件 1024 MiB 的 V8 old-space 和 300000 毫秒。某个源文件超出预算时,REA 保留已经收集到的事实,把这个文件标为明确的 unknown,然后继续分析其余文件。分析超大 bundle 时,先看摘要里的 limitations,再决定信不信那些计数。
第 8 步:在 agent 里用 MCP 做 JS 逆向
执行 npx rea-agents setup 之后,上面的操作都成了 MCP 工具:analyze_javascript_application、inspect_analysis_view、recover_javascript_sources、trace_javascript_semantics、compare_source_to_bundle、compare_application_versions、trace_application_feature。可以直接这样提问:
用 REA 分析 /absolute/path/bundle-lab/dist-plain 这个 JavaScript 应用。
先根据摘要告诉我模块和 chunk 结构,再从 endpoint "/api/v1/todos"
做 forward-influence 语义追踪。每个结论都附上 Evidence ID,
并列出所有 unknown 或 partial 的部分。
如果连接的服务端支持保留引用(retained references),agent 可以传 {"kind":"retained-evidence","evidence_id":"..."},不必每次都传完整记录。恢复工具报 capability_unavailable 时,要么服务端所在主机不是 Linux x64,要么服务进程看不到 REA_WAKARU_COMMAND。这个变量要设在 MCP 服务端的环境里,只在你自己的 shell 里设置是不够的。
核对 agent 的回答时,按 REA 的老规矩来:模块边界要能在应用图里找到,数据流要来自语义追踪,和源码的对应关系要来自 compare-source-to-bundle。agent 对某个压缩函数“大概在干什么”的猜测,不算 Evidence。
常见问题与排错
recover-javascript-sources 返回 capability_unavailable。 检查三点:主机是 Linux x64;REA_WAKARU_COMMAND 是指向平台二进制的绝对路径;prlimit 在 /usr/bin/prlimit,或在 REA_JAVASCRIPT_PRLIMIT_COMMAND 指定的位置。
提示 “Unsupported Wakaru version”。 REA 接受 ^1.13.0。这个范围内的其他版本也能用,结果里会注明报告解析器只用 1.14.0 验证过。
恢复结果只有一个模块。 bundle 做过作用域提升,或者格式不被识别。可以试试 --extraction-mode heuristic;如果还是一个文件,说明字节里已经没有模块边界了。
CLI 把 --additional-path 当成未知选项拒绝。 这个选项只在仓库 main 分支上,6.4.0 里请逐个 chunk 分别恢复。
看不到 source map 节点。 构建产物里没有 sourceMappingURL 注释,或者 map 文件不在 bundle 旁边。把 .map 文件复制到被分析的目录里。
Windows 上 import-reference-source 失败。 它需要安全的不跟随符号链接的文件打开方式,只在 Linux 和 macOS 上可用,请换到这两种主机上运行。
下一步
- 从线上页面抓脚本、映射压缩位置:/zh/trace-website-api-calls/
- 把同一套应用图用在 Electron 归档上:/zh/reverse-engineer-electron-app/
- 对比 bundle 的两个发布版本:/zh/rea-evidence-snapshots-compare/
- 参考文档:JavaScript source recovery、application workflows、artifact reconstruction