Js逆向入门:AST技术解混淆(上篇)
AST常用于Js逆向中的解混淆,但这块知识较多,所以我将其分为上下两篇进行讲解,本文(也就是上篇)主要介绍AST技术以及常用模块的功能和代码演示,下篇则主要讲解AST技术在js反混淆中的利用
一、AST技术简介
首先,我们来了解什么是AST。全称叫作Abstract Syntax Tree,中文翻译过来就是抽象语法树。
简单来说AST就是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码的一种结构,这种数据结构其实可以类比成一个大的JSON对象。
一段代码在执行编译之前,一般要经过以下三个步骤
词法分析:一段代码首先会被分解成段段有意义的词法单元,比如const name=”qc”这段代码,它可被折分成四部分:
const
name
=
qc
每个部分都具备一定的含义。
语法分析:接着编译器会尝试对一个个法单元进行语法分析,将其转换为能代表程序语法结构的数据结构。比如
const就被分析为VariableDeclaration类型,代表变量声明的具体定义;
name就被分析为ldentifier类型,代表一个标识符
qc就被分析为Literal类型,代表文本内容;
指令生成:最后将AST转为可执行指令并执行
这里推荐一个在线AST解析的网站,可以将Js代码解析为语法树,也就是AST形式,方便师傅们更好的理解什么是AST
我们随便输入两句Js代码
const name = "qc";
const age = 16;
可以看到两句Js代码被解析为body里的两段变量说明,即**(Variableclaration**)
每段变量说明中都包含变量声明的具体定义(const),起始位置(start和end),以及具体的说明(declarations)
在具体的变量定义中就可以看到参数的标识符(Identifier)和字面量(Literal),以及他们的类型,起始位置等
看懂了上面的内容,你应该就能明白为什么说AST就是源代码的抽象语法结构的树状表示
AST的常见节点类型如下:
Literal:简单理解就是字面量,比如3、"abc"、null这些都是基本的字面量。在代码中又细分为数字字面量,字符字面量等;
Declarations:声明,通常声明方法或者变量。
Expressions:表达式,通常有两个作用:一个是放在赋值语句的右边进行赋值,另外还可以作为方法的参数。
Statemonts:语句。
Identifier:标识符,指代变量名,比如上述例子中的name就是ldentifier。
Classes:类,代表一个类的定义。
Functions:方法声明。
Modules:模块,可以理解为一个Node.js模块。
Program:程序,整个代码可以称为Program。
//除此之外还有很多的类型,这里就不过多阐述了
AST技术一般在前端开发中使用非常多,但我们也可以使用AST对Js代码进行转换和改写,包括还原混淆后的Js代码,例如控制流平坦化,对象键名替换等常见的混淆手段都可以通过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
如下
(上面那两条警告并不是报错,只是提示有新版本而已,不用管)
初始化之后目录如下
接下来需要在当前目录创建一个.babelsrc文件,内容如下
到这里我们的环境准备就完成了,接下来就是AST几个常见模块的使用了
二、babel-parser
@babel/parser是Babel中的Javascript解析器,也是一个Node.js包,就是用于将Js代码转换为AST。
parser包提供了一个重要的方法,就是parse和parseExpression方法,前者支持解析一段)avaScript代码,后者则是尝试解析单个JavaScript表达式并考虑了性能问题。一般来说,我们直接使用parse方法就够了。
对于parse方法来说,输入和输出如下。
输入:一段Js代码
输出:该段Js代码对应的抽象语法树,也就是AST
例如现在有一段简单的Js代码如下,我们将其保存为code1.js文件
const a = 3;
let string = "Hello";
for (let i =0; i < a; i++){
string = "world";
}
console.log("string",stirng)
我们想用parser包将其解析为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);
很简单吧,其实就是使用了parser包提供的parse()方法,就可以将js源代码转换为AST抽象语法树
在命令行中运行该Js文件,命令如下
babel-node parser_demo1.js
运行结果如下
可以看到已经成功把code1.js的内容解析为AST了,这个AST的内容上面有提到过了,这里就不多赘述了,感兴趣的师傅们可以自己再看看
三、babel-generate
上面的parser包是将Js代码转换为AST,而generate则是可以将AST转换为Javascript代码
这里我们尝试将上面代码中parser解析的AST对象重新转换为Js代码
实现代码如下
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);
运行之后结果如下
可以看到我们通过generate函数处理Js代码解析过后得到的AST对象后,重新通过这个AST对象得到了对应的Js源代码
四、babel-traverse
前面我们了解了AST的解析,输入任意一段avaScript代码,我们便可以分析出其AST,也可以通过AST还原出对应的Js代码,但是我们还并不能实现JavaScript代码的反混淆。下面我们还需要进一步了解另一个强大的功能,就是AST的遍历和修改。
遍历我们使用的@babel/traverse,它可以接收一个AST利用tracerse方法就可以遍历其中的所有节点。在遍历时,我们便可以对每一个节点进行对应的操作了。
下面我们来看这样一段代码
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对象后,使用了traverse函数来遍历整个AST语法树,每次遍历时打印当前遍历到的内容
这里的enter代表“进入”这个事件,即每次遍历到一个path,都会有一个“进入”的行为,简单理解也就是每次遍历都会触发一个“enter”,
运行结果如下
可以看到内容比较复杂。首先我们可以看到它的类型是NodePath,拥有parent、container、node、scope、type等多个属性。比如node属性是一个Node类型的对象,他代表当前正在遍历的节点。
所以我们可以利用path.node拿到当前对应的Node对象,也可以利用path.parent拿到当前Node对象的父节点。
我们可以对代码作出如下修改
-
-
console.log(path);
//将打印nodepath改为打印node
console.log(path.node)
这样运行之后就只会输出AST中遍历到的每个node对象,如下
可以看到打印出了许多个node节点
上面我们也讲了,利用traverse函数对AST对象进行遍历时,我们也可以在这个过程中进行操作,简单来说就是当遍历到符合我们条件的node时,对当前的node进行一定的操作,实现起来也很简单,跟师傅们以前写过的代码中的循环类似
这里我们还是用这个Js源代码来演示
const a = 3;
let string = "Hello";
for (let i = 0; i < a; i++) {
string = "world";
}
console.log("string", string);
例如我们想要通过修改AST的方式将上述代码的a变量的值和string变量的值修改为我们指定的值,也就是变成如下内容
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代码解析为ast
let 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);
这段代码在每次遍历时,提取了对应的node对象并进行判断,例如第一个判断,就是进行了如下判断
判断当前node的类型是否是数字字面量,
当前node的值是否等于3
如果这两个条件都满足,就说明遍历到了a的值所在的node,也就是我们想要操作的node,也就是下图这个node
此时代码再将node的value值赋值为5,这样就实现了一次AST对象的修改,第二个判断也是同理,这里就不过多阐述了
最后再将修改过后的AST对象转换为Js源代码,这样就实现了通过AST技术对Js代码的修改操作
运行结果如下
可以看到a和string的值,都已经被成功修改了,这样我们就实现了一次修改AST节点内容来对Js代码进行修改的操作
(注意,这里不要理解为AST修改的是源文件,源文件是没有任何变化的,因为Js源代码解析为AST,我们修改AST,再转换为Js源代码并输入,整个过程中源代码只是用来解析为第一个AST对象的,我们并没有将修改后的Js代码赋值到源文件,也就是源文件是不变的)
另外,除了定义enter方法外,我们还可以直接定义对应特定类型的解析方法,这样遇到此类型的节点时,该方法就会被自动调用
简单来说就是enter是每次遍历都会触发,除此之外还有许多解析方法,是遍历到特定类型的node时才会被触发并执行内部代码
例如NumericLiteral和StringLiteral用法,我们尝试用这两个来重写上方修改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为数字字面量或者字符字面量时,就触发并执行内部代码,运行结果如下
可以看到也能实现对Js源代码的修改,效果是一样的
除此之外还有很多玩法,比如删除某个node等
这里我们演示一下删除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);
运行结果如下
可以看到最后一行的console.log操作代码已经被删除了,这个其实更简单,只需要在遍历到对应node时调用remove方法即可
上面和大家演示了简单的替换和删除,那如果想要插入一个节点,应该怎么做呢?那就需要用到我们下面讲的types了
五、babel-types
@babel/types是一个Node.js包,里面定义了各种各样的对象,我们可以方便地使用types声明一个新的节点,也就是对应的代码插入操作。
例如我们现在有这样一个代码
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<t.variableDeclarator>,即variableDeclarator对象组成的数组
let declaration = types.variableDeclaration("const", [declarator]);
// 插入到path节点之后
path.insertAfter(declaration);
// 停止遍历
path.stop();
},
});
const { code: output } = generate(ast, {
retainLines: true,
});
console.log(output);
代码简单来说就是从内到外构造节点结构,首先利用binaryExpression构造一个如下的二元表达式
"nn" + a
再用这个二元表达式创建一个变量声明,变量名为daylight,也就变成了
daylight = "nn" + a
再用variableDeclaration声明定义const,也就是形成了我们要插入的完整内容
const daylight = "nn" + a
最后再利用insertAfter将代码插入到AST指定位置,然后停止遍历,这就是上述利用AST技术在js代码中插入新代码的整个过程
最后运行,结果如下
可以看到成功利用AST技术插入了新的Js代码
六、小结
本篇文章带师傅们了解了什么是AST技术,以及AST的几个简单使用示例,包括AST的解析,遍历以及利用AST技术对Js源代码的增删改操作(建议师傅们自己运行一下代码试试),让师傅们对AST技术有了一个大致的认识,也为后面利用AST对加密过后的Js源代码进行解混淆做一个铺垫
一起期待下篇的内容吧,最后感谢一下师傅们这么久以来的支持,文章有什么不对的地方欢迎指出或者私信讨论哈