一、AST原理
jscode = 'var a = "\u0068\u0065\u006c\u006c\u006f\u002c\u0041\u0053\u0054";'
在上述代码中,a
是一个变量,它被赋值为一个由 Unicode 转义序列组成的字符串。Unicode 转义序列在 JavaScript 中以 \u
开头,后跟四个十六进制数字,表示一个 Unicode 字符。
这里的字符串 "\u0068\u0065\u006c\u006c\u006f\u002c\u0041\u0053\u0054"
实际上是使用 Unicode 转义序列表示的 "hello,AST"
。每个 \uXXXX
表示一个字符:
\u0068
-> “h”\u0065
-> “e”\u006c
-> “l”\u006c
-> “l”\u006f
-> “o”\u002c
-> “,”\u0041
-> “A”\u0053
-> “S”\u0054
-> “T”
所以,变量 a
的值是 "hello,AST"
。
AST(Abstract Syntax Tree,抽象语法树)是源代码的抽象语法结构的树状表示形式。在 JavaScript 中,AST 会将代码分解成树中的节点,每个节点代表代码中的一个构造(如变量声明、字面量、表达式等)。
如果我们将上述代码转换为 AST,它将包含以下主要节点:
-
VariableDeclaration:表示变量声明的节点。
里面是函数体的内容
-
VariableDeclarator:表示声明中的变量和赋值的节点。包含起始位置和结束位置
-
Identifier:表示变量名
a
的节点。
-
Literal:表示字符串字面量的节点,其值为
"hello,AST"
。
使用 AST 技术,我们可以分析、遍历、修改或解释代码的结构。例如,代码编辑器、编译器和代码转换工具(如 Babel)会使用 AST 来理解和操作源代码。
准备需要替换的JS代码
// 解析js代码 会把js源码转换成ast语法树,返回的结果是json的结构的数据
const parse = require('@babel/parser')
// 在lxml相当于是xpath
// 编写节点和进行转换
const traverse = require('@babel/traverse').default// 准备需要转换的js代码jscode = 'var a = "\u0068\u0065\u006c\u006c\u006f\u002c\u0041\u0053\u0054";'var ast = parse.parse(jscode);// 传递两个参数(ast语法数据, 访问器对象)
//遍历、修改 AST 语法树的各个节点
traverse(ast, {
// 根据类别定位标签 , path 是定位之后的地址VariableDeclarator(path){console.log(path.parentPath)}
})
Babel库应用场景
Babel 库的应用场景主要涉及以下几个方面:
-
跨浏览器兼容性:
- 现代 JavaScript(如 ES6/ES2015 及更高版本)引入了许多新特性,如箭头函数、类、模块、模板字符串、默认参数等。
- 不是所有浏览器都支持这些新特性,尤其是旧版本的浏览器(如 Internet Explorer)。
- Babel 可以将这些现代 JavaScript 代码转换成旧版本的浏览器也能理解和执行的 ES5 代码。
-
开发新特性:
- 开发者可以使用最新的 JavaScript 特性来编写代码,提高开发效率和代码质量。
- Babel 会处理新语法和提案中的特性,让开发者不必等到所有用户的浏览器都支持这些新特性。
-
工具链集成:
- Babel 可以集成到现代前端工具链中,如 Webpack、Rollup、Gulp 等。
- 它可以作为构建过程中的一步,自动编译所有 JavaScript 文件。
-
插件和预设:
- Babel 提供了插件系统,允许开发者自定义转换规则。
- 预设(presets)是一组插件的集合,可以轻松地为特定目的配置 Babel。
-
代码优化:
- Babel 插件可以用于代码优化,如移除开发中的代码、简化代码结构等。
-
支持 TypeScript、Flow:
- Babel 可以转换 TypeScript 或 Flow 注解的代码,使其成为普通的 JavaScript 代码。
举例说明:
假设您正在使用箭头函数(一种 ES6 特性)编写代码:
const add = (a, b) => a + b;
在旧版本的浏览器中,箭头函数可能不被支持。Babel 可以将上述代码转换为:
var add = function(a, b) {return a + b;
};
这样,即使是不支持 ES6 的旧浏览器也可以正确执行这段代码。
当我们说 Babel 允许代码“运行在当前和旧版本的浏览器或其他环境中”时,意思是 Babel 生成的代码不仅可以在支持最新 JavaScript 特性的最新浏览器中运行,也可以在那些只支持旧 JavaScript 版本的旧浏览器中运行。此外,其他环境可能包括 Node.js、Electron 或任何 JavaScript 引擎。这使得开发者可以编写最新和最优雅的代码,同时确保它在尽可能多的环境中都能工作。
Babel库学习
根据官网介绍,它是一个JavaScript 编译器,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容 的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
@babel/core
:Babel 编译器本身,提供了 babel 的编译 API;@babel/parser
:将 JavaScript 代码解析成 AST 语法树;@babel/traverse
:遍历、修改 AST 语法树的各个节点;@babel/generator
:将 AST 还原成 JavaScript 代码;@babel/types
:判断、验证节点的类型、构建新 AST 节点等。
// 安装命令
npm install @babel/parser --save-dev
二、AST语法学习
- 参考地址:https://www.babeljs.cn/docs/
- 在线解析:https://astexplorer.net
//练习语法
var a = "\u0068\u0065\u006c\u006c\u006f\u002c\u0041\u0053\u0054"
1. AST输出树结构
- type: 表示当前节点的类型,我们常用的类型判断方法,就是判断当前的节点是否为某个类型。
- start: 表示当前节点的起始位。
- end: 表示当前节点的末尾。
- loc : 表示当前节点所在的行列位置,里面也有start与end节点,这里的start与上面的start是不同 的,这里的start是表示节点所在起始的行列位置,而end表示的是节点所在末尾的行列位置。
- errors:是File节点所特有的属性,可以不用理会。
- program:包含整个源代码,不包含注释节点。
- sourceType: 通常用于标识源代码的类型,以告诉解析器或编译器它正在处理的代码是模块代码还是脚本代码(Script, Module)
- body:包含了程序的主体代码,即程序的主要逻辑。
- 语句块:“body” 可能表示一组语句,通常是一个代码块,这些语句按顺序执行。
- 函数体:对于函数或方法定义,“body” 包含了函数的主体代码,即函数内部的语句和逻辑。
- 类定义:对于类定义,“body” 可能包含类的成员,如属性和方法。
- 模块内容:对于模块或文件,“body” 可能包含文件中的顶级语句和声明。
declarations
:通常用于表示变量、常量、函数、类等的声明id
:是函数,变量,类的名称init
: 通常代表声明的初始化值
- comments:源代码中所有的注释会在这里显示。
2.常见节点类型
3.babel
库学习
根据官网介绍,它是一个JavaScript 编译器,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容 的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
@babel/core
:Babel 编译器本身,提供了 babel 的编译 API;@babel/parser
:将 JavaScript 代码解析成 AST 语法树;@babel/traverse
:遍历、修改 AST 语法树的各个节点;@babel/generator
:将 AST 还原成 JavaScript 代码;@babel/types
:判断、验证节点的类型、构建新 AST 节点等。
// 安装命令
npm install @babel/parser --save-dev
1. parser库使用
- 将JavaScript源代码 转换成一棵 AST 树、返回结果(在这里赋值给 ast )是一个 JSON 结构的数据
const parse = require('@babel/parser')
// JS 转 ast语法树
jscode = `var a = "\u0068\u0065\u006c\u006c\u006f\u002c\u0041\u0053\u0054";`
let ast = parse.parse(jscode);
console.log(JSON.stringify(ast,null,'\t'))
2. traverse 库学习
- 节点插件编写与节点转化
- 你可以使用
traverse
函数来遍历AST。通常,你需要提供两个参数:AST 和访问器对象。
const parse = require('@babel/parser')
const traverse = require('@babel/traverse').default
// JS 转 ast语法树
jscode = `var a = "\u0068\u0065\u006c\u006c\u006f\u002c\u0041\u0053\u0054";`
// 转换js代码为ast树结构
let ast = parse.parse(jscode);// 用查找定位节点(ast结构树, 访问器对象)
traverse(ast, {// 定位VariableDeclarator类别,path是定位之后的地址VariableDeclarator(path){console.log('Found identifier:', path.node.init.value);}
})
1. path属性语法学习
var a = "\u0068\u0065\u006c\u006c\u006f\u002c\u0041\u0053\u0054";
path.node
:表示当前path下的node节点
path.toString()
:当前路径所对应的源代码
path.parentPath
:用于获取当前path下的父path,多用于判断节点类型
-
解析:
VariableDeclarator
是一个访问者方法(visitor method),它会在遍历到每个VariableDeclarator
类型的 AST 节点时被调用。 -
作用:在反混淆的过程中,需要根据父节点的类型决定如何处理当前节点。例如,如果一个混淆的字符串字面量
Literal
是函数调用的一部分,那么可能需要以不同的方式处理它。-
举例说明:假设我们有以下 ES6 代码片段,我们想要转换箭头函数为普通函数表达式,但只针对在对象字面量中作为属性值的箭头函数:
const obj = {greet: () => console.log("Hello, world!"), };const standaloneGreet = () => console.log("Standalone Hello, world!");
在这个例子中,我们只想转换
obj.greet
中的箭头函数,而不转换standaloneGreet
。const parser = require('@babel/parser'); const traverse = require('@babel/traverse').default; const t = require('@babel/types'); const generator = require('@babel/generator').default; // 假设的 JavaScript 代码 const code = ` const obj = {greet: () => console.log("Hello, world!"), };const standaloneGreet = () => console.log("Standalone Hello, world!"); `;// 解析 JavaScript 代码生成 AST const ast = parser.parse(code, {sourceType: 'module', // 依据代码类型选择 "script" 或 "module" });traverse(ast, {ArrowFunctionExpression(path) {if (path.parentPath.isObjectProperty()) {// 创建一个 BlockStatement,将原来的表达式包装在 ExpressionStatement 中const blockStatement = t.blockStatement([t.expressionStatement(path.node.body)]);// 创建函数表达式,使用上面创建的 blockStatement 作为 bodyconst functionExpression = t.functionExpression(null, // idpath.node.params, // paramsblockStatement, // bodyfalse, // generatorfalse // async);path.replaceWith(functionExpression);}} });// 生成转换后的代码 const output = generator(ast, { /* options */ }, code); // 输出转换后的代码 console.log(output.code);
-
-
在某些情况下,混淆可能会改变代码的结构。通过检查父节点,开发者可以决定是否需要重建代码的某些部分以恢复其原始意图。
- 分析父节点:通过分析父节点,可以获取更多关于当前节点的信息,比如它是一个独立的变量声明还是一个变量声明列表的一部分。
- 执行进一步操作:有时候需要基于父节点的信息来决定如何操作当前节点,例如你可能只想修改某个特定函数中的变量声明。
-
path.container
:用于获取当前path下的所有兄弟节点(包括自身) -
path.type
:获取当前节点类型 -
path.get('')
:获取path的子路径,取值的方式有点像Xpath
const parse = require('@babel/parser')
const traverse = require('@babel/traverse').default
// JS 转 ast语法树
jscode = `var a = "\u0068\u0065\u006c\u006c\u006f\u002c\u0041\u0053\u0054";
var a = "1111";`
// 转换js代码为ast树结构
let ast = parse.parse(jscode);// 用查找定位节点(ast结构树, 访问器对象)
traverse(ast, {VariableDeclarator(path){// console.log(path.node); // 表示当前path下的node节点// console.log(path.type) // 获取当前节点类型// console.log(path.toString()); // 用来获取当前遍历path的js源代码// console.log(path.parentPath.node); //用于获取当前path下的父path,多用于判断节点类型// console.log(path.get('init').toString()); // 获取下面的节点// console.log(path.container); // 用于获取当前path下的所有兄弟节点(包括自身)// 只获取一个数据console.log(path.node.init.value);// 找到第一个后,可以停止遍历// path.stop();}
})
2. 替换原有节点
- path.replaceWith :(单)节点替换函数
- 还原数字相加: var b = 1 + 2
- 还原字符串拼接: var c = “coo” + “kie”
- 还原在一行的: var a = 1+1,b = 2+2;var c = 3;
- 还原在一行的: var d = “1” + 1;
- 还原在一行的: var e = 1 + ‘2’;
const parse = require('@babel/parser')
const traverse = require('@babel/traverse').default
const types = require('@babel/types')
const generator = require("@babel/generator").default;// JS 转 ast语法树
jscode = `var b = 1 + 2;
var c = "coo" + "kie";
var a = 1+1,b = 2+2;
var c = 3;
var d = "1" + 1;
var e = 1 + '2';
`
// 转换js代码为ast树结构
let ast = parse.parse(jscode);// 用查找定位节点(ast结构树, 访问器对象)
traverse(ast, {BinaryExpression(path) {// 取出数组数据的单独对象var {left, operator, right} = path.node// 数字相加处理if (types.isNumericLiteral(left) && types.isNumericLiteral(right) && operator == "+" || types.isStringLiteral(left) && types.isStringLiteral(right)) {value = left.value + right.value// console.log(value);// 会把原来的节点当中的原来的值进行替换path.replaceWith(types.valueToNode(value))// console.log(path.parentPath.node)}if (types.isStringLiteral(left) && types.isStringLiteral(right) && operator == "+") {value = left.value + right.value// console.log(value);// 会把原来的节点当中的原来的值进行替换path.replaceWith(types.valueToNode(value))}if (types.isStringLiteral(left) && types.isNumericLiteral(right) && operator == "+" || types.isNumericLiteral(left) && types.isStringLiteral(right)) {value = left.value + right.value// console.log(value);// 会把原来的节点当中的原来的值进行替换path.replaceWith(types.valueToNode(value))}}
})
// 将ast还原成JavaScript代码
let {code} = generator(ast);
console.log(code)
- replaceWithMultiple 多节点替换函数,调用方式
path.replaceWithMultiple(ArrayNode);
- 实参一般是 Array 类型,它只能用于 Array 的替换。
- 即所有需要替换的节点在一个Array里面 举例:对如下变量进行处理
替换前:var arr = '3,4,0,5,1,2'['split'](',')
替换后:var arr = ["3", "4", "0", "5", "1", "2"]
const generator = require("@babel/generator").default;
const parse = require('@babel/parser')
const traverse = require('@babel/traverse').default
const types = require('@babel/types')
// JS 转 ast语法树
jscode = `
var arr = '3,4,0,5,1,2'['split'](',')
`
// 转换js代码为ast树结构
let ast = parse.parse(jscode);traverse(ast, {CallExpression(path) {let {callee, arguments} = path.nodelet data = callee.object.valuelet func = callee.property.valuelet arg = arguments[0].valuevar res = data[func](arg)path.replaceWithMultiple(types.valueToNode(res))}
})// 将ast还原成JavaScript代码
let {code} = generator(ast);
console.log(code)
3. 自执行方法还原
!(function () {console.log('123')
})
JS反混淆工具分享
https://tool.yuanrenxue.cn/