
什么是 AST
AST(Abstract Syntax Tree),中文抽象語法樹,簡稱語法樹(Syntax Tree),是源代碼的抽象語法結構的樹狀表現形式,樹上的每個節點都表示源代碼中的一種結構,語法樹不是某一種編程語言獨有的,JavaScript、Python、Java、Golang 等幾乎所有編程語言都有語法樹,
小時候我們得到一個玩具,總喜歡把玩具拆解成一個一個小零件,然后按照我們自己的想法,把零件重新組裝起來,一個新玩具就誕生了,而 JavaScript 就像一臺精妙運作的機器,通過 AST 決議,我們也可以像童年時拆解玩具一樣,深入了解 JavaScript 這臺機器的各個零部件,然后重新按照我們自己的意愿來組裝,
AST 的用途很廣,IDE 的語法高亮、代碼檢查、格式化、壓縮、轉譯等,都需要先將代碼轉化成 AST 再進行后續的操作,ES5 和 ES6 語法差異,為了向后兼容,在實際應用中需要進行語法的轉換,也會用到 AST,AST 并不是為了逆向而生,但做逆向學會了 AST,在解混淆時可以如魚得水,
AST 有一個在線決議網站:https://astexplorer.net/ ,頂部可以選擇語言、編譯器、是否開啟轉化等,如下圖所示,區域①是源代碼,區域②是對應的 AST 語法樹,區域③是轉換代碼,可以對語法樹進行各種操作,區域④是轉換后生成的新代碼,圖中原來的 Unicode 字符經過操作之后就變成了正常字符,
語法樹沒有單一的格式,選擇不同的語言、不同的編譯器,得到的結果也是不一樣的,在 JavaScript 中,編譯器有 Acorn、Espree、Esprima、Recast、Uglify-JS 等,使用最多的是 Babel,后續的學習也是以 Babel 為例,

AST 在編譯中的位置
在編譯原理中,編譯器轉換代碼通常要經過三個步驟:詞法分析(Lexical Analysis)、語法分析(Syntax Analysis)、代碼生成(Code Generation),下圖生動展示了這一程序:

詞法分析
詞法分析階段是編譯程序的第一個階段,這個階段的任務是從左到右一個字符一個字符地讀入源程式,然后根據構詞規則識別單詞,生成 token 符號流,比如 isPanda('??'),會被拆分成 isPanda,(,'??',) 四部分,每部分都有不同的含義,可以將詞法分析程序想象為不同型別標記的串列或陣列,

語法分析
語法分析是編譯程序的一個邏輯階段,語法分析的任務是在詞法分析的基礎上將單詞序列組合成各類語法短語,比如“程式”,“陳述句”,“運算式”等,前面的例子中,isPanda('??') 就會被分析為一條表達陳述句 ExpressionStatement,isPanda() 就會被分析成一個函式運算式 CallExpression,?? 就會被分析成一個變數 Literal 等,眾多語法之間的依賴、嵌套關系,就構成了一個樹狀結構,即 AST 語法樹,

代碼生成
代碼生成是最后一步,將 AST 語法樹轉換成可執行代碼即可,在轉換之前,我們可以直接操作語法樹,進行增刪改查等操作,例如,我們可以確定變數的宣告位置、更改變數的值、洗掉某些節點等,我們將陳述句 isPanda('??') 修改為一個布爾型別的 Literal:true,語法樹就有如下變化:

Babel 簡介
Babel 是一個 JavaScript 編譯器,也可以說是一個決議庫,Babel 中文網:https://www.babeljs.cn/ ,Babel 英文官網:https://babeljs.io/ ,Babel 內置了很多分析 JavaScript 代碼的方法,我們可以利用 Babel 將 JavaScript 代碼轉換成 AST 語法樹,然后增刪改查等操作之后,再轉換成 JavaScript 代碼,
Babel 包含的各種功能包、API、各方法可選引數等,都非常多,本文不一一列舉,在實際使用程序中,應當多查詢官方檔案,或者參考文末給出的一些學習資料,Babel 的安裝和其他 Node 包一樣,需要哪個安裝哪個即可,比如 npm install @babel/core @babel/parser @babel/traverse @babel/generator
在做逆向解混淆中,主要用到了 Babel 的以下幾個功能包,本文也僅介紹以下幾個功能包:
@babel/core:Babel 編譯器本身,提供了 babel 的編譯 API;@babel/parser:將 JavaScript 代碼決議成 AST 語法樹;@babel/traverse:遍歷、修改 AST 語法樹的各個節點;@babel/generator:將 AST 還原成 JavaScript 代碼;@babel/types:判斷、驗證節點的型別、構建新 AST 節點等,

@babel/core
Babel 編譯器本身,被拆分成了三個模塊:@babel/parser、@babel/traverse、@babel/generator,比如以下方法的匯入效果都是一樣的:
const parse = require("@babel/parser").parse;
const parse = require("@babel/core").parse;
const traverse = require("@babel/traverse").default
const traverse = require("@babel/core").traverse
@babel/parser
@babel/parser 可以將 JavaScript 代碼決議成 AST 語法樹,其中主要提供了兩個方法:
parser.parse(code, [{options}]):決議一段 JavaScript 代碼;parser.parseExpression(code, [{options}]):考慮到了性能問題,決議單個 JavaScript 運算式,
部分可選引數 options:
| 引數 | 描述 |
|---|---|
allowImportExportEverywhere |
默認 import 和 export 宣告陳述句只能出現在程式的最頂層,設定為 true 則在任何地方都可以宣告 |
allowReturnOutsideFunction |
默認如果在頂層中使用 return 陳述句會引起錯誤,設定為 true 就不會報錯 |
sourceType |
默認為 script,當代碼中含有 import 、export 等關鍵字時會報錯,需要指定為 module |
errorRecovery |
默認如果 babel 發現一些不正常的代碼就會拋出錯誤,設定為 true 則會在保存決議錯誤的同時繼續決議代碼,錯誤的記錄將被保存在最終生成的 AST 的 errors 屬性中,當然如果遇到嚴重的錯誤,依然會終止決議 |
舉個例子看得比較清楚:
const parser = require("@babel/parser");
const code = "const a = 1;";
const ast = parser.parse(code, {sourceType: "module"})
console.log(ast)
{sourceType: "module"} 演示了如何添加可選引數,輸出的就是 AST 語法樹,這和在線網站 https://astexplorer.net/ 決議出來的語法樹是一樣的:

@babel/generator
@babel/generator 可以將 AST 還原成 JavaScript 代碼,提供了一個 generate 方法:generate(ast, [{options}], code),
部分可選引數 options:
| 引數 | 描述 |
|---|---|
auxiliaryCommentBefore |
在輸出檔案內容的頭部添加注釋塊文字 |
auxiliaryCommentAfter |
在輸出檔案內容的末尾添加注釋塊文字 |
comments |
輸出內容是否包含注釋 |
compact |
輸出內容是否不添加空格,避免格式化 |
concise |
輸出內容是否減少空格使其更緊湊一些 |
minified |
是否壓縮輸出代碼 |
retainLines |
嘗試在輸出代碼中使用與源代碼中相同的行號 |
接著前面的例子,原代碼是 const a = 1;,現在我們把 a 變數修改為 b,值 1 修改為 2,然后將 AST 還原生成新的 JS 代碼:
const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const code = "const a = 1;";
const ast = parser.parse(code, {sourceType: "module"})
ast.program.body[0].declarations[0].id.name = "b"
ast.program.body[0].declarations[0].init.value = https://www.cnblogs.com/ikdl/p/2
const result = generate(ast, {minified: true})
console.log(result.code)
最終輸出的是 const b=2;,變數名和值都成功更改了,由于加了壓縮處理,等號左右兩邊的空格也沒了,
代碼里 {minified: true} 演示了如何添加可選引數,這里表示壓縮輸出代碼,generate 得到的 result 得到的是一個物件,其中的 code 屬性才是最終的 JS 代碼,
代碼里 ast.program.body[0].declarations[0].id.name 是 a 在 AST 中的位置,ast.program.body[0].declarations[0].init.value 是 1 在 AST 中的位置,如下圖所示:

@babel/traverse
當代碼多了,我們不可能像前面那樣挨個定位并修改,對于相同型別的節點,我們可以直接遍歷所有節點來進行修改,這里就用到了 @babel/traverse,它通常和 visitor 一起使用,visitor 是一個物件,這個名字是可以隨意取的,visitor 里可以定義一些方法來過濾節點,這里還是用一個例子來演示:
const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default
const code = `
const a = 1500;
const b = 60;
const c = "hi";
const d = 787;
const e = "1244";
`
const ast = parser.parse(code)
const visitor = {
NumericLiteral(path){
path.node.value = https://www.cnblogs.com/ikdl/p/(path.node.value + 100) * 2
},
StringLiteral(path){
path.node.value ="I Love JavaScript!"
}
}
traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)
這里的原始代碼定義了 abcde 五個變數,其值有數字也有字串,我們在 AST 中可以看到對應的型別為 NumericLiteral 和 StringLiteral:

然后我們宣告了一個 visitor 物件,然后定義對應型別的處理方法,traverse 接收兩個引數,第一個是 AST 物件,第二個是 visitor,當 traverse 遍歷所有節點,遇到節點型別為 NumericLiteral 和 StringLiteral 時,就會呼叫 visitor 中對應的處理方法,visitor 中的方法會接收一個當前節點的 path 物件,該物件的型別是 NodePath,該物件有非常多的屬性,以下介紹幾種最常用的:
| 屬性 | 描述 |
|---|---|
toString() |
當前路徑的原始碼 |
node |
當前路徑的節點 |
parent |
當前路徑的父級節點 |
parentPath |
當前路徑的父級路徑 |
type |
當前路徑的型別 |
PS:path 物件除了有很多屬性以外,還有很多方法,比如替換節點、洗掉節點、插入節點、尋找父級節點、獲取同級節點、添加注釋、判斷節點型別等,可在需要時查詢相關檔案或查看原始碼,后續介紹 @babel/types 部分將會舉部分例子來演示,以后的實戰文章中也會有相關實體,篇幅有限本文不再細說,
因此在上面的代碼中,path.node.value 就拿到了變數的值,然后我們就可以進一步對其進行修改了,以上代碼運行后,所有數字都會加上100后再乘以2,所有字串都會被替換成 I Love JavaScript!,結果如下:
const a = 3200;
const b = 320;
const c = "I Love JavaScript!";
const d = 1774;
const e = "I Love JavaScript!";
如果多個型別的節點,處理的方式都一樣,那么還可以使用 | 將所有節點連接成字串,將同一個方法應用到所有節點:
const visitor = {
"NumericLiteral|StringLiteral"(path) {
path.node.value = "https://www.cnblogs.com/ikdl/p/I Love JavaScript!"
}
}
visitor 物件有多種寫法,以下幾種寫法的效果都是一樣的:
const visitor = {
NumericLiteral(path){
path.node.value = https://www.cnblogs.com/ikdl/p/(path.node.value + 100) * 2
},
StringLiteral(path){
path.node.value ="I Love JavaScript!"
}
}
const visitor = {
NumericLiteral: function (path){
path.node.value = https://www.cnblogs.com/ikdl/p/(path.node.value + 100) * 2
},
StringLiteral: function (path){
path.node.value ="https://www.cnblogs.com/ikdl/p/I Love JavaScript!"
}
}
const visitor = {
NumericLiteral: {
enter(path) {
path.node.value = https://www.cnblogs.com/ikdl/p/(path.node.value + 100) * 2
}
},
StringLiteral: {
enter(path) {
path.node.value ="https://www.cnblogs.com/ikdl/p/I Love JavaScript!"
}
}
}
const visitor = {
enter(path) {
if (path.node.type === "NumericLiteral") {
path.node.value = https://www.cnblogs.com/ikdl/p/(path.node.value + 100) * 2
}
if (path.node.type ==="StringLiteral") {
path.node.value = "https://www.cnblogs.com/ikdl/p/I Love JavaScript!"
}
}
}
以上幾種寫法中有用到了 enter 方法,在節點的遍歷程序中,進入節點(enter)與退出(exit)節點都會訪問一次節點,traverse 默認在進入節點時進行節點的處理,如果要在退出節點時處理,那么在 visitor 中就必須宣告 exit 方法,
@babel/types
@babel/types 主要用于構建新的 AST 節點,前面的示例代碼為 const a = 1;,如果想要增加內容,比如變成 const a = 1; const b = a * 5 + 1;,就可以通過 @babel/types 來實作,
首先觀察一下 AST 語法樹,原陳述句只有一個 VariableDeclaration 節點,現在增加了一個:

那么我們的思路就是在遍歷節點時,遍歷到 VariableDeclaration 節點,就在其后面增加一個 VariableDeclaration 節點,生成 VariableDeclaration 節點,可以使用 types.variableDeclaration() 方法,在 types 中各種方法名稱和我們在 AST 中看到的是一樣的,只不過首字母是小寫的,所以我們不需要知道所有方法的情況下,也能大致推斷其方法名,只知道這個方法還不行,還得知道傳入的引數是什么,可以查檔案,不過K哥這里推薦直接看原始碼,非常清晰明了,以 Pycharm 為例,按住 Ctrl 鍵,再點擊方法名,就進到原始碼里了:

function variableDeclaration(kind: "var" | "let" | "const", declarations: Array<BabelNodeVariableDeclarator>)
可以看到需要 kind 和 declarations 兩個引數,其中 declarations 是 VariableDeclarator 型別的節點組成的串列,所以我們可以先寫出以下 visitor 部分的代碼,其中 path.insertAfter() 是在該節點之后插入新節點的意思:
const visitor = {
VariableDeclaration(path) {
let declaration = types.variableDeclaration("const", [declarator])
path.insertAfter(declaration)
}
}
接下來我們還需要進一步定義 declarator,也就是 VariableDeclarator 型別的節點,查詢其原始碼如下:
function variableDeclarator(id: BabelNodeLVal, init?: BabelNodeExpression)
觀察 AST,id 為 Identifier 物件,init 為 BinaryExpression 物件,如下圖所示:

先來處理 id,可以使用 types.identifier() 方法來生成,其原始碼為 function identifier(name: string),name 在這里就是 b 了,此時 visitor 代碼就可以這么寫:
const visitor = {
VariableDeclaration(path) {
let declarator = types.variableDeclarator(types.identifier("b"), init)
let declaration = types.variableDeclaration("const", [declarator])
path.insertAfter(declaration)
}
}
然后再來看 init 該如何定義,首先仍然是看 AST 結構:

init 為 BinaryExpression 物件,left 左邊是 BinaryExpression,right 右邊是 NumericLiteral,可以用 types.binaryExpression() 方法來生成 init,其原始碼如下:
function binaryExpression(
operator: "+" | "-" | "/" | "%" | "*" | "**" | "&" | "|" | ">>" | ">>>" | "<<" | "^" | "==" | "===" | "!=" | "!==" | "in" | "instanceof" | ">" | "<" | ">=" | "<=",
left: BabelNodeExpression | BabelNodePrivateName,
right: BabelNodeExpression
)
此時 visitor 代碼就可以這么寫:
const visitor = {
VariableDeclaration(path) {
let init = types.binaryExpression("+", left, right)
let declarator = types.variableDeclarator(types.identifier("b"), init)
let declaration = types.variableDeclaration("const", [declarator])
path.insertAfter(declaration)
}
}
然后繼續構造 left 和 right,和前面的方法一樣,觀察 AST 語法樹,查詢對應方法應該傳入的引數,層層嵌套,直到把所有的節點都構造完畢,最終的 visitor 代碼應該是這樣的:
const visitor = {
VariableDeclaration(path) {
let left = types.binaryExpression("*", types.identifier("a"), types.numericLiteral(5))
let right = types.numericLiteral(1)
let init = types.binaryExpression("+", left, right)
let declarator = types.variableDeclarator(types.identifier("b"), init)
let declaration = types.variableDeclaration("const", [declarator])
path.insertAfter(declaration)
path.stop()
}
}
注意:path.insertAfter() 插入節點陳述句后面加了一句 path.stop(),表示插入完成后立即停止遍歷當前節點和后續的子節點,添加的新節點也是 VariableDeclaration,如果不加停止陳述句的話,就會無限回圈插入下去,
插入新節點后,再轉換成 JavaScript 代碼,就可以看到多了一行新代碼,如下圖所示:

常見混淆還原
了解了 AST 和 babel 后,就可以對 JavaScript 混淆代碼進行還原了,以下是部分樣例,帶你進一步熟悉 babel 的各種操作,
字串還原
文章開頭的圖中舉了個例子,正常字符被換成了 Unicode 編碼:
console['\u006c\u006f\u0067']('\u0048\u0065\u006c\u006c\u006f\u0020\u0077\u006f\u0072\u006c\u0064\u0021')
觀察 AST 結構:

我們發現 Unicode 編碼對應的是 raw,而 rawValue 和 value 都是正常的,所以我們可以將 raw 替換成 rawValue 或 value 即可,需要注意的是引號的問題,本來是 console["log"],你還原后變成了 console[log],自然會報錯的,除了替換值以外,這里直接洗掉 extra 節點,或者洗掉 raw 值也是可以的,所以以下幾種寫法都可以還原代碼:
const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default
const code = `console['\u006c\u006f\u0067']('\u0048\u0065\u006c\u006c\u006f\u0020\u0077\u006f\u0072\u006c\u0064\u0021')`
const ast = parser.parse(code)
const visitor = {
StringLiteral(path) {
// 以下方法均可
// path.node.extra.raw = path.node.rawValue
// path.node.extra.raw = '"' + path.node.value + '"'
// delete path.node.extra
delete path.node.extra.raw
}
}
traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)
還原結果:
console["log"]("Hello world!");
運算式還原
之前K哥寫過 JSFuck 混淆的還原,其中有介紹 ![] 可表示 false,!![] 或者 !+[] 可表示 true,在一些混淆代碼中,經常有這些操作,把簡單的運算式復雜化,往往需要執行一下陳述句,才能得到真正的結果,示例代碼如下:
const a = !![]+!![]+!![];
const b = Math.floor(12.34 * 2.12)
const c = 10 >> 3 << 1
const d = String(21.3 + 14 * 1.32)
const e = parseInt("1.893" + "45.9088")
const f = parseFloat("23.2334" + "21.89112")
const g = 20 < 18 ? '未成年' : '成年'
想要執行陳述句,我們需要了解 path.evaluate() 方法,該方法會對 path 物件進行執行操作,自動計算出結果,回傳一個物件,其中的 confident 屬性表示置信度,value 表示計算結果,使用 types.valueToNode() 方法創建節點,使用 path.replaceInline() 方法將節點替換成計算結果生成的新節點,替換方法有一下幾種:
replaceWith:用一個節點替換另一個節點;replaceWithMultiple:用多個節點替換另一個節點;replaceWithSourceString:將傳入的原始碼字串決議成對應 Node 后再替換,性能較差,不建議使用;replaceInline:用一個或多個節點替換另一個節點,相當于同時有了前兩個函式的功能,
對應的 AST 處理代碼如下:
const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default
const types = require("@babel/types")
const code = `
const a = !![]+!![]+!![];
const b = Math.floor(12.34 * 2.12)
const c = 10 >> 3 << 1
const d = String(21.3 + 14 * 1.32)
const e = parseInt("1.893" + "45.9088")
const f = parseFloat("23.2334" + "21.89112")
const g = 20 < 18 ? '未成年' : '成年'
`
const ast = parser.parse(code)
const visitor = {
"BinaryExpression|CallExpression|ConditionalExpression"(path) {
const {confident, value} = path.evaluate()
if (confident){
path.replaceInline(types.valueToNode(value))
}
}
}
traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)
最終結果:
const a = 3;
const b = 26;
const c = 2;
const d = "39.78";
const e = parseInt("1.89345.9088");
const f = parseFloat("23.233421.89112");
const g = "\u6210\u5E74";
洗掉未使用變數
有時候代碼里會有一些并沒有使用到的多余變數,洗掉這些多余變數有助于更加高效的分析代碼,示例代碼如下:
const a = 1;
const b = a * 2;
const c = 2;
const d = b + 1;
const e = 3;
console.log(d)
洗掉多余變數,首先要了解 NodePath 中的 scope,scope 的作用主要是查找識別符號的作用域、獲取并修改識別符號的所有參考等,洗掉未使用變數主要用到了 scope.getBinding() 方法,傳入的值是當前節點能夠參考到的識別符號名稱,回傳的關鍵屬性有以下幾個:
identifier:識別符號的 Node 物件;path:識別符號的 NodePath 物件;constant:識別符號是否為常量;referenced:識別符號是否被參考;references:識別符號被參考的次數;constantViolations:如果識別符號被修改,則會存放所有修改該識別符號節點的 Path 物件;referencePaths:如果識別符號被參考,則會存放所有參考該識別符號節點的 Path 物件,
所以我們可以通過 constantViolations、referenced、references、referencePaths 多個引數來判斷變數是否可以被洗掉,AST 處理代碼如下:
const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default
const code = `
const a = 1;
const b = a * 2;
const c = 2;
const d = b + 1;
const e = 3;
console.log(d)
`
const ast = parser.parse(code)
const visitor = {
VariableDeclarator(path){
const binding = path.scope.getBinding(path.node.id.name);
// 如識別符號被修改過,則不能進行洗掉動作,
if (!binding || binding.constantViolations.length > 0) {
return;
}
// 未被參考
if (!binding.referenced) {
path.remove();
}
// 被參考次數為0
// if (binding.references === 0) {
// path.remove();
// }
// 長度為0,變數沒有被參考過
// if (binding.referencePaths.length === 0) {
// path.remove();
// }
}
}
traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)
處理后的代碼(未使用的 b、c、e 變數已被洗掉):
const a = 1;
const b = a * 2;
const d = b + 1;
console.log(d);
洗掉冗余邏輯代碼
有時候為了增加逆向難度,會有很多嵌套的 if-else 陳述句,大量判斷為假的冗余邏輯代碼,同樣可以利用 AST 將其洗掉掉,只留下判斷為真的,示例代碼如下:
const example = function () {
let a;
if (false) {
a = 1;
} else {
if (1) {
a = 2;
}
else {
a = 3;
}
}
return a;
};
觀察 AST,判斷條件對應的是 test 節點,if 對應的是 consequent 節點,else 對應的是 alternate 節點,如下圖所示:

AST 處理思路以及代碼:
- 篩選出
BooleanLiteral和NumericLiteral節點,取其對應的值,即path.node.test.value; - 判斷
value值為真,則將節點替換成consequent節點下的內容,即path.node.consequent.body; - 判斷
value值為假,則替換成alternate節點下的內容,即path.node.alternate.body; - 有的 if 陳述句可能沒有寫 else,也就沒有
alternate,所以這種情況下判斷value值為假,則直接移除該節點,即path.remove()
const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default
const types = require('@babel/types');
const code = `
const example = function () {
let a;
if (false) {
a = 1;
} else {
if (1) {
a = 2;
}
else {
a = 3;
}
}
return a;
};
`
const ast = parser.parse(code)
const visitor = {
enter(path) {
if (types.isBooleanLiteral(path.node.test) || types.isNumericLiteral(path.node.test)) {
if (path.node.test.value) {
path.replaceInline(path.node.consequent.body);
} else {
if (path.node.alternate) {
path.replaceInline(path.node.alternate.body);
} else {
path.remove()
}
}
}
}
}
traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)
處理結果:
const example = function () {
let a;
a = 2;
return a;
};
switch-case 反控制流平坦化
控制流平坦化是混淆當中最常見的,通過 if-else 或者 while-switch-case 陳述句分解步驟,示例代碼:
const _0x34e16a = '3,4,0,5,1,2'['split'](',');
let _0x2eff02 = 0x0;
while (!![]) {
switch (_0x34e16a[_0x2eff02++]) {
case'0':
let _0x38cb15 = _0x4588f1 + _0x470e97;
continue;
case'1':
let _0x1e0e5e = _0x37b9f3[_0x50cee0(0x2e0, 0x2e8, 0x2e1, 0x2e4)];
continue;
case'2':
let _0x35d732 = [_0x388d4b(-0x134, -0x134, -0x139, -0x138)](_0x38cb15 >> _0x4588f1);
continue;
case'3':
let _0x4588f1 = 0x1;
continue;
case'4':
let _0x470e97 = 0x2;
continue;
case'5':
let _0x37b9f3 = 0x5 || _0x38cb15;
continue;
}
break;
}
AST 還原思路:
- 獲取控制流原始陣列,將
'3,4,0,5,1,2'['split'](',')之類的陳述句轉化成['3','4','0','5','1','2']之類的陣列,得到該陣列之后,也可以選擇把 split 陳述句對應的節點洗掉掉,因為最終代碼里這條陳述句就沒用了; - 遍歷第一步得到的控制流陣列,依次取出每個值所對應的 case 節點;
- 定義一個陣列,儲存每個 case 節點
consequent陣列里面的內容,并洗掉continue陳述句對應的節點; - 遍歷完成后,將第三步的陣列替換掉整個 while 節點,也就是
WhileStatement,
不同思路,寫法多樣,對于如何獲取控制流陣列,可以有以下思路:
- 獲取到
While陳述句節點,然后使用path.getAllPrevSiblings()方法獲取其前面的所有兄弟節點,遍歷每個兄弟節點,找到與switch()里面陣列的變數名相同的節點,然后再取節點的值進行后續處理; - 直接取
switch()里面陣列的變數名,然后使用scope.getBinding()方法獲取到它系結的節點,然后再取這個節點的值進行后續處理,
所以 AST 處理代碼就有兩種寫法,方法一:(code.js 即為前面的示例代碼,為了方便操作,這里使用 fs 從檔案中讀取代碼)
const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default
const types = require("@babel/types")
const fs = require("fs");
const code = fs.readFileSync("code.js", {encoding: "utf-8"});
const ast = parser.parse(code)
const visitor = {
WhileStatement(path) {
// switch 節點
let switchNode = path.node.body.body[0];
// switch 陳述句內的控制流陣列名,本例中是 _0x34e16a
let arrayName = switchNode.discriminant.object.name;
// 獲得所有 while 前面的兄弟節點,本例中獲取到的是宣告兩個變數的節點,即 const _0x34e16a 和 let _0x2eff02
let prevSiblings = path.getAllPrevSiblings();
// 定義快取控制流陣列
let array = []
// forEach 方法遍歷所有節點
prevSiblings.forEach(pervNode => {
let {id, init} = pervNode.node.declarations[0];
// 如果節點 id.name 與 switch 陳述句內的控制流陣列名相同
if (arrayName === id.name) {
// 獲取節點整個運算式的引數、分割方法、分隔符
let object = init.callee.object.value;
let property = init.callee.property.value;
let argument = init.arguments[0].value;
// 模擬執行 '3,4,0,5,1,2'['split'](',') 陳述句
array = object[property](argument)
// 也可以直接取引數進行分割,方法不通用,比如分隔符換成 | 就不行了
// array = init.callee.object.value.split(',');
}
// 前面的兄弟節點就可以洗掉了
pervNode.remove();
});
// 儲存正確順序的控制流陳述句
let replace = [];
// 遍歷控制流陣列,按正確順序取 case 內容
array.forEach(index => {
let consequent = switchNode.cases[index].consequent;
// 如果最后一個節點是 continue 陳述句,則洗掉 ContinueStatement 節點
if (types.isContinueStatement(consequent[consequent.length - 1])) {
consequent.pop();
}
// concat 方法拼接多個陣列,即正確順序的 case 內容
replace = replace.concat(consequent);
}
);
// 替換整個 while 節點,兩種方法都可以
path.replaceWithMultiple(replace);
// path.replaceInline(replace);
}
}
traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)
方法二:
const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default
const types = require("@babel/types")
const fs = require("fs");
const code = fs.readFileSync("code.js", {encoding: "utf-8"});
const ast = parser.parse(code)
const visitor = {
WhileStatement(path) {
// switch 節點
let switchNode = path.node.body.body[0];
// switch 陳述句內的控制流陣列名,本例中是 _0x34e16a
let arrayName = switchNode.discriminant.object.name;
// 獲取控制流陣列系結的節點
let bindingArray = path.scope.getBinding(arrayName);
// 獲取節點整個運算式的引數、分割方法、分隔符
let init = bindingArray.path.node.init;
let object = init.callee.object.value;
let property = init.callee.property.value;
let argument = init.arguments[0].value;
// 模擬執行 '3,4,0,5,1,2'['split'](',') 陳述句
let array = object[property](argument)
// 也可以直接取引數進行分割,方法不通用,比如分隔符換成 | 就不行了
// let array = init.callee.object.value.split(',');
// switch 陳述句內的控制流自增變數名,本例中是 _0x2eff02
let autoIncrementName = switchNode.discriminant.property.argument.name;
// 獲取控制流自增變數名系結的節點
let bindingAutoIncrement = path.scope.getBinding(autoIncrementName);
// 可選擇的操作:洗掉控制流陣列系結的節點、自增變數名系結的節點
bindingArray.path.remove();
bindingAutoIncrement.path.remove();
// 儲存正確順序的控制流陳述句
let replace = [];
// 遍歷控制流陣列,按正確順序取 case 內容
array.forEach(index => {
let consequent = switchNode.cases[index].consequent;
// 如果最后一個節點是 continue 陳述句,則洗掉 ContinueStatement 節點
if (types.isContinueStatement(consequent[consequent.length - 1])) {
consequent.pop();
}
// concat 方法拼接多個陣列,即正確順序的 case 內容
replace = replace.concat(consequent);
}
);
// 替換整個 while 節點,兩種方法都可以
path.replaceWithMultiple(replace);
// path.replaceInline(replace);
}
}
traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)
以上代碼運行后,原來的 switch-case 控制流就被還原了,變成了按順序一行一行的代碼,更加簡潔明了:
let _0x4588f1 = 0x1;
let _0x470e97 = 0x2;
let _0x38cb15 = _0x4588f1 + _0x470e97;
let _0x37b9f3 = 0x5 || _0x38cb15;
let _0x1e0e5e = _0x37b9f3[_0x50cee0(0x2e0, 0x2e8, 0x2e1, 0x2e4)];
let _0x35d732 = [_0x388d4b(-0x134, -0x134, -0x139, -0x138)](_0x38cb15 >> _0x4588f1);
參考資料
本文有參考以下資料,也是比較推薦的在線學習資料:
- Youtube 視頻,Babel 入門:https://www.youtube.com/watch?v=UeVq_U5obnE (作者 Nicolò Ribaudo,視頻中的 PPT 資料可在 K 哥爬蟲公眾號后臺回復 Babel 免費獲取!)
- 官方手冊 Babel Handbook:https://github.com/jamiebuilds/babel-handbook
- 非官方 Babel API 中文檔案:https://evilrecluse.top/Babel-traverse-api-doc/
END
Babel 編譯器國內的資料其實不是很多,多看原始碼、同時在線對照可視化的 AST 語法樹,耐心一點兒一層一層分析即可,本文中的案例也只是最基本操作,實際遇到一些混淆還得視情況進行修改,比如需要加一些型別判斷來限制等,后續K哥會用實戰來帶領大家進一步熟悉解混淆當中的其他操作,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/465961.html
標籤:Python
