简介:这是一款专为前端开发者与逆向分析人员设计的JS代码解密工具包,聚焦解决jsjiami.com.v7等主流混淆平台(如sojson、obfuscator)生成的高强度JavaScript加密问题。工具基于AST解析技术,依托Babel插件实现字面量还原、死代码清除、控制流扁平化逆转、条件/循环规范化及特殊函数剥离,并在全局加密场景中集成VM2沙箱环境执行动态还原,显著提升解密准确性与鲁棒性。压缩包共21个文件,含12个核心JS源码(主入口src/main.js、插件模块src/plugin)、4个JSON配置文件(package.json、eslint/prettier配置等)、1个YML工作流定义,以及LICENSE、README.md等工程支撑文件,整体仅65KB,轻量易部署。已有1682人学习下载,开箱即用:安装Node.js后执行npm i,再通过npm run decode -- -t sojsonv7等指令即可一键解密,附带完整使用教程与清晰目录结构,特别适合需快速定位混淆逻辑、调试加密脚本或开展JS安全研究的中高级前端工程师。
1. jsjiami.com.v7 解密工具:不是“一键还原”,而是 AST 层面的精准手术刀
最近三个项目交接,全卡在同一个点上:交付的 JS 文件全是jsjiami.com.v7加密后的黑盒——变量名全乱码、控制流被拆成while(![])、字符串用String.fromCharCode(97, 108, 101, 114, 116)拼接、关键逻辑藏在eval(unescape(...))里。试过在线解密网站,要么超时失败,要么还原后语法报错;用传统正则替换?刚改完a["b"]又冒出c[d]["e"],越修越乱。直到跑通这个decode-js-main工具链,才真正理解什么叫「AST 解密」:它不碰字符串、不猜变量名,而是把混淆代码喂给 Babel 解析器生成抽象语法树,再用插件逐层剥离死代码、还原字面量、规范化条件分支——就像给加密 JS 做 CT 扫描+外科手术,保留原始逻辑结构,只剔除干扰组织。适合前端逆向、安全审计、老系统维护,尤其当你面对的是sojsonv7这类强混淆(带 VM2 沙箱逃逸检测)或obfuscator的多层嵌套时,比纯正则/eval 模拟靠谱十倍。别指望它处理混杂未混淆代码的文件——这是它的边界,也是你该提前做的预处理。
2. 从零启动:Node 环境搭建、依赖安装与命令执行全流程
2.1 环境准备:Node.js 版本与全局依赖检查
这个工具链基于现代 JavaScript 生态,对 Node.js 版本有明确要求。必须使用 Node.js v16.14.0 或更高版本(推荐 v18.18.0 LTS),低于 v14 的版本会因@babel/parser的 ES2022 语法支持缺失而直接报错SyntaxError: Unexpected token '??='。验证方式很简单:
node -v # 输出应为 v16.14.0 或 v18.x.x npm -v # npm v8.x.x 或 v9.x.x(v10+ 有已知 lockfile 兼容问题)提示:如果
node -v显示 v12 或更低,请立即升级。Windows 用户推荐用 nvm-windows 切换版本;macOS/Linux 用户用nvm install 18.18.0 && nvm use 18.18.0。不要用sudo npm install -g n,权限混乱会导致后续npm i失败。
2.2 项目初始化:解压、安装与脚本映射
下载jsjiami.com.v7代码解密工具+详细教程.zip后,解压到任意目录(建议路径不含中文和空格,如~/projects/js-decode)。进入根目录,执行标准 npm 流程:
cd /path/to/your/extracted/folder npm install这一步会读取package.json中的dependencies和devDependencies,安装核心依赖:
@babel/core,@babel/parser,@babel/traverse,@babel/generator:构成 AST 处理四件套;vm2:提供隔离沙箱环境,用于安全执行eval类混淆代码(sojsonv7类型必需);commander:解析-t,-i,-o等 CLI 参数;fs-extra:替代原生fs,支持递归创建输出目录。
安装完成后,package.json的scripts字段定义了预设命令。查看可用指令:
npm run # 输出类似: # > decode-js@1.0.0 # > Available scripts: # - decode:common → npm run decode -- -t common # - decode:jjencode → npm run decode -- -t jjencode # - decode:sojson → npm run decode -- -t sojson # - decode:sojsonv7 → npm run decode -- -t sojsonv7 # - decode:obfuscator → npm run decode -- -t obfuscator这些decode:*脚本本质是npm run decode -- -t <type>的快捷方式,避免每次敲长参数。
2.3 核心命令执行:输入/输出路径、类型选择与参数组合
工具入口是src/main.js,但日常使用绝不直接node src/main.js。正确姿势是通过npm run触发预定义脚本或手动传参。以下三种调用方式等效,按场景选用:
方式一:用预定义脚本(推荐新手)
假设你要解密一个sojsonv7类型的login.min.js,输出到login.decoded.js:
npm run decode:sojsonv7 -- -i ./input/login.min.js -o ./output/login.decoded.js方式二:用通用 decode 脚本(推荐批量处理)
当需要动态切换类型时,直接调用主命令:
npm run decode -- -t sojsonv7 -i ./input/api.js -o ./output/api.js方式三:全局安装后直接调用(适合高频用户)
先在项目根目录执行npm link(将本地包注册为全局命令),之后可在任意目录运行:
decode-js -t common -i /tmp/obf.js -o /tmp/clean.js注意:
-t参数必须是common/jjencode/sojson/sojsonv7/obfuscator之一,大小写敏感。-i和-o是可选参数,默认值为input.js和output.js(相对当前工作目录)。若input.js不存在,程序会报错ENOENT: no such file,不会静默跳过。
2.4 插件机制解析:为什么sojsonv7必须用 VM2,而common不需要?
工具的核心能力来自src/plugin/目录下的插件体系。每个插件是一个独立的 Babel 插件函数,接收@babel/traverse的path对象,通过path.replaceWith()或path.remove()修改 AST。例如:
src/plugin/common.js:处理高频局部混淆,如var _0x1234=['a','b']; function _0x5678(){return _0x1234[0];}→ 还原为function getA(){return 'a';}。它只做 AST 静态分析,无需执行代码。src/plugin/sojsonv7.js:针对jsjiami.com.v7的特有模式,包含两阶段:第一阶段用@babel/parser解析出eval(unescape(...))中的字符串;第二阶段必须调用vm2在沙箱中执行该字符串,因为其解密逻辑依赖window、document等浏览器全局对象,且常含setTimeout等异步操作。若跳过 VM2 直接eval,会因ReferenceError: window is not defined崩溃。
这就是为什么sojsonv7类型强制依赖vm2,而common类型完全离线运行。你在package.json中看到"vm2": "^4.1.0",正是为此服务——它不是可选依赖,是sojsonv7插件的 runtime requirement。
3. 输入文件规范:为什么你的input.js总是解析失败?
3.1 单一主加密函数:从“整个文件”到“一段代码”的认知转变
工具设计哲学是「一次只解密一个加密单元」。这意味着input.js不能是混合体:比如你把混淆后的登录逻辑、未混淆的工具函数、HTML 注释、甚至console.log('debug')全塞进一个文件,工具会直接报错Error: Multiple top-level statements detected或SyntaxError: Unexpected token。它期望的输入结构极其纯粹:
// ✅ 正确:仅含一段混淆代码(无额外内容) eval(function(p,a,c,k,e,d){e=function(c){return c.toString(36)};if(!''.replace(/^/,String)){while(c--){d[c.toString(a)]=k[c]||c.toString(a)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('0 1(2){3 4=5.6(7);8.9(4)}',[],10,'return|function|a|var|b|window|atob|dGVzdA|console|log'.split('|'),0,{})) // ❌ 错误:混杂未混淆代码 function utils() { return 'ok'; } // ← 工具会把它当成加密函数的一部分,导致 AST 解析失败 eval(function(p,a,c,k,e,d){/*...*/}('0 1...',[],10,'...'.split('|'),0,{})) console.log('debug'); // ← 任何非混淆代码都会破坏单入口假设提示:实际工作中,你常需从
.html或.js文件中手动提取加密块。推荐用浏览器开发者工具:打开混淆页面 → Sources 面板 → 找到目标 script → Ctrl+F 搜索eval(或function(p,a,c,k,e,d)→ 复制整段eval(...)行(含括号),粘贴到新建的input.js中。宁可多建几个input.js,也不要拼凑一个“全能文件”。
3.2 注释的微妙角色:允许存在,但位置有讲究
工具允许input.js包含注释(//或/* */),但仅限于加密代码外部。例如:
// 这是合法注释:描述来源 // 来源:jsjiami.com.v7 加密,类型 sojsonv7 eval(function(p,a,c,k,e,d){/*...*/}('0 1...',[],10,'...'.split('|'),0,{})) // 这也是合法注释:标记结束但如果注释插入到eval内部,比如:
eval(function(p,a,c,k,e,d){/* 这里加注释会破坏字符串结构 */}('0 1...',[],10,'...'.split('|'),0,{}))则unescape解密后得到的 JS 字符串会包含非法字符,导致@babel/parser解析失败。所以注释只能作为“包装纸”,不能侵入加密体内部。
3.3 文件编码与 BOM:UTF-8 without BOM 是唯一安全选项
Windows 记事本默认保存为UTF-8 with BOM,BOM(Byte Order Mark)是开头的EF BB BF三个字节。当fs.readFileSync('input.js')读取时,BOM 会被当作 JS 代码的前缀,导致@babel/parser.parse()报错SyntaxError: Unexpected character 'ï'(BOM 的 UTF-8 编码首字节0xEF被解析为非法字符)。解决方案只有两个:
- 用 VS Code 打开
input.js→ 右下角点击编码(如UTF-8 with BOM)→ 选择Save with Encoding→UTF-8; - 用命令行批量转换(Linux/macOS):
sed -i '1s/^\xEF\xBB\xBF//' input.js # 移除 BOM
注意:
iconv-lite等库虽能自动检测 BOM,但本工具未集成,硬编码处理反而增加复杂度。坚持UTF-8 without BOM是最省心的约定。
3.4 死代码清理的副作用:为什么还原后少了alert('hack')?
common和obfuscator类型插件默认启用「死代码清理」(Dead Code Elimination),即移除永远不会执行的分支。例如混淆代码中常见的:
if (false) { alert('hack'); } // 工具会直接删除整行 while(0){ console.log('dead loop'); } // 删除 while 块这本是优化行为,但如果你的加密逻辑故意用if(false)包裹真实代码(某些变种混淆会这样绕过静态分析),工具就会误删。此时需修改src/plugin/common.js,注释掉path.parentPath.remove()相关逻辑,或在main.js中传参禁用 DCE(当前版本未暴露开关,需改源码)。这是「安全 vs 完整」的权衡——默认开启 DCE 是为了产出干净代码,但逆向时你得知道它删了什么。
4. 避坑指南:五个血泪经验总结的高频翻车点
4.1 现象:npm run decode:sojsonv7报错ReferenceError: window is not defined
原因:sojsonv7插件内部调用vm2执行解密逻辑时,代码依赖浏览器全局对象(如window,document,location),但vm2默认沙箱是 Node.js 环境,没有这些对象。
解决:在src/plugin/sojsonv7.js的vm2创建处,显式注入浏览器模拟对象。找到const vm = new NodeVM({ ... }),改为:
const vm = new NodeVM({ console: 'redirect', sandbox: { window: {}, // 提供空 window 对象 document: { createElement: () => ({}) }, // 最小化 document location: { href: '' }, setTimeout: global.setTimeout, clearTimeout: global.clearTimeout } });血泪经验:
jsjiami.com.v7的sojsonv7模式常检查window.location.href是否含特定域名来触发解密,不模拟location会导致解密函数返回空字符串。
4.2 现象:decode:obfuscator运行后output.js为空文件
原因:obfuscator类型插件依赖@babel/preset-env进行语法降级,但package.json中未声明该 preset,导致@babel/core配置缺失,generate()时 AST 转 JS 失败。
解决:在项目根目录创建babel.config.json,内容为:
{ "presets": ["@babel/preset-env"] }并确保npm install @babel/preset-env --save-dev。否则obfuscator插件的path.replaceWith(t.stringLiteral(''))等操作无法正确生成目标代码。
4.3 现象:input.js含中文字符串,解密后output.js出现乱码(如查询)
原因:混淆代码中unescape('%u67E5%u8BE2')解码为 Unicode,但vm2执行时默认编码为ISO-8859-1,无法正确处理 UTF-16 编码。
解决:在src/plugin/sojsonv7.js的vm.run()前,强制设置process.env.NODE_ENCODING = 'utf8',并在vm选项中添加env: { NODE_ENCODING: 'utf8' }。更彻底的方案是重写unescape函数:
const unescapeFix = (s) => decodeURIComponent(s.replace(/%u([0-9A-F]{4})/gi, (_, hex) => '\\u' + hex)); // 在 vm.sandbox 中挂载 unescapeFix 替代原生 unescape4.4 现象:npm run decode:common报错TypeError: Cannot read property 'name' of undefined
原因:混淆代码使用了this或arguments等动态上下文,而common插件的字面量还原逻辑假设所有变量都是静态声明(var a = 'x'),遇到function(){return this.a}就崩溃。
解决:这不是 bug,而是能力边界。common插件只处理「确定性字面量」,对this/arguments/callee等动态引用无能为力。此时应切换到obfuscator类型(它用@babel/preset-env更全面地处理上下文),或手动补全this绑定(如const obj = {a:'x'}; obj.fn = function(){return this.a};)。
4.5 现象:output.js生成成功,但浏览器运行时报Uncaught TypeError: Cannot set property 'xxx' of undefined
原因:工具还原了字符串和变量名,但未处理with语句或eval动态作用域。例如with(obj){a=1}被还原为obj.a=1,但原始代码中obj可能是window下的属性,还原后obj未定义。
解决:这是 AST 解密的固有局限——它不模拟运行时环境。对策是:1)在output.js开头手动注入const obj = window || {};;2)用grep -n "with(" output.js定位问题行,人工转译;3)接受现实:with和深度eval是解密的禁区,优先用sojsonv7模式在vm2中执行获取结果,而非还原源码。
5. 进阶技巧:定制插件、批量解密与结果验证三板斧
5.1 定制插件:为私有混淆算法添加专属解密器
当遇到jsjiami.com.v7的定制变种(如修改了p,a,c,k参数顺序,或加入额外的 XOR 层),官方插件失效。此时需在src/plugin/下新建文件,例如custom-v8.js:
// src/plugin/custom-v8.js module.exports = function customV8Plugin({ types: t }) { return { name: 'custom-v8', visitor: { CallExpression(path) { const { callee, arguments: args } = path.node; // 匹配自定义混淆函数:customDecrypt('abc', 0x123) if (t.isIdentifier(callee) && callee.name === 'customDecrypt' && args.length === 2 && t.isStringLiteral(args[0]) && t.isNumericLiteral(args[1])) { const encrypted = args[0].value; const key = args[1].value; // 实现你的 XOR 解密逻辑 const decrypted = encrypted.split('').map(c => String.fromCharCode(c.charCodeAt(0) ^ (key & 0xFF)) ).join(''); path.replaceWith(t.stringLiteral(decrypted)); } } } }; };然后在src/main.js的pluginMap中注册:
const pluginMap = { // ...原有映射 'custom-v8': require('./plugin/custom-v8') };最后运行npm run decode -- -t custom-v8 -i input.js -o output.js。关键点:插件必须导出 Babel 插件函数,visitor对象监听 AST 节点,path.replaceWith()替换节点。不要试图在插件里require('fs')—— 这违反 Babel 插件设计原则。
5.2 批量解密:Shell 脚本自动化处理百个文件
面对几十个*.min.js文件,手动执行npm run效率低下。写一个batch-decode.sh(macOS/Linux):
#!/bin/bash # batch-decode.sh INPUT_DIR="./input" OUTPUT_DIR="./output" TYPE="sojsonv7" mkdir -p "$OUTPUT_DIR" for file in "$INPUT_DIR"/*.js; do if [ -f "$file" ]; then basename=$(basename "$file") output_file="$OUTPUT_DIR/${basename%.js}.decoded.js" echo "Processing $basename..." npm run decode -- -t "$TYPE" -i "$file" -o "$output_file" 2>/dev/null # 检查输出是否成功(非空且含有效 JS) if [ -s "$output_file" ] && head -n 1 "$output_file" | grep -q "function\|var\|const"; then echo "✓ $basename -> ${basename%.js}.decoded.js" else echo "✗ $basename failed (empty or invalid output)" rm "$output_file" fi fi doneWindows 用户可用 PowerShell:
$inputDir = ".\input" $outputDir = ".\output" $files = Get-ChildItem "$inputDir\*.js" foreach ($file in $files) { $outputFile = Join-Path $outputDir "$($file.BaseName).decoded.js" Write-Host "Processing $($file.Name)..." npm run decode -- -t sojsonv7 -i $file.FullName -o $outputFile 2>$null if ((Get-Item $outputFile).Length -gt 100 -and (Select-String -Path $outputFile -Pattern "function|var|const" -Quiet)) { Write-Host "✓ $($file.Name) -> $($file.BaseName).decoded.js" } else { Write-Host "✗ $($file.Name) failed" Remove-Item $outputFile } }注意:批量处理时,
-i和-o必须用绝对路径或相对于当前 shell 的路径,避免npm run工作目录混乱。
5.3 结果验证:三步法确认解密质量(不是“能跑就行”)
解密完成不等于逻辑正确。我习惯用以下三步交叉验证:
第一步:语法校验(防低级错误)
用eslint检查output.js是否有语法错误:
npx eslint output.js --no-eslintrc --rule 'no-unused-vars: off, no-undef: off' # 若输出空,说明语法合格;若有 `Parsing error`,说明 AST 生成有缺陷第二步:行为比对(核心逻辑)
在浏览器控制台分别执行原始混淆代码和output.js,对比关键输出:
// 原始混淆代码(复制到 console) eval("..."); // 假设它定义了 window.calcToken() console.log(window.calcToken('abc')); // 记录输出,如 "xyz123" // output.js(复制到 console) // ...还原后的 calcToken 函数 console.log(calcToken('abc')); // 应输出相同结果第三步:AST 差异分析(深度可信)
用astexplorer.net分别粘贴混淆前(如有)、混淆后、解密后代码,对比 AST 结构。重点关注:
FunctionDeclaration的body是否完整保留;StringLiteral的value是否与原始明文一致;BinaryExpression(如a+b)是否未被错误拆分为a.concat(b)。
从那以后我每次交付解密结果,都强制走一遍这三步:语法校验扫雷、行为比对保逻辑、AST 分析验结构。少一步,就可能把
calcToken还原成getToken,上线后 token 校验全挂。希望帮到你。
本文还有配套的精品资源,点击获取