jsjiami v7解密工具实战:AST与Babel插件还原JavaScript混淆代码

发布时间:2026/10/9 12:36:47
jsjiami v7解密工具实战:AST与Babel插件还原JavaScript混淆代码
简介这是一款面向前端安全分析、混淆对抗与代码还原场景的 jsjiami.com.v7 解密工具包适合需要处理 v7 版本混淆 JS 的逆向工程师或爬虫开发者。工具基于 AST 与 Babel 插件体系覆盖字面量还原、死代码清理、控制流扁平化、条件循环语句规范化及特殊函数清理等典型净化类型并借助 VM2 构造隔离环境应对全局加密内容。压缩包共 21 个文件核心为 12 个 js 脚本与 4 个 json 配置另有 yml 工作流、license、gitignore、eslintignore 及说明文档整体仅 65KB轻量易部署。资源配有详细教程明确 Node.js 环境配置与 npm i 依赖安装方式支持通过 package.json 预定义指令或 decode 命令传入 common、sojson、obfuscator 等类型进行解码输入输出文件可灵活指定。目前已有 1686 人学习适合具备一定 JavaScript 基础、希望快速落地 v7 混淆还原流程的读者直接参考使用。1. 拿到 jsjiami v7 解密工具先分清它能解什么、不能解什么一个反直觉的结论jsjiami v7 解密工具真正解决的不是“读懂混淆”而是“把还原流程固化下来”。我最近接手的几个老项目里全是这类混淆代码里面变量名全是_0x开头的乱码、字符串被打散进数组、控制流被 switch 拆得面目全非。这个工具基于 AST 方式依赖 Babel 插件做代码净化支持字面量还原、死代码清理、扁平化还原、条件和循环语句规范化、特殊函数清理处理全局加密内容时用 VM2 提供隔离环境。它适合的是需要快速摸清被混淆代码逻辑的从业者——外包维护、二次开发、事故排查这类场景下你需要的不是一部混淆原理教材而是一条能稳定跑通的批处理流水线。边界也很清楚一个输入文件只能包含一段混淆代码且不允许混入非混淆内容。2. 解密工具的运行底座AST 遍历与 Babel 插件2.1 为什么是 AST而不是文本替换处理混淆代码时最直觉的想法是用正则去匹配_0x前缀变量然后批量替换。真这么做过一次就知道正则只能处理平铺的文本一旦遇到嵌套括号、数组下标里再套函数调用正则直接失控而且你根本不知道哪些字符串是变量名、哪些是属性名、哪些只是注释里的内容。AST 的方式完全不同。babel/parser把源码解析成一棵语法树变量、表达式、语句都变成树上的节点插件就是针对特定节点类型做改写。比如字面量还原实际做的是访问StringLiteral或CallExpression节点把被加密的字符串参数取出来替换成真实值。改的是树最后再重新生成代码这样不会误伤结构。我一般会先单独跑一段解析确认语法树能建出来再交给插件链处理否则后面所有步骤都是在空中楼阁上操作。工具在src/main.js里组织整个还原流程读入配置参数、选择 type、按顺序挂载对应插件然后遍历、改写、生成输出。src/plugin目录下放的是各类还原能力的独立插件每类混淆类型对应一组插件组合。这种结构的好处是遇到新的混淆变体你只需要新写一个插件不用改动主流程。2.2 项目结构与依赖准备解压后是一个标准的 Node.js 项目结构核心路径如下decode-js-main/ ├── src/ │ ├── main.js │ └── plugin/ ├── package.json ├── package-lock.json ├── .eslintrc.json ├── .prettierrc.json └── README.mdmain.js是程序入口plugin是插件目录package.json里定义了预执行命令。整个工具跑在 Node.js 环境中所以第一步是确认本机环境node -v npm -v我建议使用 LTS 版本具体原因后面避坑章节会讲。环境确认没问题后在项目根目录安装依赖npm install这一步会安装babel/parser、babel/traverse、babel/types、vm2等核心依赖。不需要手动逐个安装package.json里的依赖声明会全部拉取到位。安装完成后可以先执行一次脚本确认能正常调用npm run decode -- --help如果能正常输出参数说明说明环境已经就绪。如果提示脚本找不到多半是 npm 版本太老或者node_modules没装进去重新执行npm install即可。2.3 最基础的一次解密调用工具提供了预定义命令和完整命令两种调用方式。预定义命令写在package.json的scripts字段里直接npm run xxx即可。完整命令则在decode脚本后通过--传参npm run decode -- -t common -i input.js -o output.js命令行里的--是 npm 的参数透传标记它的作用是把后面跟的参数原样传给node src/main.js。-t指定解密类型-i指定输入文件-o指定输出文件。三个参数里-t是必填的-i默认取当前目录下的input.js-o默认生成output.js。第一次跑的时候我建议在项目根目录建一个work/子目录把待处理的文件放进去避免污染项目本身。调用时这样写mkdir work cp ~/Desktop/confused.js work/input.js npm run decode -- -t common -i work/input.js -o work/output.js跑完后打开work/output.js如果能看到正常的字符串和可读的变量名说明基础流程通了。如果代码没有任何变化先不要怀疑工具失效大概率是 type 选错了下一章专门讲怎么选。2.4 预定义命令与完整命令的关系package.json里的scripts字段长这样{ scripts: { decode: node src/main.js } }所以npm run decode实际执行的就是node src/main.js。预定义命令本质上是把某些 type 参数固化成了脚本比如{ scripts: { decode: node src/main.js, decode:sojsonv7: node src/main.js -t sojsonv7 } }这种设计是给固定场景准备的。如果你的项目里长期处理某一种混淆类型就可以自己在scripts里加一条快捷命令省得每次输入一长串参数。我用得比较多的是在scripts里加decode:all把多个 type 串起来批量执行这个在最后一章细说。3. 五种 type 怎么选先辨认混淆特征再动手3.1 common不确定时的兜底选项common 标注的是高频局部混淆也是我默认的入口。它针对的是那种没有明显的大型加密函数、但代码里到处散布变量名乱码和字符串数组的混淆方式。这种混淆的特征是整个文件看起来还算正常函数结构没被破坏但变量名是_0x加十六进制字符串被抽到一个数组里通过一个解密函数取用。它不像全局加密那样把整个函数体包在一个大自执行函数里所以处理起来相对温和。如果你打开文件后第一眼判断不出它属于哪个混淆服务就用 common 先跑一遍。它的处理思路是通用的不依赖特定混淆器的实现细节。跑完后对比输入输出能恢复一部分可读性就有继续深入的底气。3.2 jjencode 与 sojson老牌编码器的特征识别jjencode 类型代码里大量出现类似$~[]、{}加下标取字符串的写法数字通过位运算构造出来整体看起来像一串乱码的符号堆叠。它对应的是一种把 JavaScript 编码成符号序列的老式手法多见于早期混淆器。处理这种类型时还原的重点是拆掉那层编码包装把语义还原成正常的字面量。sojson 类型相对直观一些它会生成一个大数组数组元素是分割好的字符串片段然后通过移位函数重新排列。你会在代码里看到形如function(_0x4b2c) { ... }的长函数还有类似var _0x5a4d [..., ..., ...]的结构。这种混淆的字符串表集中放在一处还原的关键是定位移位函数的还原逻辑把数组下标映射成实际字符串。3.3 sojsonv7本工具的主要目标sojsonv7 是这套工具重点处理的对象。它的典型特征是加密函数体积大整个业务逻辑被包进一个大的匿名自执行函数里字符串表声明在函数内部通过下标和算法取用控制流扁平化明显大量 switch-case 结构被用于打乱语句顺序内置一部分全局环境检测直接抛到浏览器里执行会触发各种环境判断判断是不是 v7 版本最直接的方式是看字符串数组外面是否套了一层解密函数引用且这个函数接收十六进制下标参数。另外 v7 还会在代码里塞大量死代码分支看起来分支很多实际能走到的没几个。处理这类混淆时光用完整体还原还不够后面还得用 VM2 沙箱模拟浏览器环境去处理全局加密部分这正是这个工具和其他简单还原脚本的主要区别。3.4 obfuscator另一类生成器的处理思路obfuscator 类型对应的是另一类 JavaScript 混淆服务的产物特征也很明显代码前部通常有一段自执行函数专门做控制台禁用函数名被重排成无意义字符代码整体被拆成大量小函数互相调用。它和 sojson 系的核心差别在于字符串表的使用方式。sojson 系偏爱集中式大数组obfuscator 系则倾向于分散式字符串拼接每次取用都现场构造。处理时死代码清理的比重更大因为这类混淆器会塞入大量声明了但永远不会被调用的函数来干扰阅读。判断方法是搜索console如果找到一段专门重写console.log的代码并且函数名全部是短横线加数字的变体基本就是这一类。选对应 type 跑完后再配合一遍 common效果通常好于单独跑。3.5 选型的试错流程与结果对比选 type 这件事不值得靠猜我自己的流程是这样cp unknown.js work/input.js npm run decode -- -t common -i work/input.js -o work/output_common.js先跑一遍 common 作为基准输出。然后看文件特征如果明显是 v7 大函数套控制流再跑npm run decode -- -t sojsonv7 -i work/input.js -o work/output_v7.js最后对比两次输出的体积和可读性wc -c work/input.js work/output_common.js work/output_v7.js grep -c _0x work/output_common.js work/output_v7.jswc -c看体积变化grep -c _0x统计还原后还剩多少混淆变量名。数字越少说明还原越彻底。如果两个 type 跑出来的结果差不多那就以体积更小的为标准。这个对比流程值得养成习惯特别是当你面对一份来源不明的混淆代码时它能帮你快速找到正确的处理方向。4. 核心还原能力拆解插件层在处理哪些节点4.1 字面量还原全局与代码块的双层策略字面量还原处理的是那类把字符串抽进数组、再通过解密函数取用的混淆。还原思路是找到数组声明位置建立下标到字符串的映射表然后遍历所有取用该数组的调用表达式直接替换成真实字符串。全局还原和代码块还原的区别在于映射表的定位方式。全局混淆把数组挂在最外层作用域数组下标可以直接建立全局映射代码块混淆则把数组塞进某个函数内部每次进入函数才初始化。后者的处理难点在于同一个函数可能在不同作用域声明了同名数组建立映射时不能串表。我一般这样验证字面量还原是否生效grep -n 0x work/output.js | head -20如果还原成功输出里不会再出现_0x加十六进制下标的取用表达式而是直接看到中文或英文的字符串内容。这里有个容易踩的细节有些字符串值本身包含0x字符比如0x123这种十六进制文本grep 搜到不代表没还原到位要看它是不是出现在字符串内容里。4.2 死代码清理从一个黑匣子里找出不变量死代码清理是还原可读性收益最高的步骤。混淆器为了干扰阅读会生成大量永不可达的分支、声明后从不调用的函数、以及赋值后不再读取的变量。手工找这些代码非常费劲插件自动做就快得多。原理不复杂先遍历整棵 AST收集所有标识符的声明与引用。如果一个函数声明在整棵树上没有任何调用点就标记为可删除如果一个 if 分支的条件经过字面量还原后变成了true或false就把确定不执行的分支修剪掉如果一个变量被赋值后从未被读取就把赋值语句删掉。修剪的过程不能只做一轮因为清理掉一层死代码后可能暴露出更多的死代码。比如一个被声明但无人调用的函数它的内部参数可能又满足清理条件。所以这个工具的内部流程是循环遍历直到某一次遍历没有产生新的删除节点为止。只跑一轮就结束的还原脚本效果通常不完整你会看到输出里还有大量unused function的残留。4.3 控制流扁平化还原switch-case 的顺序恢复扁平化还原是整块还原中最复杂的一部分。混淆器把原本顺序执行的语句打散塞进一个大的 switch-case 分发器通过一个状态变量记录当前应该执行哪个分支。每个分支执行完后又给状态变量赋新值跳转到下一个分支。还原思路是先找到状态变量的赋值链建立起状态值到具体代码块的映射关系然后把每个 case 分支按状态值的递增顺序重新拼接成顺序语句。这里有个细节需要注意部分混淆器会在 case 分支里再嵌套循环或者在分支里插入 try-catch 干扰分析。遇到嵌套时先对内层做同样的还原再回到外层处理。效果直接体现在代码结构上。还原前的代码是几十行的 switch-case还原后变成一句句按逻辑顺序排列的语句块。如果你看到输出文件里还残留大段 switch说明这一步没有完全生效可以考虑换对应 type 再跑一遍。4.4 VM2 沙箱处理全局加密内容的隔离层处理全局加密内容是这套工具里比较独特的设计。全局加密内容指的是那些不在单个函数内完成、而是依赖整个代码运行环境来还原的混淆片段。这类内容如果直接在 Node 环境里执行代码里引用的window、document等对象会直接报错。VM2 在这里的作用是提供一个隔离的类浏览器环境。这段配置是工具内部处理全局加密内容时常见的方式const { NodeVM } require(vm2); const vm new NodeVM({ console: inherit, sandbox: { window: { ... }, document: { ... }, navigator: { userAgent: node-vm } }, timeout: 1000 });关键参数是sandbox和timeout。sandbox里的对象是提供给待执行代码的全局环境window和document不需要实现完整功能只需要抛不出ReferenceError即可timeout限制单次执行为 1000 毫秒防止加密代码里写了死循环把进程卡死。VM2 的价值在于隔离——就算加密代码里有恶意逻辑它也只能在沙箱里执行没法访问到宿主的文件系统。关于 VM2 本身的服务端逃逸问题业界有讨论所以处理来源完全不可信的代码时我会在更隔离的环境里跑这个放到第五章细说。4.5 条件与循环语句规范化从乱序到可读条件与循环规范化解决的是代码可读性下降的问题。混淆后的代码里for循环可能被改写成while加自增变量的形式if-else可能被改写成三元表达式嵌套或switch分支。规范化处理会把这类变体尽量恢复成常规写法。比如var i 0; while (i 10) { console.log(i); i; }会被规范成for (var i 0; i 10; i) { console.log(i); }这一步的价值不在于改变代码行为而在于让代码恢复到开发者日常阅读习惯的形式。虽然不影响执行但对你后续维护、排查问题、或者交给团队其他成员接手都有明显帮助。5. 避坑与常见问题排查解密翻车的五个典型场景5.1 输入文件里混了非混淆代码现象解密后输出的代码语法错乱output.js打开后甚至出现明显不完整的语句或者在还原脚本执行时直接报错中断。原因工具设计时要求输入文件是全球仅含一段混淆代码的纯混淆文件。如果你复制代码时把外层定义、业务调用、或者注释之外的普通代码一起带进去了解析器就会把非混淆部分也当作待还原对象处理还原逻辑会尝试“解密”本来就正常的代码把函数声明和表达式改得面目全非。解决处理前先剥离非混淆部分。我一般这样做先 grep 看文件里有没有大段的普通函数定义有的话手动移到另一个文件保留的input.js只留纯混淆的片段。这个步骤虽然手动操作多但非常关键省得跑完出来一堆不可用结果再返工。5.2 同一个文件里有多个主加密函数现象运行命令后提示只能识别一个主加密函数或者输出文件里只处理了第一段后面的内容原样排列。原因工具按单段混淆代码设计它从文件入口处开始定位“主加密函数”找到第一个符合特征的自执行函数后就开始还原不会去识别第二个、第三个。解决把多段混淆拆开处理。先人工看一遍文件找到每一个独立的加密函数边界把每段分别存成一个文件逐个跑还原最后再把还原结果合并。合并时注意变量冲突每个文件还原出来的变量名可能重复合并且前手动加前缀或二次重命名最稳妥。5.3 VM2 沙箱环境不完整导致解密中断现象解密过程中抛出ReferenceError: window is not defined或TypeError: document.getElementById is not a function这类环境相关错误还原流程中断。原因加密代码在还原过程中会引用浏览器环境对象做检测或取值sandbox里如果没有预置对应对象代码执行到这一步就崩溃。解决往sandbox里补环境变量。常用的做法是构造一个 Proxy 转发所有属性读取const sandbox new Proxy({}, { get(target, prop) { return target[prop] || function () {}; }, set(target, prop, value) { target[prop] value; return true; } });get拦截让任何环境变量读操作都能拿到一个兜底函数或对象set允许加密代码往沙箱里写值。这样跑完整个流程后可以再console.log沙箱里的值用来观察加密代码到底访问了哪些环境属性。另外timeout参数不要设太大。某些加密代码本身有循环逻辑在沙箱里跑太久可能不是死循环而是环境不匹配导致的重复重试1 秒到 2 秒已经足够判断基本行为超过 5 秒的建议直接放弃沙箱方案改用 mock 环境更彻底。5.4 Node.js 版本不兼容导致 Babel 解析失败现象npm run decode执行后报SyntaxError: Unexpected token或者 Babel 插件内部抛出Cannot read properties of undefined之类的错误。原因工具依赖的babel/parser和插件体系对 Node 版本有要求。Node.js 版本过老缺少现代 JavaScript 语法解析支持版本过新某些老依赖又可能没跟上。这种版本玄学问题在实际使用里很常见。解决固定在一个 LTS 版本上跑。我的做法是在项目里直接指定nvm use 18 node -v如果手头没有 nvm也可以用volta或fnm做版本锁定。版本锁定后重新删掉node_modules再装一次依赖rm -rf node_modules package-lock.json npm install重新安装是因为旧依赖可能编译于不同 Node ABI换个版本后直接npm install有时不会重新构建原生模块会留下隐患。这条流程治好了我几次莫名其妙的 Babel 报错。5.5 还原后的代码能跑但没法二次编译现象output.js用 Node 直接执行没有问题但拿到 webpack 或打包工具里编译时报语法错误或者压缩时报Unexpected character。原因还原工具保留了原混淆代码的部分语法特征比如多余的逗号表达式、不必要的括号嵌套、以及非法标识符残留。这些在运行时没问题因为 JavaScript 引擎的解析容错性比打包字节码解析器更强但二次编译时就显形了。解决对输出做一次彻底的格式化。用 Prettier 跑一遍是最快的npx prettier --write work/output.js格式化后再手动检查几个高频位置文件头部是不是直接以表达式开头、有没有连续的分号、有没有空函数体。检查完再扔给打包工具基本就不会卡了。这个习惯后来成了我处理所有混淆文件的固定步骤解密 → 格式化 → 二次编译验证。6. 还原结果的验证用差异统计代替肉眼检查解密完成后不能只看“文件变小了”就认定还原成功。可靠的做法是把还原前后的代码做统计对比用数字判断本次还原的深度。const fs require(fs); const parser require(babel/parser); function getStats(code) { const ast parser.parse(code, { sourceType: script }); let identifierCount 0; let stringCount 0; let callCount 0; function walk(node) { if (!node || typeof node ! object) return; if (node.type Identifier) identifierCount; if (node.type StringLiteral) stringCount; if (node.type CallExpression) callCount; for (const key in node) { if (key loc || key start || key end) continue; const value node[key]; if (Array.isArray(value)) { value.forEach(walk); } else if (typeof value object) { walk(value); } } } walk(ast); return { identifierCount, stringCount, callCount }; } const original fs.readFileSync(work/input.js, utf8); const output fs.readFileSync(work/output.js, utf8); console.log(还原前, getStats(original)); console.log(还原后, getStats(output));这段脚本统计了三个关键指标Identifier节点数量代表变量名和属性名的密度StringLiteral节点数量代表字符串字面量的数量CallExpression节点数量代表函数调用次数。还原质量好的结果应该是字符串节点数量上升、标识符数量下降、调用次数减少。把这个统计脚本加进你的常规流程里配合上一章的格式化步骤每次解密后跑一次能直观看到这次还原是否值得继续深挖。除了差异统计之外还有一个我后来养成的验证技巧把还原后的代码再扔回浏览器环境或 Node 里做一次“行为快照”。还原前和还原后分别执行一次打印输出结果两边一致说明还原没有破坏原有逻辑不一致说明某个还原步骤把代码语义改坏了需要回到对应插件排查。这套工具和详细教程的路子说白了就是让你把还原流程固化下来。从那以后我每次处理同类混淆代码都强制走一遍同样的流程先看特征选 type再跑 common 做基准中间每一步结束后立刻用统计脚本看差异最后格式化并做行为验证。整套动作走完时间成本稳定在十分钟以内。希望帮到你。本文还有配套的精品资源点击获取