JS逆向不用头秃:用大模型自动分析混淆代码完整流程
做了五六年前端逆向与协议分析,最常被问的问题就是:这段混淆JS怎么解?控制流拆得七零八落、字符串全是十六进制拼接、变量名全是无意义的乱码、还插了一堆反调试死代码,新人看了直接懵,老手也要抠上大半天。
传统的JS逆向,本质上是「人工肉眼分析 + 动态调试验证」的体力劳动:格式化、找入口、打断点、单步跟、抠字符串、还原逻辑,大量时间都花在机械性的清理和梳理上。遇到强混淆的代码,分析核心逻辑的时间甚至不到20%,80%的精力都消耗在和混淆器的对抗上。
而随着代码大模型的成熟,这套工作模式完全可以重构。不是说直接把整段混淆代码扔给大模型就能出结果——那样大概率会得到一段看起来对但功能不对的“幻觉代码”。真正工程化的方案,是「预处理工程化 + 大模型语义还原 + 动态校验闭环」:把机械性的清理、梳理、还原工作交给AI,人只做核心决策、边界约束和结果校验,效率提升数倍的同时,保证结果的准确性。
本文就完整拆解这套自动化分析流程,从预处理、分块、语义还原到动态校验,每一步都附带实现逻辑和实战脚本,看完就能落地属于自己的AI辅助逆向工具链。
一、先讲清楚:传统逆向的痛点与AI的定位
1.1 传统JS逆向的核心痛点
混淆手段迭代快,特征匹配失效
早期的混淆简单,写个AST遍历脚本就能批量还原变量名、字符串。现在的混淆器都是生成式的,每次混淆结果都不一样,特征匹配脚本用几次就失效了。机械性工作占比高,效率极低
控制流平坦化、死代码插入、字符串拆分,这些混淆手段本身没有技术难度,但就是费时间。几千行的混淆代码,真正核心的逻辑可能就几百行,大部分时间都在剔垃圾、理结构。经验依赖强,新人上手慢
同样一段混淆代码,老手可能一小时定位核心逻辑,新人可能三天都找不到入口。大量的技巧、经验、坑点,都靠人工积累,没法批量复制。强对抗场景下,人工分析成本极高
遇到虚拟机保护、代码虚拟化、环境检测这类强混淆,人工分析的成本会指数级上升,一个核心算法抠一周都是常事。
1.2 AI的定位:辅助工具,不是替代
很多人觉得AI逆向就是“扔代码进去出结果”,这是最大的误区。
- AI擅长的是:语义理解、结构梳理、代码翻译、规律总结
- AI不擅长的是:精确的逻辑还原、边界条件处理、对抗性识别
正确的定位是:人做策略和决策,AI做执行和体力活。人负责找入口、定边界、做校验;AI负责清理死代码、还原控制流、重命名变量、补充注释。把人从80%的机械劳动里解放出来,专注核心的20%。
二、第一步:工程化预处理,先把代码“洗干净”
直接把混淆代码扔给大模型,效果最差。就像你要翻译一篇乱码的文章,得先把污渍擦掉、把字符认全,再翻译。预处理的目标,就是把混淆代码里的噪声尽量去掉,还原成语法正确、结构清晰的基础代码,大幅提升后续AI还原的准确率。
2.1 格式化与语法标准化
第一步永远是格式化。混淆代码一般都是压缩成一行或者乱缩进的,先统一格式化,变成正常的缩进结构。
推荐用js-beautify,比编辑器自带的格式化更强大,能处理各种变形语法。
constbeautify=require('js-beautify').js;constfs=require('fs');letcode=fs.readFileSync('obfuscated.js','utf-8');letformatted=beautify(code,{indent_size:2,space_in_empty_paren:true,wrap_line_length:120});fs.writeFileSync('formatted.js',formatted);2.2 对抗代码剥离
混淆代码里通常会有反调试、反格式化、环境检测的代码,这些代码不影响核心逻辑,但会干扰分析和调试。
常见的对抗特征:
debugger语句无限循环- 检测控制台打开,死循环或者报错
- 检测代码缩进变化,执行假逻辑
- 检测开发者工具,跳转错误分支
处理方法:通过特征匹配+AST识别,找到这些代码块,直接注释或者替换掉。
比如最常见的无限debugger:
// 识别并替换所有debugger语句code=code.replace(/debugger;/g,'/* debugger removed */');复杂的对抗逻辑,可以用AST遍历识别特定模式,批量处理。
2.3 常量字符串还原
这是最常见的混淆手段:把明文字符串拆成十六进制、Unicode编码,或者放在数组里通过索引取值。
比如:
// 混淆前leturl="/api/sign";// 混淆后leta=["\x2f\x61\x70\x69\x2f\x73\x69\x67\x6e"];leturl=a[0];还原方法:
- 简单的十六进制、Unicode转义,直接正则替换还原
- 数组取值类的,通过AST遍历,把常量数组的取值直接替换成字面量
- 加密的字符串,先找到解密函数,批量解密替换
还原之后,大模型才能看懂代码里的字符串含义,准确理解逻辑。
2.4 死代码初步过滤
混淆器会插入大量永远执行不到的死代码、只赋值不使用的变量、无效的表达式,用来干扰阅读。
初步过滤可以用简单的数据流分析:
- 识别从未使用的变量
- 识别条件永远为真/假的分支
- 识别没有副作用的表达式
这一步不用追求100%干净,去掉大部分明显的死代码就行,剩下的交给大模型处理。
三、第二步:功能分块拆解,化整为零逐个击破
预处理完的代码还是有几千行,直接喂给大模型,要么上下文不够,要么语义漂移,效果极差。正确的做法是分而治之:把大段代码按功能拆成小块,逐块还原。
3.1 先锚定核心入口
不要从头开始读,先找核心逻辑的入口。逆向的核心目标一般是签名、加密、验证这些,直接搜特征关键词定位。
常用锚点:
- 网络请求:
fetch、XMLHttpRequest、send - 加密算法:
CryptoJS、MD5、SHA、AES - Canvas指纹:
getContext、toDataURL - 关键参数:
sign、token、password、key
找到核心入口点之后,向上回溯调用链,就能梳理出完整的逻辑链路,不用在无关代码里浪费时间。
3.2 按功能模块切块
锚定核心之后,把代码按功能拆成独立的块,每块负责一个功能:
- 初始化模块:环境检测、参数初始化
- 工具函数模块:加密、编码、字符串处理
- 核心逻辑模块:签名生成、算法实现
- 请求模块:接口调用、参数拼接
- 验证模块:结果校验、反爬验证
切块原则:
- 每块控制在100~300行,保证大模型的上下文能完整覆盖
- 每个块尽量独立,减少跨块依赖
- 标注清楚每个块的输入、输出、依赖的外部函数
3.3 依赖关系标注
切块不是随便切,要标注清楚块之间的依赖关系。比如B块依赖A块的函数,就要先还原A块,再还原B块。
可以简单梳理依赖顺序,从底层工具函数开始,逐层向上还原,还原好的函数可以直接作为下一块的上下文。
四、第三步:大模型语义还原,核心步骤的正确姿势
这是最核心的一步,但也是最容易踩坑的一步。很多人直接扔代码说“帮我还原”,结果出来的代码看起来很漂亮,功能完全不对。正确的还原,必须有明确的约束和校验。
4.1 还原提示词的正确写法
提示词是决定还原质量的关键。核心原则是:明确目标、划定边界、强制约束、指定格式。
错误的写法:
帮我把这段代码还原成可读的。
正确的写法:
你是一名资深JavaScript逆向工程师。请分析下面的混淆代码,还原成可读性强的等价代码。
约束规则
- 功能完全等价:还原后的代码和原代码输入输出必须完全一致,不能修改任何逻辑
- 保留核心API:所有底层浏览器API、加密函数、网络调用必须保留,不能修改
- 语义化重命名:所有无意义的变量名、函数名,根据功能重命名成有意义的英文名称
- 结构还原:把平坦化的控制流还原成正常的顺序、分支、循环结构
- 剔除死代码:删除所有不影响逻辑的死代码、无效变量、冗余表达式
- 补充中文注释:关键逻辑、函数功能、参数含义都要加注释
- 只输出完整的还原代码,不要任何解释、说明、markdown标记
代码
{code}
4.2 逐块还原,上下文接力
从最底层的工具函数开始,逐块向上还原。
- 还原第一层工具函数,验证正确
- 还原第二层的时候,把第一层还原好的函数作为上下文一起喂进去
- 逐层向上,直到核心逻辑
这样每一步的上下文都足够精准,还原准确率远高于整段处理。
4.3 不同混淆类型的针对性策略
- 变量名混淆:重点是语义化重命名,根据函数功能推测变量含义
- 控制流平坦化:重点是梳理状态变量,还原成标准的分支循环结构
- 字符串加密:结合上下文和功能,还原字符串的实际含义
- 死代码插入:识别并剔除不影响逻辑的冗余代码
- 虚拟机保护:先还原指令集和调度逻辑,再还原具体操作
五、第四步:动态校验闭环,保证结果100%准确
AI还原的代码,哪怕看起来再对,也不能直接信。语义漂移是大模型的固有问题,特别是复杂的位运算、加密逻辑,很容易细节出错。必须通过动态校验,形成闭环。
5.1 输入输出对比法
最核心的校验方法:同样的输入参数,分别运行原混淆代码和还原后的代码,对比输出结果是否完全一致。
实现脚本示例:
constvm=require('vm');constfs=require('fs');// 加载原代码和还原代码constoriginalCode=fs.readFileSync('obfuscated.js','utf-8');constrestoredCode=fs.readFileSync('restored.js','utf-8');// 测试用例consttestCases=[{input:'test123',key:'abc'},{input:'',key:''},{input:'1234567890',key:'xyz123'},{input:'中文测试',key:'密钥'}];functionrunCode(code,input,key){constsandbox={result:null,input,key};vm.runInNewContext(code+`result = sign(input, key);`,sandbox);returnsandbox.result;}// 对比测试console.log('开始校验...');letallPass=true;testCases.forEach((tc,i)=>{constr1=runCode(originalCode,tc.input,tc.key);constr2=runCode(restoredCode,tc.input,tc.key);if(r1===r2){console.log(`用例${i+1}: 通过`);}else{console.log(`用例${i+1}: 失败`);console.log('原输出:',r1);console.log('还原输出:',r2);allPass=false;}});console.log(allPass?'全部校验通过':'存在差异,需重新还原');5.2 多维度覆盖
测试用例要覆盖多个维度:
- 正常输入
- 空输入
- 超长输入
- 特殊字符、中文
- 边界值
覆盖的场景越多,校验越可靠。加密算法这类精确逻辑,必须所有用例全部通过才算还原正确。
5.3 差异定位与重还原
如果校验不通过,不要整段重新还原。先定位到具体哪个函数、哪个块输出不一致,针对有问题的块重新还原,给出更明确的约束和提示。
反复迭代,直到所有用例全部通过。
六、工程化封装:打造自己的AI逆向工具链
单步操作熟练之后,可以把整个流程封装成自动化工具链,批量处理混淆代码。
6.1 完整自动化链路
输入混淆代码 ↓ 自动预处理(格式化、去对抗、还原字符串) ↓ 自动识别入口点,按功能切块 ↓ 调用大模型API逐块还原 ↓ 自动运行测试用例校验 ↓ 输出还原后的可读代码 + 校验报告6.2 批量处理能力
对于大量相似的混淆代码,可以批量处理。比如同一个站点的多个页面、同一个混淆器生成的多份代码,统一预处理、统一还原策略,批量输出结果。
6.3 知识库沉淀
把常见的混淆模式、还原策略、校验方法沉淀成知识库,不断优化提示词和处理流程。用的越多,准确率越高,速度越快。
七、踩坑与最佳实践
坑1:直接整段代码扔给大模型
后果:上下文不够、语义漂移、逻辑错误,还原出来的代码中看不中用。
正确:分块处理,从底层到上层,逐块还原,逐步验证。
坑2:只看代码美观,不做动态校验
后果:看起来还原得很漂亮,实际跑起来结果不对,关键逻辑错了都不知道。
正确:所有还原的代码必须经过动态校验,输入输出完全一致才算通过。
坑3:完全依赖AI,自己不看逻辑
后果:遇到对抗性代码、假逻辑,AI会直接当真逻辑还原,结果完全跑偏。
正确:人把控核心方向和边界,AI做体力活,关键逻辑人工复核。
坑4:提示词太简单,没有约束
后果:AI自由发挥,删改关键逻辑,添加自己的想象。
正确:提示词明确约束规则,哪些不能改、哪些必须保留、输出什么格式,都写清楚。
最佳实践
- 先动态调试找入口,再AI还原,效率最高
- 越小的块,还原准确率越高,不要贪大
- 核心加密算法,一定要多组用例校验
- 还原过程结合AST工具做预处理,比纯AI效果好
- 常用的混淆模式,沉淀成固定的处理模板
最后
JS逆向这个领域,从来不是比谁能扛、谁能熬,而是比谁的方法更高效、工具链更强大。
大模型没有替代逆向工程师,但它重构了这个工种的工作模式:从以前的「从头到尾人工抠」,变成现在的「策略+工具+AI辅助」。人从体力劳动里解放出来,专注于核心逻辑、对抗策略和流程设计,效率和产出都完全不是一个量级。
说到底,工具在进化,人的能力也要进化。善用AI,把重复的工作交给机器,把精力放在真正有价值的地方,才是技术人的核心竞争力。
