ARTICLE DETAIL

资讯详情

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

JS反混淆工具实测:从字符串阵列到控制流平坦化还原指南

JS反混淆工具实测:从字符串阵列到控制流平坦化还原指南 先看一段代码。你打开一个 JS 文件第一屏长这样var _0x4b3c [\x68\x65\x6c\x6c\x6f, \x77\x6f\x72\x6c\x64, \x74\x65\x73\x74, \x20]; (function(_0x1a2b, _0x3c4d) { var _0x5e6f function(_0x7a8b) { while (--_0x7a8b) { _0x1a2b[push](_0x1a2b[shift]()); } }; _0x5e6f(_0x3c4d); }(_0x4b3c, 0x120)); function greet(_0x2d3e) { var _0x1a _0x4b3c[0]; var _0x37 _0x4b3c[1]; return _0x1a _0x4b3c[3] _0x2d3e _0x4b3c[3] _0x37; } console[_0x4b3c[2]](greet(_0x4b3c[0]));别怀疑自己是不是下错了文件。JS反混淆这个圈子里最不缺的就是这种_0x开头的变量和莫名其妙的数组移位。我去年接手一个外部脚本分析任务被这种代码折磨了整整两个晚上。后来把市面上能叫得上名字的工具都跑了一遍jsunpark、jsnice、de4js、ob-decrypt再加上一些没出现在标题里的辅助项目折腾出了自己的一套处理流程。这篇就是用实际测试结果告诉你这些工具到底能干什么、不能干什么、在什么场景下优先用哪一个。如果你不是专业做恶意代码分析只是在做爬虫、调试某个混淆前端项目或者打 CTF 时需要快速还原一段逻辑这篇文章应该都能帮你省下不少时间。我会尽量不绕弯子直接讲实测感受和踩坑经历。1. 为什么我突然把市面上所有 JS 反混淆工具都拉出来跑了一遍事情起因是这样的我接手的一个项目里出现了一段被javascript-obfuscator处理过的脚本看起来就不是给人读的。当时我第一反应是找个在线工具把代码丢进去点一个按钮然后等结果。结果试了一圈之后发现有的工具对字符串阵列处理得很好却把控制流平坦化还原成一堆空壳有的工具能把变量名拟人化但对字符串解密完全无动于衷。于是我开始一个个去读它们的文档、翻源码、跑样本。这个过程远比想象中费时间因为很多工具更新并不勤快却都默认自己支持“所有混淆”或者“obfuscator.io 产物”。但实际情况是没有一个工具能真正把混淆代码还原回人类手写的样子。反混淆本质上是在做“可读性恢复”不是“源码恢复”。这里先给新手打个底混淆过程是不可逆的。原始变量名被替换成无意义的_0x2d3e注释全部丢弃常量被展开成逻辑甚至插入大量死代码。再厉害的还原工具也只能通过统计规律、AST 分析和部分动态执行把代码恢复到一个“看起来有逻辑、能人工分析”的状态而不是变回项目最初的源码。你追求的目标应该是“能读”不是“复原”。也正因为这样工具之间的差距才会这么明显。有的擅长字符串还原有的擅长结构合并有的擅长变量重命名。单一工具很难把所有步骤都做到位所以才有反复折腾的必要。我自制的处理流程大概是这样的先用jsunpark或de4js做第一遍结构还原再用自己的 AST 脚本做常数折叠最后丢给jsnice做变量语义化。这样一套下来大部分obfuscator.io产物都能恢复到“人工可读”的程度虽然离“像人写的”还有距离。2. 混淆器与反混淆工具的基本盘先搞懂你在跟什么打架在聊工具之前我建议先用几分钟把javascript-obfuscator这类工具常用的手段梳理一遍。不然很多工具选项你都不知道该不该勾出来的结果也看不懂。2.1 常见的几种“变脸”手法第一个是字符串阵列。混淆器会把代码里所有字符串集中放到一个数组里再用一个移位函数打乱顺序真实使用字符串时通过下标获取。比如console.log(hello)可能变成console[_0x2d3e(0)][_0x2d3e(1)]。这种结构在反混淆里相对好处理找到解密函数模拟执行或者做 AST 常量替换就行。第二个是控制流平坦化。这是真正的“硬骨头”。原来的if/else和循环逻辑会被改造成一个while(true)加一个巨大的switch每一次循环通过状态变量跳转到下一个分支。阅读顺序被打乱可读性直线下降。还原时需要做控制流分析把状态转换关系理清然后合并成原始的分支结构。第三个是自执行函数包裹。混淆产物通常在开头会有几个 IIFE用来生成数组、解密字符串、执行反调试逻辑。这些包裹函数可以拆掉一部分但有的包裹本身和业务逻辑纠缠在一起不能盲目删。还有一些更麻烦的死代码注入、标识符缓存、域名锁定、debug protection、self-defending。后面两个会让动态调试变得极其痛苦甚至代码跑起来自动崩溃。一旦遇到带self-defending的样本最好先考虑静态分析或者在做任何修改之后立刻恢复原始文件否则代码自己会改成不可执行的状态。2.2 反混淆的目标不是“让代码能跑”而是“让人能读”很多人没想明白这一点。反混淆工具的输出不一定要能直接运行甚至不应该直接运行因为有些输出只是中间态。你真正想要的是函数之间的调用关系清楚、字符串还原为明文、关键分支不再被switch拆成碎块、变量名有一定的语义提示。我用一个比喻来解释混淆代码是一堆被打乱顺序的拼图反混淆工具能帮你把边角拼好但中间有些碎片是重复的有些是干扰项你必须靠自己的判断把它们归位。工具的目标是把“完全无法分析”变成“能分析但费劲”而不可能变成“和原始代码一模一样”。3. 参评选手介绍jsunpark、jsnice、de4js、ob-decrypt 到底各自什么来头下面这几款是我在这个时间点上实际用过的不涉及云评测。我尽量说清楚它们各自侧重什么不吹不黑。3.1 jsunpark为 obfuscator.io “死磕”的我方选手jsunpark是我这次测试里最意外的一个。听名字就知道它不是来干通用反混淆的而是盯着obfuscator.io那套产物做深度还原。它最擅长的是控制流平坦化还原会把switch while的骨架重新拉平生成可读性高很多的中间代码。对于字符串阵列它也能做替换但更多时候返回的是一个中间 AST方便二次处理。它是命令行工具适合写进自动化流水线。我自己拿它做第一遍粗还原再把输出丢给其他工具做变量重命名。如果你只是偶尔用一次可能会嫌装依赖麻烦但它对复杂样本的效果确实是最稳的。装好后大致的调用形式是jsunpark -i input.js -o output.js不同版本参数可能不一样具体以你本地 README 为准。它输出的代码通常不是最终成品而是“可分析版本”。3.2 de4js老牌在线工具箱支持面广de4js我用的时间最长因为它是个网页工具打开浏览器粘贴代码就能跑。它支持很多混淆类型不只是obfuscator.io像是各种压缩混淆变体也能处理一部分。优势是选项丰富可以自己勾选字符串替换、数组解包、布尔常量化、变量简化之类的还原步骤。弱项也很明显对控制流平坦化的处理相对规矩遇到复杂的多层嵌套会力不从心。另外在线工具意味着你得把代码粘到第三方网站里如果是敏感样本记得脱敏或者自己部署。好在这个项目本身有离线分支源码公开可以本地跑。我平时最常用的操作是粘贴代码勾选基础还原项先跑一遍看字符串能不能出来。这一步主要是摸清楚样本到底有没有加密字符串而不是指望它一次搞定。3.3 jsnice不做字符串解码专攻变量名和类型修复jsnice的角度和前面几位不一样。它不会去管字符串解密也不负责控制流还原它只做一件事重命名变量和函数名让_0xabc这种无意义标识符变成login、formData这种猜测性名称同时恢复一些类型信息。效果相当惊艳很多时候一眼就能看出代码意图。正因为它只做语义化所以最适合放在整个还原流程的最后一步。先把字符串和控制流处理好再交给jsnice做“语义化”代码分析速度会大幅提升。如果你拿到的是压缩代码而不是混淆代码jsnice也能让变量名变得可读这种情况完全不需要动用重型反混淆工具。3.4 ob-decrypt轻量级针对性工具ob-decrypt看名字就知道它主要处理obfuscator产物。它是轻量级的适合快速解字符串阵列和一部分结构恢复。在简单样本上它跟de4js表现类似但在复杂样本上的处理深度不如jsunpark。我一般把它当作快速查看的“探针”一旦发现它处理不了就换重型工具。为了让你快速抓住差异我把这次实测的大致印象整理在下面这个表里。注意这里的评级是针对常见 obfuscator.io 产物的个人感受不是绝对权威。工具字符串还原控制流平坦化变量语义化自动化接入上手门槛jsunpark良优中高中de4js优良中低低jsnice无此能力无此能力优低低ob-decrypt良中中低低4. 同一样本下的拆解实测从字符串阵列到控制流平坦化说再多理论不如直接看同一段样本在各个工具下的表现。我准备了一个简化过的混淆样本只保留真实场景里最常见的几个特征字符串阵列、IIFE 解密、以及一个简单控制流平坦化块。4.1 测试样本与判分标准原始代码很简单function greet(name) { const prefix hello; const suffix world; return prefix name suffix; } console.log(greet(test));经过混淆器处理并简化之后长这样var _0x4b3c [\x68\x65\x6c\x6c\x6f, \x77\x6f\x72\x6c\x64, \x74\x65\x73\x74, \x20]; (function(_0x1a2b, _0x3c4d) { var _0x5e6f function(_0x7a8b) { while (--_0x7a8b) { _0x1a2b[push](_0x1a2b[shift]()); } }; _0x5e6f(_0x3c4d); }(_0x4b3c, 0x120)); function greet(_0x2d3e) { var _0x1a _0x4b3c[0]; var _0x37 _0x4b3c[1]; return _0x1a _0x4b3c[3] _0x2d3e _0x4b3c[3] _0x37; } console[_0x4b3c[2]](greet(_0x4b3c[0]));这段代码特征很典型数组顺序不是最终顺序字符串都是\x编码。还原目标是把greet函数变成人能读的样子。4.2 第一步字符串阵列替换谁做得最干净这一步我直接用各工具的默认或自动选项处理不看手工微调。结果是de4js和ob-decrypt对字符串阵列的替换最直观基本能把_0x4b3c[0]直接变成hello。jsunpark也能做到而且它不只替换还会尽可能把解密函数重新折叠成常量。这里有细节很多工具对数组元素顺序的还原依赖那个移位函数。如果混淆器用的是push shift循环工具的模拟执行需要足够准确否则会把前后顺序搞错。我在测试里遇到过两次结果字符串出现乱码的情况原因就是数组移位没有被正确还原。碰到这种问题先看看样本是否同时开了“字符串编码”选项导致解密出来的不是明文而是另外一层编码。jsnice在这个环节完全插不上手因为它压根不做字符串提取。你在 jsnice 里看到的还是那些下标调用只是变量名可能从_0xabc变成了arr之类的东西。工具字符串明文可读度数组移位处理误还原率个人观感jsunpark高高低de4js高高中ob-decrypt中中中jsnice不处理不处理无4.3 第二步控制流平坦化谁的还原率更高由于我的测试样本控制流比较简单这里换成从真实项目里截取的一个带while switch的分片简化后大概是var _0xstate 0x2; while (true) { switch (_0xstate) { case 0x2: _0xresult _0x4b3c[0] _0x4b3c[3] _0x2d3e; _0xstate 0x7; break; case 0x7: _0xresult _0x4b3c[3] _0x4b3c[1]; _0xstate 0x9; break; case 0x9: return _0xresult; } }对这种结构jsunpark的还原效果最强输出会接近语义化的if/else顺序。de4js也有控制流还原选项但合并策略偏保守有些分支会保留冗余需要手工再清理。ob-decrypt在简单平铺场景表现还行但一旦状态变量不是简单的2 - 7 - 9比如中间夹着死状态分支它容易卡在某个case里不肯继续。jsnice对控制流无能为力因为它根本没有做 AST 结构重写。一个比较意外的地方是jsunpark对状态变量的识别不依赖变量名。它会把整个switch的状态传递图提出来然后根据基本块之间的跳转关系重新排序。这意味着就算状态变量的名字是随机生成的它也能处理。这也是为什么它拿到复杂样本时表现更稳。4.4 第三步自执行函数与 IIFE 处理的差异几乎所有obfuscator.io产物开头都会有一到两个 IIFE用来给字符串数组重排顺序。测试样本里也有一个。这里的关键区别de4js通常会把 IIFE 保留成一个可执行的小模块然后通过常量替换间接抵消它的作用jsunpark更倾向于直接分析数组变化关系输出里不再出现那个移位 IIFEob-decrypt的表现取决于样本是否触发它的“包装名”规则有时会留下一个空壳函数。我在实际处理中见过很多次这样的结果字符串已经变成明文但代码外面还套着(function(_0x1a2b, _0x3c4d) { ... })(_0x4b3c, 0x120);。这时如果不手动清理后续格式化或执行都会带着大包袱。清理方法很简单如果确认这个 IIFE 没有返回值也不影响任何函数声明替换完数组之后可以直接删掉整段。提示删 IIFE 之前要确认它没有被后面代码引用。常见陷阱是var _0x2d3e (function(){...}())这种带赋值语句的包裹不能整段删除只能把内部函数逻辑保留下来。5. 深水区的坑解密函数定位、rigid 分支和死代码前面的测试还相对干净。真正干活的时候我遇到的大坑主要有三个。5.1 解密函数不是“一键解”能搞定的很多在线工具为了安全会在浏览器沙箱里模拟执行字符串解密逻辑。听起来很美好但一旦混淆样本带有自校验逻辑你的“模拟执行”就等于把代码的防御机制也执行了一遍结果要么解密函数返回错误结果要么代码直接自我破坏。我在一次恶意脚本分析里就碰到过把一段样本喂给在线工具输出结果里面所有字符串都变成了乱码我还以为是编码问题后来单独跑了解密函数才发现这段代码在解密前会先检查typeof某个环境变量一旦发现不在预期环境里就返回垃圾数据。这种问题只能靠人工定位解密函数手动补环境或者用更细的符号执行方式去绕过环境检查。5.2 self-defending 和 debug protection跑一遍就崩怎么办self-defending是javascript-obfuscator里很让工具头疼的选项。生成的代码里通常有一段正则或进制检测专门用来检测代码有没有被修改或格式化。如果你直接把反混淆结果打印出来或者用美化工具格式化再次运行时可能直接报错或静默失败。遇到这类样本我的经验是第一遍绝对不要急着格式化先静态替换字符串再用jsunpark做控制流还原如果中间结果需要保存用文件流方式写入不要把代码贴到控制台再拷贝。某些工具会在输出里保留debugger语句de4js有一项“移除 debugger”功能记得勾选不然每分析一个分支都弹一次调试器。5.3 死代码注入让 AST 变换雪崩死代码注入是另一个让人头大的设计。混淆器会随机插入一些永远不会执行的分支但这些分支照样引用解密函数和变量看起来像可执行逻辑。反混淆工具在做 AST 合并时有时会被这些不可达分支骗过去生成一层又一层无意义的嵌套 if。处理这类问题最好的办法不是手工去删而是在反混淆之后再做一轮 dead code elimination。很多工具不做这一步你要自己补。我后面会谈一个简单的手工 AST 后处理思路能处理掉大部分不可达分支。6. 按场景选型爬虫、恶意 JS 分析、CTF、代码迁移分别该用谁工具的火力要怎么分配取决于你的场景而不是单纯比谁“更好”。6.1 恶意 JS 分析优先静态可读性别裸奔执行如果你在做恶意代码或广告脚本分析我强烈建议把动态执行放在最后一步甚至只在受控沙箱里做。这个场景下jsunpark加de4js是比较稳的组合先用jsunpark把控制流拉平再用de4js或手动脚本完成字符串替换最后交给jsnice做变量语义化。为什么要先控制流后字符串因为控制流还原依赖 AST 结构完整性如果字符串早就替换成了明文但某些分支还是会访问原来的数组下标AST 工具会判断错误。先粗还原再替换最后重命名这是我自己试下来最不容易出错的顺序。6.2 CTF / 算法还原jsunpark jsnice 配合CTF 里给定一段混淆脚本要还原算法时过程通常比较温和一般没有反调试也不会做域名锁定。这种情况用jsunpark还原率最高因为它针对obfuscator.io的还原规则很激进。跑完之后再用jsnice重命名往往能直接看出函数是md5、base64还是某种自研算法。如果题目里用的是其他混淆器比如UglifyJS的压缩产物那就没必要上重型工具jsnice一个就够。6.3 爬虫逆向de4js 当作第一道提词器爬虫逆向场景和恶意代码分析不一样你通常能遇到很多“半混淆”的代码比如前端构建产物压缩后带一些可控变量名。de4js的在线体验在这里很友好粘贴一下勾选第一轮基础还原项很快能看到一个可以读的结构。然后你再根据结果去浏览器里打断点、覆盖函数效率会高很多。不过提醒一句爬虫相关操作要遵守目标站点条款。反混淆工具本身只是分析技术怎么用是你的选择。6.4 代码迁移如果只是变量名乱用 jsnice 足够了有时候你拿到的不是混淆代码只是压缩代码变量名全是a,b,c。这种场景下用jsunpark纯属杀鸡用牛刀。jsnice就能自动推断变量用途并重命名处理后的代码已经足够做逻辑迁移。如果再配合 Prettier 格式化观感会接近正常工程代码。场景首选工具次选工具最关键动作恶意 JS 静态分析jsunparkde4js先静态后沙箱CTF 算法逆向jsunparkjsnice还原后手动验证逻辑爬虫脚本调试de4jsob-decrypt快速看结构后再断点压缩代码迁移jsnicePrettier只重命名不做结构处理7. 我自己的预处理与后处理三招工具用多了以后我会习惯在正式还原之前和之后各加两道手工工序。这些技巧不复杂但对结果质量提升很大。7.1 先用正则快速定位“解密函数簇”不要一上来就反混淆。先用文本编辑器搜索这些特征[push]([shift]、parseInt、toString()[slice]、charCodeAt这类高密度调用。看到它们说明这段代码大概率有字符串阵列和编码逻辑。定位到这些位置后你就能判断工具应该重点处理哪个片段而不是让工具对整份文件“盲还原”。我自己常用的定位命令很简单比如找数组移位特征grep -n push.*shift target.js如果命中很多处说明字符串阵列逻辑很重de4js的数组还原选项必须开。如果命中很少可能混淆器用的是别的手段比如base64编码或自定义编码函数那就得换思路。7.2 把反混淆结果丢回 AST 再做一次常数折叠很多工具只做结构变换不做常数合并。还原后代码里可能残留1 2、true false这类表达式。我自己的做法是先用esprima或acorn解析再写一个几十行的折叠脚本把字面量运算、布尔表达式、空语句替换掉。这一步对最终可读性提升非常大尤其适合跟jsunpark配合因为它输出的中间态已经接近普通代码常数折叠能直接让它变得接近手写。处理方式大致是遍历 AST遇到两个数字相加就替换成结果遇到if (true)就保留真分支遇到if (false)直接删掉分支。代码量不大但能把很多“工具遗留下来的渣子”清理干净。7.3 最后一步用 jsnice 重命名变量整个流程的最后我会把代码贴到jsnice里做一遍重命名。这一步拖到最后有好处前面做了结构还原和死代码清理后变量出现频率更“真实”jsnice的统计模型能给出更准确的猜测。如果一上来就让它重命名反而会因为原始上下文太杂、标识符之间没有语义信息预测效果大打折扣。整体用下来我的体感是jsunpark负责“把结构拍平”de4js负责“把字符串点亮”jsnice负责“让代码像人写的”。这三个工具串联起来比我过去只依赖任何一个在线工具要稳得多。如果你和我一样只是偶尔碰上一段混淆代码也别追求“和原始源码一模一样”最终目标是让逻辑能在大脑里跑通。能读比能跑更重要。
返回列表