文章目录
前言
什么是AST技术?
AST(Abstract Syntax Tree),译为抽象语法树,是编译原理中的一个概念,为源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这种数据结构可以类别为一个大的JSON对象。通过 AST 技术,我们面对的就不再是各种符号混杂空格而成的文本字符串,而是一个严谨规范的树形结构,我们可以通过对 AST树节点的一系列操作,借助机器高效且精准地修改代码。
AST 的用途很广,IDE 的语法高亮、代码检查、格式化、压缩、转译等,都需要先将代码转化成 AST 再进行后续的操作,ES5 和 ES6 语法差异,为了向后兼容,在实际应用中需要进行语法的转换,也会用到 AST。AST 并不是为了逆向而生,但做逆向学会了 AST,在解混淆时可以如鱼得水。
AST对爬虫工程师有什么意义或者用途?
随着技术的革新,越来越多的前端为了保护其网站不被第三方爬取,使用了大量的混淆代码,让爬虫工程师越来越难找到其核心参数加密代码。
有时候定位到加密的地方,面对一大坨乱如麻的代码,内心也是拒绝的。
这个时候,希望有一种工具,尽可能的将被混淆的代码进行处理,使其可读性大大增加,使爬虫工程师能够轻而易举的进行定位,抠出其核心的加密代码。
AST用在这里,只是一种辅助工具,并不能直接帮你找到核心的代码,还是得自己去分析,静态分析也好,动态调试也好,目的只有一个
AST在编译中的位置
在编译原理中,编译器转换代码通常要经过三个步骤:词法分析(Lexical Analysis)、语法分析(Syntax Analysis)、代码生成(Code Generation),下图生动展示了这一过程:
一. 解混淆API学习
- AST在线解析网站:https://astexplorer.net/
- AST解混淆API学习文档:https://evilrecluse.top/Babel-traverse-api-doc/#/
- type: 表示当前节点的类型,我们常用的类型判断方法 t.is ****(node),就是判断当前的节点是否为某个类型。
- start: 表示当前节点的起始位。
- end: 表示当前节点的末尾。
- loc : 表示当前节点所在的行列位置,里面也有start与end节点,这里的start与上面的start是不同的,这里的start是表示节点所在起始的行列位。置,而end表示的是节点所在末尾的行列位置。
- errors:是File节点所特有的属性,可以不用理会。
- program:包含整个源代码,不包含注释节点。
- comments:源代码中所有的注释会在这里显示。
掌握:
1、鼠标跟随
2、ast各节点学习
常见节点类型
二. 安装balel库
根据官网介绍,它是一个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/core --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库的使用
- 节点插件编写与节点转化
2.1 path属性语法学习path.node:表示当前path下的node节点
path.toString():当前路径所对应的源代码
path.parentPath:用于获取当前path下的父path,多用于判断节点类型
path.container:用于获取当前path下的所有兄弟节点(包括自身)
path.type:获取当前节点类型
path.get(‘’):获取path的子路径
2.2 path方法学习
- path.replaceWith:(单)节点替换函数
简单拼接还原,例如:
还原数字相加:var b = 1 + 2
还原字符串拼接:var c = “coo” + “kie”
还原在一行的:var a = 1+1,b = 2+2;var c = 3;
const visitor1 =
{
BinaryExpression(path){
let {
left,operator,right} = path.node;
if (types.isNumericLiteral(left) && operator == "+" && types.isNumericLiteral(right)){
value = left.value + right.value
console.log(value);
path.replaceWith(types.valueToNode(value)) // 进行方法替换
}
// 处理字符串拼接 var b = "he" + "llo"
if (types.isStringLiteral(left) && types.isStringLiteral(right) && operator == '+'){
console.log(path.toString())
let value = left.value + right.value
path.replaceWith(types.valueToNode(value))
}
}
}
- replaceWithMultiple多节点替换函数
实参一般是 Array 类型,它只能用于 Array 的替换。即所有需要替换的节点在一个Array里面
2.3 ast遇到只执行方法还原
!(function (){
console.log('123')
})
插件编写
const visit = {
UnaryExpression(path)
{
let {
argument} = path.node;
if (!types.isFunctionExpression(argument))
{
return;
}
let {
body,id,params} = argument;
if (id != null || params.length != 0 )
{
return;
}
path.replaceWithMultiple(body.body)
}
}
traverse(ast,visit)
2.4 通用插件
假设 var d =true?1:2; 还需要编写插件规则,有没有通用模板?
const constantFold = {
"BinaryExpression|UnaryExpression|ConditionalExpression"(path) {
// 防止溢出
if(path.isUnaryExpression({
operator:"-"}) ||
path.isUnaryExpression({
operator:"void"}))
{
return;
}
const {
confident, value} = path.evaluate();
if (!confident)
return;
if (typeof value == 'number' && (!Number.isFinite(value))) {
return;
}
path.replaceWith(types.valueToNode(value));
},
}
traverse(_ast, constantFold);
2.5 evaluate()方法学习
针对作用域和引用,直接依据引用来计算出执行结果。
function _xl(){
x = 1 + 2 + 3 +4 + 5 + 6 + 7
}
插件编写
visitor2 = {
"BinaryExpression|Identifier"(path){
const {
confident,value} = path.evaluate();
confident && path.replaceInline(types.valueToNode(value))
}
}
traverse(_ast,visitor2)
2.5 编码类型还原
处理前
var a = 0x25,b = 0b10001001,c = 0o123456,
d = "\x68\x65\x6c\x6c\x6f\x2c\x41\x53\x54",
e = "\u0068\u0065\u006c\u006c\u006f\u002c\u0041\u0053\u0054";
处理后
var a = 37,b = 137,c = 42798,d = "hello,AST",e = "hello,AST";
插件编写
const transform_literal = {
NumericLiteral({
node}) {
if (node.extra && /^0[obx]/i.test(node.extra.raw)) {
node.extra = undefined;
}
},
StringLiteral({
node})
{
if (node.extra && /\\[ux]/gi.test(node.extra.raw)) {
node.extra = undefined;
}
},
}
3. fs库的使用
读取文件
jscode = fs.readFileSync('encode.js',{
encoding:'utf-8'})
写入文件
fs.writeFile('decode.js', code, (err)=>{
});
4. generator库学习
- 生成新的js code,并保存到文件中输出
const generator = require("@babel/generator").default;
let {
code} = generator(ast);
_fs.writeFile('decode.js', code, (err)=>{
});
拓展:
1 删除所有的注释
const {
code} = generator(ast,opts = {
"comments":false});
2 Unicode转中文或者其他非ASCII码字符。 是否压缩代码
const output = generator(ast,opts = {
jsescOption:{
"minimal":true}},code);
写在最后:
本人写作水平有限,如有讲解不到位或者讲解错误的地方,还请各位大佬在评论区多多指教,共同进步.如有需要代码和讲解交流,可以加本人微信18847868809