AST 反混淆实战:babel 写还原插件

本节目标

我们写一个插件:把「_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 底子,这一节的插件你写不出来。