逆向進階瀑粥,利用 AST 技術還原 JavaScript 混淆代碼

什么是 AST

AST(Abstract Syntax Tree)饺蚊,中文抽象語法樹萍诱,簡稱語法樹(Syntax Tree),是源代碼的抽象語法結(jié)構(gòu)的樹狀表現(xiàn)形式卸勺,樹上的每個節(jié)點都表示源代碼中的一種結(jié)構(gòu)砂沛。語法樹不是某一種編程語言獨有的,JavaScript曙求、Python碍庵、Java、Golang 等幾乎所有編程語言都有語法樹悟狱。

小時候我們得到一個玩具静浴,總喜歡把玩具拆解成一個一個小零件,然后按照我們自己的想法挤渐,把零件重新組裝起來苹享,一個新玩具就誕生了。而 JavaScript 就像一臺精妙運作的機器浴麻,通過 AST 解析得问,我們也可以像童年時拆解玩具一樣,深入了解 JavaScript 這臺機器的各個零部件软免,然后重新按照我們自己的意愿來組裝宫纬。

AST 的用途很廣,IDE 的語法高亮膏萧、代碼檢查漓骚、格式化、壓縮榛泛、轉(zhuǎn)譯等蝌蹂,都需要先將代碼轉(zhuǎn)化成 AST 再進行后續(xù)的操作,ES5 和 ES6 語法差異曹锨,為了向后兼容孤个,在實際應用中需要進行語法的轉(zhuǎn)換,也會用到 AST沛简。AST 并不是為了逆向而生齐鲤,但做逆向?qū)W會了 AST硅急,在解混淆時可以如魚得水。

AST 有一個在線解析網(wǎng)站:https://astexplorer.net/ 佳遂,頂部可以選擇語言、編譯器撒顿、是否開啟轉(zhuǎn)化等丑罪,如下圖所示,區(qū)域①是源代碼凤壁,區(qū)域②是對應的 AST 語法樹吩屹,區(qū)域③是轉(zhuǎn)換代碼,可以對語法樹進行各種操作拧抖,區(qū)域④是轉(zhuǎn)換后生成的新代碼煤搜。圖中原來的 Unicode 字符經(jīng)過操作之后就變成了正常字符。

語法樹沒有單一的格式唧席,選擇不同的語言擦盾、不同的編譯器,得到的結(jié)果也是不一樣的淌哟,在 JavaScript 中迹卢,編譯器有 Acorn、Espree徒仓、Esprima腐碱、Recast、Uglify-JS 等掉弛,使用最多的是 Babel症见,后續(xù)的學習也是以 Babel 為例。

01

AST 在編譯中的位置

在編譯原理中殃饿,編譯器轉(zhuǎn)換代碼通常要經(jīng)過三個步驟:詞法分析(Lexical Analysis)谋作、語法分析(Syntax Analysis)、代碼生成(Code Generation)壁晒,下圖生動展示了這一過程:

02

詞法分析

詞法分析階段是編譯過程的第一個階段瓷们,這個階段的任務是從左到右一個字符一個字符地讀入源程序,然后根據(jù)構(gòu)詞規(guī)則識別單詞秒咐,生成 token 符號流谬晕,比如 isPanda('??'),會被拆分成 isPanda携取,(攒钳,'??') 四部分雷滋,每部分都有不同的含義不撑,可以將詞法分析過程想象為不同類型標記的列表或數(shù)組文兢。

03

語法分析

語法分析是編譯過程的一個邏輯階段,語法分析的任務是在詞法分析的基礎上將單詞序列組合成各類語法短語焕檬,比如“程序”姆坚,“語句”,“表達式”等实愚,前面的例子中兼呵,isPanda('??') 就會被分析為一條表達語句 ExpressionStatementisPanda() 就會被分析成一個函數(shù)表達式 CallExpression腊敲,?? 就會被分析成一個變量 Literal 等击喂,眾多語法之間的依賴、嵌套關系碰辅,就構(gòu)成了一個樹狀結(jié)構(gòu)懂昂,即 AST 語法樹。

04

代碼生成

代碼生成是最后一步没宾,將 AST 語法樹轉(zhuǎn)換成可執(zhí)行代碼即可凌彬,在轉(zhuǎn)換之前,我們可以直接操作語法樹榕吼,進行增刪改查等操作饿序,例如,我們可以確定變量的聲明位置羹蚣、更改變量的值原探、刪除某些節(jié)點等,我們將語句 isPanda('??') 修改為一個布爾類型的 Literaltrue顽素,語法樹就有如下變化:

05

Babel 簡介

Babel 是一個 JavaScript 編譯器咽弦,也可以說是一個解析庫,Babel 中文網(wǎng):https://www.babeljs.cn/ 胁出,Babel 英文官網(wǎng):https://babeljs.io/ 型型,Babel 內(nèi)置了很多分析 JavaScript 代碼的方法,我們可以利用 Babel 將 JavaScript 代碼轉(zhuǎn)換成 AST 語法樹全蝶,然后增刪改查等操作之后闹蒜,再轉(zhuǎn)換成 JavaScript 代碼。

Babel 包含的各種功能包抑淫、API绷落、各方法可選參數(shù)等,都非常多始苇,本文不一一列舉砌烁,在實際使用過程中,應當多查詢官方文檔,或者參考文末給出的一些學習資料函喉。Babel 的安裝和其他 Node 包一樣避归,需要哪個安裝哪個即可管呵,比如 npm install @babel/core @babel/parser @babel/traverse @babel/generator

在做逆向解混淆中捐下,主要用到了 Babel 的以下幾個功能包,本文也僅介紹以下幾個功能包:

  1. @babel/core:Babel 編譯器本身排抬,提供了 babel 的編譯 API蹲蒲;
  2. @babel/parser:將 JavaScript 代碼解析成 AST 語法樹;
  3. @babel/traverse:遍歷卡睦、修改 AST 語法樹的各個節(jié)點表锻;
  4. @babel/generator:將 AST 還原成 JavaScript 代碼;
  5. @babel/types:判斷确镊、驗證節(jié)點的類型骚腥、構(gòu)建新 AST 節(jié)點等廓块。
06

@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 表達式旋奢。

部分可選參數(shù) options

參數(shù) 描述
allowImportExportEverywhere 默認 importexport 聲明語句只能出現(xiàn)在程序的最頂層,設置為 true 則在任何地方都可以聲明
allowReturnOutsideFunction 默認如果在頂層中使用 return 語句會引起錯誤锹引,設置為 true 就不會報錯
sourceType 默認為 script躬它,當代碼中含有 import 倘待、export 等關鍵字時會報錯凸舵,需要指定為 module
errorRecovery 默認如果 babel 發(fā)現(xiàn)一些不正常的代碼就會拋出錯誤渐苏,設置為 true 則會在保存解析錯誤的同時繼續(xù)解析代碼琼富,錯誤的記錄將被保存在最終生成的 AST 的 errors 屬性中,當然如果遇到嚴重的錯誤,依然會終止解析

舉個例子看得比較清楚:

const parser = require("@babel/parser");

const code = "const a = 1;";
const ast = parser.parse(code, {sourceType: "module"})
console.log(ast)

{sourceType: "module"} 演示了如何添加可選參數(shù),輸出的就是 AST 語法樹涩金,這和在線網(wǎng)站 https://astexplorer.net/ 解析出來的語法樹是一樣的:

07

@babel/generator

@babel/generator 可以將 AST 還原成 JavaScript 代碼,提供了一個 generate 方法:generate(ast, [{options}], code)全度。

部分可選參數(shù) options

參數(shù) 描述
auxiliaryCommentBefore 在輸出文件內(nèi)容的頭部添加注釋塊文字
auxiliaryCommentAfter 在輸出文件內(nèi)容的末尾添加注釋塊文字
comments 輸出內(nèi)容是否包含注釋
compact 輸出內(nèi)容是否不添加空格,避免格式化
concise 輸出內(nèi)容是否減少空格使其更緊湊一些
minified 是否壓縮輸出代碼
retainLines 嘗試在輸出代碼中使用與源代碼中相同的行號

接著前面的例子,原代碼是 const a = 1;痒筒,現(xiàn)在我們把 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 = 2
const result = generate(ast, {minified: true})

console.log(result.code)

最終輸出的是 const b=2;,變量名和值都成功更改了,由于加了壓縮處理苹祟,等號左右兩邊的空格也沒了树枫。

代碼里 {minified: true} 演示了如何添加可選參數(shù),這里表示壓縮輸出代碼搔涝,generate 得到的 result 得到的是一個對象,其中的 code 屬性才是最終的 JS 代碼诬留。

代碼里 ast.program.body[0].declarations[0].id.name 是 a 在 AST 中的位置,ast.program.body[0].declarations[0].init.value 是 1 在 AST 中的位置,如下圖所示:

08

@babel/traverse

當代碼多了樟蠕,我們不可能像前面那樣挨個定位并修改吓懈,對于相同類型的節(jié)點,我們可以直接遍歷所有節(jié)點來進行修改甘穿,這里就用到了 @babel/traverse,它通常和 visitor 一起使用募判,visitor 是一個對象,這個名字是可以隨意取的装处,visitor 里可以定義一些方法來過濾節(jié)點找前,這里還是用一個例子來演示:

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 = (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 五個變量项戴,其值有數(shù)字也有字符串界斜,我們在 AST 中可以看到對應的類型為 NumericLiteralStringLiteral

09

然后我們聲明了一個 visitor 對象项贺,然后定義對應類型的處理方法棕叫,traverse 接收兩個參數(shù),第一個是 AST 對象伏钠,第二個是 visitor,當 traverse 遍歷所有節(jié)點打掘,遇到節(jié)點類型為 NumericLiteralStringLiteral 時,就會調(diào)用 visitor 中對應的處理方法,visitor 中的方法會接收一個當前節(jié)點的 path 對象琴锭,該對象的類型是 NodePath,該對象有非常多的屬性地回,以下介紹幾種最常用的:

屬性 描述
toString() 當前路徑的源碼
node 當前路徑的節(jié)點
parent 當前路徑的父級節(jié)點
parentPath 當前路徑的父級路徑
type 當前路徑的類型

PS:path 對象除了有很多屬性以外并闲,還有很多方法纹冤,比如替換節(jié)點萌京、刪除節(jié)點靠瞎、插入節(jié)點、尋找父級節(jié)點、獲取同級節(jié)點何吝、添加注釋、判斷節(jié)點類型等,可在需要時查詢相關文檔或查看源碼跪者,后續(xù)介紹 @babel/types 部分將會舉部分例子來演示,以后的實戰(zhàn)文章中也會有相關實例仗谆,篇幅有限本文不再細說藻雪。

因此在上面的代碼中勉耀,path.node.value 就拿到了變量的值,然后我們就可以進一步對其進行修改了像街。以上代碼運行后,所有數(shù)字都會加上100后再乘以2,所有字符串都會被替換成 I Love JavaScript!吗讶,結(jié)果如下:

const a = 3200;
const b = 320;
const c = "I Love JavaScript!";
const d = 1774;
const e = "I Love JavaScript!";

如果多個類型的節(jié)點叼丑,處理的方式都一樣关翎,那么還可以使用 | 將所有節(jié)點連接成字符串,將同一個方法應用到所有節(jié)點:

const visitor = {
    "NumericLiteral|StringLiteral"(path) {
        path.node.value = "I Love JavaScript!"
    }
}

visitor 對象有多種寫法鸠信,以下幾種寫法的效果都是一樣的:

const visitor = {
    NumericLiteral(path){
        path.node.value = (path.node.value + 100) * 2
    },
    StringLiteral(path){
        path.node.value = "I Love JavaScript!"
    }
}
const visitor = {
    NumericLiteral: function (path){
        path.node.value = (path.node.value + 100) * 2
    },
    StringLiteral: function (path){
        path.node.value = "I Love JavaScript!"
    }
}
const visitor = {
    NumericLiteral: {
        enter(path) {
            path.node.value = (path.node.value + 100) * 2
        }
    },
    StringLiteral: {
        enter(path) {
            path.node.value = "I Love JavaScript!"
        }
    }
}
const visitor = {
    enter(path) {
        if (path.node.type === "NumericLiteral") {
            path.node.value = (path.node.value + 100) * 2
        }
        if (path.node.type === "StringLiteral") {
            path.node.value = "I Love JavaScript!"
        }
    }
}

以上幾種寫法中有用到了 enter 方法纵寝,在節(jié)點的遍歷過程中星立,進入節(jié)點(enter)與退出(exit)節(jié)點都會訪問一次節(jié)點爽茴,traverse 默認在進入節(jié)點時進行節(jié)點的處理,如果要在退出節(jié)點時處理绰垂,那么在 visitor 中就必須聲明 exit 方法室奏。

@babel/types

@babel/types 主要用于構(gòu)建新的 AST 節(jié)點,前面的示例代碼為 const a = 1;劲装,如果想要增加內(nèi)容胧沫,比如變成 const a = 1; const b = a * 5 + 1;,就可以通過 @babel/types 來實現(xiàn)占业。

首先觀察一下 AST 語法樹绒怨,原語句只有一個 VariableDeclaration 節(jié)點,現(xiàn)在增加了一個:

10

那么我們的思路就是在遍歷節(jié)點時谦疾,遍歷到 VariableDeclaration 節(jié)點南蹂,就在其后面增加一個 VariableDeclaration 節(jié)點,生成 VariableDeclaration 節(jié)點念恍,可以使用 types.variableDeclaration() 方法六剥,在 types 中各種方法名稱和我們在 AST 中看到的是一樣的晚顷,只不過首字母是小寫的,所以我們不需要知道所有方法的情況下疗疟,也能大致推斷其方法名该默,只知道這個方法還不行,還得知道傳入的參數(shù)是什么策彤,可以查文檔权均,不過K哥這里推薦直接看源碼,非常清晰明了锅锨,以 Pycharm 為例叽赊,按住 Ctrl 鍵,再點擊方法名必搞,就進到源碼里了:

11
function variableDeclaration(kind: "var" | "let" | "const", declarations: Array<BabelNodeVariableDeclarator>)

可以看到需要 kinddeclarations 兩個參數(shù)必指,其中 declarationsVariableDeclarator 類型的節(jié)點組成的列表,所以我們可以先寫出以下 visitor 部分的代碼恕洲,其中 path.insertAfter() 是在該節(jié)點之后插入新節(jié)點的意思:

const visitor = {
    VariableDeclaration(path) {
        let declaration = types.variableDeclaration("const", [declarator])
        path.insertAfter(declaration)
    }
}

接下來我們還需要進一步定義 declarator塔橡,也就是 VariableDeclarator 類型的節(jié)點,查詢其源碼如下:

function variableDeclarator(id: BabelNodeLVal, init?: BabelNodeExpression)

觀察 AST霜第,id 為 Identifier 對象葛家,init 為 BinaryExpression 對象,如下圖所示:

12

先來處理 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 結(jié)構(gòu):

13

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)
    }
}

然后繼續(xù)構(gòu)造 left 和 right苞轿,和前面的方法一樣茅诱,觀察 AST 語法樹,查詢對應方法應該傳入的參數(shù)搬卒,層層嵌套瑟俭,直到把所有的節(jié)點都構(gòu)造完畢,最終的 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() 插入節(jié)點語句后面加了一句 path.stop()秀睛,表示插入完成后立即停止遍歷當前節(jié)點和后續(xù)的子節(jié)點尔当,添加的新節(jié)點也是 VariableDeclaration莲祸,如果不加停止語句的話蹂安,就會無限循環(huán)插入下去椭迎。

插入新節(jié)點后,再轉(zhuǎn)換成 JavaScript 代碼田盈,就可以看到多了一行新代碼畜号,如下圖所示:

14

常見混淆還原

了解了 AST 和 babel 后,就可以對 JavaScript 混淆代碼進行還原了允瞧,以下是部分樣例备韧,帶你進一步熟悉 babel 的各種操作州叠。

字符串還原

文章開頭的圖中舉了個例子,正常字符被換成了 Unicode 編碼:

console['\u006c\u006f\u0067']('\u0048\u0065\u006c\u006c\u006f\u0020\u0077\u006f\u0072\u006c\u0064\u0021')

觀察 AST 結(jié)構(gòu):

15

我們發(fā)現(xiàn) Unicode 編碼對應的是 raw,而 rawValuevalue 都是正常的拒课,所以我們可以將 raw 替換成 rawValuevalue 即可,需要注意的是引號的問題轧叽,本來是 console["log"]罗洗,你還原后變成了 console[log],自然會報錯的艺配,除了替換值以外察郁,這里直接刪除 extra 節(jié)點,或者刪除 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)

還原結(jié)果:

console["log"]("Hello world!");

表達式還原

之前K哥寫過 JSFuck 混淆的還原皮钠,其中有介紹 ![] 可表示 false,!![] 或者 !+[] 可表示 true赠法,在一些混淆代碼中麦轰,經(jīng)常有這些操作,把簡單的表達式復雜化砖织,往往需要執(zhí)行一下語句原朝,才能得到真正的結(jié)果,示例代碼如下:

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 ? '未成年' : '成年'

想要執(zhí)行語句镶苞,我們需要了解 path.evaluate() 方法喳坠,該方法會對 path 對象進行執(zhí)行操作,自動計算出結(jié)果茂蚓,返回一個對象壕鹉,其中的 confident 屬性表示置信度,value 表示計算結(jié)果聋涨,使用 types.valueToNode() 方法創(chuàng)建節(jié)點晾浴,使用 path.replaceInline() 方法將節(jié)點替換成計算結(jié)果生成的新節(jié)點,替換方法有一下幾種:

  • replaceWith:用一個節(jié)點替換另一個節(jié)點牍白;
  • replaceWithMultiple:用多個節(jié)點替換另一個節(jié)點脊凰;
  • replaceWithSourceString:將傳入的源碼字符串解析成對應 Node 后再替換,性能較差,不建議使用狸涌;
  • replaceInline:用一個或多個節(jié)點替換另一個節(jié)點切省,相當于同時有了前兩個函數(shù)的功能。

對應的 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)

最終結(jié)果:

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() 方法脸秽,傳入的值是當前節(jié)點能夠引用到的標識符名稱儒老,返回的關鍵屬性有以下幾個:

  • identifier:標識符的 Node 對象;
  • path:標識符的 NodePath 對象记餐;
  • constant:標識符是否為常量贷盲;
  • referenced:標識符是否被引用;
  • references:標識符被引用的次數(shù)剥扣;
  • constantViolations:如果標識符被修改巩剖,則會存放所有修改該標識符節(jié)點的 Path 對象;
  • referencePaths:如果標識符被引用钠怯,則會存放所有引用該標識符節(jié)點的 Path 對象佳魔。

所以我們可以通過 constantViolationsreferenced晦炊、references鞠鲜、referencePaths 多個參數(shù)來判斷變量是否可以被刪除,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();
        }

        // 被引用次數(shù)為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 節(jié)點,if 對應的是 consequent 節(jié)點板丽,else 對應的是 alternate 節(jié)點呈枉,如下圖所示:

16

AST 處理思路以及代碼:

  1. 篩選出 BooleanLiteralNumericLiteral 節(jié)點,取其對應的值,即 path.node.test.value猖辫;
  2. 判斷 value 值為真酥泞,則將節(jié)點替換成 consequent 節(jié)點下的內(nèi)容,即 path.node.consequent.body住册;
  3. 判斷 value 值為假,則替換成 alternate 節(jié)點下的內(nèi)容瓮具,即 path.node.alternate.body荧飞;
  4. 有的 if 語句可能沒有寫 else,也就沒有 alternate名党,所以這種情況下判斷 value 值為假叹阔,則直接移除該節(jié)點,即 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)

處理結(jié)果:

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 還原思路:

  1. 獲取控制流原始數(shù)組,將 '3,4,0,5,1,2'['split'](',') 之類的語句轉(zhuǎn)化成 ['3','4','0','5','1','2'] 之類的數(shù)組欧啤,得到該數(shù)組之后睛藻,也可以選擇把 split 語句對應的節(jié)點刪除掉,因為最終代碼里這條語句就沒用了邢隧;
  2. 遍歷第一步得到的控制流數(shù)組店印,依次取出每個值所對應的 case 節(jié)點;
  3. 定義一個數(shù)組倒慧,儲存每個 case 節(jié)點 consequent 數(shù)組里面的內(nèi)容按摘,并刪除 continue 語句對應的節(jié)點;
  4. 遍歷完成后纫谅,將第三步的數(shù)組替換掉整個 while 節(jié)點炫贤,也就是 WhileStatement

不同思路付秕,寫法多樣兰珍,對于如何獲取控制流數(shù)組,可以有以下思路:

  1. 獲取到 While 語句節(jié)點询吴,然后使用 path.getAllPrevSiblings() 方法獲取其前面的所有兄弟節(jié)點俩垃,遍歷每個兄弟節(jié)點,找到與 switch() 里面數(shù)組的變量名相同的節(jié)點汰寓,然后再取節(jié)點的值進行后續(xù)處理口柳;
  2. 直接取 switch() 里面數(shù)組的變量名,然后使用 scope.getBinding() 方法獲取到它綁定的節(jié)點有滑,然后再取這個節(jié)點的值進行后續(xù)處理跃闹。

所以 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 節(jié)點
        let switchNode = path.node.body.body[0];
        // switch 語句內(nèi)的控制流數(shù)組名苛秕,本例中是 _0x34e16a
        let arrayName = switchNode.discriminant.object.name;
        // 獲得所有 while 前面的兄弟節(jié)點,本例中獲取到的是聲明兩個變量的節(jié)點找默,即 const _0x34e16a 和 let _0x2eff02
        let prevSiblings = path.getAllPrevSiblings();
        // 定義緩存控制流數(shù)組
        let array = []
        // forEach 方法遍歷所有節(jié)點
        prevSiblings.forEach(pervNode => {
            let {id, init} = pervNode.node.declarations[0];
            // 如果節(jié)點 id.name 與 switch 語句內(nèi)的控制流數(shù)組名相同
            if (arrayName === id.name) {
                // 獲取節(jié)點整個表達式的參數(shù)艇劫、分割方法、分隔符
                let object = init.callee.object.value;
                let property = init.callee.property.value;
                let argument = init.arguments[0].value;
                // 模擬執(zhí)行 '3,4,0,5,1,2'['split'](',') 語句
                array = object[property](argument)
                // 也可以直接取參數(shù)進行分割惩激,方法不通用店煞,比如分隔符換成 | 就不行了
                // array = init.callee.object.value.split(',');
            }
            // 前面的兄弟節(jié)點就可以刪除了
            pervNode.remove();
        });

        // 儲存正確順序的控制流語句
        let replace = [];
        // 遍歷控制流數(shù)組,按正確順序取 case 內(nèi)容
        array.forEach(index => {
                let consequent = switchNode.cases[index].consequent;
                // 如果最后一個節(jié)點是 continue 語句风钻,則刪除 ContinueStatement 節(jié)點
                if (types.isContinueStatement(consequent[consequent.length - 1])) {
                    consequent.pop();
                }
                // concat 方法拼接多個數(shù)組顷蟀,即正確順序的 case 內(nèi)容
                replace = replace.concat(consequent);
            }
        );
        // 替換整個 while 節(jié)點,兩種方法都可以
        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 節(jié)點
        let switchNode = path.node.body.body[0];
        // switch 語句內(nèi)的控制流數(shù)組名骡技,本例中是 _0x34e16a
        let arrayName = switchNode.discriminant.object.name;
        // 獲取控制流數(shù)組綁定的節(jié)點
        let bindingArray = path.scope.getBinding(arrayName);
        // 獲取節(jié)點整個表達式的參數(shù)鸣个、分割方法、分隔符
        let init = bindingArray.path.node.init;
        let object = init.callee.object.value;
        let property = init.callee.property.value;
        let argument = init.arguments[0].value;
        // 模擬執(zhí)行 '3,4,0,5,1,2'['split'](',') 語句
        let array = object[property](argument)
        // 也可以直接取參數(shù)進行分割布朦,方法不通用囤萤,比如分隔符換成 | 就不行了
        // let array = init.callee.object.value.split(',');

        // switch 語句內(nèi)的控制流自增變量名,本例中是 _0x2eff02
        let autoIncrementName = switchNode.discriminant.property.argument.name;
        // 獲取控制流自增變量名綁定的節(jié)點
        let bindingAutoIncrement = path.scope.getBinding(autoIncrementName);
        // 可選擇的操作:刪除控制流數(shù)組綁定的節(jié)點是趴、自增變量名綁定的節(jié)點
        bindingArray.path.remove();
        bindingAutoIncrement.path.remove();

        // 儲存正確順序的控制流語句
        let replace = [];
        // 遍歷控制流數(shù)組阁将,按正確順序取 case 內(nèi)容
        array.forEach(index => {
                let consequent = switchNode.cases[index].consequent;
                // 如果最后一個節(jié)點是 continue 語句,則刪除 ContinueStatement 節(jié)點
                if (types.isContinueStatement(consequent[consequent.length - 1])) {
                    consequent.pop();
                }
                // concat 方法拼接多個數(shù)組右遭,即正確順序的 case 內(nèi)容
                replace = replace.concat(consequent);
            }
        );
        // 替換整個 while 節(jié)點做盅,兩種方法都可以
        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);

參考資料

本文有參考以下資料,也是比較推薦的在線學習資料:

END

Babel 編譯器國內(nèi)的資料其實不是很多远剩,多看源碼、同時在線對照可視化的 AST 語法樹骇窍,耐心一點兒一層一層分析即可瓜晤,本文中的案例也只是最基本操作,實際遇到一些混淆還得視情況進行修改腹纳,比如需要加一些類型判斷來限制等痢掠,后續(xù)K哥會用實戰(zhàn)來帶領大家進一步熟悉解混淆當中的其他操作驱犹。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市足画,隨后出現(xiàn)的幾起案子雄驹,更是在濱河造成了極大的恐慌,老刑警劉巖淹辞,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件医舆,死亡現(xiàn)場離奇詭異,居然都是意外死亡象缀,警方通過查閱死者的電腦和手機蔬将,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來攻冷,“玉大人娃胆,你說我怎么就攤上這事遍希〉嚷” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵凿蒜,是天一觀的道長禁谦。 經(jīng)常有香客問我,道長废封,這世上最難降的妖魔是什么州泊? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮漂洋,結(jié)果婚禮上遥皂,老公的妹妹穿的比我還像新娘。我一直安慰自己刽漂,他們只是感情好演训,可當我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著贝咙,像睡著了一般样悟。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上庭猩,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天窟她,我揣著相機與錄音,去河邊找鬼蔼水。 笑死震糖,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的趴腋。 我是一名探鬼主播试伙,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼嘁信,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了疏叨?” 一聲冷哼從身側(cè)響起潘靖,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蚤蔓,沒想到半個月后卦溢,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡秀又,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年单寂,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吐辙。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡宣决,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出昏苏,到底是詐尸還是另有隱情尊沸,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布贤惯,位于F島的核電站洼专,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏孵构。R本人自食惡果不足惜屁商,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望颈墅。 院中可真熱鬧蜡镶,春花似錦、人聲如沸恤筛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽叹俏。三九已至妻枕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間粘驰,已是汗流浹背屡谐。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蝌数,地道東北人愕掏。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像顶伞,于是被迫代替她去往敵國和親饵撑。 傳聞我的和親對象是個殘疾皇子剑梳,可洞房花燭夜當晚...
    茶點故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容