花指令(Obfuscation)是代码保护中常见的混淆手段,通过添加冗余逻辑、复杂表达式等方式降低代码可读性。本文将手把手教你如何利用Babel的AST(抽象语法树)解析能力,逐步还原被混淆的JavaScript代码。不同于单纯的理论讲解,我们更关注可复现的操作流程和工程实践中的避坑指南。
在开始前,确保已安装Node.js(建议v16+)并初始化项目:
bash复制mkdir ast-deobfuscation && cd ast-deobfuscation
npm init -y
npm install @babel/parser @babel/traverse @babel/types @babel/generator
关键工具链说明:
@babel/parser:将JS代码转换为AST@babel/traverse:遍历和修改AST节点@babel/types:AST节点类型校验与构建@babel/generator:将AST转回JS代码提示:建议使用VS Code配合AST Explorer(https://astexplorer.net/)实时查看AST结构
花指令的典型特征包括:
_0xb28de8["abcd"]代替直接比较)function(a,b){return a+b}代替a+b)"a"+"b"代替"ab")以下是一个典型的被混淆代码片段:
javascript复制// demo.js
var _0xb28de8 = {
"abcd": function(_0x22293f, _0x5a165e) {
return _0x22293f == _0x5a165e;
},
"dbca": function(_0xfbac1e, _0x23462f, _0x556555) {
return _0xfbac1e(_0x23462f, _0x556555);
}
};
var aa = _0xb28de8["abcd"](123, 456);
创建dec_main.js处理文件:
javascript复制const fs = require('fs');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');
const generator = require('@babel/generator').default;
const jscode = fs.readFileSync('./demo.js', 'utf-8');
const ast = parser.parse(jscode);
关键节点类型说明:
| 节点类型 | 说明 | 示例 |
|---|---|---|
| VariableDeclarator | 变量声明节点 | var a = 1 |
| MemberExpression | 成员表达式节点 | obj["property"] |
| CallExpression | 函数调用节点 | func(arg1, arg2) |
| FunctionExpression | 函数表达式节点 | function(){} |
针对对象属性调用的处理逻辑:
javascript复制function deobfuscateObject(path) {
const { node, scope } = path;
// 仅处理对象表达式初始化的变量
if (!t.isObjectExpression(node.init)) return;
const objName = node.id.name;
const binding = scope.getBinding(objName);
// 检查变量是否被修改
if (binding?.constantViolations.length > 0) return;
const properties = node.init.properties;
let processedCount = 0;
properties.forEach(prop => {
const key = prop.key.value; // 获取属性名如'abcd'
if (t.isFunctionExpression(prop.value)) {
const returnStmt = prop.value.body.body[0];
// 处理函数调用替换
binding.referencePaths.forEach(refPath => {
if (t.isCallExpression(refPath.parentPath.node) &&
t.isMemberExpression(refPath.parentPath.node.callee)) {
const memberExpr = refPath.parentPath.node.callee;
if (t.isIdentifier(memberExpr.object, { name: objName }) &&
(t.isStringLiteral(memberExpr.property, { value: key }) ||
t.isIdentifier(memberExpr.property, { name: key }))) {
const args = refPath.parentPath.node.arguments;
// 根据返回类型进行不同处理
if (t.isBinaryExpression(returnStmt.argument)) {
refPath.parentPath.replaceWith(
t.binaryExpression(
returnStmt.argument.operator,
args[0],
args[1]
)
);
processedCount++;
}
// 其他类型处理...
}
}
});
}
});
// 如果所有引用都已处理,则移除原对象
if (processedCount === binding.referencePaths.length) {
path.remove();
}
}
注意:作用域分析是安全处理的关键,必须检查变量是否被后续修改
当全局变量与局部变量同名时:
javascript复制var _0xb28de8 = { /* 全局 */ };
function test() {
var _0xb28de8 = { /* 局部 */ }; // 应单独处理
console.log(_0xb28de8["key"]);
}
处理策略:
scope.getBinding()获取精确绑定path.scope.uid对于无法静态分析的场景应保留原结构:
javascript复制function dynamicAccess(key) {
return _0xb28de8[key]; // 无法确定key值
}
安全删除检查清单:
最终执行流程:
javascript复制traverse(ast, {
VariableDeclarator: {
exit: [deobfuscateObject]
}
});
const { code } = generator(ast, {
jsescOption: { minimal: true }
});
fs.writeFileSync('./demo_deobfuscated.js', code);
处理前后对比:
原始代码:
javascript复制var aa = _0xb28de8["abcd"](123, 456);
处理后代码:
javascript复制var aa = 123 == 456;
性能优化建议:
scope.crawl()更新作用域信息调试AST处理过程的实用技巧:
javascript复制// 在遍历器中添加调试输出
traverse(ast, {
CallExpression(path) {
console.log('处理调用:', generator(path.node).code);
path.stop(); // 单步调试时使用
}
});
常见问题排查表:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 节点未正确替换 | 类型检查不严格 | 加强t.isXXX类型校验 |
| 意外删除有效代码 | 作用域分析不完整 | 检查binding.referencePaths |
| 生成代码格式异常 | generator配置不当 | 调整jsescOption参数 |
处理过程中的经验之谈:当遇到多层嵌套的花指令时,建议从内向外逐层处理,并每步都验证生成代码的正确性。我曾在一个项目中因为急于求成,试图一次性处理所有混淆层,结果导致语义错误,调试花了比预期多三倍的时间。