> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Babel AST 入门
- URL: https://blog.vercanti.com/babel-ast-ru-men/
- Published: 2026-08-28T14:35:07.000Z
- Updated: 2026-08-28T14:58:06.000Z
- Description: 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
- Author: yellowdog
- Tags: js逆向, 技巧步骤

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

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%E7%A0%81%EF%BC%88parser%EF%BC%89)
4. [常用 AST 节点类型](#%E5%B8%B8%E7%94%A8-ast-%E8%8A%82%E7%82%B9%E7%B1%BB%E5%9E%8B)
5. [第二步：遍历 AST（traverse）](#%E7%AC%AC%E4%BA%8C%E6%AD%A5%EF%BC%9A%E9%81%8D%E5%8E%86-ast%EF%BC%88traverse%EF%BC%89)
6. [Path 对象的常用方法](#path-%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%B8%B8%E7%94%A8%E6%96%B9%E6%B3%95)
7. [第三步：构造/修改节点（types）](#%E7%AC%AC%E4%B8%89%E6%AD%A5%EF%BC%9A%E6%9E%84%E9%80%A0%E4%BF%AE%E6%94%B9%E8%8A%82%E7%82%B9%EF%BC%88types%EF%BC%89)
8. [第四步：生成代码（generator）](#%E7%AC%AC%E5%9B%9B%E6%AD%A5%EF%BC%9A%E7%94%9F%E6%88%90%E4%BB%A3%E7%A0%81%EF%BC%88generator%EF%BC%89)
9. [完整工作流](#%E5%AE%8C%E6%95%B4%E5%B7%A5%E4%BD%9C%E6%B5%81)
10. [实战示例](#%E5%AE%9E%E6%88%98%E7%A4%BA%E4%BE%8B)

---

## 工具链组成

Babel AST 的处理分四个步骤，对应四个包：

```
源代码（字符串）
    ↓  @babel/parser（解析）
   AST（树结构）
    ↓  @babel/traverse（遍历 + 修改）
   AST（修改后）
    ↓  @babel/generator（生成）
目标代码（字符串）

```

| 包                | 作用                     |
| ---------------- | ---------------------- |
| @babel/parser    | 把 JS 字符串解析成 AST        |
| @babel/traverse  | 遍历 AST 的每个节点，提供访问/修改接口 |
| @babel/types     | 创建新节点、判断节点类型的工具函数库     |
| @babel/generator | 把 AST 重新生成为 JS 字符串     |

---

## 安装

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

```

---

## 第一步：解析代码（parser）

```javascript
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](https://astexplorer.net/) 实时查看任意代码对应的 AST 结构。

### 基础节点

| 节点类型            | 对应代码              | 核心字段               |
| --------------- | ----------------- | ------------------ |
| Identifier      | foo、bar           | name: string       |
| NumericLiteral  | 42、3.14           | value: number      |
| StringLiteral   | 'hello'、"world"   | value: string      |
| BooleanLiteral  | true、false        | value: boolean     |
| NullLiteral     | null              | —                  |
| TemplateLiteral | \`hello ${name}\` | quasis、expressions |

### 表达式节点

| 节点类型                    | 对应代码             | 核心字段                      |
| ----------------------- | ---------------- | ------------------------- |
| BinaryExpression        | a + b、x > 0      | operator、left、right       |
| UnaryExpression         | !x、\-n、typeof x  | operator、argument、prefix  |
| LogicalExpression       | a && b、x \|| y   | operator、left、right       |
| AssignmentExpression    | x = 1、a += b     | operator、left、right       |
| CallExpression          | fn(a, b)         | callee、arguments          |
| MemberExpression        | obj.key、arr\[0\] | object、property、computed  |
| ConditionalExpression   | a ? b : c        | test、consequent、alternate |
| SequenceExpression      | (a, b, c)        | expressions               |
| ArrowFunctionExpression | (x) => x + 1     | params、body、async         |
| FunctionExpression      | function(x) {}   | id、params、body            |

### 语句节点

| 节点类型                | 对应代码              | 核心字段                             |
| ------------------- | ----------------- | -------------------------------- |
| VariableDeclaration | var x = 1、const y | kind（var/let/const）、declarations |
| VariableDeclarator  | x = 1（声明中的单个变量）   | id、init                          |
| FunctionDeclaration | function foo() {} | id、params、body                   |
| ReturnStatement     | return x          | argument                         |
| IfStatement         | if (x) {} else {} | test、consequent、alternate        |
| BlockStatement      | { ... }           | body: Statement\[\]              |
| ExpressionStatement | foo() （作为语句）      | expression                       |
| WhileStatement      | while (x) {}      | test、body                        |
| ForStatement        | for (;;) {}       | init、test、update、body            |

### 一个示例：`a + b` 的 AST 结构

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

```

---

## 第二步：遍历 AST（traverse）

`@babel/traverse` 使用**访问者模式（Visitor Pattern）**：你提供一个对象，键是节点类型，值是访问该类型时要执行的函数。

```javascript
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)
    },
  },
})

```

### 同时访问多种节点类型

用 `|` 分隔多种类型：

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

```

---

## Path 对象的常用方法

`path` 是对节点的包装，提供了节点本身（`path.node`）以及操作节点的方法。

### 读取信息

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

```

### 类型判断

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

```

### 修改节点

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

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

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

```

### 控制遍历

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

```

### 作用域相关

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

```

---

## 第三步：构造/修改节点（types）

`@babel/types`（通常缩写为 `t`）提供每种节点的**工厂函数**和**判断函数**。

```javascript
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）

```javascript
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)

```

---

## 完整工作流

```javascript
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`

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

```

### 示例二：删除所有 `console.log` 调用

```javascript
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`。

```javascript
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'`）

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

```

### 示例五：重命名混淆变量名

```javascript
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)
      }
    }
  },
})

```

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

```javascript
const strings = []

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

console.log(strings)

```

---

## 注意事项

- **避免死循环**：在 `StringLiteral` 或 `NumericLiteral` 内部用 `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.node` 和 `node` 混用导致作用域分析失效

**现象：** 手动修改了 `path.node.name`，但后续 `path.scope.rename` 没有生效，或引用计数不正确。  
**原因：** 直接修改 `node` 的属性会绕过 Babel 的作用域跟踪；应该用 `path.replaceWith` 或 `path.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 }` 保留注释。

---

## 参见

[混淆还原](https://blog.vercanti.com/hun-yao-huan-yuan/)  
[js逆向调试技巧](https://blog.vercanti.com/js-ni-xiang-diao-shi-ji-qiao/)  
[generator状态机原理](https://blog.vercanti.com/javascript-generator-zhuang-tai-ji-yuan-li/)  
[Webpack逆向还原](https://blog.vercanti.com/webpack-ni-xiang-huan-yuan/)