搞前端的人十有八九在项目里撞见过这种场景一段关键的JavaScript被Obfuscator.io打过包打开文件满屏_0xabc、_0x1234变量名全是十六进制看起来比加密还吓人。但我要先泼一盆冷水Obfuscator.io的混淆不是加密它只是把代码“揉成一团”逻辑还在只是变得极其难读。这篇文章不教你去破解别人的商业代码而是面向你自己写的、或已获授权的脚本告诉你如何系统地识别并还原这类混淆把“天书”重新变成人话。1. 先看清Obfuscator.io到底给代码做了哪些“手脚”1.1 混淆不是加密而是一种等价变换很多人第一次接触Obfuscator.io混淆第一反应是“这代码被加密了没法解”。其实这个判断不准确。加密需要密钥最终会把明文变成不可逆的密文而混淆是一种等价变换它保留了原始代码的所有执行语义只是把变量名、函数名、常量、流程结构尽量变得难以阅读。打个比方加密是把一封信烧成灰混淆则是把信中的句子全部打乱、用代号指代但信的内容依然完整存在。理解这一点非常关键。因为混淆既然是等价变换就存在一条从“混淆后”恢复到“混淆前”的路径。不管是靠人读还是靠AST自动做变换都只是在做“逆运算”。这也是为什么Obfuscator.io的混淆可以被还原而不是像真正加密那样只能靠密钥。1.2 常见混淆特征对照表Obfuscator.io提供多级混淆选项最新版本的参数非常丰富。根据我实际处理过的样本最常见的“手脚”集中在下面几个方面混淆类型典型特征对阅读的影响标识符重命名变量/函数名变成_0x1a2b3c这类十六进制串完全丧失命名语义函数作用难以判断字符串数组化字符串常量集中到一个大数组中运行时通过解码函数取值看不到任何明文障碍最直接字符串编码数组中的字符串被Base64、RC4或自写算法二次编码就算看到数组也无法直接读懂原文控制流平坦化while(!![])或for(;;)里套switch状态机原本顺序执行的语句被拆散逻辑顺序彻底打乱死代码注入插入大量无业务意义的假分支、假循环混淆代码体积膨胀干扰定位自我保护debugger内联、环境检测、反格式化检测打开调试工具就不断中断或退出这些特征经常叠加出现。一个完整的Obfuscator.io混淆样本可能同时包含五六种手段。还原的时候不能指望一步到位而是要把每一层逐一拆开。1.3 动手前的安全边界既然聊到“破解”就绕不开边界问题。我只建议在三种场景下做这种还原第一代码是你自己写的源码丢了通过混淆产物反向找回第二你正在做安全审计对方授权你分析某个脚本第三你是为了学习混淆原理用自己生成的样本做实验。除这些情况外不要拿这类技术去绕过别人的授权、破解商业软件或者窃取逻辑。技术本身是中性的但用在哪、怎么用决定了它的性质。2. 搭建还原环境与第一轮“格式化”2.1 环境清单工欲善其事必先利其器。还原JavaScript混淆我建议按下面这套环境准备一个现代浏览器最好是Chromium内核主要用它的开发者工具。本机安装Node.js运行环境版本18以上后面跑脚本和动态调试都靠它。一个支持JavaScript的代码编辑器用来查看大文件和处理脚本。一些npm包babel/parser、babel/traverse、babel/generator这些是AST解析、遍历、生成代码的必备工具。一个格式化工具比如常见的Prettier或者代码编辑器自带的格式化插件。这套环境没有任何花哨的地方很多做前端的本来就具备。重点是后面怎么用它们组合成一条还原流水线。如果你之前没有装过相关依赖可以直接在项目目录下执行npm init -y npm install --save-dev babel/parser babel/traverse babel/generator prettier这几个包体积不大但后续写还原脚本时会反复用到。2.2 用美化器先生成可读骨架拿到一段Obfuscator.io混淆代码不要急着读先用格式化工具把压缩代码“展开”。混淆后的代码通常是单行压缩格式化后至少能看到语句层面的结构。我习惯把代码复制到本地文件然后直接在编辑器里运行格式化。格式化本身不会改变逻辑但会把一长串代码拆成多行方便观察顶层结构。格式化之后你会看到类似下面的骨架开头通常有一大段数组初始化例如var _0xabc [..., ..., ...]紧接着是一个或两个解码函数比如function _0xdec(_0xidx) { ... }再往后才是业务逻辑但业务逻辑里到处调用_0xdec(0x1f)这种函数。这个阶段的目标不是读懂代码而是确认“它到底用了哪几种混淆”。看清楚了后面的还原计划才好定。我一般会在格式化之后先在编辑器里搜索while (!![])、debugger、String.fromCharCode这些关键词快速摸清混淆种类。2.3 静态阅读的辅助技巧格式化之后先用眼睛“摸”一遍代码。具体看几个位置数组区附近是否有长字符串是否有二进制数据解码函数内部是否调用了atob、parseInt、charCodeAt、String.fromCharCode等API主逻辑里是否频繁出现带数字参数的函数调用。这些信息能帮你快速判断编码强度。我还会把格式化后的代码先跑一遍确认它能在Node里正常执行。如果执行报错优先处理环境缺失而不是直接开始还原。因为有些混淆为了反调试会在开头检测运行环境比如检查window、document是否存在在Node里跑就容易崩。这种情况下用浏览器开发者工具执行反而更稳。3. 字符串数组与解码函数最容易突破的点3.1 识别数组和函数之间的关系Obfuscator.io最经典的混淆手法是把所有字符串先抽到一个大数组里然后定义一个解码函数。业务代码里凡是需要字符串的地方都通过这个解码函数去数组里取。举个例子一段代码可能长这样var _0xarr [0x68, 0x65, 0x6c, 0x6c, 0x6f]; function _0xdec(_0xkey) { return _0xarr[_0xkey - 0x10]; } var msg _0xdec(0x10) _0xdec(0x11);这里_0xdec(0x10)取的是数组第一个元素_0xdec(0x11)取第二个。数组元素本身可能还是十六进制编码也可能是Base64字符串需要再解一层。最直接的方法是先把数组和函数分离出来放到浏览器Console里执行看它返回什么。3.2 在浏览器Console里完成一次手动解码这一步非常简单但非常有效。把_0xarr和_0xdec的完整定义粘贴到Console然后手动执行几个调用点。比如上面那段代码在Console里执行_0xdec(0x10)会得到hello。这就把字符串的明文拿到了。如果数量不多完全可以靠这种方式手工替换。但现实里一个复杂的混淆样本可能有几百个字符串手工替换不现实。而且很多解码函数的参数不是简单的数字可能是十六进制字符串、负数、甚至表达式这时候就要考虑自动化。3.3 自动化替换用AST批量还原字符串自动化还原的核心思路是把代码解析成AST遍历所有调用解码函数的位置将调用结果直接替换成字符串字面量。以babel/parser为例大致步骤是const parser require(babel/parser); const traverse require(babel/traverse).default; const generate require(babel/generator).default; const code ...你的混淆代码...; const ast parser.parse(code); // 先模拟执行解码函数得到映射表 const decodedMap { 0x10: hello, 0x11: world }; traverse(ast, { CallExpression(path) { const callee path.node.callee; if (callee.name _0xdec) { const key path.node.arguments[0].value; if (decodedMap[key] ! undefined) { path.replaceWith({ type: StringLiteral, value: decodedMap[key] }); } } } }); const output generate(ast).code; console.log(output);这段脚本针对最简单的场景。实际操作时decodedMap需要通过执行解码函数生成而不是手写。更稳妥的做法是在Node里require混淆代码然后调用解码函数填充映射表。要注意的是有些新的Obfuscator.io版本会把数组中每个字符串再经过Base64、RC4之类处理所以模拟执行可能是最快的因为它直接调用了原来的算法。字符串还原是整个还原流程里性价比最高的一步。做完这一步代码里至少能看到大量明文阅读难度会直线下降。4. 控制流平坦化最难啃的骨头4.1 控制流平坦化的结构长相如果说字符串数组是混淆的第一道锁控制流平坦化就是第二道也是真正让很多人放弃的难点。原始代码里语句是顺序执行的a(); b(); c();经过控制流平坦化会变成类似一个状态机var state 3; while (true) { switch (state) { case 3: a(); state 7; break; case 7: b(); state 12; break; case 12: c(); state -1; break; default: return; } }读起来完全乱套你根本不知道先执行哪个case必须跟踪state的变化才能把原始顺序拼出来。Obfuscator.io还会对状态值做加减乘除运算甚至用字符串拼接再取hash让状态值难以一眼看出顺序。4.2 手动还原状态机的思路遇到控制流平坦化先别急着写自动化脚本。我一般先手动找规律。先定位分发器通常是switch(变量)这个变量就是状态。再看每个case的代码块找到状态变量的下一次赋值。把这些赋值顺序连起来就能还原执行顺序。你可以把每个case块编号然后画一张“状态转移表”当前状态执行内容下一状态3a()77b()1212c()-1画完这张表原始逻辑已经清楚了。接下来就可以把case块按照执行顺序拼回去去掉switch和状态变量。这一步手动做起来很枯燥但却是理解状态机的最佳方式。对于小型脚本手动还原完全可行对于几千行的大型脚本才需要上AST脚本。4.3 用AST写一个简化版的还原器自动化还原控制流平坦化核心是两步识别状态机结构然后按顺序重组代码块。以下是一个很简化的思路代码不追求覆盖所有变种但能说明原理const parser require(babel/parser); const traverse require(babel/traverse).default; const generate require(babel/generator).default; function extractStateFlow(ast) { const blocks []; let dispatcherVar null; traverse(ast, { WhileStatement(path) { const body path.node.body.body; const switchNode body.find(node node.type SwitchStatement); if (!switchNode) return; dispatcherVar switchNode.discriminant.name; switchNode.cases.forEach(c { const execBlock []; let nextState null; c.consequent.forEach(stmt { if (stmt.type BreakStatement) return; if (stmt.type ExpressionStatement stmt.expression.type AssignmentExpression stmt.expression.left.name dispatcherVar) { nextState stmt.expression.right; } else { execBlock.push(stmt); } }); blocks.push({ test: c.test?.value ?? null, exec: execBlock, next: nextState }); }); } }); return blocks; }实际做的时候你还需要处理break、continue、return以及嵌套的状态机。有开源库专门做这类还原但理解原理比直接调库更重要。因为Obfuscator.io每个版本生成的AST形态都有差异只知道调库遇到变种就抓瞎。能根据当前样本特征修改脚本才是真正核心的能力。5. 从还原到可运行动态插桩与补环境5.1 静态还原后的常见症状静态还原做完了并不代表代码一定能运行。最常见的症状是语法报错、变量未定义、函数执行顺序错乱。这些问题的根源往往是混淆在还原过程中丢失了某些环境信息或者你手动替换字符串时误伤了变量名。遇到这类问题我推荐回到动态调试。别把静态还原当成终点当成一道预处理。动态调试的价值在于代码能在真实执行环境里告诉你正确的执行路径和数据结构。5.2 动态调试优先定位执行顺序打开浏览器的开发者工具把还原后的脚本直接粘贴进Console运行观察报错点。如果报错集中在某个还原后的case块就说明控制流重组的顺序有误。此时可以回到原混淆代码在相同位置打断点执行到错误行号附近对比两条路径的执行顺序。这里有个小技巧原混淆代码虽然难读但它是能跑的。把它和还原后代码放在同一环境分别打日志输出每次状态变量和关键函数参数的变化。两份日志一对比哪个case块放错了位置立刻就能看出来。5.3 针对防调试代码的临时处理Obfuscator.io有一些反调试功能最典型的是在代码里内联大量debugger语句或者检测开发者工具是否打开一旦打开就进入死循环。动态调试时这些措施会严重干扰。我的处理方式很简单先用正则把代码里的debugger语句全部替换成空语句再执行。如果遇到环境检测比如检测console被改写、检测窗口尺寸变化就伪造环境。但这部分要根据具体样本调整没有万能方法。记住调试的目的是理解执行流程而不是跟防调试代码较劲。6. 常见问题与实操排坑记录6.1 格式化后代码直接报错格式化本身不会改变逻辑但有些格式化工具会把长字符串截断或者把注释位置改变。建议用成熟的格式化器并且不要在格式化后使用“保存时自动修复”功能避免工具自动增删分号导致混淆代码语法变化。如果格式化后报错先检查是否有字符串断行问题。另外Obfuscator.io在生成代码时可能故意制造一些“畸形但可运行”的写法比如很长的逗号表达式、变量声明前的分号缺失。某些格式化器为了追求规范会把它们“修正”成语法错误。遇到这种情况我会手动回退格式化的部分选项或者干脆只用格式化结果做参考不在原文件上保存。6.2 字符串解码出现乱码或undefined出现乱码通常是因为数组和解码函数之间存在偏移量。Obfuscator.io生成解码函数时可能用数组下标 - 偏移量的形式定位。直接调用某一个_0xdec(0x10)时结果是正常的但放到AST脚本里用固定的映射表就漏掉了一个关键细节同一个解码函数可能在不同执行阶段被填充不同的数组内容。解决办法是不要只映射单个数组而是同时模拟执行整个数组初始化逻辑拿到完整的运行时数组。另外注意数组元素被二次编码的情况先调用解码函数内的解码逻辑再得到明文。还有一种情况是解码函数本身依赖另一个解码函数形成多层嵌套。此时要从最内层开始解逐层向外替换而不是只做一层。6.3 控制流还原后逻辑错乱错乱主要出在状态变量被重复赋值或者case块里存在return、break等跳转语句。还原时不仅要跟踪顺序还要保留跳转语义。一个case块内如果有return后面的内容不会执行如果你把它硬拉平逻辑就变了。因此在自动化还原脚本里必须先把return、break、continue单独识别出来再决定如何处理。我见过不少人把状态转移表画对了但重组代码时漏掉了某个case里的return结果整个函数的行为完全变了。所以最后跑起来测试是很重要的一步不要只看输出代码“像没混淆”还要反复比对关键函数的入参出参。6.4 Obfuscator.io升级后如何处理新变种Obfuscator.io迭代很快每次大版本升级都可能调整字符串编码方式、控制流分发器的写法、甚至引入自定义的加密算法。还原脚本不可能一劳永逸。我的经验是先跑一遍已有的脚本看哪些部分失效再对照新版混淆代码的特征修改识别规则。重点观察新样本和旧样本的差异不要盲目套用网上教程。比如早期版本的字符串数组是纯静态的直接取下标就行后来改成解密函数逐步填充数组就要先执行数组填充逻辑再后来某些变种会把字符串编码成RC4密文每次调用解密函数时还要传入密钥。遇到这类变化不要慌核心思路没变还是要从“数组解码函数”这个组合入手。我自己做过很多次这类还原之后最大的体会是不要迷信“一键还原工具”。Obfuscator.io的混淆种类是组合式的没有任何工具能做到全自动还原。真正有价值的是你熟悉的AST操作、状态机分析和动态调试能力。把这几个基本功练扎实遇到任何新变种都不慌。最后再分享一个小技巧每次还原完一个样本把识别特征和脚本片段留下来按版本归档。下次遇到相似混淆直接复用效率能翻好几倍。
阅读完成 · 觉得有帮助?