最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
如何通过静态分析 AST 手动编写一个简易的 JavaScript 代码关键词混淆工具
时间:2026-06-29 10:07:14 编辑:袖梨 来源:一聚教程网
AST是JavaScript代码静态分析的基础,需先解析为树结构;混淆核心是安全重命名绑定标识符(如变量、函数名),保留关键字、内置对象及外部引用;须构建作用域链区分声明与引用,再用计数器生成唯一短名替换。
理解 AST 与混淆目标
静态分析 JavaScript 代码的第一步是将其转换为抽象语法树(AST)。AST 是源码的结构化表示,每个节点对应一种语法结构(如 Identifier、VariableDeclaration、FunctionExpression 等)。关键词混淆的核心目标是:在不改变程序行为的前提下,将可被安全重命名的标识符(如变量名、函数名、参数名)替换成无意义的短名称(如 _a、$0),同时保留关键字(if、return、class 等)、内置对象(Array、JSON)和外部引用(如全局变量 console、模块导入名)不变。
选择解析器并提取可混淆标识符
推荐使用 Acorn(轻量、标准兼容)或 @babel/parser(生态完善、支持新语法)。以 Acorn 为例:
- 调用
acorn.parse(code, { ecmaVersion: 2022, sourceType: 'module' })得到 AST 根节点 - 遍历 AST,收集所有类型为
Identifier的节点,但需过滤掉以下情况:- 处于
Keyword或NullLiteral等非标识符上下文(实际中 Identifier 节点本身不会代表关键字,但需检查其name是否为保留字) -
node.name属于 JS 保留字(可用 is-reserved-word 库判断) - 是对象属性访问中的属性名(如
obj.prop中的prop),且未被声明为局部变量——这类属于“字面量属性名”,不能混淆(除非启用更激进的属性名压缩,此处不考虑) - 出现在
MemberExpression的property位置且computed === false,且该属性名未在作用域中声明过
- 处于
构建作用域链与识别绑定标识符
仅靠 AST 结构不足以判断一个 Identifier 是否可重命名——必须区分“引用”和“声明”。例如:
function foo(x) { let y = x + 1; return y; }
立即学习“Java免费学习笔记(深入)”;
其中 foo、x、y 是**绑定标识符**(binding identifier),可混淆;而 x 在函数体内的两次出现是**引用标识符**(reference),需与声明匹配后统一替换。
手动实现简易作用域分析(无需完整 ES 规范):
- 深度优先遍历 AST,维护一个作用域栈(数组),每进入一个作用域(
FunctionDeclaration、FunctionExpression、ArrowFunctionExpression、BlockStatement配合let/const)就 push 新作用域对象 - 遇到声明类节点(
VariableDeclarator.id、FunctionDeclaration.id、ArrowFunctionExpression.params、CatchClause.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 可变),最后用escodegen或recast生成代码
示例片段逻辑:
// 声明节点处理伪代码<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.jsvm模块做快速 smoke test) - 禁止混淆
this、arguments、super等特殊标识符 - ES6 模块导入绑定(
import { a as b } from './x')中,b是本地绑定,可混淆;但a是导出名,不可混淆(除非你控制模块导出端) - 动态属性访问(
obj[expr])中的expr不受混淆影响,无需特殊处理 - 正则字面量、模板字符串、注释中的文本不参与 AST 分析,天然不受影响
不复杂但容易忽略。