如何通过静态分析 AST 手动编写一个简易的 JavaScript 代码关键词混淆工具

作者:袖梨 2026-06-29
AST是JavaScript代码静态分析的基础,需先解析为树结构;混淆核心是安全重命名绑定标识符(如变量、函数名),保留关键字、内置对象及外部引用;须构建作用域链区分声明与引用,再用计数器生成唯一短名替换。

理解 AST 与混淆目标

静态分析 JavaScript 代码的第一步是将其转换为抽象语法树(AST)。AST 是源码的结构化表示,每个节点对应一种语法结构(如 IdentifierVariableDeclarationFunctionExpression 等)。关键词混淆的核心目标是:在不改变程序行为的前提下,将可被安全重命名的标识符(如变量名、函数名、参数名)替换成无意义的短名称(如 _a$0),同时保留关键字(ifreturnclass 等)、内置对象(ArrayJSON)和外部引用(如全局变量 console、模块导入名)不变。

选择解析器并提取可混淆标识符

推荐使用 Acorn(轻量、标准兼容)或 @babel/parser(生态完善、支持新语法)。以 Acorn 为例:

  • 调用 acorn.parse(code, { ecmaVersion: 2022, sourceType: 'module' }) 得到 AST 根节点
  • 遍历 AST,收集所有类型为 Identifier 的节点,但需过滤掉以下情况:
    • 处于 KeywordNullLiteral 等非标识符上下文(实际中 Identifier 节点本身不会代表关键字,但需检查其 name 是否为保留字)
    • node.name 属于 JS 保留字(可用 is-reserved-word 库判断)
    • 是对象属性访问中的属性名(如 obj.prop 中的 prop),且未被声明为局部变量——这类属于“字面量属性名”,不能混淆(除非启用更激进的属性名压缩,此处不考虑)
    • 出现在 MemberExpressionproperty 位置且 computed === false,且该属性名未在作用域中声明过

构建作用域链与识别绑定标识符

仅靠 AST 结构不足以判断一个 Identifier 是否可重命名——必须区分“引用”和“声明”。例如:

function foo(x) { let y = x + 1; return y; }

立即学习“Java免费学习笔记(深入)”;

其中 fooxy 是**绑定标识符**(binding identifier),可混淆;而 x 在函数体内的两次出现是**引用标识符**(reference),需与声明匹配后统一替换。

手动实现简易作用域分析(无需完整 ES 规范):

  • 深度优先遍历 AST,维护一个作用域栈(数组),每进入一个作用域(FunctionDeclarationFunctionExpressionArrowFunctionExpressionBlockStatement 配合 let/const)就 push 新作用域对象
  • 遇到声明类节点(VariableDeclarator.idFunctionDeclaration.idArrowFunctionExpression.paramsCatchClause.param)时,在当前作用域中记录该 name{ kind: 'var'|'let'|'const'|'function', node: ... }
  • 遇到 Identifier 节点时,从内层向外层查找作用域,若命中则标记为“可混淆引用”,并关联到其声明节点
  • 顶层作用域中未声明的 Identifier(如直接写 console.log 中的 console)视为全局引用,跳过混淆

生成混淆名并执行替换

混淆名策略要避免冲突、保持确定性(相同输入始终输出相同结果),推荐用计数器 + 字符集:

  • 定义字符集:const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_'
  • 实现 nextName():从 1 开始编号,转为 chars 进制字符串(如 1→'a',27→'aa',53→'ba')
  • 为每个**声明节点**分配唯一混淆名(首次访问该声明时生成并缓存),所有指向它的引用均替换为同一名称
  • 注意:不同作用域中同名变量(如嵌套函数里的 i)应独立命名,避免跨作用域污染
  • 替换时直接修改 AST 节点的 name 属性(Acorn AST 可变),最后用 escodegenrecast 生成代码

示例片段逻辑:

// 声明节点处理伪代码<br>if (node.type === 'Identifier' && isBinding(node)) {<br>  if (!bindingMap.has(node)) {<br>    bindingMap.set(node, nextName());<br>  }<br>}

// 引用节点处理伪代码<br>if (node.type === 'Identifier' && isReference(node)) {<br>  const bindingNode = findBinding(node);<br>  if (bindingNode && bindingMap.has(bindingNode)) {<br>    node.name = bindingMap.get(bindingNode);<br>  }<br>}

验证与边界处理

混淆后必须确保功能等价。几个关键检查点:

  • 运行混淆前后代码,对比输出(简单脚本可用 eval 或 Node.js vm 模块做快速 smoke test)
  • 禁止混淆 thisargumentssuper 等特殊标识符
  • ES6 模块导入绑定(import { a as b } from './x')中,b 是本地绑定,可混淆;但 a 是导出名,不可混淆(除非你控制模块导出端)
  • 动态属性访问(obj[expr])中的 expr 不受混淆影响,无需特殊处理
  • 正则字面量、模板字符串、注释中的文本不参与 AST 分析,天然不受影响

不复杂但容易忽略。

相关文章

精彩推荐