AST 简述

news/2024/9/20 10:25:47

AST 是源代码的抽象语法结构的树状表示。利用它可以还原混淆后的js代码。
@babel/parser 是js语法编译器 Babel 的 nodejs 包,内置很多分析 js 的方法,可以实现js到AST的转换。
JS 转为 AST:https://astexplorer.net/

准备工作:

需安装nodejs环境以及babel,babel 安装:
npm install @babel/node @babel/core @babel/cli @babel/preset-env
新建目录 AST,其下新建文件 .babelrc,内容如下:

{"presets": ["@babel/preset-env"]
}

这样就完成了初始化。

节点类型

打开 https://astexplorer.net/
Parser Settings选择@babel/parser, 然后在左侧任意输入一段js,右侧会展示对应的AST,其是由一层层的数据结构嵌套构成,每一个含有type属性的内容都可以视为该类型的一个节点,常见的节点类型如下:

Literal 字面量,简单的文字表示,如3,abc,null,true 等。它进一步分为 RegExpLiteral、NullLiteral、StringLiteral、BooleanLiteral、NumericLiteral、BigIntLiteral 等类型;
Declarations 声明,如 FunctionDeclaration、VariableDeclaration 分别表示声明一个方法和变量;
Expressions 表达式,它本身会返回一个计算结果,通常有两个作用,一个是放在赋值语句的右边赋值,另一个是作为方法的参数,如 LogicalExpression、ConditionalExpression、ArrayExpression 分别表示逻辑运算表达式、三元运算表达式、数组表达式;此外,还有一些特殊的表达式,如YieldExpression、AwaitExpression、ThisExpression;
Statements 语句,如 IfStatements、SwitchStatements、BreakStatement 等控制语句,和一些特殊语句 DebuggerStatement、BlockStatements等;
Identifier 标识符,指代一些变量的名称,如 name
Classes 类,代表一个类的定义,包括 Class、ClassBody、ClassMethod、ClassProperty等
Functions 方法声明,一般代表 FunctionDeclaration、FunctionExpression 等
Modules 模块,可以理解为一个 nodejs 模块,包括 ModuleDeclaration、ModuleSpecifier 等
Program 程序,整个代码可以成为 Program

@babel/parser 的使用

它是 Babel 的js解释器,也是一个nodejs包,提供一些重要的方法,parse 解析js代码,parseExpression 尝试解析单个js表达式并考虑性能。一般使用parse就足够了。
parse 输入:一段js代码;输出:该js代码对应的抽象语法树AST
js代码包含多种类型的表达,归类如下:
https://github.com/babel/babel/blob/master/packages/babel-parser/ast/spec.md

接下来,使用一下parse,首先在目录AST下新建一个文件夹code,新建文件 code1.js ,内容如下:

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

简单写了一段js代码,然后同目录下新建文件 basic1.js,内容如下:

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

使用parse将代码转为ast抽象语法树,命令行输入 babel-node basic1.js 运行,输出如下:

Node {type: 'File',start: 0,end: 128,loc: SourceLocation {start: Position { line: 1, column: 0, index: 0 },end: Position { line: 8, column: 0, index: 128 },filename: undefined,identifierName: undefined},errors: [],program: Node {type: 'Program',start: 0,end: 128,loc: SourceLocation {start: [Position],end: [Position],filename: undefined,identifierName: undefined},sourceType: 'script',interpreter: null,body: [ [Node], [Node], [Node], [Node] ],directives: []},comments: []
}

可以看到,整个AST的根节点就是一个Node,type是File,代表其是一个 File 类型的节点;其下有很多属性 start、end 等等,其中的 progarm 也是一个 Node,type为Program,代表其是一个程序。同样,program 也包含一些属性,其中 body 是比较重要的属性,这里是一个列表类型,其中每个元素也都是一个Node,只不过输出结果没有详细展示了。
可以通过 console.log(ast.program.body) 详细打印Node的内容。
js转为ast后,如何转换回来呢,可以使用generate方法。

@babel/generate 的使用

它也是一个nodejs包,提供了 generate 方法将 AST 还原为 js 代码

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

同样命令行键入 babel-node basic1.js 运行,输出如下:

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

generate 方法还可以传入第二个参数,接收一些配置选项,第三个参数接收原代码作为输出的参考,用法:

const output = generate(ast, { /* options */ }, code);

options 可选部分配置:

auxiliaryCommentBefore string类型,在输出文件开头添加注释可选字符串;
auxiliaryCommentAfter string类型,在输出文件末尾添加注释可选字符串;
retainLines boolean类型,默认false,尝试在输出代码中使用与源代码相同的行号;
retainFunctionParens boolean类型,默认false,保留表达式周围的括号;
comments boolean类型,默认true,输出中是否应包含注释;
compact boolean或auto类型,默认opts.minfied,设置为true以避免添加空格进行格式化;
minified boolean类型,默认false,是否压缩后输出;

@babel/traverse 的使用

知道了如何在 js 和 AST 间转换,还是不能实现 js 代码的反混淆,还需要了解另一个强大的功能,AST的遍历和修改。
遍历的使用的是 @babel/traverse,它接收一个 AST,利用 traverse 方法就可以遍历其中的所有节点。在遍历方法中,就可以对所有节点操作了。
先感受下遍历的基本实现,新建 basic2.js:

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

命令行输入 babel-node basic2.js 运行,结果很长,会输出每一个path对象,取其中一部分如下:

NodePath {contexts: [ [TraversalContext] ],state: undefined,opts: { enter: [Array], _exploded: true, _verified: true },_traverseFlags: 0,skipKeys: null,parentPath: NodePath {contexts: [Array],state: undefined,opts: [Object],_traverseFlags: 0,skipKeys: null,parentPath: [NodePath],container: [Array],listKey: 'body',key: 3,node: [Node],type: 'ExpressionStatement',parent: [Node],hub: undefined,data: null,context: [TraversalContext],scope: [Scope]},container: Node {type: 'ExpressionStatement',start: 95,end: 124,loc: [SourceLocation],expression: [Node]},listKey: undefined,key: 'expression',node: Node {type: 'CallExpression',start: 95,end: 124,loc: [SourceLocation],callee: [Node],arguments: [Array]},type: 'CallExpression',parent: Node {type: 'ExpressionStatement',start: 95,end: 124,loc: [SourceLocation],expression: [Node]},hub: undefined,data: null,context: TraversalContext {queue: [Array],priorityQueue: [],parentPath: [NodePath],scope: [Scope],state: undefined,opts: [Object]},scope: Scope {uid: 0,path: [NodePath],block: [Node],labels: Map(0) {},inited: true,bindings: [Object: null prototype],references: [Object: null prototype],globals: [Object: null prototype],uids: [Object: null prototype] {},data: [Object: null prototype] {},crawling: false}}

可以看到,这是一个 NodePath 类型的节点,里面还有 node、parent 等多个属性,我们可以利用 path.node 拿到当前对应的Node对象,也可以利用 path.parent 拿到当前Node对象的父节点。
这样,就可以使用它来对Node进行一些处理,如把最初的代码修改为 a=5, string = "hi",可以这样:

import { parse } from "@babel/parser";
import generate from "@babel/generator"
import fs from 'fs';
import traverse from "@babel/traverse";const code = fs.readFileSync('./code1.js', 'utf-8');
let ast = parse(code);
traverse(ast, {enter(path) {let node = path.node;if (node.type === "NumericLiteral" && node.value === 3) {node.value = 5;}if (node.type === "StringLiteral" && node.value === "hello") {node.value = "hi";}},
});const { code: output } = generate(ast, {retainLines: true,
});
console.log(output)
// 输出如下
const a = 5;
let string = "hi";
for (let i = 0; i < a; i++) {string += 'world';
}
console.log('string', string);

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

traverse(ast, {NumericLiteral(path) {if (path.node.value === 3) {path.node.value = 5;}},StringLiteral(path) {if (path.node.value === 'hello') {path.node.value = 'hi';}}
})

traverse部分改成如上内容,输出是一样的。

还可以通过 remove 方法删除某个节点,如:

traverse(ast, {CallExpression(path) {let node = path.node;if (node.callee.object.name === 'console' && node.callee.property.name === 'log') {path.remove();}},
});

这样就删除了所有console.log语句。
如果想插入节点,就要用到 types 了。

@babel/types 的使用

使用它可以方便的声明新的节点,比如 const a = 1; 如果想增加一行 const b = a + 1; 可以这么写:

import { parse } from "@babel/parser";
import generate from "@babel/generator";
import * as types from "@babel/types";
import traverse from "@babel/traverse";const code = 'const a = 1;';
let ast = parse(code);traverse(ast, {VariableDeclaration(path) {let init = types.binaryExpression('+',types.identifier('a'),types.numericLiteral(1));let declarator = types.variableDeclarator(types.identifier('b'), init);let declaration = types.variableDeclaration("const", [declarator]);path.insertAfter(declaration);path.stop();},
})const { code: output } = generate(ast, {retainLines: true,
});
console.log(output)
# 输出为 const a = 1;const b = a + 1;

至于为什么这么写,可以结合js转为 AST 的内容,配合官方文档 (https://astexplorer.net/ 右上角点击 Parser: @babel/parser-.**.),来构造节点。
本例中首先把 const b = a + 1; 转为 AST 查看对应内容:

可以看到,这个语句转为 AST 后是一个 type 为 VariableDeclaration 的节点,查看官方文档对应内如如下,

想构造一个 type 为 VariableDeclaration 的节点,需使用 types 的 variableDeclaration 方法,传入的第一个参数为声明的关键词,第二个为 Array<VariableDeclarator> 类型的节点,对比 AST 可以看到

第一个传入的是const,第二个传入的是一个 type 为 VariableDeclarator 的节点构成的列表,当然本例中这个列表只有一个元素。
所以,构造一个 type 为 VariableDeclaration 的节点,代码大致是这样的:let declaration = types.variableDeclaration("const", [VariableDeclarator]);
VariableDeclarator 类型的节点又该如何构造呢?继续查询官方文档:

可以看到,需要传入一个id和一个init,对比 AST 可以知道需要传入的具体内容,id 是一个Identifier类型的节点,其name是 b,init 是一个BinaryExpression类型的节点,这两个节点如何构造,继续查阅官方文档,这里不再赘述,最终实现的结果就如上方代码。

了解了这些知识,来看几个简单的反混淆 js 的例子。
1. 表达式还原
原始代码:

const a = !![];
const b = "abc" === "bcd"
const c = (1 << 3) | 2
const d = parseInt('5' + '0')

还原代码:

import { parse } from "@babel/parser";
import generate from "@babel/generator"
import fs from 'fs';
import traverse from "@babel/traverse";
import * as types from "@babel/types";const code = fs.readFileSync('./code1.js', 'utf-8');
let ast = parse(code);traverse(ast, {// 键名分别对应于处理 一元表达式、布尔表达式、条件表达式、调用表达式"UnaryExpression|BinaryExpression|ConditionalExpression|CallExpression"(path) {// evaluate 方法会对path对象进行计算(执行),得到可信度和结果,confident = truelet { confident, value } = path.evaluate();// 如果是标识符或NaN,就跳过if (value === Infinity || value === -Infinity || isNaN(value)) return;// 如果可信,即 confident 为 true,就替换 evaluate(计算)得到的值confident && path.replaceWith(types.valueToNode(value));},
});const { code: output } = generate(ast);
console.log(output)
// 输出如下
const a = true;
const b = false;
const c = 10;
const d = 50;

2. 字符串还原
有一些字符会被混淆为 Unicode 或 UTF-8 编码,如

const strings = ["\x68\x65\x6c\x6c\x6f", "\x77\x6f\x72\x6c\x64"]

把此行代码放入 AST Explore 查看对应的 AST,部分如下:

可以看到,extra下显示了编码字符及对应的原始值,只需要把 raw 的内容修改为 rawValue 的内容即可,代码如下:

import { parse } from "@babel/parser";
import generate from "@babel/generator"
import fs from 'fs';
import traverse from "@babel/traverse";
import * as types from "@babel/types";const code = fs.readFileSync('./code1.js', 'utf-8');
let ast = parse(code);traverse(ast, {StringLiteral({ node }) {if (node.extra && /\\[ux]/gi.test(node.extra.raw)) {node.extra.raw = node.extra.rawValue;}}
})const { code: output } = generate(ast);
console.log(output)
// 输出如下:
const strings = [hello, world];

3. 无用代码剔除

const _0x16c18d = function () {if (!![[]]) {console.log('hello world');} else {console.log('this');console.log('is');console.log('dead');console.log('code');}
};
const _0x1f7292 = function () {if ("xmv2nOdfy2N".charAt(4) !== String.fromCharCode(110)) {console.log('this');console.log('is');console.log('dead');console.log('code');} else {console.log('nice to meet you')}
};
_0x16c18d();
_0x1f7292();

这段代码只是打印了两行内容,多了很多无效代码,将其转为 AST,部分如下:

这是第一个if语句转换为的 AST,test 为 if 语句的判断条件,consequent 是 if 语句块内的代码;alternate 是 else 语句块内的代码;
据此,删除无用代码的代码如下:

import { parse } from "@babel/parser";
import generate from "@babel/generator"
import fs from 'fs';
import traverse from "@babel/traverse";
import * as types from "@babel/types";const code = fs.readFileSync('./code1.js', 'utf-8');
let ast = parse(code);traverse(ast, {IfStatement(path) {let { consequent, alternate } = path.node;let testPath = path.get('test');// evaluateTruthy 方法返回path对应的真值,比如第一个if条件是 !![[]],它为 true,该方法就返回trueconst evaluateTest = testPath.evaluateTruthy();if (evaluateTest === true) {// 如果if的判断条件是true,就把 if 语句块的内容节点替换原本的IfStatement节点if (types.isBlockStatement(consequent)) {consequent = consequent.body;}path.replaceWithMultiple(consequent);} else if (evaluateTest === false) {// 如果if的判断条件是false,就把 else 语句块的内容节点替换原本的IfStatement节点if (evaluateTest != null) {if (types.isBlockStatement(alternate)) {alternate = alternate.body;}path.replaceWithMultiple(alternate);} else {path.remove();}}}
})const { code: output } = generate(ast);
console.log(output)
// 输出如下
const _0x16c18d = function () {console.log('hello world');
};
const _0x1f7292 = function () {console.log('nice to meet you');
};
_0x16c18d();
_0x1f7292();

4. 反控制流平坦化
一个简单的代码如下:

const c = 0;
const a = 1;
const b = 3;

经过简单的控制流平坦化后代码如下:

const s = '3|1|2'.split('|');
let x = 0;
while (true) {switch (s[x++]) {case '1':const a = 1;continue;case '2':const b = 3;continue;case '3':const c = 0;continue;}break;
}

还原思路:
首先找到switch语句相关节点,拿到对应的节点对象,如各个case语句对应的代码区块;
分析 switch 语句的判定条件 s 变量对应的列表结果,比如将 "3|1|2".split("|") 转化为 ["3", "1", "2"];
遍历 s 变量对应的列表,将其和各个 case 匹配,顺序得到对应的结果并保存;
用上一步得到的代码替换原来的代码。

还是把上面代码转为 AST 查看,switch 部分如下:

可以看到,它是一个 SwitchStatement 节点,discriminant 就是判断条件,这个例子中对应 s[x++],cases 就是case语句的集合,对应多个 SwitchCase 节点。
可以先把可能用到的节点取到,如 discriminant、case、discriminant的 object 和 property

traverse(ast, {WhileStatement(path) {const { node, scope } = path;const { test, body } = node;let switchNode = body.body[0];let { discriminant, cases } = switchNode;let { object, property } = discriminant}
})

接下来追踪下判定条件 s[x++] ,展开 object,可以看到其 name 是 s,可以通过 scope 的 getBinding 方法获取到绑定它的节点;绑定的就是 "3|1|2".split("|") ,查看绑定的代码,可以看到是一个 CallExpression 节点,根据AST逐层拿到对应的值,然后动态调用:

let arrName = object.name;
let binding = scope.getBinding(arrName);
let { init } = binding.path.node;
object = init.callee.object;
property = init.callee.property;
let argument = init.arguments[0].value;
let arrayFlow = object.value[property.name](argument);

拿到 arrayFlow (["3", "1", "2"])后遍历它,找到对应的 case 语句对应的代码即可

let resultBody = [];
arrayFlow.forEach((index) => {let switchCase = cases.filter((c) => c.test.value === index)[0];let caseBody = switchCase.consequent;if (types.isContinueStatement(caseBody[caseBody.length - 1])) {caseBody.pop();}resultBody = resultBody.concat(caseBody);
});

最后替换即可:path.replaceWithMultiple(resultBody)

完整代码(全部代码均对照AST实现):

import { parse } from "@babel/parser";
import generate from "@babel/generator"
import fs from 'fs';
import traverse from "@babel/traverse";
import * as types from "@babel/types";const code = fs.readFileSync('./code1.js', 'utf-8');
let ast = parse(code);traverse(ast, {WhileStatement(path) {const { node, scope } = path;const { test, body } = node;let switchNode = body.body[0];let { discriminant, cases } = switchNode;let { object, property } = discriminant;let arrName = object.name;let binding = scope.getBinding(arrName);let { init } = binding.path.node;object = init.callee.object;property = init.callee.property;let argument = init.arguments[0].value;let arrayFlow = object.value[property.name](argument);let resultBody = [];arrayFlow.forEach((index) => {let switchCase = cases.filter((c) => c.test.value === index)[0];let caseBody = switchCase.consequent;if (types.isContinueStatement(caseBody[caseBody.length - 1])) {caseBody.pop();}resultBody = resultBody.concat(caseBody);});path.replaceWithMultiple(resultBody)}
})const { code: output } = generate(ast);
console.log(output)
// 输出如下:
const s = '3|1|2'.split('|');
let x = 0;
const c = 0;
const a = 1;
const b = 3;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/31908.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

arco-init 项目初始化失败!Error: spawnSync pnpm.cmd ENOENT

今天使用arco创建项目时初始化失败,失败截图: 只需要安装 npm i -g pnpm 就可以了,不需要做额外配置,之后再初始化项目arco init hello-arco

小程序框架是智能融媒体平台构建的最佳线路

2023年,以AIGC(人工智能生产内容)为代表的人工智能技术加速迭代演进,媒体融合进入媒体智能化快速发展新时代。过去5年,媒体行业一直都在进行着信息化建设向融媒体平台建设的转变。一些融媒体的建设演变总结如下:新闻终端的端侧内容矩阵建设,如App新闻端,社交平台上的官…

高效调度新篇章:详解DolphinScheduler 3.2.0生产级集群搭建

转载自tuoluzhe8521导读:通过简化复杂的任务依赖关系, DolphinScheduler为数据工程师提供了强大的工作流程管理和调度能力。在3.2.0版本中,DolphinScheduler带来了一系列新功能和改进,使其在生产环境中的稳定性和可用性得到了显著提升。 为了帮助读者更好地理解和应用这一版…

k8s-Service资源

Service资源的作用: 因为k8s是使用RC来管理保证它的高可用,RC是管理k8s pod的.如果一个pod挂掉了,就会马上自动启动一个可用的pod,那么新的pod的IP肯定就是新的。如果是采用端口映射的话,就会出现刚配置好的pod端口和ip 在pod挂了后 新的pod被启动了 新pod又是一个新的ip,…

串口通信原理

异步串行:异步说明不带时钟信号,串行说明是按位(一位=8bit),一位一位传输

ESP32-P4 --- vscode 指定用哪个 ESP-IDF

vscode 可以安装多个 ESP-IDF,如下但是只有一个 ESP-IDF 可以生效,要生效哪个,就点哪个,弹出如下表示生效成功

Delphi DX10.2安装TeeChartPro2022找不到指定文件

1、显示报错TeeChart Pro Compilation started: 2024-05-15 17:12:48 Win32 v25 Enterprise (Delphi 10.2 and C++Builder 10.2 Update 3) (C++) ERROR Tee925 This version of the product does not support command line compiling. TeeUI925 This version of the product do…

mit6.828笔记 - lab3 Part B:页面故障、断点异常和系统调用

Part B 页面故障、断点异常和系统调用 虽然说,我们故事的主线是让JOS能够加载、并运行 user/hello.c 编译出来的镜像文件。 虽然说,经过Part A最后几节,我们初步实现了异常处理的基础设施。 但是对于操作系统来说,还远远不够,比如说那个 trap_dispatch 还没完成。 所以在回…