压缩过的 JavaScript 难读,是因为空白和局部变量名都没了。混淆过的更麻烦:所有字符串被塞进一个打乱顺序的数组,只能通过解码函数取出来;if、switch 的逻辑又被拆进查表对象和永远不会执行的分支里。这个工具把两种代码都变回能读的样子。适合排查自己线上构建的问题、在把某个脚本放进网站之前先看看它做了什么,或者审计你有权查看的第三方代码。
把代码粘贴进来,或者打开一个 .js 文件。页面会重新排版、尽量解码,并列出改动了什么。如果是 webpack 或 browserify 打包,还会列出里面的模块,可以打包成 .zip 下载。代码不会上传,你粘贴的代码也不会被执行。
使用步骤
- 拿到脚本:从 DevTools 的 Sources 面板复制,或从 Network 面板保存,也可以用你自己压缩或混淆过的构建产物。
- 粘贴到上方的输入框,或拖入、选择
.js文件,保持四个开关打开,点“开始反混淆”。 - 先看“处理结果”(解开了多少字符串、处理前后的
_0x名字数、打包格式),再读可读代码;如果是打包文件,打开模块表,或把模块打包成 .zip 下载。
真实输出示例
我们自己写的三文件购物车小应用,先用 esbuild 打包,再用 javascript-obfuscator 5.9.0 的 high-obfuscation 预设混淆(去掉了 debugProtection 和 disableConsoleOutput)。把它粘贴到本页,用默认开关点“开始反混淆”,再把页面显示的结果复制下来。排版做了压缩(标签和值放在同一行);代码原样保留,删节处已标出。输入以 (function(_0x567fa5,_0x462189){var _0x30f486=_0x567fa5();… 开头,里面每个字符串都经过 RC4 加密。
样本: 本站代码仓库里的 test/jsdeob/fixtures/obfuscator-high.js(52,088 字节),由 test/jsdeob/build-fixtures.mjs 从我们自己的演示应用生成;2026-11-02 抓取
处理结果
大小 50.9 KB(2 行)→ 2.8 KB(116 行)
字符串数组 解开并内联了 353 个字符串
_0x 标识符 处理前 942 个不同的名字,处理后 0 个
打包格式 不是 webpack 或 browserify 打包
耗时 0.7 s
可读代码
…
var vVF22 = vF2({
"test/jsdeob/app/cart.js": function (p9, p10) {
'use strict';
var {
formatPrice
} = vVF2();
var vC = class {
constructor(p11) {
this.owner = p11;
this.items = [];
}
addItem(p12, p13, p14 = 1) {
if (p14 <= 0) {
throw new Error("quantity must be positive");
}
…
receipt() {
var v8 = this.items.map(p15 => p15.quantity + " x " + p15.name + " = " + formatPrice(p15.unitPrice * p15.quantity));
v8.push("TOTAL " + formatPrice(this.totalCents()));
return v8.join("\n");
}
};
…
var vLSHttpsapiexamplecomv1 = "https://api.example.com/v1/orders";
…能做什么
输入。 把代码粘贴到输入框里,或者把 .js、.mjs、.cjs 文件拖进去,也可以点按钮选文件。上限 5 MiB。解析器能从不少语法错误里恢复,所以被截断的文件通常也能处理。
转换。 核心是 webcrack 2.16.0(MIT),底层是 Babel。有四个开关:
- 解开字符串数组。 obfuscator.io(也就是
javascript-obfuscator这个包)会把字符串放进一个数组,启动时不断旋转这个数组直到校验值对上,再通过解码函数取出每个字符串,解码时可能还套了一层 Base64 或 RC4。webcrack 会找到这个数组、旋转函数和解码函数,连那些带着偏移量去调用解码函数的小包装函数也能认出来。解码函数随后在本页自带的求值器里运行(见下文),每一处调用都换成明文字符串。 - 重命名
_0x…和解构出的变量。 像_0x4e2a1b这样的名字会换成能看出用途的短名字:参数叫p,函数叫f,一开始被赋值为字符串"demo-user"的变量叫vLSDemoUser。从对象里解构出来的值(const { formatPrice: i } = require(947))会直接用回属性名:const { formatPrice } = …。 - 拆分 webpack / browserify 打包。 webpack 4、5 的打包文件,webpack 的 chunk 文件(
self.webpackChunk… .push(…))和 browserify 打包,都会按模块拆成单独的文件,require(id)改成指向./id.js。browserify 保留了原始文件名(./cart、./format),这些也能还原。 - 还原 JSX。 把
React.createElement(…)和jsx(…)调用变回 JSX 标签。
其余的处理总是会做:重新排版,!0 变成 true,void 0 变成 undefined,a["b"] 变成 a.b,逗号表达式拆成多条语句,被当作语句用的三元表达式和 && 链改写成 if,折叠数字和字符串表达式,并去掉 obfuscator.io 的自我保护(self-defending)和反调试(debug protection)代码。控制流平坦化(while(true) 加 switch 的分发结构)、代理函数对象和死代码,是在 webcrack 处理字符串数组的同一步里还原的,所以只有找到了字符串数组、并且开着解开字符串数组时才会处理。
输出。 可读的代码,带 复制代码 和 下载 .js 按钮;一段处理结果(前后的大小和行数、解开了多少个字符串、处理前后各有多少个不同的 _0x 名字、打包格式和模块数);如果是打包文件,还有模块列表,每个模块可以单独预览、下载,也可以 把模块打包成 .zip 下载。
不执行代码,怎么解开字符串
解字符串数组的常规做法是直接运行混淆器自带的解码函数,webcrack 的命令行版本就是在 isolated-vm 沙箱里跑的。网页要这么做只能用 eval 或 new Function,本站的内容安全策略不允许,也不应该允许:这段代码来自你并不信任的文件。
所以本页自带一个小型求值器。webcrack 只把字符串数组函数、旋转函数和解码函数剪出来,求值器沿着它们的语法树逐步计算出结果。这段代码只能接触它自己创建的值,外加一小组没有副作用的内置函数:parseInt、String.fromCharCode、decodeURIComponent、RegExp#test、Math,以及常用的字符串、数组和数字方法。它碰不到 window、self、fetch、定时器、postMessage、真正的原型对象和 constructor。步数上限(5000 万步求值)、按内置函数读写的字符串和数组大小计费的工作量上限(1 亿个字符或元素)、调用深度上限以及字符串和数组的大小上限,能挡住失控的循环和内存膨胀。正则表达式只允许 obfuscator.io 自我保护代码用的那一个,而且只对最多 512 个字符做匹配,构造出来的恶意正则没法把页面卡死。解出来的字符串加起来超过输入的 4 倍(或 1 MiB)时不会内联。函数转成字符串时得到的是它自己的源码,和浏览器里一样,因为 obfuscator.io 的自我保护代码会拿函数源码去匹配一个正则表达式。
如果解码函数用到了这个子集以外的写法(比如展开运算符、对内置对象用 new、obfuscator.io 自带那一个以外的正则表达式),求值会停下,字符串保持原样,处理结果里显示 发现了,但没有解开。排版、重命名和拆包照常完成;控制流对象、switch 分发结构和死代码会保留,因为 webcrack 是在处理字符串的同一步里还原它们的。你也可以用第一个开关直接关掉解码。
代码怎么保证不外传
你的代码不会上传:
- 反混淆工具运行在一个单独的框架(iframe)里,来源是
tools.rea-reverse.com。下面说的处理都在这个框架里、在你的设备上完成。 - 这个页面在正文之后展示一个广告。广告脚本运行在
rea-reverse.com上,而框架属于另一个源(origin),浏览器不允许广告脚本或本页的任何其他脚本读取你粘贴或打开的代码或结果。 - 框架的内容安全策略拦截它的脚本能发起的所有请求(
connect-src 'none',也不加载任何第三方资源)。框架不能自行打开窗口或让本页跳转;只有你点击报告里的链接时才会打开页面。它传给本页的只有自身高度和下面这些统计事件。 - 解析、转换、字符串解码、重命名、拆包和生成
.zip都在你设备上的 Web Worker 里完成。代码从不执行,在 worker 里也不执行:上面那个求值器不用eval就能算出解码结果。 - 代码、字符串和模块名都以纯文本显示,输入里的任何内容都不会被当作 HTML 解析。
- 统计事件只有粗粒度的信息:成功或错误码、代码是粘贴的还是以文件打开的、字符串解码状态(
decoded、none、failed或off)、打包格式、模块数量区间、大小区间和耗时区间。不包含代码、字符串、文件名或模块名。框架把事件交给本页,由本页发送。完整列表见隐私政策。 - 不想要任何广告和统计?在单独页面打开反混淆工具。
如何自己验证
- 打开浏览器开发者工具的 Network(网络) 面板,加载本页。发往
tools.rea-reverse.com的请求就是框架:它的 HTML、样式、字体和脚本,以及 Worker 脚本/_astro/jsdeob-worker-*.js。发往 Google 域名的请求是本页的广告。 - 清空日志,然后反混淆一段代码、复制结果、下载
.zip。在过滤框里输入domain:tools.rea-reverse.com:最多只会再出现一次 Worker 脚本/_astro/jsdeob-worker-*.js(为下一次运行准备的新 Worker),没有任何请求带着你的数据。广告请求可能还会继续,但它们来自本页,不带框架里的任何内容。 - 点开来自
tools.rea-reverse.com的javascript-deobfuscator/文档,查看 Headers:它的Content-Security-Policy里有connect-src 'none'和frame-ancestors https://rea-reverse.com。在 Chrome 里,Application(应用)> Frames 也能看到框架的源和策略。Worker 运行在default-src 'none'; script-src 'self'; connect-src 'none'这条策略下,浏览器不允许它发出任何网络请求。这里没有'unsafe-eval',即使有代码想用eval或new Function,浏览器也会拦下。 - 检查隔离:在 Console(控制台) 里,上下文选
top,运行document.getElementById('tool-frame').contentDocument,结果是null;frames[0].document会抛出SecurityError。广告脚本拿到的也是这个结果。 - 或者先打开页面再断网(关掉 Wi-Fi,或在 Network 面板勾选 Offline),然后运行,照样能用。
我们怎么验证
在上面的实测输出里,方法名、字符串和 formatPrice 都回来了;参数名(p12、p13、p14)没有,因为混淆器根本没保留它们。我们的测试会在 Node 里把原始程序和反混淆后的程序各跑一遍,确认两者打印的内容完全一致。覆盖的样本包括上面这个文件、default 预设、terser 和 esbuild 压缩的版本,以及同一个程序的 webpack 5 和 browserify 打包。
耗时(Node 22,Apple 芯片笔记本实测):52 KB 的高强度混淆文件大约 1 秒,大部分时间花在旋转循环寻找校验值时反复做 RC4 解码;同一台机器上用无头 Chromium 打开本页,页面显示耗时 0.8 秒。普通的压缩文件快得多:用 esbuild 压缩的 TypeScript 编译器(3.6 MB)6.5 秒处理完,输出 5.6 MB、181,896 行。Babel 会把整棵语法树放在内存里,所以输入上限定在 5 MiB。
局限
- 原来的局部变量名找不回来。 压缩器和混淆器已经把它们丢掉了,文件里没有任何线索。工具只能给出占位名。如果网站发布了 source map,用 source map 还原工具能直接拿到原始文件。
- 字符串解码只支持一个子集。 我们的样本覆盖了 obfuscator.io 的
none、base64和rc4三种编码,以及下标偏移、包装函数和数组旋转。其他混淆器可能用到求值器拒绝的写法,那时字符串保持原样。 - 不处理虚拟机类混淆。 把代码编译成自定义字节码、再附带一个解释器的混淆方式(JScrambler 较激进的模式、各种自制 VM),这里还原不了。
- 作用域提升过的打包只能是一个文件。 Rollup、Vite 和 esbuild 的生产构建会把模块合并进共享作用域,已经没有模块边界可拆。只有 webpack 和 browserify 会保留模块表。
- 自我保护代码。 对开启了
selfDefending的 obfuscator.io 输出关闭字符串解码后,重新排版的代码一运行就可能卡死,因为那段检查就是在检测代码有没有被格式化。只读,别运行。 - 输入大小。 5 MiB,处理时间上限 3 分钟。
合规
请只对你自己的代码、你获得授权审查的代码,或许可证允许的开源和第三方脚本做反混淆。读懂代码在做什么,不等于有权复制它或绕开它的许可证;有些许可证和法律会限制逆向工程。参见免责声明。
对应的命令行
本页所用引擎的命令行版 webcrack 2.16.0 处理同一个文件,日志(stderr,已删节)显示同样解开了 353 个字符串:
$ bunx [email protected] -m obfuscator-high.js > out.js
…
2026-11-02T01:16:53.425Z webcrack:deobfuscate String Array: _0x12c2, length 358
2026-11-02T01:16:53.428Z webcrack:deobfuscate String Array Rotate: yes
2026-11-02T01:16:53.428Z webcrack:deobfuscate String Array Decoders: _0xc5f8
…
2026-11-02T01:16:53.471Z webcrack:transforms inline-decoded-strings: finished with 353 changes
…$ wc -lc obfuscator-high.js out.js
1 52088 obfuscator-high.js
116 2881 out.js
117 54969 total一处差别:命令行的 --mangle 把解构出来的导入保留为 _0x 名字,而本页把它还原成了 formatPrice:
$ grep -n _0x out.js | head -3
50: formatPrice: _0x17f481
81: var v8 = this.items.map(p15 => p15.quantity + " x " + p15.name + " = " + _0x17f481(p15.unitPrice * p15.quantity));
82: v8.push("TOTAL " + _0x17f481(this.totalCents()));常见问题
怎样还原 obfuscator.io 混淆的 JavaScript?
粘贴代码,保持“解开字符串数组”打开,点“开始反混淆”。页面会找到字符串数组、它的旋转函数和解码函数,在不运行代码的前提下算出每个字符串,同时撤销控制流平坦化和代理函数。
怎样在线反压缩 JavaScript?
粘贴压缩后的文件,点“开始反混淆”。格式化、拆分逗号表达式、把 !0 改成 true 等整理总会执行;压缩时丢掉的名字会换成简短的描述性名字,但没有 source map 就拿不回原名。
输出的代码一定和原来行为一致吗?
没有工具能对任意输入打这个包票,但每一步转换的目标都是不改变行为,我们的测试也会把每个样本的两个版本都跑一遍来核对。把输出当作阅读材料;真要运行,请运行原始代码。
为什么会出现 vLSHttpsapiexamplecomv1 这样的名字?
重命名时会参考变量第一次被赋的值:vLS… 表示字符串字面量(这里是 "https://api.example.com/v1/orders"),vLN0 是数字,vO 是对象,vC 是类,f 是函数,p 是参数。关掉重命名就会保留 _0x 名字。
处理结果显示“发现了,但没有解开”,怎么办?
说明解码函数用到了页面求值器不支持的写法,括号里写了原因。解码函数和它的调用都原样留在输出里;命令行版 webcrack 会在 isolated-vm 里运行解码函数,在你自己的机器上也许能解开。
能反混淆 Hermes 字节码、WebAssembly 或 Python 吗?
不能,只处理 JavaScript 源码。WebAssembly 请用本站的 WASM 转 WAT 工具。
更新于
相关教程
- JS bundle 与 source map 还原:bundle 带着 source map 时,直接还原原始文件和变量名,不必反混淆。
- JS 逆向实战:用 REA 还原 webpack 打包后的源码:source map、webpack chunk 恢复、Wakaru 拆包和命令行下的数据流追踪。
- 用 REA 逆向分析 Electron 应用:ASAR、preload 与 IPC 全流程:Electron 应用把压缩过的 JavaScript 放在
app.asar里,用 ASAR 分析工具取出来,再放到这里读。 - javascript-obfuscator 混淆代码能还原多少?14 组参数实测:用 14 组混淆参数实测本工具,列出体积、解出的字符串数,以及哪些选项还原不了。