博客配套代码发布于github:半自动化cookie更新(欢迎顺手Star一下⭐)
相关逆向知识:
[逆向知识] AST抽象语法树:混淆与反混淆的逻辑互换(二)-CSDN博客
相关爬虫专栏:JS逆向爬虫实战 爬虫知识点合集 爬虫实战案例 逆向知识点合集
前言:
AST作为逆向知识中一个非常重要的知识点,在面对难度稍大的反爬时必须知道其概念与处理的方式。本文将全面讲解AST抽象语法树的相关知识以及处理方法,先对其有个相对清晰的认知。
而在AST(二)的下一篇文章中,我们将进一步插入大量图片与实际演示代码辅助理解,尽可能帮助读者掌握混淆代码的应对方式。
一、 什么是抽象语法树(AST)?
在我们深入探讨混淆技术之前,必须先理解编译器(或解释器)是如何“阅读”我们的代码的。当你写下一行代码时,例如:
var result = 10 + (20 / 2);
计算机并不能直接理解这个字符串。它需要经过几个阶段的处理:
-
词法分析: 将代码字符串分解成一个个有意义的最小单元,称为“Token”。例如,
var
,result
,=
,10
,+
,(
,20
,/
,2
,)
,;
。 -
语法分析: 根据编程语言的语法规则,将这些Token组合成一个树形结构,这个结构就是AST。
AST精确地描绘了代码的语法结构。对于上面那行代码,其AST(简化后)长这个样子:
- VariableDeclaration (声明一个变量)- id: Identifier { name: 'result' } (变量名)- init: BinaryExpression (初始值为一个二元表达式)- operator: '+' (运算符)- left: Literal { value: 10 } (左操作数)- right: BinaryExpression (右操作数是另一个二元表达式)- operator: '/'- left: Literal { value: 20 }- right: Literal { value: 2 }
核心要点:AST抛弃了代码中的空格、注释和括号等非结构性元素,只保留了代码的骨架和逻辑。这使得我们可以通过程序来分析、修改和生成代码,而这正是混淆与反混淆的技术基石。
二、 利用AST进行代码混淆:把清晰变模糊
代码混淆的目的是在不改变代码执行结果的前提下,使其逻辑变得难以阅读和分析。有了AST,我们不再需要用复杂的正则表达式去替换字符串,而是可以直接操作代码的“骨架”。
以下是几种常见的基于AST的混淆技术:
-
标识符重命名
-
逻辑:遍历AST中的所有 Identifier(标识符) 节点,将有意义的变量名(如
result
、userName
)替换成无意义的短字符(如_0x1a2b
、_0x5c8f
)。 -
AST操作:找到所有定义变量/函数的作用域,记录下其中的标识符,然后将它们和所有引用到它们的地方统一重命名。
-
示例:
// 原始代码 function calculateSum(a, b) {var total = a + b;return total; }// 混淆后代码 function _0xabc1(a, b) {var _0xdef2 = a + b;return _0xdef2; }
-
-
常量替换与表达式转换
-
逻辑:将代码中的常量(如数字、字符串)替换成一个等价的、但更复杂的表达式。
-
AST操作:找到 Literal(字面量) 节点,并将其替换为一个 BinaryExpression(二元表达式) 或其他更复杂的结构。
-
示例:
// 原始代码 var key = "secret"; var value = 1000;// 混淆后代码 var key = "\x73\x65\x63\x72\x65\x74"; // 字符串拆分为十六进制表示 var value = 500 + 500; // 数字拆分为表达式
-
-
控制流平坦化
-
逻辑:将代码块(如
if/else
、for
循环)打散,放进一个巨大的switch
语句中,由一个状态变量来控制执行顺序。这使得代码的执行流程不再是线性的,而是跳跃式的,极大地干扰了静态分析。 -
AST操作:识别出代码中的基本逻辑块,将它们包裹成
case
语句。然后用一个while
循环和switch
语句替换掉原始的控制流结构(如IfStatement
、ForStatement
等)。 -
示例:
// 原始代码 function checkAccess(level) {let message;if (level > 5) {message = "Allowed";} else {message = "Denied";}return message; }// 混淆后代码 (简化版) function checkAccess(level) {let message;var state = '1|0|2'.split('|');var i = 0;while (true) {switch (state[i++]) {case '0':message = "Denied";continue;case '1':if (level > 5) {// 如果满足条件,下一个状态是'2'state[i-1] = '2';} else {// 否则,下一个状态是'0'state[i-1] = '0';}continue;case '2':message = "Allowed";continue;}return message;} }
-
-
僵尸代码注入 (Dead Code Injection)
-
逻辑:在代码中插入一些永远不会执行或不影响最终结果的“垃圾”代码。
-
AST操作:在AST的任意合法位置(如
BlockStatement
中)插入新的、无用的节点,例如一个永远为false
的if
语句块。 -
示例:
// 原始代码 function getResult(a) {return a * 10; }// 混淆后代码 function getResult(a) {// 注入一个永远不会执行的if分支if ("" === "abc") {var x = 1 + 2;console.log(x);return x - 3;} else {// 原始逻辑被放在这里return a * 10;} }
-
三、 利用AST进行代码反混淆:从模糊到清晰的实践
反混淆的本质,就是混淆的逆向过程。既然混淆是通过修改AST来增加复杂性,那么反混淆就是通过修改AST来消除这些复杂性。
二者的工具和原理是完全一致的:解析 (Parse) -> 遍历 (Traverse) -> 修改 (Modify) -> 生成 (Generate)。下面我们将结合实际操作,看看如何处理常见的混淆技术。
1. 表达式化简与常量折叠
这是反混淆最基础、最有效的步骤之一。
-
处理思路:遍历AST,找到所有能立即计算出结果的表达式,如
500 + 500
、"a" + "b"
,以及像"\x73\x65\x63\x72\x65\x74"
这样的十六进制字符串。 -
AST操作:
-
遍历:使用工具(如 JavaScript 的
babel/traverse
)遍历 AST,寻找 BinaryExpression(二元表达式)和 StringLiteral(字符串字面量)节点。 -
识别:如果一个
BinaryExpression
的左右操作数都是Literal
(常量),或者一个StringLiteral
包含转义字符,就可以进行处理。 -
修改:在遍历函数中,执行表达式计算(例如
eval()
或手动实现计算逻辑),然后用一个新的Literal
节点替换掉整个表达式节点。
-
-
示例:
// 待反混淆代码 var value = 500 + 500; var key = "\x73\x65\x63\x72\x65\x74";// 反混淆后代码 var value = 1000; var key = "secret";
2. 字符串解密
许多混淆器会将所有字符串加密,并用一个解密函数在运行时还原。
-
处理思路:找到这个解密函数及其调用点,在静态分析时提前执行它,将加密的字符串还原为明文。
-
AST操作:
-
识别:首先,需要识别出解密函数的定义。它通常是一个接收加密字符串作为参数并返回明文字符串的函数。
-
定位:遍历AST,找到所有对这个解密函数的CallExpression(函数调用)节点。
-
沙箱执行:创建一个安全的沙箱环境(例如,使用 Node.js 的
vm
模块),将解密函数的代码和它的参数传入,执行后获取返回值。 -
修改:用一个新的
StringLiteral
节点替换掉整个函数调用节点。
-
-
示例:
// 待反混淆代码 function _decrypt(str) {// ...复杂的解密逻辑return decodedStr; } var url = _decrypt("加密的字符串1"); var msg = _decrypt("加密的字符串2");// 反混淆后代码 // (解密函数可以被移除,或者保持原样) var url = "http://example.com"; var msg = "Hello, World!";
3. 控制流反平坦化
这是反混淆中最具挑战性的任务,需要复杂的静态分析。
-
处理思路:分析
while-switch
结构中的状态变量和case
之间的跳转关系,从而重建出原始的if/else
和循环结构。这通常需要模拟程序执行,跟踪状态变量的值。 -
AST操作:
-
识别:找到包含
while(true)
和switch
语句的结构。识别出状态变量(控制switch
的变量)和分发函数(通常是一个数组,例如_0xabc.split('|')
)。 -
分析:通过污点分析或符号执行等技术,跟踪状态变量的流向。记录每个
case
代码块以及它们是如何通过continue
或变量赋值跳转到下一个case
的。 -
重建:根据分析结果,将这些
case
块重新组织成更高级的结构。例如,如果case '1'
后的下一个状态取决于一个if
条件,那么就可以将这两个case
重新组合成一个IfStatement
。这个过程通常非常复杂,需要自定义的分析算法。
-
四、 混淆与反混淆的逻辑互换:同一场游戏,不同方向
现在,我们可以清晰地看到这两者之间的对称关系:
混淆操作 (增加复杂度) | 反混淆操作 (降低复杂度) | |
AST层面 | 常量 -> 复杂表达式 替换 | 复杂表达式 -> 常量 替换 |
有意义变量名 -> 无意义符号 修改 | 作用域分析与重命名 修改 | |
线性控制流 -> while-switch平坦化 替换 | while-switch -> 还原高级结构 重新组合 | |
插入无用代码块 插入或删除特定的AST节点 | 移除无用或不可达代码块 删除不可达的AST节点 |
结论就是:无论是混淆还是反混淆,其核心都是在 AST 这个层面上,对代码的结构进行程序化的、大规模的增熵(使其更混乱)或减熵(使其更有序)操作。
五、小结
AST为我们提供了一个上帝视角来审视和操作代码。它不仅仅是编译器工作的中间产物,更是代码自动化处理的利器。对于软件开发者而言,了解AST可以帮助你编写更强大的代码转换工具(如 Babel 插件、代码格式化工具)。而对于安全研究人员来说,掌握基于AST的分析技术,则是深入理解、破解和防御复杂代码混淆攻击的必备技能。
在下篇文章[逆向知识] AST抽象语法树:混淆与反混淆的逻辑互换(二)-CSDN博客中,我们将进一步,了解如何实际运用各种工具来真正理解并借助AST破解掉混淆的代码。