博客配套代碼發布于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破解掉混淆的代碼。