一、AST 技术核心原理
抽象语法树(AST) 是代码的“骨架”,它把代码拆解成一个个节点,就像把一棵大树拆成树枝、树叶一样。通过分析和修改这些节点,我们可以精准地还原代码的逻辑。
二、实战案例 1:还原字符串编码
混淆代码特征
混淆代码会把字符串拆成数组,然后通过索引访问,比如:
const _0x5c0d = ["Hello", "World", "log", "split", "join"];
console[_0x5c0d[2]](_0x5c0d[0][_0x5c0d[3]]("")[_0x5c0d[4]]("-"));
// 输出 "H-e-l-l-o"
反混淆步骤
-
安装依赖
npm install @babel/parser @babel/traverse @babel/generator
-
解析和还原代码:
-
用 Babel 把代码解析成 AST。
-
遍历 AST,找到字符串数组的定义。
-
替换所有数组索引访问为实际的字符串值。
-
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;
const code = `
const _0x5c0d = ["Hello", "World", "log", "split", "join"];
console[_0x5c0d[2]](_0x5c0d[0][_0x5c0d[3]]("")[_0x5c0d[4]]("-"));
`;
// 解析为 AST
const ast = parser.parse(code);
// 遍历 AST 节点
traverse(ast, {
VariableDeclarator(path) {
// 找到字符串数组
if (path.node.id.name === "_0x5c0d") {
const stringArray = path.node.init.elements.map(e => e.value);
// 替换所有数组引用
path.scope.traverse(path.scope.block, {
MemberExpression(subPath) {
const node = subPath.node;
if (node.object.name === "_0x5c0d" && node.property.type === "NumericLiteral") {
const value = stringArray[node.property.value];
subPath.replaceWith({ type: "StringLiteral", value });
}
}
});
}
}
});
// 生成还原后的代码
const output = generator(ast).code;
console.log(output);
输出结果:
console["log"]("Hello"["split"]("")["join"]("-"));
// 进一步简化为:console.log("H-e-l-l-o");
三、实战案例 2:修复控制流扁平化
混淆代码特征
混淆代码会把逻辑打乱成一个状态机,比如:
function getSum(n) {
let state = 0, total = 0, i = 0;
while (true) {
switch (state) {
case 0: state = 1; break;
case 1: if (i < n) state = 2; else state = 3; break;
case 2: total += i; i++; state = 1; break;
case 3: return total;
}
}
}
反混淆步骤
-
识别状态机模式:
-
找到
switch-case
结构。 -
分析
state
变量的跳转逻辑。
-
-
重构逻辑:
-
把状态机逻辑还原成正常的循环结构。
-
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const code = `...上述控制流代码...`;
const ast = parser.parse(code);
traverse(ast, {
FunctionDeclaration(path) {
const body = path.get("body.body");
const switchStmt = body.find(p => p.isSwitchStatement());
// 提取状态转移逻辑
const stateMap = new Map();
switchStmt.get("cases").forEach(casePath => {
const test = casePath.node.test.value;
const consequent = casePath.get("consequent");
stateMap.set(test, consequent);
});
// 重构为 for 循环
const newBody = [
t.variableDeclaration("let", [t.variableDeclarator(t.identifier("total"), t.numericLiteral(0))]),
t.forStatement(
t.variableDeclaration("let", [t.variableDeclarator(t.identifier("i"), t.numericLiteral(0))]),
t.binaryExpression("<", t.identifier("i"), t.identifier("n")),
t.updateExpression("++", t.identifier("i")),
t.blockStatement([t.expressionStatement(
t.assignmentExpression("+=", t.identifier("total"), t.identifier("i"))
)])
),
t.returnStatement(t.identifier("total"))
];
path.get("body").replaceWith(t.blockStatement(newBody));
}
});
// 输出还原后代码
console.log(generator(ast).code);
输出结果:
function getSum(n) {
let total = 0;
for (let i = 0; i < n; i++) {
total += i;
}
return total;
}
四、对抗混淆的高级技巧
-
处理多层嵌套:
-
混淆代码可能把数组多层嵌套,需要递归展开所有层级。
-
-
动态解密函数处理:
-
若字符串解密通过函数实现,AST 需模拟执行函数逻辑。
-
-
符号执行优化:
-
对条件进行数学推导,消除永远无法执行的分支。
-
五、工具链推荐
工具 | 用途 | 示例命令 |
---|---|---|
@babel/parser | 代码解析为 AST | parser.parse(code) |
@babel/traverse | 节点遍历与修改 | traverse(ast, { ... }) |
@babel/generator | 从 AST 生成代码 | generator(ast).code |
六、总结
AST 反混淆的核心步骤:
-
解析:把代码转换为结构化的 AST。
-
识别模式:找到字符串数组、控制流状态机等特征。
-
语义还原:通过节点替换或逻辑重构恢复原始逻辑。
-
验证:确保重构后的代码功能与原混淆代码一致。
注意事项:
-
混淆工具可能插入虚假节点(如无用循环),需结合数据流分析过滤。
-
遇到
eval
或Function
动态代码时,需结合动态执行。 -
推荐结合 Prettier 格式化还原后的代码,提升可读性。
通过 AST 技术,我们可以从“代码加密 → 结构解析 → 逻辑还原”完成全链路逆向工程,是应对现代混淆技术的核心手段!