利用 AST 技术还原 JavaScript 混淆代码
原文作者:K哥爬虫 | 发布于 2022-04-28


一、先搞懂:AST 到底是什么?
AST(Abstract Syntax Tree),即抽象语法树,是源代码语法结构的树状表现形式——树上的每个节点,都对应代码里的标识符、变量声明、函数调用、if判断等基础语法单元。
可以把 JS 混淆代码想象成被打乱组装的乐高积木:通过 AST 解析,我们能精准定位每一块积木的类型和位置;通过修改 AST,我们可以按逻辑顺序重新拼好它。
AST 的应用场景
它并非逆向专属,日常开发的很多工具都在默默用它:
- IDE 辅助:语法高亮、代码补全、自动格式化
- 代码转译:Babel 将 ES6+ 语法降级为 ES5
- 代码压缩:Uglify、Terser 等工具删除冗余、缩短变量名
- 逆向解混淆:还原被打乱/加密的 JS 逻辑 ← 本文重点
入门神器:AST 可视化工具
推荐直接用 https://astexplorer.net/ 调试:
- 顶部栏:选择语言(JavaScript)、编译器(本文选 @babel/parser)
- 区域①:输入混淆/测试代码
- 区域②:可视化展示对应的 JSON 树状结构(点击节点可定位到代码位置,反之亦然)
- 区域③:编写 Babel 转换脚本(可对语法树增删改查)
- 区域④:实时预览转换后的代码

二、基础铺垫:AST 在编译中的三步定位
虽然大部分前端开发者很少写编译器,但理解这三步是玩转逆向的核心:
源代码 → 词法分析 → 语法分析 → AST → [我们在这里操作] → 代码生成 → 目标代码

1. 词法分析(拆成「单词」)
从左到右逐字符读代码,识别出有意义的「Token 符号流」。比如 log('Hi') 会被拆成:
log → 标识符
( → 左圆括号
'Hi' → 字符串字面量
) → 右圆括号
2. 语法分析(拼成「句子」)
把 Token 序列按语法规则组合,建立嵌套/依赖关系,最终生成 AST。比如 log('Hi') 的 AST 结构是:
ExpressionStatement(表达式语句)
└── CallExpression(函数调用表达式)
├── Identifier(标识符):log
└── [Arguments]
└── StringLiteral(字符串字面量):Hi
3. 代码生成(还原成「代码」)
把修改后的 AST 重新生成可执行代码。我们的解混淆操作,全在「中间的 AST 层」完成。
三、核心工具:Babel 全家桶的快速上手
Babel 是目前最主流的 JS 编译器,内置了完善的 AST 操作 API,完全能满足入门级逆向需求。
安装依赖
npm install @babel/core @babel/parser @babel/traverse @babel/generator @babel/types
核心功能包
@babel/parser:解析代码为 AST
提供两个核心方法,这里用最常用的 parse:
const parser = require("@babel/parser");
const code = "const a = 1;";
const ast = parser.parse(code, {
sourceType: "module" // 支持 ES Module 语法,可选
});
console.log(ast); // 输出嵌套的 JSON,直接看 astexplorer 更直观
@babel/traverse:批量处理节点的利器
单独用路径修改节点(比如 ast.program.body[0])太死板,批量处理必须配合 traverse + visitor。
基础示例:修改所有数字和字符串
const parser = require("@babel/parser");
const generate = require("@babel/generator").default;
const traverse = require("@babel/traverse").default;
const code = `
const a = 1500;
const b = "hi";
const c = 787;
`;
const ast = parser.parse(code);
// visitor 对象:键是节点类型,值是处理函数
const visitor = {
NumericLiteral(path) {
path.node.value = (path.node.value + 100) * 2;
},
StringLiteral(path) {
path.node.value = "I Love JavaScript!";
}
};
traverse(ast, visitor);
console.log(generate(ast).code);
输出结果:
const a = 3200;
const b = "I Love JavaScript!";
const c = 1774;
visitor 的常用写法
以下四种效果完全相同,按需选择:
// 1. 简写方法(最常用)
const visitor = { NumericLiteral(path) {...} };
// 2. 统一入口 + 类型判断(适合通用处理)
const visitor = {
enter(path) {
if (path.node.type === "NumericLiteral") {...}
}
};
// 3. 多类型共用一个处理函数
const visitor = {
"NumericLiteral|StringLiteral"(path) {...}
};
提示:enter 是默认钩子(进入节点时触发),exit 是退出钩子(后序处理时用)。
@babel/types:构建新节点的工具
需要新增代码时必须用它,方法名是节点类型的「首字母小写」。
偷懒技巧
不确定方法的参数时,直接在支持 TypeScript 的 IDE 里按住 Ctrl + 点击 方法名看类型定义,比翻官方文档快 N 倍!
四、实战演练:5个常见混淆的还原方案
4.1 Unicode/十六进制字符串还原
混淆代码常把普通字符串转成编码,比如 console['\u006c\u006f\u0067']。
核心观察
在 AST 中,编码后的字符串虽然 extra.raw 是乱码,但 value 已经是 Node.js 自动转义后的正常字符!
还原代码
const visitor = {
StringLiteral(path) {
// 删除 raw,让 generator 用 value 重新生成
delete path.node.extra?.raw;
delete path.node.extra?.rawValue;
}
};
4.2 静态表达式计算还原
混淆代码常把简单值替换成复杂表达式,比如 const a = !![]+!![]+!。
核心方法
用 Babel 内置的 path.evaluate(),它会自动判断表达式是否静态可计算。
还原代码
const types = require("@babel/types");
const visitor = {
"BinaryExpression|CallExpression|ConditionalExpression"(path) {
const { confident, value } = path.evaluate();
if (confident) {
// 把计算后的 value 转为 AST 节点并替换
path.replaceInline(types.valueToNode(value));
}
}
};
注意:如果表达式有变量引用,confident 会是 false,此时不要硬替换。
4.3 删除未使用的变量/函数
混淆代码会插入大量无用代码干扰分析。
核心方法
用 path.scope.getBinding() 检查变量的引用情况:
referenced:是否被引用
constant:是否为常量(被修改过的变量谨慎删除)
还原代码
const visitor = {
VariableDeclarator(path) {
const binding = path.scope.getBinding(path.node.id.name);
// 未定义、或被修改过、或被引用过 → 保留
if (!binding || binding.constantViolations.length > 0 || binding.referenced) return;
// 否则删除
path.remove();
}
};
4.4 删除冗余的 if-else 分支
混淆代码常有 if (false)、if (1) 这类永远不会走/必走的分支。
还原代码
const types = require("@babel/types");
const visitor = {
IfStatement(path) {
const test = path.node.test;
// 只处理布尔/数字字面量作为条件的情况(变量条件无法静态判断)
if (!types.isBooleanLiteral(test) && !types.isNumericLiteral(test)) return;
if (test.value) {
// 条件为真 → 替换为 if 分支
path.replaceInline(path.node.consequent.body);
} else if (path.node.alternate) {
// 条件为假且有 else → 替换为 else 分支
path.replaceInline(path.node.alternate.body);
} else {
// 否则删除整个 if
path.remove();
}
}
};
4.5 switch-case 反控制流平坦化(入门版)
控制流平坦化是最常用的混淆手段之一,通过 while-switch-case + 数组打乱代码顺序。
核心思路
- 找到控制流数组(比如
'3,4,0,5,1,2'['split'](','))
- 按数组顺序依次取出对应
case 的内容
- 删除
continue 语句
- 用还原后的代码替换整个
while 节点
还原代码(前置依赖查找版)
const visitor = {
WhileStatement(path) {
const switchNode = path.node.body.body[0];
// 获取控制流数组的变量名
const arrayName = switchNode.discriminant.object.name;
let controlFlowArr = [];
// 遍历 while 前面的所有兄弟节点,找到数组定义
path.getAllPrevSiblings().forEach(prevPath => {
const { id, init } = prevPath.node.declarations?.[0] || {};
if (id?.name === arrayName) {
// 模拟执行 split 操作(简化版,实际可能需要更复杂的计算)
const str = init.callee.object.value;
const separator = init.arguments[0].value;
controlFlowArr = str.split(separator);
// 删除前置的数组定义
prevPath.remove();
}
});
// 按正确顺序拼接 case 内容
let replaceNodes = [];
controlFlowArr.forEach(index => {
const caseBody = switchNode.cases[index].consequent;
// 删除末尾的 continue
if (types.isContinueStatement(caseBody.at(-1))) caseBody.pop();
replaceNodes = replaceNodes.concat(caseBody);
});
// 替换整个 while
path.replaceWithMultiple(replaceNodes);
}
};
五、总结与学习资源
核心技术点速查
推荐学习资源
解混淆没有万能公式,都是根据具体混淆工具的输出,用 AST 工具一点点抠出来的。但掌握了本文的基础,90%的入门级混淆都难不倒你!