AST 反混淆实战:babel 写还原插件
本节目标
- 综合应用 AST 知识(见 ast-pro 课程)写「反混淆插件」
- 用 @babel/core 跑通「字符串数组还原」示例
- 体会「脚本替代人肉」的降维打击
我们写一个插件:把「_0xArr[2] 这种数组下标取用」还原成「真实字符串」(假设你已有下标→明文映射表)。
// 运行环境:Node.js 16+;需要依赖 @babel/core(项目已安装)
// 运行:node jsr-l16.cjs
const babel = require('@babel/core')
// 假设我们从断点得到的映射:下标 -> 明文
const map = { 0: 'password', 1: 'encrypt', 2: 'hello' }
const code = `const _s=['password','encrypt','hello']; function f(x){ return x===_s[0]?_s[1]:_s[2] }`
const out = babel.transformSync(code, {
plugins: [{
visitor: {
MemberExpression(path) {
// 命中 _s[数字] 形式
const { object, property } = path.node
if (object.name === '_s' && property.type === 'NumericLiteral') {
const txt = map[property.value]
if (txt !== undefined) {
// 替换成字符串字面量,字符串混淆即被还原
path.replaceWith(babel.types.stringLiteral(txt))
}
}
}
}
}]
})
console.log(out.code)
// 输出:const _s=[...]; function f(x){ return x==="password"?"encrypt":"hello" }这正是「用 AST 把混淆代码自动还原」的缩影:你写一次 visitor,成千上万处 _s[n] 一次性还原。详细 AST 原理见 ast-pro 课程。
名词解释
AST 反混淆插件:用 babel 的 visitor 模式,自动遍历并改写代码里的混淆结构(如字符串数组下标、平坦化 switch)。它把「人肉读几千行」变成「写一次脚本自动改全文件」。这是 AST 知识在逆向里最直接的变现,也是「脚本替代人力」的典型。
babel.transformSync:babel 的「同步转换」API,传入代码和插件,返回改写后的代码字符串。逆向里它是「反混淆流水线」的发动机:解析成 AST → 你的插件改 AST → 生成干净代码。注意 babel 8 里 transform 变异步,旧教程的 babel.transform 要改成 transformSync(详见 ast-pro 课程踩坑)。
课后练习
练习 1:上面插件为什么只处理 MemberExpression 且 object.name==='_s'?
答案:因为字符串混淆的特征就是「数组名[数字]」这种成员表达式。限定
object.name==='_s'避免误伤其他数组访问;property是数字字面量才说明是「下标取字符串」。精准匹配特征,才能既还原又不破坏正常代码。
练习 2:如果映射表 map 不全(某些下标没值),插件会怎样?
答案:代码中
if (txt !== undefined)守卫了——没值的下标保持原样不动,不会报错也不会误改。实际逆向里你常「先还原大部分、剩几个手动补」,这个守卫保证脚本可增量工作。
总结
这一节是 AST 课程在逆向里的「高光时刻」。我的观点:人肉反混淆是「用时间换理解」,AST 插件是「用脚本换时间」——当你面对上万行混淆代码,后者是唯一可持续的路。但别神话它:插件再强,也依赖你「先人工看懂混淆模式、建好映射表」(比如下标→明文那张表,往往得靠断点打印获得)。所以正确关系是「人定模式、脚本执行」:你负责分析出 _s[n] 这种规律并拿到映射,脚本负责把它铺满全文件。这也是为什么我把 ast-pro 课程摆在前面——没有 AST 底子,这一节的插件你写不出来。