前言
当你在浏览器开发者工具中看到这样一段代码时,是什么感受?
var _0xabcd=['\x4a\x6f\x69\x6e','\x63\x61\x6c\x6c','\x70\x72\x6f\x74\x6f\x74\x79\x70\x65'];
function _0x1e2f3(_0x4a5b6c,_0x7d8e9f){return _0x4a5b6c+_0x7d8e9f;}
大多数逆向工程师的第一反应是:"又来?"。混淆代码是爬虫逆向路上最大的绊脚石之一。而今天我们要聊的 **AST(抽象语法树)**,恰恰是击碎这块绊脚石最有力的武器。
本文将从实战角度出发,带你深入 AST 在爬虫逆向中的高阶应用。不会花时间解释什么是 AST(那是入门教程的事),而是直接聚焦在 如何用 AST 构建自动化逆向工具 上。
一、AST在爬虫逆向中的核心价值
爬虫逆向的本质是 "理解并复现目标代码的逻辑"。混淆代码的存在,让这个过程的复杂度呈指数级上升。AST 之所以能成为破局关键,是因为它让我们的逆向工作从 **"人肉分析字符串"** 跃迁到了 **"程序化分析代码结构"** 的层面。
具体来说,AST 在逆向中的核心价值体现在三个维度:
| 维度 | 传统方式 | AST 方式 |
|------|----------|----------|
| 分析速度 | 逐行阅读、手动记录变量映射 | 毫秒级遍历整个 AST,自动建立映射 |
| 准确性 | 容易遗漏,肉眼疲劳导致误判 | 确定性匹配,100% 覆盖 |
| 可复用性 | 每次逆向从头再来 | 编写一次脚本,同类混淆通杀 |
更重要的是,AST 让我们能 在代码执行前就完成分析和转换。这意味着我们可以在不依赖运行时环境的情况下,还原混淆代码的逻辑结构。
二、常见JS混淆技术分析
在动手之前,先系统地梳理一下目前主流的 JS 混淆技术。理解了这些技术的 AST 特征,我们就能像医生一样"对症下药"。
2.1 变量名混淆
这是最基础但也最普遍的混淆手段。将语义化的变量名替换为无意义字符:
// 原始
function encryptData(data, key) { ... }
// 混淆后
function _0x12a4b(_0x5c6d7e, _0x8f9a0b) { ... }
AST 特征:Identifier 节点的 `name` 属性由可读字符串变为 `_0x`、`$` 等前缀的短字符串。
2.2 字符串加密
将明文字符串转为数组索引引用或十六进制编码:
// 常见形式一:数组索引
var _0xabc = ["encrypt", "decrypt", "key"];
var method = _0xabc[0]; // 等效于 "encrypt"
// 常见形式二:十六进制编码
var str = '\x68\x65\x6c\x6c\x6f'; // "hello"
// 常见形式三:RC4/Base64 动态解密
var key = _0x5c6d('3f2a1b', '9c8d7e');
AST 特征:大量 StringLiteral 节点被替换为 `MemberExpression`(数组索引)或 `CallExpression`(解密函数调用)。
2.3 控制流平坦化
这是最具破坏力的混淆之一。将程序的顺序逻辑打散,放入一个 dispatch 循环中:
// 原始
function calc(a, b) {
var c = a + b;
var d = c * 2;
return d;
}
// 控制流平坦化后
function calc(a, b) {
var state = 0, result;
while (true) {
switch (state) {
case 0: result = a + b; state = 2; break;
case 2: result = result * 2; state = 4; break;
case 4: return result;
}
}
}
AST 特征:WhileStatement 内嵌 `SwitchStatement`,`switch` 的 `cases` 数量远超正常逻辑。state 变量以常量方式赋值和流转。
2.4 死代码注入
插入大量永远不会被执行到的垃圾代码,增加分析负担:
function realLogic() {
var a = 1;
if (false) { /* 200 行死代码 */ }
if (void 0) { /* 又 200 行死代码 */ }
return a + 2;
}
AST 特征:IfStatement 的 `test` 节点是常量布尔值、`void 0`、`typeof x === 'undefined'` 等可静态求值的表达式。
三、使用AST进行代码反混淆
现在进入实战环节。我们将使用 @babel/parser、`@babel/traverse`、`@babel/generator` 和 `@babel/types` 来构建反混淆工具。这些包是 AST 操作的事实标准。
3.1 环境搭建
npm init -y
npm install @babel/parser @babel/traverse @babel/generator @babel/types
反混淆的通用骨架代码如下:
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
const fs = require('fs');
// 读取混淆代码
const code = fs.readFileSync('obfuscated.js', 'utf-8');
// 解析为 AST
const ast = parser.parse(code, {
sourceType: 'script',
plugins: ['literalRegex']
});
// === 反混淆转换写在这里 ===
// 生成目标代码
const output = generate(ast, { retainLines: false, compact: false }).code;
fs.writeFileSync('deobfuscated.js', output, 'utf-8');
3.2 字符串解密
字符串加密最常见的实现方式是把所有字符串放入一个数组,然后通过下标引用。反混淆的核心是 将数组下标引用替换为实际字符串值。
function deobfuscateStringArray(ast) {
let stringArray = null;
let stringArrayName = '';
// 第一步:找到字符串数组的定义
traverse(ast, {
VariableDeclarator(path) {
const { id, init } = path.node;
if (t.isArrayExpression(init) &&
init.elements.every(e => t.isStringLiteral(e))) {
stringArray = init.elements.map(e => e.value);
stringArrayName = id.name;
}
}
});
if (!stringArray) return;
// 第二步:替换所有对字符串数组的引用
traverse(ast, {
MemberExpression(path) {
const { object, property } = path.node;
if (t.isIdentifier(object, { name: stringArrayName }) &&
t.isNumericLiteral(property) &&
property.value >= 0 &&
property.value < stringArray.length) {
path.replaceWith(
t.stringLiteral(stringArray[property.value])
);
}
}
});
// 第三步:移除字符串数组定义(可选)
traverse(ast, {
VariableDeclarator(path) {
if (path.node.id.name === stringArrayName) {
path.parentPath.remove();
}
}
});
}
3.3 变量名还原
变量名混淆的反向操作不是"还原原始名"(因为原始名已经被丢弃了),而是 生成有意义的可读名称:
let counter = 0;
const nameMap = new Map();
function renameIdentifiers(ast) {
traverse(ast, {
Scopable(path) {
// 遍历当前作用域的所有绑定
for (const [name, binding] of Object.entries(path.scope.bindings)) {
// 只处理混淆风格的变量名
if (/^_0x[a-f0-9]+$/i.test(name) || /^_\$+/.test(name)) {
// 生成有意义的名称
const newName = binding.kind === 'function'
? `func_${counter++}`
: `var_${counter++}`;
nameMap.set(name, newName);
binding.scope.rename(name, newName);
}
}
}
});
}
这里的关键点在于利用 babel 的 Scopable 接口,它会自动处理作用域内的所有引用,避免命名冲突。
3.4 控制流恢复
控制流平坦化的恢复是最复杂的操作。核心思路是 静态执行 dispatch 逻辑,重新排列语句顺序:
function recoverControlFlow(ast) {
traverse(ast, {
WhileStatement(path) {
const { test, body } = path.node;
// 匹配 while(true) { switch(state) { ... } }
if (!t.isBooleanLiteral(test, { value: true })) return;
const switchStmt = body.body.find(n => t.isSwitchStatement(n));
if (!switchStmt) return;
const stateVar = resolveStateVariable(switchStmt);
if (!stateVar) return;
// 按 state 值排序 cases
const orderedCases = new Map();
for (const caseClause of switchStmt.cases) {
const stateValue = caseClause.test?.value;
if (stateValue !== undefined) {
orderedCases.set(stateValue, caseClause);
}
}
// 从 state = 0 开始,按 state 流转顺序重构
const restoredStatements = [];
let currentState = 0;
const visited = new Set();
while (!visited.has(currentState)) {
visited.add(currentState);
const caseClause = orderedCases.get(currentState);
if (!caseClause) break;
// 提取有效语句(排除 state 赋值和 break)
const statements = caseClause.consequent.filter(stmt => {
if (t.isBreakStatement(stmt)) return false;
if (t.isExpressionStatement(stmt) &&
t.isAssignmentExpression(stmt.expression) &&
t.isIdentifier(stmt.expression.left, { name: stateVar })) {
currentState = stmt.expression.right?.value ?? -1;
return false;
}
return true;
});
restoredStatements.push(...statements);
}
if (restoredStatements.length > 0) {
path.replaceWithMultiple(restoredStatements);
}
}
});
}
function resolveStateVariable(switchStmt) {
// 分析 switch discriminant 来确定 state 变量
const discriminant = switchStmt.discriminant;
if (t.isIdentifier(discriminant)) {
return discriminant.name;
}
return null;
}
3.5 死代码消除
死代码的检测基于 常量折叠:在编译期就能确定值的表达式,可以直接求值并替换:
function removeDeadCode(ast) {
traverse(ast, {
IfStatement(path) {
const { test, consequent, alternate } = path.node;
// 计算 test 的常量值
const value = evaluateConstant(test);
if (value === undefined) return;
if (value) {
// 条件为真,替换为 consequent
path.replaceWithMultiple(consequent.body);
} else if (alternate) {
// 条件为假,替换为 alternate
path.replaceWithMultiple(
t.isBlockStatement(alternate) ? alternate.body : [alternate]
);
} else {
// 条件为假且无 else,移除整个 if
path.remove();
}
},
// 处理 return 后的死代码
ReturnStatement(path) {
const siblings = path.parent.body || [];
const idx = siblings.indexOf(path.node);
if (idx !== -1 && idx < siblings.length - 1) {
// 移除 return 后面的语句(在当前块中)
path.parent.body = siblings.slice(0, idx + 1);
}
}
});
}
function evaluateConstant(node) {
if (t.isBooleanLiteral(node)) return node.value;
if (t.isNumericLiteral(node)) return node.value;
if (t.isStringLiteral(node)) return node.value;
if (t.isUnaryExpression(node) && node.operator === '!') {
const val = evaluateConstant(node.argument);
if (val !== undefined) return !val;
}
if (t.isUnaryExpression(node) && node.operator === 'void') {
return undefined; // void 0 => undefined (falsy)
}
if (t.isIdentifier(node, { name: 'undefined' })) return undefined;
return undefined;
}
四、自动化提取加密函数
在爬虫逆向中,我们通常不需要还原整段代码——我们只需要找到 加密函数,然后在外部环境模拟执行它。
4.1 定位加密函数
加密函数通常有一些特征:调用原生加密方法(window.btoa、`crypto.createHash` 等)、处理 `sign`、`token`、`nonce` 等关键参数。我们可以通过 AST 分析自动化定位:
function locateEncryptionFunctions(ast) {
const candidates = [];
traverse(ast, {
CallExpression(path) {
const callee = path.node.callee;
// 特征1:调用内置加密方法
const cryptoAPIs = [
'btoa', 'atob', 'md5', 'sha1', 'sha256',
'crypto.createHash', 'CryptoJS',
'window.btoa', 'Buffer.from'
];
const calleeName = generate(callee).code;
if (cryptoAPIs.some(api => calleeName.includes(api))) {
// 回溯找到包裹函数
const funcParent = findEnclosingFunction(path);
if (funcParent && !candidates.includes(funcParent)) {
candidates.push({
type: 'crypto_api',
name: funcParent.key?.name || '',
node: funcParent,
reason: `使用 ${calleeName}`
});
}
}
// 特征2:参数中包含关键字段
const sensitiveKeys = ['sign', 'token', 'nonce', 'timestamp',
'encrypt', 'decrypt', 'auth'];
const args = path.node.arguments;
args.forEach(arg => {
if (t.isObjectExpression(arg)) {
const props = arg.properties.map(p => p.key?.name || p.key?.value);
if (props.some(p => sensitiveKeys.includes(p))) {
const funcParent = findEnclosingFunction(path);
if (funcParent && !candidates.includes(funcParent)) {
candidates.push({
type: 'sensitive_param',
name: funcParent.key?.name || '',
node: funcParent,
reason: `包含敏感参数: ${props.join(', ')}`
});
}
}
}
});
}
});
return candidates;
}
function findEnclosingFunction(path) {
while (path.parentPath) {
if (t.isFunctionDeclaration(path.parentPath.node) ||
t.isFunctionExpression(path.parentPath.node) ||
t.isArrowFunctionExpression(path.parentPath.node)) {
return path.parentPath.node;
}
path = path.parentPath;
}
return null;
}
4.2 提取并封装加密函数
定位到加密函数后,我们需要将其从原始代码中提取出来,并封装为可独立调用的模块:
function extractEncryptionFunction(ast, funcNode, funcName) {
// 1. 收集函数依赖的所有外部变量
const dependencies = new Set();
const dependencyValues = {};
traverse(ast, {
enter(path) {
if (path.node === funcNode) {
// 停止进入函数内部——只分析外部依赖
path.skip();
return;
}
// 收集变量定义
if (t.isVariableDeclarator(path.node)) {
const varName = path.node.id.name;
const initCode = generate(path.node.init).code;
dependencyValues[varName] = initCode;
}
// 收集函数定义
if (t.isFunctionDeclaration(path.node)) {
const fnName = path.node.id.name;
dependencyValues[fnName] = generate(path.node).code;
}
}
});
// 2. 构建独立执行文件
let output = '/**\n * Auto-extracted encryption function\n */\n\n';
// 添加依赖
for (const [name, code] of Object.entries(dependencyValues)) {
output += `// Dependency: ${name}\n`;
output += `var ${name} = ${code};\n\n`;
}
// 添加目标函数导出
output += '// Target encryption function\n';
output += generate(funcNode).code + '\n\n';
output += `module.exports = ${funcNode.id?.name || funcName};\n`;
return output;
}
五、实战案例:还原一段混淆的JS加密函数
让我们用一个完整的实战案例,把上述技术串联起来。
5.1 被混淆的代码
假设我们遇到了这样一段混淆代码:
var _0xabcd = ['\x6d\x64\x35', '\x63\x72\x65\x61\x74\x65', '\x48\x61\x73\x68',
'\x68\x65\x78', '\x75\x70\x64\x61\x74\x65', '\x64\x69\x67\x65\x73\x74',
'\x73\x75\x62\x73\x74\x72\x69\x6e\x67', '2', '5', '10'];
function _0x1e2f(_0x3a4b) {
var _0x5c6d = function(_0x7e8f) {
while (--_0x7e8f) {
_0x3a4b['\x70\x75\x73\x68'](_0x3a4b['\x73\x68\x69\x66\x74']());
}
};
_0x5c6d(++_0x3a4b['\x6c\x65\x6e\x67\x74\x68']);
return _0x3a4b;
}
var _0x2f3a = _0x1e2f(_0xabcd);
function _0x4b5c(_0x6d7e) {
var _0x8f9a = {
'\x64\x61\x74\x61': _0x6d7e,
'\x6b\x65\x79': '\x73\x65\x63\x72\x65\x74\x4b\x65\x79',
'\x6d\x65\x74\x68\x6f\x64': _0x2f3a[0x0]
};
var _0x0a1b = require('crypto')
['\x63\x72\x65\x61\x74\x65\x48\x61\x73\x68'](_0x8f9a['\x6d\x65\x74\x68\x6f\x64']);
_0x0a1b['\x75\x70\x64\x61\x74\x65'](_0x8f9a['\x64\x61\x74\x61'] + _0x8f9a['\x6b\x65\x79']);
return _0x0a1b['\x64\x69\x67\x65\x73\x74']('\x68\x65\x78');
}
console['\x6c\x6f\x67'](_0x4b5c('\x68\x65\x6c\x6c\x6f'));
5.2 编写反混淆脚本
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
const fs = require('fs');
const code = fs.readFileSync('sample-obfuscated.js', 'utf-8');
const ast = parser.parse(code, { sourceType: 'script' });
// 步骤一:字符串解密 - 还原数组索引
let strings = [];
let arrayName = '';
traverse(ast, {
VariableDeclarator(path) {
const { id, init } = path.node;
if (t.isCallExpression(init) && arrayName === '') {
// 找到被 _0x1e2f 处理过的数组
if (init.callee?.name === '_0x1e2f' &&
t.isIdentifier(init.arguments[0])) {
arrayName = id.name;
// 查找原始数组定义
const origArray = init.arguments[0].name;
const binding = path.scope.getBinding(origArray);
if (binding) {
const decl = binding.path.node;
if (t.isVariableDeclarator(decl) &&
t.isArrayExpression(decl.init)) {
// 模拟 _0x1e2f 的运行
// 注意:_0x1e2f 将数组元素左移了一位
const elements = [...decl.init.elements];
const shiftCount = 1 % elements.length;
for (let i = 0; i < shiftCount; i++) {
elements.push(elements.shift());
}
strings = elements.map(e =>
Buffer.from(e.value, 'hex').toString()
);
}
}
}
}
}
});
// 步骤二:替换所有成员表达式索引
traverse(ast, {
MemberExpression(path) {
const { object, property } = path.node;
if (t.isIdentifier(object, { name: arrayName }) &&
t.isNumericLiteral(property) &&
strings[property.value]) {
path.replaceWith(t.stringLiteral(strings[property.value]));
}
}
});
// 步骤三:替换十六进制字符串
traverse(ast, {
StringLiteral(path) {
const value = path.node.value;
if (/^[\\]x[0-9a-f]{2}/i.test(value)) {
// 只替换全十六进制转义的字符串
const decoded = Buffer.from(
value.replace(/\\x/g, ''), 'hex'
).toString();
if (/^[\x20-\x7E]+$/.test(decoded)) {
path.replaceWith(t.stringLiteral(decoded));
}
}
}
});
// 步骤四:移除 dead code
traverse(ast, {
FunctionDeclaration(path) {
if (path.node.id?.name === '_0x1e2f' &&
path.parentPath.isProgram()) {
path.remove(); // 移除数组洗牌函数
}
}
});
// 步骤五:生成可读的输出
traverse(ast, {
Scopable(path) {
let idx = 0;
for (const [name, binding] of Object.entries(path.scope.bindings)) {
if (/^_0x[a-f0-9]+$/i.test(name) || /^_0x/.test(name)) {
const prefix = binding.kind === 'function' ? 'func' :
binding.kind === 'param' ? 'param' : 'var';
binding.scope.rename(name, `${prefix}_${idx++}`);
}
}
// 阻止继续遍历到子作用域(每个作用域独立处理)
}
});
const result = generate(ast, {
retainLines: false,
compact: false,
concise: false
}).code;
fs.writeFileSync('sample-deobfuscated.js', result, 'utf-8');
console.log('反混淆完成!');
5.3 反混淆结果
var strings = ['md5', 'create', 'Hash', 'hex', 'update', 'digest', 'substring', '2', '5', '10'];
function func_0(param_0) {
var data = param_0;
var key = 'secretKey';
var method = 'md5';
var hash = require('crypto').createHash(method);
hash.update(data + key);
return hash.digest('hex');
}
console.log(func_0('hello'));
经过 AST 处理后,原本无法阅读的混淆代码被还原为了清晰的逻辑:这是一个 MD5 加密函数,使用 secretKey 作为盐值。整个还原过程耗时不到 1 秒。
六、AST + 动态调试:混合逆向方法
AST 是静态分析的利器,但有些混淆在静态层面无法完全还原。例如:
1. 动态生成的字符串:eval(atob("cmVxdWVzdA=="))
2. Prototype 污染:Array.prototype.push = function() { ... }
3. 基于环境检测的代码分支:if (typeof navigator === 'undefined') { ... }
在这些场景下,我们需要将 AST 分析与动态调试结合。
6.1 混合逆向工作流
// 1. AST 预处理:去混淆、提取结构
// 2. 注入断点/日志:在关键位置插入调试代码
// 3. 动态执行:在受控环境中运行,收集运行时数据
// 4. 分析结果:用运行时数据回注 AST,做二次优化
function hybridReverseEngineering(obfuscatedCode) {
// 阶段一:AST 静态预处理
const ast = parser.parse(obfuscatedCode);
removeDeadCode(ast);
deobfuscateStringArray(ast);
// 阶段二:注入日志代理
traverse(ast, {
CallExpression(path) {
const callee = generate(path.node.callee).code;
// 在加密函数调用前注入参数记录
if (callee.includes('encrypt') || callee.includes('sign')) {
path.insertBefore(
parser.parse(`
console.log('[ENCRYPT_CALL]', ${path.node.arguments
.map((_, i) => `arguments[${i}]`).join(', ')});
`).program.body[0]
);
}
}
});
// 阶段三:生成增强代码
const enhancedCode = generate(ast).code;
return enhancedCode;
}
6.2 Chrome DevTools Protocol 集成
对于更复杂的场景,可以通过 CDP 在真实的浏览器环境中执行代码,同时通过 AST 控制注入点:
const CDP = require('chrome-remote-interface');
async function hybridDebugWithCDP(ast, injectionPoints) {
const client = await CDP();
const { Runtime, Debugger, Page } = client;
await Page.enable();
await Runtime.enable();
await Debugger.enable();
// 在 AST 定位的注入点设置断点
for (const point of injectionPoints) {
await Debugger.setBreakpoint({
location: {
scriptId: point.scriptId,
lineNumber: point.line,
columnNumber: point.column
}
});
}
// 断点触发时,通过 Runtime.evaluate 获取当前作用域变量
Debugger.paused(async (params) => {
const { callFrames } = params;
const topFrame = callFrames[0];
const result = await Runtime.evaluate({
expression: `Object.keys(${topFrame.this})`,
contextId: params.executionContextId
});
console.log('Scope variables:', result.result.value);
await Debugger.resume();
});
}
这种混合方法的威力在于:AST 告诉你"代码长什么样",动态调试告诉你"代码实际做了什么"。两者结合,能破解绝大多数混淆方案。
七、构建自动化逆向工具链
当你理解了上述所有技术后,下一步就是将它们组装成一条自动化逆向流水线。
7.1 工具链架构
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ 输入层 │ │ 分析层 │ │ 输出层 │
│ │ │ │ │ │
│ 混淆 JS 代码 │────→│ AST 解析 │────→│ 反混淆代码 │
│ URL / 文件 │ │ 字符串解密 │ │ 提取的加密函数 │
│ 网络请求响应 │ │ 控制流恢复 │ │ 可调用模块 │
│ │ │ 死代码消除 │ │ 分析报告 │
└─────────────────┘ │ 加密函数定位 │ └─────────────────┘
│ 依赖分析 │
└──────────────────┘
7.2 完整实现
class ReverseEngineeringPipeline {
constructor() {
this.pipeline = [];
}
// 注册处理阶段
use(stage) {
this.pipeline.push(stage);
return this;
}
// 执行流水线
async run(inputCode) {
let currentCode = inputCode;
const reports = [];
for (const stage of this.pipeline) {
console.log(`[Pipeline] 执行阶段: ${stage.name}`);
const result = await stage.handler(currentCode);
currentCode = result.code;
reports.push({
stage: stage.name,
stats: result.stats,
duration: result.duration
});
}
return { code: currentCode, reports };
}
}
// 使用示例
const pipeline = new ReverseEngineeringPipeline();
pipeline.use({
name: '字符串解密',
handler: async (code) => {
const start = Date.now();
const ast = parser.parse(code);
deobfuscateStringArray(ast);
deobfuscateHexStrings(ast);
const newCode = generate(ast).code;
return {
code: newCode,
stats: { stringLiteralsResolved: countChanges(code, newCode) },
duration: Date.now() - start
};
}
});
pipeline.use({
name: '控制流恢复',
handler: async (code) => {
const start = Date.now();
const ast = parser.parse(code);
recoverControlFlow(ast);
const newCode = generate(ast).code;
return {
code: newCode,
stats: { controlFlowBlocks: countChanges(code, newCode) },
duration: Date.now() - start
};
}
});
pipeline.use({
name: '加密函数提取',
handler: async (code) => {
const start = Date.now();
const ast = parser.parse(code);
const candidates = locateEncryptionFunctions(ast);
const extracted = candidates.map(c => ({
name: c.name,
code: generate(c.node).code,
reason: c.reason
}));
return {
code,
stats: { functionsFound: candidates.length, extracted },
duration: Date.now() - start
};
}
});
// 运行
const result = await pipeline.run(obfuscatedCode);
console.log(JSON.stringify(result.reports, null, 2));
7.3 执行环境模拟
提取出加密函数后,我们通常需要在 Node.js 中模拟执行。这里的关键是 识别并填补缺失的运行时依赖:
function createSandboxForExtractedFunction(funcCode, dependencies) {
const sandboxCode = `
// 模拟浏览器环境
global.window = global;
global.navigator = { userAgent: 'Mozilla/5.0 ...' };
global.document = { cookie: '', createElement: () => ({}) };
global.location = { href: 'https://target.com' };
// 填补 Polyfill
if (typeof atob === 'undefined') {
global.atob = (str) => Buffer.from(str, 'base64').toString();
}
if (typeof btoa === 'undefined') {
global.btoa = (str) => Buffer.from(str).toString('base64');
}
// 加载目标代码
${Object.entries(dependencies).map(([key, val]) =>
`var ${key} = ${val};`
).join('\n')}
// 提取的函数
${funcCode}
// 导出接口
module.exports = { ${funcCode.match(/function (\w+)/)?.[1] || 'encrypt'} };
`;
// 在 VM 中执行以获得隔离
const vm = require('vm');
const script = new vm.Script(sandboxCode, { timeout: 5000 });
const sandbox = { module: { exports: {} }, require, Buffer, console };
const context = vm.createContext(sandbox);
script.runInContext(context);
return sandbox.module.exports;
}
八、反混淆工具开发实战
让我们把上述所有知识整合起来,开发一个完整的反混淆命令行工具。
8.1 工具设计
#!/usr/bin/env node
// deobfuscator-cli.js
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
class Deobfuscator {
constructor(options = {}) {
this.options = {
renameVariables: true,
removeDeadCode: true,
resolveStrings: true,
flattenControlFlow: true,
extractFunctions: false,
outputType: 'replaced', // 'replaced' | 'extracted' | 'both'
...options
};
this.stats = {
stringsResolved: 0,
variablesRenamed: 0,
deadCodeRemoved: 0,
controlFlowRecovered: 0,
functionsExtracted: 0
};
}
process(inputFile, outputFile) {
const code = fs.readFileSync(inputFile, 'utf-8');
console.log(`[Deobfuscator] 分析文件: ${inputFile}`);
console.log(`[Deobfuscator] 原始大小: ${(code.length / 1024).toFixed(2)} KB`);
const ast = parser.parse(code, {
sourceType: 'unambiguous',
plugins: ['dynamicImport', 'optionalChaining', 'nullishCoalescing']
});
if (this.options.resolveStrings) this._resolveStrings(ast);
if (this.options.removeDeadCode) this._removeDeadCode(ast);
if (this.options.flattenControlFlow) this._flattenControlFlow(ast);
if (this.options.renameVariables) this._renameVariables(ast);
// 生成反混淆代码
const result = generate(ast, {
retainLines: false,
compact: false,
comments: false
}).code;
// 写入输出
fs.writeFileSync(outputFile, result, 'utf-8');
console.log(`[Deobfuscator] 输出文件: ${outputFile}`);
console.log(`[Deobfuscator] 处理后大小: ${(result.length / 1024).toFixed(2)} KB`);
console.log(`[Deobfuscator] 压缩率: ${(result.length / code.length * 100).toFixed(1)}%`);
// 如果开启了函数提取
if (this.options.extractFunctions) {
this._extractFunctions(ast, outputFile);
}
this._printStats();
}
_resolveStrings(ast) {
// 收集所有字符串数组
const stringArrays = new Map();
traverse(ast, {
VariableDeclarator(path) {
const { id, init } = path.node;
if (t.isArrayExpression(init) &&
init.elements.length > 2 &&
init.elements.every(e => t.isStringLiteral(e) || t.isNullLiteral(e))) {
stringArrays.set(id.name, init.elements.map(e =>
e.value !== null ? e.value : null
));
}
}
});
if (stringArrays.size === 0) return;
// 替换成员表达式引用
traverse(ast, {
MemberExpression(path) {
const { object, property, computed } = path.node;
if (computed && t.isIdentifier(object) && t.isNumericLiteral(property)) {
const arr = stringArrays.get(object.name);
if (arr && property.value >= 0 && property.value < arr.length) {
const value = arr[property.value];
if (value !== null) {
path.replaceWith(t.stringLiteral(value));
this.stats.stringsResolved++;
}
}
}
}
}, this);
}
_removeDeadCode(ast) {
traverse(ast, {
IfStatement(path) {
const testValue = this._evaluateConstant(path.node.test);
if (testValue === undefined) return;
if (testValue) {
path.replaceWithMultiple(
t.isBlockStatement(path.node.consequent)
? path.node.consequent.body
: [path.node.consequent]
);
} else if (path.node.alternate) {
path.replaceWithMultiple(
t.isBlockStatement(path.node.alternate)
? path.node.alternate.body
: [path.node.alternate]
);
} else {
path.remove();
}
this.stats.deadCodeRemoved++;
},
ConditionalExpression(path) {
const testValue = this._evaluateConstant(path.node.test);
if (testValue !== undefined) {
path.replaceWith(testValue ? path.node.consequent : path.node.alternate);
this.stats.deadCodeRemoved++;
}
},
// 移除不可达代码
ReturnStatement(path) {
const body = path.parent.body || path.parent.alternate?.body;
if (body) {
const idx = body.indexOf(path.node);
if (idx !== -1 && idx < body.length - 1) {
// 标记后续语句为死代码
path.parent.__deadCodeAfter = idx;
}
}
},
// 清理标记后的死代码
BlockStatement: {
exit(path) {
if (path.node.__deadCodeAfter !== undefined) {
path.node.body = path.node.body.slice(
0, path.node.__deadCodeAfter + 1
);
delete path.node.__deadCodeAfter;
}
}
}
}, this);
}
_evaluateConstant(node) {
if (t.isBooleanLiteral(node)) return node.value;
if (t.isNumericLiteral(node)) return node.value;
if (t.isStringLiteral(node)) return node.value;
if (t.isIdentifier(node, { name: 'undefined' })) return undefined;
if (t.isNullLiteral(node)) return null;
if (t.isUnaryExpression(node)) {
const arg = this._evaluateConstant(node.argument);
if (arg === undefined) return undefined;
switch (node.operator) {
case '!': return !arg;
case 'void': return undefined;
case 'typeof': return typeof arg;
case '+': return +arg;
case '-': return -arg;
case '~': return ~arg;
}
}
if (t.isBinaryExpression(node)) {
const left = this._evaluateConstant(node.left);
const right = this._evaluateConstant(node.right);
if (left === undefined || right === undefined) return undefined;
switch (node.operator) {
case '+': return left + right;
case '-': return left - right;
case '*': return left * right;
case '/': return left / right;
case '%': return left % right;
case '===': return left === right;
case '!==': return left !== right;
case '>': return left > right;
case '<': return left < right;
case '&&': return left && right;
case '||': return left || right;
}
}
return undefined;
}
_flattenControlFlow(ast) {
traverse(ast, {
WhileStatement(path) {
if (!t.isBooleanLiteral(path.node.test, { value: true })) return;
const switchStmt = path.node.body.body.find(n => t.isSwitchStatement(n));
if (!switchStmt) return;
const discriminant = switchStmt.discriminant;
if (!t.isIdentifier(discriminant)) return;
const stateVar = discriminant.name;
// 按 case 顺序重组
const cases = new Map();
for (const c of switchStmt.cases) {
if (c.test && t.isNumericLiteral(c.test)) {
cases.set(c.test.value, c);
}
}
const statements = [];
let state = 0;
const visited = new Set();
while (!visited.has(state)) {
visited.add(state);
const c = cases.get(state);
if (!c) break;
for (const stmt of c.consequent) {
if (t.isBreakStatement(stmt)) continue;
// 检测 state 赋值
if (t.isExpressionStatement(stmt) &&
t.isAssignmentExpression(stmt.expression) &&
t.isIdentifier(stmt.expression.left, { name: stateVar })) {
const nextVal = this._evaluateConstant(stmt.expression.right);
if (nextVal !== undefined) {
state = nextVal;
continue;
}
}
statements.push(stmt);
}
}
if (statements.length > 0) {
path.replaceWithMultiple(statements);
this.stats.controlFlowRecovered++;
}
}
}, this);
}
_renameVariables(ast) {
let counter = 0;
traverse(ast, {
Scopable(path) {
for (const [name, binding] of Object.entries(path.scope.bindings)) {
if (/^_0x[a-f0-9]+$/i.test(name) ||
/^_\$\$?[a-z0-9]+/i.test(name) ||
/^[a-z]{1,2}$/.test(name)) {
const prefix = binding.kind === 'function' ? 'fn' :
binding.kind === 'param' ? 'p' :
binding.kind === 'const' ? 'c' : 'v';
const newName = `${prefix}_${counter++}`;
binding.scope.rename(name, newName);
this.stats.variablesRenamed++;
}
}
}
}, this);
}
_extractFunctions(ast, baseOutputPath) {
const extractDir = path.join(
path.dirname(baseOutputPath),
'extracted'
);
fs.mkdirSync(extractDir, { recursive: true });
const functions = locateEncryptionFunctions(ast);
functions.forEach((func, idx) => {
const output = extractEncryptionFunction(ast, func.node, func.name);
const outputPath = path.join(
extractDir,
`${func.name || `encrypt_${idx}`}.js`
);
fs.writeFileSync(outputPath, output, 'utf-8');
this.stats.functionsExtracted++;
console.log(` └─ 已提取: ${outputPath} (${func.reason})`);
});
}
_printStats() {
console.log('\n[Deobfuscator] === 反混淆统计 ===');
console.log(` 字符串解密: ${this.stats.stringsResolved}`);
console.log(` 变量名重命名: ${this.stats.variablesRenamed}`);
console.log(` 死代码移除: ${this.stats.deadCodeRemoved}`);
console.log(` 控制流恢复: ${this.stats.controlFlowRecovered}`);
console.log(` 函数提取: ${this.stats.functionsExtracted}`);
console.log('[Deobfuscator] ==================\n');
}
}
// CLI 入口
if (require.main === module) {
const args = process.argv.slice(2);
const options = {
renameVariables: !args.includes('--no-rename'),
removeDeadCode: !args.includes('--no-dce'),
resolveStrings: !args.includes('--no-strings'),
flattenControlFlow: !args.includes('--no-cff'),
extractFunctions: args.includes('--extract')
};
const inputFile = args.find(a => !a.startsWith('-'));
if (!inputFile) {
console.error('用法: node deobfuscator-cli.js [options]');
console.error('Options:');
console.error(' --no-rename 不重命名变量');
console.error(' --no-dce 不删死代码');
console.error(' --no-strings 不解密字符串');
console.error(' --no-cff 不恢复控制流');
console.error(' --extract 提取加密函数');
process.exit(1);
}
const outputFile = inputFile.replace(/\.js$/, '') + '.deobfuscated.js';
const deobfuscator = new Deobfuscator(options);
deobfuscator.process(inputFile, outputFile);
}
8.2 使用示例
# 基础反混淆
node deobfuscator-cli.js obfuscated.js
# 全功能反混淆 + 加密函数提取
node deobfuscator-cli.js obfuscated.js --extract
# 仅提取加密函数(不做其他转换)
node deobfuscator-cli.js obfuscated.js --no-rename --no-dce --no-strings --no-cff --extract
8.3 应对反调试技巧
高级混淆还会加入反调试逻辑。我们可以在 AST 层面识别并移除它们:
function removeAntiDebug(ast) {
traverse(ast, {
// 移除计时器检测
CallExpression(path) {
const callee = path.node.callee;
if (t.isIdentifier(callee, { name: 'setInterval' }) ||
t.isIdentifier(callee, { name: 'setTimeout' })) {
const args = path.node.arguments;
if (args.length >= 2 && t.isNumericLiteral(args[1])) {
if (args[1].value < 100) {
// 短间隔定时器 - 可能是反调试心跳
path.remove();
}
}
}
},
// 移除 debugger 语句
DebuggerStatement(path) {
path.remove();
},
// 移除 DevTools 检测
IfStatement(path) {
const test = path.node.test;
// 检测类似: if (element['__proto__'] !== div['__proto__'])
if (t.isBinaryExpression(test, { operator: '!==' })) {
const code = generate(test).code;
if (code.includes('__proto__') ||
code.includes('__defineGetter__') ||
code.includes('Firebug') ||
code.includes('devtools')) {
path.remove();
}
}
}
});
}
九、总结与展望
9.1 核心要点回顾
AST 在爬虫逆向中的应用,本质上是一种 "以程序对抗程序" 的方法论。当你面对的是经过混淆工具处理的机器生成代码时,用人肉分析去对抗机器生成,从一开始就是不对等的。AST 让我们把逆向提升到了同一个量级。
本文的核心脉络可以概括为:
1. 理解混淆:每种混淆都有明确的 AST 特征模式
2. 精确转化:用 AST 遍历 + 精准匹配,对特定节点做变换
3. 自动化流水线:将多个处理阶段串联,形成可复用的工具链
4. 混合互补:静态分析(AST)做广度覆盖,动态调试做深度验证
9.2 技术趋势
随着 WASM、JavaScript JIT 编译优化、以及更高级的混淆方案(如直接将关键逻辑编译为 WASM,或在 V8 字节码层面做保护)的普及,纯 AST 分析也有其局限性。未来的逆向工具链会是:
AST 静态分析(处理混淆壳层)
↓
WASM 反编译 / V8 字节码分析(处理核心逻辑)
↓
动态污点追踪 + API Hooks(运行时行为捕获)
↓
AI 辅助模式识别(自动化识别加密算法特征)
但无论如何演变,AST 作为代码结构分析的基础设施,其地位不可撼动。它能处理 80% 的混淆场景,而这 80% 中,大部分问题只需要几十行代码就能解决。
9.3 最后的话
写这篇文章的目的,不是为了教你"破解"某个网站,而是分享一种 "用代码理解代码" 的思维方式。AST 在爬虫逆向中的应用,只是这种思维方式的一个缩影。同样的技术,也可以用在代码重构、自动化修复、lint 工具开发、Polyfill 注入等正向工程领域。
真正的高手,不是会多少工具,而是能理解问题的本质,然后用最合适的工具去解决它。AST 是你工具箱里的一把手术刀——精准、优雅、威力巨大。
希望这篇文章能给你带来启发。代码见真章,动手试试吧。
*本文所有代码均可在 [GitHub Gist](https://gist.github.com) 获取。如果你有更好的思路或发现了漏洞,欢迎在评论区讨论。*
评论