博客标题:AST反混淆:提升JavaScript代码的可读性与调试便利性
引言
JavaScript代码混淆是一种常见的保护源码的方法,但这也给代码的维护和调试带来了不小的挑战。抽象语法树(AST)提供了一种结构化的方式来分析和转换代码。本文将通过一个具体的案例,展示如何使用AST进行代码反混淆的具体流程。
AST基础知识
AST是源代码的树状结构表示,每个节点代表了代码中的一个构成元素,如表达式、语句等。使用AST,我们可以方便地对代码进行遍历和变换。
准备工作
首先,我们需要安装@babel/parser
、@babel/traverse
、@babel/generator
和@babel/types
这几个npm包,它们将帮助我们解析代码、遍历AST、生成新的代码以及操作AST节点。
bash
npm install @babel/parser @babel/traverse @babel/generator @babel/types
AST反混淆的目标
AST反混淆的目的是为了使代码更加易于阅读和调试。具体目标包括:
- 代码可读性:将混淆后的代码转换为清晰、易于理解的形式。
- 调试便利性:简化调试过程,使开发者能够快速定位问题js。
AST反混淆的代码分析步骤
1 判断混淆的类型,找到混淆的位置
是字符串编码的混淆、赋值操作混淆还是表达式混淆等
任何技术都是有限的,没有统一的解混淆的办法,只有具体问题具体分析。查看js代码的语法树,分析混淆的类型就和写Xpath解析找父节点还是兄弟节点一样。
2 明确解混淆后的js代码
AST解析网址
https://astexplorer.net/
AST反混淆的代码实现步骤
1. 读取JS文件并转化为AST
首先,我们需要读取混淆后的JavaScript文件,并使用@babel/parser
将其转换为AST语法树。这一步是整个反混淆过程的基础。
javascript
let jsCode = fs.readFileSync('path/to/obfuscated.js', 'utf-8');
let ast = parser.parse(jsCode);
2. 遍历AST并进行增删改查
接下来,我们使用@babel/traverse
遍历AST语法树。在遍历过程中,我们可以对节点进行增加、删除、修改等操作,以实现代码的反混淆,这里每种混淆都不一样,具体问题具体分析,每个人思路也不完全一样。
- 增加:为代码添加注释或辅助信息,提高可读性。
- 删除:移除无用的代码或混淆的部分。
- 修改:将混淆的变量名、属性名等替换为更易读的形式。
- 查询:分析代码结构,为后续的修改提供依据。
3. 将AST重新转化为JS代码
最后,我们使用@babel/generator
将修改后的AST语法树重新转换为JavaScript代码。这样,我们就得到了可读性更强、更方便调试的代码。
javascript
const fs = require('fs');
let result = generator(ast).code;
fs.writeFileSync('path/to/demangled.js', result, 'utf-8');
案例1 表达式混淆
我们有一段混淆后的JavaScript代码,其目的是将几个字符串拼接赋值给变量f
,然后通过f
访问全局对象G
的一个属性。我们的任务是使用AST来反混淆这段代码,使其更加易于阅读和调试。
具体解混淆流程
步骤1:读取并解析代码为AST
首先,我们读取混淆后的JavaScript代码,并使用@babel/parser
将其转换为AST语法树。
javascript
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generator = require('@babel/generator').default;
const types = require('@babel/types');
const fs = require('fs');
let jsCode = `f = "cdc_adoQpoasnfa76pfcZLmcf";
f += "cd";
f += "qwe";
f += "qwe";
f += "qad";
f += "\u0065\u006e\u0075\u006d\u0065\u0072\u0061\u0062\u006c\u0065";
C = G[f += "_Array"];`; // 混淆后的JavaScript代码
let ast = parser.parse(jsCode);
步骤2:定义变换逻辑
接着,我们定义变换逻辑,使用@babel/traverse
遍历AST。我们将关注两个节点类型:MemberExpression
和AssignmentExpression
。
- MemberExpression :处理成员表达式,例如
G[f]
。 - AssignmentExpression :处理赋值表达式,例如
f += "cd"
。
步骤3:处理MemberExpression(成员函数对象)
我们遍历AST,寻找MemberExpression
节点。如果属性名是_Array
,我们将这个属性名追加到前一个表达式的值上,并移除当前的MemberExpression
。
javascript
traverse(ast, {
'MemberExpression'(path){
let {object, property} = path.node;
let {left, operator, right} = property;
let prevExp = path.parentPath.parentPath.getPrevSibling().node;
prevExp.expression.right.value += right.value;
path.node.property = left;
}
})
步骤4:处理AssignmentExpression(赋值操作对象)
然后,我们继续遍历AST,寻找AssignmentExpression
节点。如果右侧是字符串字面量,并且操作符是+=
,我们将这个字符串追加到前一个赋值表达式的值上,并移除当前的赋值表达式。
javascript
traverse(ast, {
'AssignmentExpression'(path){
let {left, operator, right} = path.node;
if (!types.isStringLiteral(right) || operator != '+='){
return
}
let value = right.value;
let prevExp = path.parentPath.getPrevSibling().node;
prevExp.expression.right.value += right.value;
path.remove()
}
})//移除当前的赋值表达式。
步骤5:生成新的代码
最后,我们使用@babel/generator
将修改后的AST语法树重新转换为JavaScript代码。
javascript
let result = generator(ast).code;
console.log(result);
示例二:属性名解码
代码分析
这段代码中的对象属性名被编码,我们的目标是将其解码为可读的形式。
javascript
// ast反混淆目的为了代码可读性更强,更方便调试。
//
// 1. 读取js文件,使用parser.parse转化为ast语法树
//
// 2. 遍历ast,增删改查ast语法树 path.node
//
// 3. 重新把ast转化为js代码。
const types = require('@babel/types');
const parser = require('@babel/parser');
const generator = require('@babel/generator').default;
const traverse = require('@babel/traverse').default;
let jsCode = `
a = {
"\u0065\u006e\u0075\u006d\u0065\u0072\u0061\u0062\u006c\u0065": !0,
"\u0067\u0065\u0064": n
}
`;
let ast = parser.parse(jsCode);
// 遍历AST,解码属性名
traverse(ast, {
'StringLiteral': function(path) {
let decoded = decodeURIComponent(path.node.value);
path.node.value = decoded;
}
});
// 打印反混淆后的代码
console.log(generator(ast).code);
为什么是StringLiteral,因为这里混淆的类型type是Literal
知识点总结
在JavaScript的抽象语法树(AST)中,不同的节点类型代表了代码中的各种语法结构。
-
MemberExpression:
- 表示对对象成员的访问,包括属性和方法。例如,在表达式
obj.prop
或obj.method()
中,obj.prop
和obj.method()
都是 MemberExpression 节点。
- 表示对对象成员的访问,包括属性和方法。例如,在表达式
-
AssignmentExpression:
- 表示赋值操作。这是基本的赋值(如
x = y
),复合赋值(如x += y
),或扩展赋值(如x ??= y
)的语法结构。在AssignmentExpression
节点中,赋值运算符连接了左侧的变量(或解构模式)和右侧的值或表达式。
- 表示赋值操作。这是基本的赋值(如
-
StringLiteral:
- 表示字符串字面量。这是用双引号
"
或单引号'
包围的文本,例如"hello"
或'world'
。在AST中,StringLiteral
节点包含了字符串的值。
- 表示字符串字面量。这是用双引号
除了这些,还有一些常用的AST节点类型:
- Identifier :表示一个变量名或标识符,如
x
、y
。 - BinaryExpression :表示二元表达式,它包含两个操作数和一个运算符,如
x + y
或x > y
。 - CallExpression :表示函数或方法的调用,如
func()
或obj.method()
。 - FunctionDeclaration / FunctionExpression :两者都表示函数,区别在于
FunctionDeclaration
是通过function
关键字声明的,而FunctionExpression
是通过赋值给变量的函数表达式。 - ReturnStatement :表示
return
语句,用于从函数中返回一个值。 - IfStatement :表示
if
条件语句。 - ForStatement / WhileStatement :表示循环结构,如
for
、while
循环。
理解这些AST节点类型对于进行代码分析、转换或生成等操作非常重要,特别是在使用编译器工具如Babel时。
结语
通过上述步骤,我们成功地将一段混淆的JavaScript代码反混淆,使其更加易于阅读和调试。掌握这项技能对于前端和逆向开发者来说非常有价值。AST为我们提供了一种强大的方式来分析和转换代码,这不仅有助于提高代码质量,还能在开发过程中节省大量的时间和精力。掌握AST的使用,对于前端开发者来说是一项宝贵的技能。