Babel AST 入门

AST(Abstract Syntax Tree,抽象语法树)是将源代码解析成树形数据结构的表示方式。Babel 提供了一套完整的 AST 工具链,在 JS 逆向中可以用来还原混淆代码、提取加密参数、批量替换变量名等。 1. 工具链组成(#%E5%B7%A5%E5%85%B7%E9%93%BE%E7%BB%84%E6%88%90) 2. 安装(#%E5%AE%89%E8%A3%85) 3. 第一步:解析代码(parser)(#%E7%AC%AC%E4%B8%80%E6%AD%A5%EF%BC%9A%E8%A7%A3%E6%9E%90%E4%BB%A3%E

分享

官方文档:https://babeljs.io/docs/babel-types
适用版本:@babel/core 7.x+(2026-05-08 核实)

AST(Abstract Syntax Tree,抽象语法树)是将源代码解析成树形数据结构的表示方式。Babel 提供了一套完整的 AST 工具链,在 JS 逆向中可以用来还原混淆代码提取加密参数批量替换变量名等。


目录

  1. 工具链组成
  2. 安装
  3. 第一步:解析代码(parser)
  4. 常用 AST 节点类型
  5. 第二步:遍历 AST(traverse)
  6. Path 对象的常用方法
  7. 第三步:构造/修改节点(types)
  8. 第四步:生成代码(generator)
  9. 完整工作流
  10. 实战示例

工具链组成

Babel AST 的处理分四个步骤,对应四个包:

源代码(字符串)
    ↓  @babel/parser(解析)
   AST(树结构)
    ↓  @babel/traverse(遍历 + 修改)
   AST(修改后)
    ↓  @babel/generator(生成)
目标代码(字符串)
作用
@babel/parser 把 JS 字符串解析成 AST
@babel/traverse 遍历 AST 的每个节点,提供访问/修改接口
@babel/types 创建新节点、判断节点类型的工具函数库
@babel/generator 把 AST 重新生成为 JS 字符串

安装

npm install @babel/parser @babel/traverse @babel/types @babel/generator

第一步:解析代码(parser)

const parser = require('@babel/parser')

const code = `
function add(a, b) {
  return a + b
}
const result = add(1, 2)
`

const ast = parser.parse(code, {
  sourceType: 'module',   // 'script' | 'module' | 'unambiguous'
  plugins: ['jsx'],       // 支持 JSX 语法(可选)
})

console.log(ast.type)    // 'File'
console.log(ast.program) // Program 节点,包含所有顶层语句

parser.parse 选项

选项 类型 默认值 说明
sourceType string 'script' 'module' 支持 import/export;'unambiguous' 自动判断
plugins string[] [] 启用语法插件,如 'jsx''typescript''decorators'
errorRecovery boolean false 遇到语法错误时继续解析(处理混淆代码时很有用)
strictMode boolean false 强制严格模式
tokens boolean false 把 token 列表附加到 AST
ranges boolean false 每个节点附加 [start, end] 位置数组

常用 AST 节点类型

推荐用 AST Explorer 实时查看任意代码对应的 AST 结构。

基础节点

节点类型 对应代码 核心字段
Identifier foobar name: string
NumericLiteral 423.14 value: number
StringLiteral 'hello'"world" value: string
BooleanLiteral truefalse value: boolean
NullLiteral null
TemplateLiteral `hello ${name}` quasisexpressions

表达式节点

节点类型 对应代码 核心字段
BinaryExpression a + bx > 0 operatorleftright
UnaryExpression !x-ntypeof x operatorargumentprefix
LogicalExpression a && bx || y operatorleftright
AssignmentExpression x = 1a += b operatorleftright
CallExpression fn(a, b) calleearguments
MemberExpression obj.keyarr[0] objectpropertycomputed
ConditionalExpression a ? b : c testconsequentalternate
SequenceExpression (a, b, c) expressions
ArrowFunctionExpression (x) => x + 1 paramsbodyasync
FunctionExpression function(x) {} idparamsbody

语句节点

节点类型 对应代码 核心字段
VariableDeclaration var x = 1const y kind(var/let/const)、declarations
VariableDeclarator x = 1(声明中的单个变量) idinit
FunctionDeclaration function foo() {} idparamsbody
ReturnStatement return x argument
IfStatement if (x) {} else {} testconsequentalternate
BlockStatement { ... } body: Statement[]
ExpressionStatement foo() (作为语句) expression
WhileStatement while (x) {} testbody
ForStatement for (;;) {} inittestupdatebody

一个示例:a + b 的 AST 结构

{
  "type": "BinaryExpression",
  "operator": "+",
  "left": {
    "type": "Identifier",
    "name": "a"
  },
  "right": {
    "type": "Identifier",
    "name": "b"
  }
}

第二步:遍历 AST(traverse)

@babel/traverse 使用访问者模式(Visitor Pattern):你提供一个对象,键是节点类型,值是访问该类型时要执行的函数。

const traverse = require('@babel/traverse').default

traverse(ast, {
  // 进入节点时触发
  Identifier(path) {
    console.log('Identifier:', path.node.name)
  },

  // 也可以用 enter / exit 分开
  FunctionDeclaration: {
    enter(path) {
      console.log('进入函数:', path.node.id.name)
    },
    exit(path) {
      console.log('离开函数:', path.node.id.name)
    },
  },
})

同时访问多种节点类型

| 分隔多种类型:

traverse(ast, {
  'StringLiteral|NumericLiteral'(path) {
    console.log('字面量:', path.node.value)
  },
})

Path 对象的常用方法

path 是对节点的包装,提供了节点本身(path.node)以及操作节点的方法。

读取信息

path.node          // 当前 AST 节点
path.parent        // 父节点(AST 节点,非 path)
path.parentPath    // 父节点的 path
path.key           // 当前节点在父节点中的键名(如 'body', 'left')
path.listKey       // 当前节点在数组中的 key(如 'body')
path.inList        // 是否在数组中
path.scope         // 当前作用域

类型判断

path.isIdentifier()                   // 是否是 Identifier 节点
path.isIdentifier({ name: 'foo' })    // 是否是名为 foo 的 Identifier
path.isFunctionDeclaration()
path.isStringLiteral()
path.isLiteral()                      // 是否是任意字面量
path.isExpression()                   // 是否是表达式
path.isStatement()                    // 是否是语句
path.isReferenced()                   // 是否被引用(而不是定义)

修改节点

// 替换节点
path.replaceWith(newNode)             // 用单个新节点替换
path.replaceWithMultiple([n1, n2])    // 用多个节点替换(仅限在 body 中)
path.replaceWithSourceString('1 + 1') // 用代码字符串替换

// 删除节点
path.remove()                         // 删除当前节点

// 插入节点
path.insertBefore(node)               // 在当前节点前插入
path.insertAfter(node)                // 在当前节点后插入

控制遍历

path.skip()    // 跳过当前节点的所有子节点(不再向下遍历)
path.stop()    // 停止整个遍历

作用域相关

path.scope.bindings               // 当前作用域内所有绑定(变量声明)
path.scope.hasBinding('x')        // 是否有变量 x
path.scope.getBinding('x')        // 获取变量 x 的绑定信息
path.scope.rename('oldName', 'newName')  // 重命名变量(自动处理所有引用)

第三步:构造/修改节点(types)

@babel/types(通常缩写为 t)提供每种节点的工厂函数判断函数

const t = require('@babel/types')

// 创建节点
t.identifier('foo')                           // Identifier: foo
t.numericLiteral(42)                          // NumericLiteral: 42
t.stringLiteral('hello')                      // StringLiteral: 'hello'
t.booleanLiteral(true)                        // BooleanLiteral: true
t.nullLiteral()                               // NullLiteral: null

t.binaryExpression('+', t.identifier('a'), t.identifier('b'))
// → a + b

t.callExpression(t.identifier('fn'), [t.numericLiteral(1)])
// → fn(1)

t.memberExpression(t.identifier('obj'), t.identifier('key'))
// → obj.key

t.memberExpression(t.identifier('arr'), t.numericLiteral(0), true)
// → arr[0](computed: true 表示方括号访问)

t.variableDeclaration('const', [
  t.variableDeclarator(t.identifier('x'), t.numericLiteral(1))
])
// → const x = 1

t.functionDeclaration(
  t.identifier('add'),
  [t.identifier('a'), t.identifier('b')],
  t.blockStatement([
    t.returnStatement(
      t.binaryExpression('+', t.identifier('a'), t.identifier('b'))
    )
  ])
)
// → function add(a, b) { return a + b }

// 判断函数(所有节点类型都有对应的 isXxx)
t.isIdentifier(node)
t.isStringLiteral(node)
t.isFunctionDeclaration(node)

第四步:生成代码(generator)

const generate = require('@babel/generator').default

const { code } = generate(ast, {
  // 选项
  comments: true,     // 是否保留注释,默认 true
  compact: false,     // 是否压缩(去掉空格换行),默认 false
  minified: false,    // 是否完全压缩,默认 false
  concise: false,     // 适度减少空格,默认 false
  retainLines: false, // 尽量保持原始行号,默认 false
  jsescOption: {      // 字符串转义选项
    minimal: true,    // 只转义必须转义的字符
  },
})

console.log(code)

完整工作流

const parser   = require('@babel/parser')
const traverse = require('@babel/traverse').default
const t        = require('@babel/types')
const generate = require('@babel/generator').default

const code = `var x = 1`

// 1. 解析
const ast = parser.parse(code)

// 2. 遍历 + 修改
traverse(ast, {
  NumericLiteral(path) {
    // 把所有数字字面量加 10
    path.node.value += 10
  },
})

// 3. 生成
const { code: output } = generate(ast)
console.log(output) // var x = 11

实战示例

示例一:将所有 var 替换为 const

traverse(ast, {
  VariableDeclaration(path) {
    if (path.node.kind === 'var') {
      path.node.kind = 'const'
    }
  },
})

示例二:删除所有 console.log 调用

traverse(ast, {
  ExpressionStatement(path) {
    const { expression } = path.node
    // 判断是否是 console.log(...)
    if (
      t.isCallExpression(expression) &&
      t.isMemberExpression(expression.callee) &&
      t.isIdentifier(expression.callee.object, { name: 'console' }) &&
      t.isIdentifier(expression.callee.property, { name: 'log' })
    ) {
      path.remove()
    }
  },
})

示例三:计算常量表达式(常见混淆还原)

混淆代码经常把 1 写成 0x1,把 'hello' 写成 '\x68\x65\x6c\x6c\x6f',或把 2 写成 1 + 1

traverse(ast, {
  BinaryExpression(path) {
    const { left, right, operator } = path.node
    // 只处理两边都是数字字面量的情况
    if (!t.isNumericLiteral(left) || !t.isNumericLiteral(right)) return

    let result
    switch (operator) {
      case '+': result = left.value + right.value; break
      case '-': result = left.value - right.value; break
      case '*': result = left.value * right.value; break
      case '/': result = left.value / right.value; break
      case '|': result = left.value | right.value; break
      case '^': result = left.value ^ right.value; break
      case '>>': result = left.value >> right.value; break
      case '<<': result = left.value << right.value; break
      default: return
    }

    path.replaceWith(t.numericLiteral(result))
  },
})
// 1 + 1  →  2
// 3 * 4  →  12

示例四:还原十六进制字符串('\x68\x65\x6c\x6c\x6f''hello'

traverse(ast, {
  StringLiteral(path) {
    // 强制用普通字符串重写节点,babel generator 会自动用可读形式输出
    path.replaceWith(t.stringLiteral(path.node.value))
    path.skip() // 避免死循环
  },
})

示例五:重命名混淆变量名

traverse(ast, {
  Identifier(path) {
    // 把 _0x1234 形式的变量名改为 var_N
    if (/^_0x[0-9a-f]+$/.test(path.node.name)) {
      // scope.rename 会自动重命名所有引用(声明 + 使用)
      const binding = path.scope.getBinding(path.node.name)
      if (binding) {
        path.scope.rename(path.node.name)
      }
    }
  },
})

示例六:提取所有字符串字面量

const strings = []

traverse(ast, {
  StringLiteral(path) {
    strings.push({
      value: path.node.value,
      line: path.node.loc?.start.line,
    })
  },
})

console.log(strings)

注意事项

  • 避免死循环:在 StringLiteralNumericLiteral 内部用 replaceWith 创建同类节点时,新节点会再次触发同一 visitor,需要在替换后调用 path.skip() 跳过。
  • 节点不可复用:同一个节点对象不能同时挂载在树的两个位置,需要复制时用 t.cloneNode(node)
  • 位置信息:修改后生成的代码不保留原始行列号,source map 会失效,调试时注意。
  • errorRecovery:处理混淆代码可能有非标准语法,解析时建议开启 errorRecovery: true


最佳实践

用 AST Explorer 可视化调试 Visitor:在 astexplorer.net 选择 @babel/parser 解析代码,实时查看每个节点类型和结构,在右侧写 Visitor 代码并即时看到转换结果,比本地开发调试快 10 倍。

path.replaceWith 替换节点时用 t.xxx 构建新节点:从 @babel/types 导入构建器(const t = require('@babel/types')),用 t.identifier('newName')t.callExpression(...) 等 API 构建节点,不要手写字符串拼接 JS 代码。

混淆代码先处理字符串解密函数:OB 混淆的第一步是还原所有字符串——遍历 CallExpression 节点,若调用目标是字符串解密函数,直接在 Visitor 中调用该函数取到明文字符串,用 t.stringLiteral(result) 替换调用节点。

path.scope.rename 批量重命名作用域内变量:不需要手动 traverse 找所有引用,Babel 作用域分析已经处理好了,path.scope.rename('_0xabc', 'userId') 一行代码重命名所有引用。

每步还原后保存中间文件babel/generator 输出中间结果后保存文件,逐步还原而非一次完成,方便定位某一步出错的位置,也方便团队协作。


常见陷阱

陷阱:path.nodenode 混用导致作用域分析失效

现象: 手动修改了 path.node.name,但后续 path.scope.rename 没有生效,或引用计数不正确。
原因: 直接修改 node 的属性会绕过 Babel 的作用域跟踪;应该用 path.replaceWithpath.scope.rename 等操作让 Babel 感知变化。
解决: 使用 Babel Path API 而非直接修改 node 属性;重命名用 path.scope.rename,替换用 path.replaceWith(newNode)

陷阱:Visitor 的 enter / exit 时机不对导致无限递归

现象: Visitor 运行后文件大小急剧增长或进程 OOM。
原因:enter 中替换节点为相同类型的新节点,Babel 又对新节点触发同一 Visitor,产生无限递归。
解决: 替换节点后调用 path.skip() 跳过新节点的后续遍历;或把处理逻辑放到 exit 钩子中(已完成子节点处理)。

陷阱:generate 输出的代码缺少原始格式信息

现象: Babel 还原后的代码是单行,调试困难;或注释丢失。
原因: @babel/generator 默认不保留原始格式;Babel 的 AST 默认不保留注释。
解决: generate(ast, { comments: true, compact: false }) 保留注释并格式化输出;或在 @babel/parser 解析时传 { attachComment: true } 保留注释。


参见

混淆还原
js逆向调试技巧
generator状态机原理
Webpack逆向还原

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog