前言

当你在浏览器开发者工具中看到这样一段代码时,是什么感受?


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) 获取。如果你有更好的思路或发现了漏洞,欢迎在评论区讨论。*