ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

jsjiami.com.v7前端JS混淆代码解密还原实战

jsjiami.com.v7前端JS混淆代码解密还原实战 简介面向前端安全分析、JS 逆向与反混淆场景的 jsjiami.com.v7 代码解密工具基于 AST 与 Babel 插件实现可批量处理字面量还原、死代码清理、扁平化还原、条件与循环语句规范化等混淆类型适合需要分析 v7 版本加密脚本的进阶学习者。资源包内含完整项目源码与详细教程共 21 个文件以 JavaScript 源码为主12 个 js 文件辅以 package.json、package-lock.json 等依赖配置以及 README 说明、ESLint 与 Git 忽略规则等工程化文件整体压缩包仅 65KB结构清晰便于安装依赖后直接运行。已有 1685 人学习下载。包中提供 src/main.js 程序入口与 src/plugin 插件目录可查看具体插件实现逻辑教程覆盖 common、sojson、obfuscator 等 type 类型的调用方式并结合 VM2 环境处理全局加密内容能帮助读者快速掌握工具用法并理解 AST 净化原理。1. jsjiami.com.v7 是什么为什么一个解密工具能救回整份前端源码事情通常是这么开始的你从某个压缩包里拿到一份前端脚本打开一看整个文件只有两行第一行是几万个字符挤在一起的乱码第二行还是乱码。格式化之后你看到的是一个自执行函数套着另一个自执行函数变量名全是_0x3f2a、_0xabcd这种十六进制编号字符串全被拆成\x65\x76\x61\x6c这种形状。如果你正在做前端安全审计、JS 逆向分析或者接手一份被商业混淆工具处理过的历史项目这就是 jsjiami.com.v7 的典型产物。这类代码不是不能读是不能直接读。jsjiami.com.v7 把字符串、数组下标、控制流和反调试逻辑搅在一起目的就是不让你一眼看懂业务逻辑。代码解密工具要做的事就是把这些混淆手段一层层拆开还原成接近人写的可读代码。这篇笔记不讲玄学只讲一件事用工具和脚本把 v7 混淆的代码还原到能继续分析的程度。适合正在和混淆代码搏斗的前端工程师、安全测试人员以及刚从 zip 包里解出这类脚本、还不知道从哪下手的同学。2. 解密前先识货jsjiami.com.v7 的混淆特征与解密工具定位2.1 三种混淆风格的识别表变量名、字符串数组化与控制流平坦化jsjiami.com.v7 不是单一一种混淆而是把几类手段叠在一起。想还原得先认得出每一层。混淆层典型特征对分析的影响变量名混淆_0x开头的十六进制变量名密集出现函数逻辑难对应代码可读性归零字符串数组化大量\x十六进制转义字符串堆在一个数组里关键词搜索全部失效关键字符串不可见控制流平坦化一个大switchcase 里全是函数调用看代码的人被强制跳转打断思路反调试debugger语句、setInterval检测、constructor校验打开 DevTools 就卡死打断分析节奏自执行填充函数数组定义后面跟一个自执行函数重新排列数组元素静态分析拿到的数组顺序是错的替换必翻车记这五条就够了。拿到一段疑似混淆代码先对着表逐项勾选勾完你就知道该用什么工具、要处理哪几层。2.2 先用特征脚本快速判定怎么确认这是 v7 而不是其他混淆jsjiami.com.v7 的特征非常明显判定时间用不了五分钟。第一步把解出来的 JS 文件丢进编辑器用正则搜一下_0x开头的变量。# 快速统计 _0x 变量出现次数判断是否走入了 v7 常见套路 grep -oE _0x[0-9a-f]{4,6} target.js | wc -l # 看看文件头部 20 行确认是否有自执行函数包住整个文件 head -20 target.js # 检查是否存在 debugger 关键字v7 常把反调试和业务逻辑混在一起 grep -c debugger target.js这段脚本的逻辑很简单_0x开头的十六进制变量越多说明变量名混淆越彻底文件头部如果是(function(){...})()这种整体包壳说明是整文件混淆debugger出现次数大于 0说明反调试逻辑已埋入。我第一次拿到 v7 脚本时_0x出现了三千多次debugger出现十八次基本可以确定是商业混淆服务生成的结果而不是某个开发自己写的混淆插件。参数说明_0x[0-9a-f]{4,6}里的{4,6}是变量名长度范围v7 生成的变量名一般在 4 到 6 位十六进制之间。如果你拿到的文件里变量名超过 6 位说明不是这一代服务别硬套后面的还原脚本。2.3 代码解密工具帮我们省掉的三件事字符串还原、数组 dump 和反调试清除明确了混淆层之后解密工具的真实定位就清楚了。它有且只有三个核心能力把十六进制字符串还原成可读字符、从内存里把最终的大数组 dump 出来、批量清除反调试逻辑。除此之外所谓“还原源码”不可能做到 100% 和原始代码一模一样变量名还能优化成更有语义的名字但那是锦上添花的事不是必需。我一般会自己动手而不是直接依赖某个一键工具原因很简单v7 的混淆结果每次生成都可能略有差异一键工具处理不了边界情况但工具能帮你省掉 90% 的重复劳动。下面每一层的处理方式都是可复现的。3. 拿到 zip 后的处理流程解压、环境准备与数据还原3.1 解压前的三件事校验完整性、检查注释、确认是否需要密码标题里带zip意味着你的工作是从一个压缩包开始的。解压看起来简单但这里的坑比想象中多。先做完整性校验再解压。# 检查 zip 文件是否完整 unzip -t suspicious.zip # 查看压缩包内文件列表不急着解压 unzip -l suspicious.zip # 如果需要密码用 ASCII 模式解压防止编码问题 unzip -P PASSWORD suspicious.zip -d output_dir/unzip -t会逐文件检查 CRC只要有一个文件校验失败说明压缩包在传输过程中损坏了直接解压会得到残缺脚本。unzip -l是为了先看里面有几个文件、文件多大v7 混淆后的脚本往往单文件就几百 KB如果解压出来文件大小和列表对不上八成是 zip 出了问题。加密码的场景不常见但要确认密码。有时候压缩包注释里会写一串提示unzip -z可以查看注释这一步容易被忽略。解压完成后把脚本文件放到一个独立的目录别直接放在项目根目录里后面跑脚本时可能会产生临时文件污染现有项目。3.2 本地搭一个临时 Node 沙箱防止脏脚本搞乱环境解密 v7 混淆代码有一个很尴尬的事实你需要运行这份代码才能拿到最终的数组顺序但运行它又可能触发反调试、网络请求、甚至本地文件写入。所以强烈建议在 Node 里搭一个空壳沙箱环境不和正式环境混在一起。# 新建一个临时目录安装解密分析需要的依赖 mkdir v7_lab cd v7_lab npm init -y npm install esprima escodegen 2/dev/null || true # 把待解密的脚本复制进来 cp ../output_dir/target.js .这里装esprima和escodegen是为了后面做 AST 层面的控制流还原。参数说明esprima负责把混淆脚本解析成 ASTescodegen负责把修改后的 AST 重新生成代码。这两个库是安全分析场景里的常客v7 的 case 分支还原就是靠它们完成的。沙箱的意义在于当你在沙箱里运行脚本时Node 环境的process、Buffer、global对象可能存在但业务代码里引用的window、document不存在这反而帮你提前暴露了代码对环境的核心依赖。真到浏览器里去跑反而会被反调试干扰。3.3 批量还原十六进制字符串不用 AST 的 Python 预处理脚本拿到混淆代码后第一层要处理的是\x十六进制字符串。这层不还原后面搜关键词全是空的。#!/usr/bin/env python3 # 还原 \x 与 \u 转义序列保留其他字符不动 import re def unescape_match(match): token match.group(0) if token.startswith(\\x): return chr(int(token[2:4], 16)) if token.startswith(\\u): return chr(int(token[2:6], 16)) return token path_in target.js path_out target_unescaped.js with open(path_in, r, encodingutf-8, errorsreplace) as f: code f.read() # 只处理字符串字面量里的 \x 和 \u不做全局盲替换 escaped_pattern re.compile(r\\x[0-9a-fA-F]{2}|\\u[0-9a-fA-F]{4}) code escaped_pattern.sub(unescape_match, code) with open(path_out, w, encodingutf-8) as f: f.write(code) print(done -, path_out)这段脚本做的事很纯粹正则匹配\xHH和\uHHHH两种转义序列逐个还原成对应字符其余内容原样保留。逻辑说明chr(int(..., 16))是核心函数把十六进制数转成 Unicode 码点再转成字符。参数说明errorsreplace很关键混淆代码里偶尔会出现半个 UTF-8 字符不加这个参数读取时会直接报错。这段脚本跑完文件里那些\x65\x76\x61\x6c就变成eval了。到这一步字符串关键词搜索就能用了但距离可分析还差最关键的一步——大数组还原。3.4 从内存里 dump 最终大数组避开静态替换的陷阱 陷阱v7 混淆代码里有一段非常典型的结构数组定义在前一个自执行函数紧跟在后。这个自执行函数会在运行时把数组元素打乱重排。所以你在文本编辑器里看到的数组下标顺序和代码实际运行时的顺序是两回事。静态替换下标必翻车。正确做法是让代码自己跑一遍再从内存里把最终数组取出来。前提是沙箱环境已经备好。// dump-array.js在 Node 里执行混淆脚本捕获最终大数组 const fs require(fs); // 备份原始挂载点避免污染全局 const original global._0x3f2a ? { ...global } : {}; try { // 执行混淆脚本沙箱里只跑一次观察输出 require(./target_unescaped.js); } catch (e) { // v7 脚本运行时报错是常态先忽略主要目的是触发数组填充函数 console.error(exec error:, e.message); } // 把可疑的 _0x 全局变量打印成 JSON用于后续替换 const candidates {}; for (const key of Object.keys(global)) { if (/^_0x[0-9a-f]{4,6}$/.test(key) Array.isArray(global[key])) { candidates[key] global[key]; } } fs.writeFileSync(array_dump.json, JSON.stringify(candidates, null, 2)); console.log(dumped arrays:, Object.keys(candidates));这段脚本的思路是用 Node 的require加载混淆脚本让数组填充函数执行完再把挂在global上的_0x数组落盘。逻辑说明require执行完整个脚本后全局变量会保留在global对象上我们可以从这个对象里拿走真正用到的数组。参数说明正则/^_0x[0-9a-f]{4,6}$/只捕获数组类型的全局变量避免把普通对象也 dump 出来。dump 出最终数组后下一步就是把代码里所有_0x数组名[下标]的引用替换成实际值。这里我推荐的做法是写一个 Node 脚本读取array_dump.json然后对target_unescaped.js做批量替换。但不要用全局正则盲目替换要先收集所有数组下标再逐个映射。// replace-array-refs.js const fs require(fs); const dump JSON.parse(fs.readFileSync(array_dump.json, utf8)); let code fs.readFileSync(target_unescaped.js, utf8); for (const [name, arr] of Object.entries(dump)) { // 匹配 _0x3f2a[数字] 或 _0x3f2a[0x1a] 两种写法 const refPattern new RegExp(${name}\\[(\\d|0x[0-9a-f]|0x[0-9a-f])\\], g); code code.replace(refPattern, (match, indexStr) { let idx; if (indexStr.startsWith(0x) || indexStr.startsWith(0x) || indexStr.startsWith(0x)) { idx parseInt(indexStr.replace(/[]/g, ), 16); } else { idx parseInt(indexStr, 10); } const val arr[idx]; if (val undefined) return match; // 边界情况下标超出范围不替换 return JSON.stringify(val); }); } fs.writeFileSync(target_restored.js, code); console.log(replaced. output - target_restored.js);这段脚本的核心逻辑是把所有数组名[下标]的引用替换成实际字符串内容。逻辑说明下标可能以十进制数字或0x十六进制字符串形式出现正则里两种都覆盖了。参数说明val undefined时不替换保留原样这是防止数组元素缺失时强行替换导致代码更乱。这一步做完字符串还原和数组还原都完成了文件可读性比最初高了不少。但还有两层硬骨头控制流平坦化和反调试。下一章处理这两个。4. 还原控制流与反调试从 switch 大海捞针到一键清除4.1 控制流平坦化长什么样一个大 switch、无数 case 和回调数组还原之后代码里可能还有一大段switch结构。v7 会把原本顺序执行的逻辑改写成while循环里的switch每个 case 只做一小步操作然后改状态值继续循环。这就是所谓控制流平坦化。它长什么样呢大概是一段while (true)里面一个switch (_0x状态变量)case 编号从case 0排到case 30甚至更多每个 case 末尾都有一句continue或break然后回到开头重新判断。人眼读这种代码思路会被切割成碎片没几分钟就迷失了。要还原控制流先要把这个switch的骨架找出来然后逐个 case 把逻辑拼回原来的顺序。这一步靠手动做不现实靠 AST 做是正路。4.2 用 AST 把 case 展开成可读顺序核心思路和必要边界用esprima把代码解析成 AST 后我们能直接定位到那个while循环和内部的switch。展开的思路是找到状态变量的赋值位置从初始状态开始按状态值把 case 顺序串起来模拟执行一遍最后生成新的顺序代码。// flatten-switch.js还原控制流平坦化的骨架 const fs require(fs); const esprima require(esprima); const escodegen require(escodegen); const code fs.readFileSync(target_restored.js, utf8); // 第一轮解析 AST定位 while 循环里的 switch const ast esprima.parseScript(code, { tolerant: true }); // 一个简化的 case 展开器收集所有 case 的 body存入顺序数组 const caseBodies []; // 遍历 AST找到类型为 SwitchStatement 的节点 function walk(node) { if (!node || typeof node ! object) return; if (node.type SwitchStatement) { for (const c of node.cases) { caseBodies.push({ discriminant: node.discriminant.name || , caseTest: c.test ? c.test.value : null, body: c.consequent.map((stmt) escodegen.generate(stmt)), }); } } for (const key in node) { if (node[key] typeof node[key] object) { walk(node[key]); } } } walk(ast); const flatCode caseBodies .sort((a, b) (a.caseTest || 0) - (b.caseTest || 0)) .flatMap((c) c.body) .join(\n); fs.writeFileSync(target_flat.js, flatCode); console.log(flattened cases:, caseBodies.length);这段脚本的作用是把每个 case 里的语句按 case 顺序拼接成顺序代码。逻辑说明caseTest就是case 0后面的数字排序后能大致还原顺序。边界问题很明显这个简化版没有处理continue、break和return这几种跳转语义遇到复杂的嵌套循环会拼错。参数说明tolerant: true让 esprima 在解析遇到不标准语法时尽量不崩v7 生成的代码有些写法恰好踩在标准边缘。如果你拿到的脚本跑这个脚本后顺序明显杂乱说明嵌套太深需要自己根据状态变量的赋值顺序手动拼没有全自动的银弹。4.3 一键清 debugger 和反调试定时器我常用的替换规则反调试逻辑是解密过程里最烦人的层。v7 会在代码里埋debugger关键字还会用setInterval轮询检测开发工具是否打开一旦检测到就清空页面或死循环。还原阶段必须把它们清掉。// strip-anti-debug.js const fs require(fs); let code fs.readFileSync(target_flat.js, utf8); // 1. 删除裸 debugger 语句 code code.replace(/^\s*debugger\s*;?\s*$/gm, ); // 2. 屏蔽 setInterval 检测轮询把 setInterval 替换成空函数 code code.replace(/setInterval\s*\(/g, (function(){ return 0; })(); // 3. 处理 constructor 相关的环境检测常见反调试写法 code code.replace(/\.constructor\s*?\s*[]Function[]/g, true); // 4. 把检测函数调用点替换为 false避免进入死循环 code code.replace(/\b_0x[a-f0-9]{4,6}\s*\(\s*[]debug[]\s*\)/g, false); fs.writeFileSync(target_clean.js, code); console.log(cleaned. output - target_clean.js);这段脚本做四件事删除裸debugger语句、屏蔽setInterval、绕开constructor检测、把疑似调试检测的函数调用替换为false。逻辑说明第二行把setInterval(...)替换成(function(){ return 0; })(...)等于让定时器永远不执行。参数说明第四条正则里的[]debug[]匹配的是传入字符串debug的调用点这类调用点在 v7 混淆代码里很常见但如果你拿到的版本字符串不是debug而是别的词这行不会生效需要自己搜一下代码里检测用的字符串。4.4 解密后代码跑起来还有错先分清是还原失败还是环境桩缺失反调试清完之后代码应该能看个大概了。但如果你试图真正运行它大概率还是会报错。这时候要冷静判断报错是还原没做干净还是脚本本身依赖了window、document这些浏览器环境。一个简单的判别方法把错误信息里提到的变量名在还原后的代码里搜索一遍。如果这个变量是混淆生成的_0x名字说明数组替换漏了或替换错了如果变量名是window或document说明这是环境依赖不是还原问题。常见做法是给脚本补一个浏览器环境桩比如用jsdom提供基础的window对象但这不是解密任务的核心。解密做到能读、能查、能定位业务逻辑就已经达到了目的。5. 避坑清单jsjiami v7 解密最常翻车的 5 个现场5.1 现象还原后代码里全是undefined原因大数组填充函数还没执行完数组元素还是空位提前 dump 导致下标对应值是undefined。解决不要在require脚本之前 dump先让脚本执行完成再取global里的数组。如果脚本执行到一半抛错中断数组初始化可能没走完这种情况要看错误信息里的堆栈找到填充函数被中断的原因临时屏蔽掉那一段再试。5.2 现象格式化后括号乱、编辑器红一片原因十六进制字符串还原和格式化顺序搞反了。有人先做美化再做字符还原结果转义序列被格式化工具处理坏了。解决严格按照本文顺序先还原\x再还原数组最后做格式化。格式化工具推荐用 Prettier但它会把代码里的特殊注释吃掉所以还原后的代码如果不需要继续跑格式化前先备份一份。5.3 现象一打开控制台网页就卡死原因反调试逻辑没清干净setInterval或debugger定时触发。解决用第四节的清理脚本过一遍然后全局搜索setInterval、debugger、constructor这几个关键词清干净为止。有时反调试逻辑不是直接写在代码里而是通过数组拼接出来的所以要先做字符串还原再来清反调试。5.4 现象搜索关键词搜不到代码里根本没有原因字符串被拆成了十六进制数组元素或者字符串分多段拼接运行时才组合。解决先做字符串还原再去搜索。如果还原后还是搜不到考虑代码是不是用了String.fromCharCode或 base64 二次编码。这种情况要先对数组值做一轮解码再做关键词搜索。5.5 现象解密后运行报错变量不存在原因变量名混淆层没处理原变量名被替换成_0x后你的分析代码里引用的还是旧名字。解决这一步没有自动化的完美方案建议在还原后的代码里用编辑器全局搜索_0x把高频出现的变量做一次重命名替换成var1、var2这种可读名。批量重命名时注意作用域别跨函数硬替换否则会改出一堆新错。6. 验证还原成果用单变量对照法确认解压后的代码是否可继续用6.1 建立一个“还原前/还原后”双变量对照环境还原工作做完怎么证明还原结果是对的我的习惯是建立一组对照还原前的混淆代码和还原后的可读代码在同一个测试环境里跑对比输出。具体操作把还原后的代码放入一个空白 HTML 页面套上 jsdom 或直接在浏览器控制台执行记录console.log输出和网络请求列表。然后同样跑一遍原始混淆代码对比两次的输出差异。如果输出一致说明逻辑被还原得非常接近如果有差异差异点就是还原错误的位置。6.2 验证指标不要只看“不报错”“不报错”远远不够。v7 混淆代码里可能有一段逻辑只在特定用户操作时不生效或者某个依赖只在某个状态更新时触发。光看有没有报错会漏掉隐蔽的错误还原。我的验证表长这样验证项还原前还原后判定标准页面初始化行为正常正常一致用户点击后的请求参数请求体明文请求体明文一致关键业务字符串混合编码可读文本可搜索、可分析反调试触发卡死不卡死已清除函数调用链黑匣子可追踪可断点调试这张表在每次解密任务里都能用。你不一定要全部验证但至少要把“关键业务字符串”和“函数调用链”两项做掉否则很难说这次还原是成功的。6.3 最后一招给还原脚本加一个“后悔药”解密过程中最怕改错之后没法回头。我的习惯是每个步骤输出的文件都保留一份比如target_unescaped.js、target_restored.js、target_flat.js、target_clean.js全部留在目录里不清理。有人觉得这些文件占空间但实际每个文件也就几百 KB留着能让你在还原出错时快速回到上一步而不用从头再跑一遍。这个习惯救过我好几次血泪经验。解密 v7 不是一个点一下就完成的事它是一层层剥洋葱。剥的时候要有耐心每一层都验证、都备份最后才能拿到能用的东西。希望你也能顺利把那份黑匣子一样的代码拆开看到里面真正的业务逻辑。希望帮到你。本文还有配套的精品资源点击获取
返回列表