AST常用于Js逆向中的解混淆,但这块知识较多,所以我将其分为上下两篇进行讲解,本文(也就是上篇)主要介绍AST技术以及常用模块的功能和代码演示,下篇则主要讲解AST技术在js反混淆中的利用

一、

AST技术简介

首先,我们来了解什么是AST。全称叫作 Tree,中文翻译过来就是抽象语法树。

ast树_ast树_ast树

简单来说AST就是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码的一种结构,这种数据结构其实可以类比成一个大的JSON对象。

ast树_ast树_ast树

一段代码在执行编译之前,一般要经过以下三个步骤

01

词法分析

一段代码首先会被分解成段段有意义的词法单元,比如-size: 14px;font-: -, SC;font-: 400;color: #;line-: 20px;" data-mid="">

1、const

2、name

3、=

4、qc

每个部分都具备一定的含义。

02

语法分析

接着编译器会尝试对一个个法单元进行语法分析,将其转换为能代表程序语法结构的数据结构。比如

1、const就被分析为类型,代表变量声明的具体定义;

2、name就被分析为类型,代表一个标识符

3、qc就被分析为类型,代表文本内容;

03

指令生成

最后将AST转为可执行指令并执行

这里推荐一个在线AST解析的网站ast树,可以将Js代码解析为语法树,也就是AST形式,方便师傅们更好的理解什么是AST

我们随便输入两句Js代码

const name = "qc";const age = 16;

可以看到两句Js代码被解析为body里的两段变量说明,即()

ast树_ast树_ast树

每段变量说明中都包含变量声明的具体定义(const),起始位置(start和end),以及具体的说明()

ast树_ast树_ast树

在具体的变量定义中就可以看到参数的标识符()和字面量(),以及他们的类型,起始位置等

ast树_ast树_ast树

看懂了上面的内容,你应该就能明白为什么说AST就是源代码的抽象语法结构的树状表示

AST的常见节点类型

Literal:简单理解就是字面量,比如3、"abc"、null这些都是基本的字面量。在代码中又细分为数字字面量,字符字面量等;
Declarations:声明,通常声明方法或者变量。
Expressions:表达式,通常有两个作用:一个是放在赋值语句的右边进行赋值,另外还可以作为方法的参数。
Statemonts:语句。
Identifier:标识符,指代变量名,比如上述例子中的name就是ldentifier。
Classes:类,代表一个类的定义。
Functions:方法声明。
Modules:模块,可以理解为一个Node.js模块。
Program:程序,整个代码可以称为Program。
//除此之外还有很多的类型,这里就不过多阐述了

ast树_ast树_ast树

AST技术一般在前端开发中使用非常多,但我们也可以使用AST对Js代码进行转换和改写,包括还原混淆后的Js代码,例如控制流平坦化,对象键名替换等常见的混淆手段都可以通过AST技术进行还原

ast树_ast树_ast树

接下来,我们尝试使用Babel(目前最流行的Js语法编译器)来实现AST的解析与修改

Babel的使用需要Node.js环境以及Babel命令行工具,这里默认你应该有了Node.js环境,就带着你们安装一下Babel工具

//安装化babel命令行工具npm install -g @babel/node

接下来再初始化一个Node.js项目ast-learn用于本次AST演示

// 初始化node.js项目npm install -D @babel/core @babel/cli @babel/preset-env

如下

ast树_ast树_ast树

(上面那两条警告并不是报错,只是提示有新版本而已,不用管)

初始化之后目录如下

ast树_ast树_ast树

接下来需要在当前目录创建一个.文件,内容如下

ast树_ast树_ast树

到这里我们的环境准备就完成了,接下来就是AST几个常见模块的使用了

二、babel-

babel-

@babel/是Babel中的解析器,也是一个Node.js包,就是用于将Js代码转换为AST。

ast树_ast树_ast树

包提供了一个重要的方法,就是parse和方法,前者支持解析一段)代码,后者则是尝试解析单个表达式并考虑了性能问题。一般来说,我们直接使用parse方法就够了。

ast树_ast树_ast树

对于parse方法来说,输入和输出如下。

ast树_ast树_ast树

输入:一段Js代码

输出:该段Js代码对应的抽象语法树,也就是AST

ast树_ast树_ast树

例如现在有一段简单的Js代码如下,我们将其保存为code1.js文件

const a = 3;let string = "Hello";for (let i =0; i < a; i++){  string = "world";}console.log("string",stirng)

我们想用包将其解析为AST的形式,具体的代码如下

import { parse } from "@babel/parser";import fs from "fs";//根据自己的实际路径来const code = fs.readFileSync("../code_demo/code1.js","utf-8");let ast = parse(code);console.log(ast);

很简单吧,其实就是使用了包提供的parse()方法,就可以将js源代码转换为AST抽象语法树

在命令行中运行该Js文件,命令如下

babel-node parser_demo1.js

运行结果如下

ast树_ast树_ast树

可以看到已经成功把code1.js的内容解析为AST了,这个AST的内容上面有提到过了,这里就不多赘述了,感兴趣的师傅们可以自己再看看

三、

babel-

上面的包是将Js代码转换为AST,而则是可以将AST转换为代码

ast树_ast树_ast树

这里我们尝试将上面代码中解析的AST对象重新转换为Js代码

ast树_ast树_ast树

实现代码如下

import { parse } from "@babel/parser";import generate from "@babel/generator";import fs from "fs";
const code = fs.readFileSync("../code_demo/code1.js", "utf-8");let ast = parse(code);// console.log(ast);const { code: output } = generate(ast);console.log(output);

运行之后结果如下

ast树_ast树_ast树

可以看到我们通过函数处理Js代码解析过后得到的AST对象后,重新通过这个AST对象得到了对应的Js源代码

四、

babel-

前面我们了解了AST的解析,输入任意一段代码,我们便可以分析出其AST,也可以通过AST还原出对应的Js代码,但是我们还并不能实现代码的反混淆。下面我们还需要进一步了解另一个强大的功能,就是AST的遍历和修改。

ast树_ast树_ast树

遍历我们使用的@babel/,它可以接收一个AST利用方法就可以遍历其中的所有节点。在遍历时,我们便可以对每一个节点进行对应的操作了。

ast树_ast树_ast树

下面我们来看这样一段代码

import traverse from "@babel/traverse";import { parse } from "@babel/parser";import fs from "fs";
const code = fs.readFileSync("../code_demo/code1.js", "utf-8");let ast = parse(code);traverse(ast, { enter(path) { console.log(path); },});

使用parse将源码解析为AST对象后,使用了函数来遍历整个AST语法树,每次遍历时打印当前遍历到的内容

ast树_ast树_ast树

这里的enter代表“进入”这个事件,即每次遍历到一个path,都会有一个“进入”的行为,简单理解也就是每次遍历都会触发一个“enter”,

ast树_ast树_ast树

运行结果如下

ast树_ast树_ast树

可以看到内容比较复杂。首先我们可以看到它的类型是,拥有、、node、scope、type等多个属性。比如node属性是一个Node类型的对象,他代表当前正在遍历的节点。

ast树_ast树_ast树

所以我们可以利用path.node拿到当前对应的Node对象,也可以利用path.拿到当前Node对象的父节点。

ast树_ast树_ast树

我们可以对代码作出如下修改

console.log(path);
//将打印nodepath改为打印node
console.log(path.node)

这样运行之后就只会输出AST中遍历到的每个node对象,如下

ast树_ast树_ast树

可以看到打印出了许多个node节点

ast树_ast树_ast树

上面我们也讲了,利用函数对AST对象进行遍历时,我们也可以在这个过程中进行操作,简单来说就是当遍历到符合我们条件的node时,对当前的node进行一定的操作,实现起来也很简单,跟师傅们以前写过的代码中的循环类似

ast树_ast树_ast树

这里我们还是用这个Js源代码来演示

const a = 3;let string = "Hello";for (let i = 0; i < a; i++) {  string = "world";}console.log("string", string);

例如我们想要通过修改AST的方式将上述代码的a变量的值和变量的值修改为我们指定的值,也就是变成如下内容

const a = 5;let string = "Hello";for (let i = 0; i < a; i++) {  string = "qc";}console.log("string", string);

具体的实现代码如下

import traverse from "@babel/traverse";import { parse } from "@babel/parser";import generate from "@babel/generator";import fs from "fs";
const code = fs.readFileSync("../code_demo/code1.js", "utf-8");// js代码解析为astlet ast = parse(code);traverse(ast, { //enter方法在遍历到每个节点时都会被调用,path就是被遍历到的当前节点的相关信息 //这样就相当于遍历AST,作出对应操作 enter(path) { // 遍历AST时修改指定位置的节点内容 let node = path.node; if (node.type === "NumericLiteral" && node.value === 3) { node.value = 5; } if (node.type === "StringLiteral" && node.value === "world") { node.value = "qc"; } },});
// ast还原为js代码const { code: output } = generate(ast);console.log(output);

ast树_ast树_ast树

这段代码在每次遍历时,提取了对应的node对象并进行判断,例如第一个判断,就是进行了如下判断

1、判断当前node的类型是否是数字字面量,

2、当前node的值是否等于3

ast树_ast树_ast树

如果这两个条件都满足,就说明遍历到了a的值所在的node,也就是我们想要操作的node,也就是下图这个node

ast树_ast树_ast树

ast树_ast树_ast树

此时代码再将node的value值赋值为5,这样就实现了一次AST对象的修改,第二个判断也是同理,这里就不过多阐述了

最后再将修改过后的AST对象转换为Js源代码,这样就实现了通过AST技术对Js代码的修改操作

ast树_ast树_ast树

运行结果如下

ast树_ast树_ast树

可以看到a和的值,都已经被成功修改了,这样我们就实现了一次修改AST节点内容来对Js代码进行修改的操作

ast树_ast树_ast树

(注意,这里不要理解为AST修改的是源文件,源文件是没有任何变化的,因为Js源代码解析为AST,我们修改AST,再转换为Js源代码并输入,整个过程中源代码只是用来解析为第一个AST对象的,我们并没有将修改后的Js代码赋值到源文件,也就是源文件是不变的)

ast树_ast树_ast树

另外,除了定义enter方法外,我们还可以直接定义对应特定类型的解析方法,这样遇到此类型的节点时,该方法就会被自动调用,

ast树_ast树_ast树

简单来说就是enter是每次遍历都会触发,除此之外还有许多解析方法,是遍历到特定类型的node时才会被触发并执行内部代码

ast树_ast树_ast树

例如和用法,我们尝试用这两个来重写上方修改AST对象的代码,如下

import traverse from "@babel/traverse";import { parse } from "@babel/parser";import generate from "@babel/generator";import fs from "fs";
// 遍历到指定类型时修改node
const code = fs.readFileSync("../code_demo/code1.js", "utf-8");let ast = parse(code);
traverse(ast, { // 遍历到数字字面量修改值 NumericLiteral(path) { if (path.node.value === 3) { path.node.value = 5; } }, // 遍历到字符字面量修改值 StringLiteral(path) { if (path.node.value === "Hello") { path.node.value = "hi"; } },});
const { code: output } = generate(ast);console.log(output);

简单来说就是遍历到node节点的type为数字字面量或者字符字面量时,就触发并执行内部代码,运行结果如下

ast树_ast树_ast树

可以看到也能实现对Js源代码的修改,效果是一样的

ast树_ast树_ast树

除此之外还有很多玩法,比如删除某个node等

ast树_ast树_ast树

这里我们演示一下删除code1.js最后一行代码对应的节点,代码如下

import traverse from "@babel/traverse";import { parse } from "@babel/parser";import generate from "@babel/generator";import fs from "fs";
const code = fs.readFileSync("../code_demo/code1.js", "utf-8");let ast = parse(code);
traverse(ast, { CallExpression(path) { let node = path.node; if ( node.callee.object.name === "console" && node.callee.property.name == "log" ) { path.remove(); } },});
const { code: output } = generate(ast, { retainLines: true,});console.log(output);

运行结果如下

ast树_ast树_ast树

可以看到最后一行的.log操作代码已经被删除了,这个其实更简单,只需要在遍历到对应node时调用方法即可

ast树_ast树_ast树

上面和大家演示了简单的替换和删除,那如果想要插入一个节点,应该怎么做呢?那就需要用到我们下面讲的types了

ast树_ast树_ast树

五、

bebel-types

ast树_ast树_ast树

@babel/)types是一个Node.js包,里面定义了各种各样的对象,我们可以方便地使用types声明一个新的节点,也就是对应的代码插入操作。

ast树_ast树_ast树

例如我们现在有这样一个代码

const a = 'qc';

我们想增加一行代码,改为

const a = 'qc';const daylight = "nn" + a;

这时候我们就可以使用types包实现这个操作,实现代码如下

import traverse from "@babel/traverse";import { parse } from "@babel/parser";import generate from "@babel/generator";import * as types from "@babel/types";const code = "const a = 'qc';";let ast = parse(code);
traverse(ast, { VariableDeclaration(path) { // 创建一个二元表达式,参数依次操作符,左操作数和右操作数,并赋值给init let init = types.binaryExpression( "+", types.stringLiteral("nn"), types.identifier("a") ); // 创建变量声明的AST节点,并定义变量名为a // t.variableDeclarator(id,init) // id: 必需 是t.identifier,即标识符 // init: 可选 是expression对象,即表达式 let declarator = types.variableDeclarator( types.identifier("daylight"), init ); // t.variableDeclaration(kind,declarations) // kind: 必需 'var'|'let'|'const' // declarations: 必需 是array,即variableDeclarator对象组成的数组 let declaration = types.variableDeclaration("const", [declarator]); // 插入到path节点之后 path.insertAfter(declaration); // 停止遍历 path.stop(); },});const { code: output } = generate(ast, { retainLines: true,});console.log(output);

代码简单来说就是从内到外构造节点结构,首先利用构造一个如下的二元表达式

"nn" + a

再用这个二元表达式创建一个变量声明,变量名为,也就变成了

daylight = "nn" + a

再用声明定义const,也就是形成了我们要插入的完整内容

const daylight = "nn" + a

最后再利用将代码插入到AST指定位置,然后停止遍历,这就是上述利用AST技术在js代码中插入新代码的整个过程

最后运行,结果如下

ast树_ast树_ast树

可以看到成功利用AST技术插入了新的Js代码

六、

小结

本篇文章带师傅们了解了什么是AST技术,以及AST的几个简单使用示例,包括AST的解析,遍历以及利用AST技术对Js源代码的增删改操作(建议师傅们自己运行一下代码试试)ast树,让师傅们对AST技术有了一个大致的认识,也为后面利用AST对加密过后的Js源代码进行解混淆做一个铺垫

ast树_ast树_ast树

一起期待下篇的内容吧,最后感谢一下师傅们这么久以来的支持,文章有什么不对的地方欢迎指出或者私信讨论哈

ast树_ast树_ast树

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注