上周处理一批从公开威胁情报库拉回来的JS样本时,我差点把键盘拍碎。正则匹配URL、提取API路径的老套路全部失效:控制流平坦化把函数调用关系搅成一锅粥,字符串加密把所有敏感的API名字变成了一串下标引用。我被迫把AST和LLM大语言模型组合起来,搭了一套能穿透现代代码混淆的“组合透视镜”——先用AST把代码拆成结构化骨架,再用LLM去理解骨架背后的真实意图。实际跑下来,之前要花一下午人工逆的样本,现在半小时内能出结论。这篇文章就把这套思路的原理、完整流程和踩过的坑都摊开来讲。
1. 为什么传统反混淆手段“看不动了”
1.1 一次真实分析任务:正则和AST各碰了一鼻子灰
那批样本的情况其实不复杂,就是一个带下载行为的网页脚本,但作者做了三层混淆:第一层是变量名全部改成 _0x3f2a 这种无意义标识符;第二层把字符串常量抽到一个大数组里,运行时通过下标取回;第三层用控制流平坦化把原本顺序执行的逻辑打散到一个 switch-case 分发循环中。
我先用正则硬扫,想提取里面的URL和API路径。结果发现所有字符串都是 _0x9d4c[0x3] 这种形式,根本看不到明文。接着我用现成的AST解析库把代码解析成语法树,确实能看到每个数组下标引用,但问题是节点数量膨胀到了几万个,且大量分支都在做无意义的自增和跳转。人眼扫了一遍,只确认了“这是一个循环”,却说不清循环在干什么。
这次经历让我意识到一个核心问题:AST不是不够强,而是“信息量太大”。混淆在结构上制造噪音,让真正的语义被淹没在无数无关节点里。传统工具擅长告诉你“代码长什么样”,但很难告诉你“代码在干什么”。要补上“干什么”这一环,需要把结构信息交给一个能做高层语义推断的系统,LLM恰好是 candidate。
1.2 正则、符号执行、去混淆插件的天花板
很多人遇到混淆第一反应是用正则。正则适合匹配“模式规整”的文本,但现代混淆几乎都是故意打破规整性的产物:字符串可以是运行时拼接、下标引用、甚至经过AES解密后才出现。正则无法追踪数据流,也无法处理嵌套结构,遇到 ${String.fromCharCode(...)} 这类动态构造直接傻眼。
符号执行是另一条路。它能把程序路径抽象成约束条件,理论上可以还原出每个变量取何值时走哪条分支。可现代混淆工具会在代码里注入大量无意义的循环和条件分支,符号执行在这些地方容易路径爆炸。更麻烦的是,很多样本会检测运行环境,一旦发现不是真实浏览器,就会走另一条错误路径或者直接退出。符号执行在分析这类带环境依赖的样本时,大量时间都耗在求解环境约束上。
市面上的自动化反混淆插件,比如针对某个混淆器的 deobfuscator 脚本,确实能解决一部分问题。但它们的适用边界很窄:换一个混淆器版本、调整一组参数,规则可能就全部失效。因为这类工具依赖“已知模式”,而混淆的本质就是不断制造新模式。我并不是说这些手段完全没用,而是它们在处理“组合混淆”时已经明显吃力。我们需要一个不靠死规则、能理解高层次语义的层,这就是LLM能发挥价值的地方。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. AST:给代码做“结构CT”的第一步
2.1 AST到底长什么样:用“拆乐高”的方式理解抽象语法树
抽象语法树(Abstract Syntax Tree,AST)是把源码解析成树状结构,树上的每个节点对应一个语法元素。和源码相比,AST去掉了空格、分号、注释这些纯格式信息,只保留“代码是怎么组织起来”的结构关系。
举个例子,let x = a[1] + 2; 这行代码,在AST里大致是:顶层是一个 VariableDeclaration 节点,里面有一个 VariableDeclarator,它的 id 是标识符 x,init 是一个 BinaryExpression,表示加法,左侧是 MemberExpression(取 a[1]),右侧是数字字面量 2。
理解AST最简单的方式,是把它想象成“乐高说明书”。源码是一堆积木拼好的成品照片,AST则是说明书上那套分解图,标明每个积木块的类型和连接方式。混淆在做的,就是打散成品、换一些外观相似的积木块,但说明书上的“连接关系”本质上还在。我们后续要做的是,从这份被涂改过的说明书里,找到那些不变的关系。
