主頁 >  其他 > 逆向進階,利用 AST 技識訓原 JavaScript 混淆代碼

逆向進階,利用 AST 技識訓原 JavaScript 混淆代碼

2022-05-07 07:46:52 其他

什么是 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 為例,

01

AST 在編譯中的位置

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

02

詞法分析

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

03

語法分析

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

04

代碼生成

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

05

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 的以下幾個功能包,本文也僅介紹以下幾個功能包:

  1. @babel/core:Babel 編譯器本身,提供了 babel 的編譯 API;
  2. @babel/parser:將 JavaScript 代碼決議成 AST 語法樹;
  3. @babel/traverse:遍歷、修改 AST 語法樹的各個節點;
  4. @babel/generator:將 AST 還原成 JavaScript 代碼;
  5. @babel/types:判斷、驗證節點的型別、構建新 AST 節點等,

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 運算式,

部分可選引數 options

引數 描述
allowImportExportEverywhere 默認 importexport 宣告陳述句只能出現在程式的最頂層,設定為 true 則在任何地方都可以宣告
allowReturnOutsideFunction 默認如果在頂層中使用 return 陳述句會引起錯誤,設定為 true 就不會報錯
sourceType 默認為 script,當代碼中含有 importexport 等關鍵字時會報錯,需要指定為 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/ 決議出來的語法樹是一樣的:

07

@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 中的位置,如下圖所示:

08

@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 中可以看到對應的型別為 NumericLiteralStringLiteral

09

然后我們宣告了一個 visitor 物件,然后定義對應型別的處理方法,traverse 接收兩個引數,第一個是 AST 物件,第二個是 visitor,當 traverse 遍歷所有節點,遇到節點型別為 NumericLiteralStringLiteral 時,就會呼叫 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 節點,現在增加了一個:

10

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

11

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

可以看到需要 kinddeclarations 兩個引數,其中 declarationsVariableDeclarator 型別的節點組成的串列,所以我們可以先寫出以下 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 物件,如下圖所示:

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 結構:

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

然后繼續構造 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 代碼,就可以看到多了一行新代碼,如下圖所示:

14

常見混淆還原

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

字串還原

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

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

觀察 AST 結構:

15

我們發現 Unicode 編碼對應的是 raw,而 rawValuevalue 都是正常的,所以我們可以將 raw 替換成 rawValuevalue 即可,需要注意的是引號的問題,本來是 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 中的 scopescope 的作用主要是查找識別符號的作用域、獲取并修改識別符號的所有參考等,洗掉未使用變數主要用到了 scope.getBinding() 方法,傳入的值是當前節點能夠參考到的識別符號名稱,回傳的關鍵屬性有以下幾個:

  • identifier:識別符號的 Node 物件;
  • path:識別符號的 NodePath 物件;
  • constant:識別符號是否為常量;
  • referenced:識別符號是否被參考;
  • references:識別符號被參考的次數;
  • constantViolations:如果識別符號被修改,則會存放所有修改該識別符號節點的 Path 物件;
  • referencePaths:如果識別符號被參考,則會存放所有參考該識別符號節點的 Path 物件,

所以我們可以通過 constantViolationsreferencedreferencesreferencePaths 多個引數來判斷變數是否可以被洗掉,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 節點,如下圖所示:

16

AST 處理思路以及代碼:

  1. 篩選出 BooleanLiteralNumericLiteral 節點,取其對應的值,即 path.node.test.value
  2. 判斷 value 值為真,則將節點替換成 consequent 節點下的內容,即 path.node.consequent.body
  3. 判斷 value 值為假,則替換成 alternate 節點下的內容,即 path.node.alternate.body
  4. 有的 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 還原思路:

  1. 獲取控制流原始陣列,將 '3,4,0,5,1,2'['split'](',') 之類的陳述句轉化成 ['3','4','0','5','1','2'] 之類的陣列,得到該陣列之后,也可以選擇把 split 陳述句對應的節點洗掉掉,因為最終代碼里這條陳述句就沒用了;
  2. 遍歷第一步得到的控制流陣列,依次取出每個值所對應的 case 節點;
  3. 定義一個陣列,儲存每個 case 節點 consequent 陣列里面的內容,并洗掉 continue 陳述句對應的節點;
  4. 遍歷完成后,將第三步的陣列替換掉整個 while 節點,也就是 WhileStatement

不同思路,寫法多樣,對于如何獲取控制流陣列,可以有以下思路:

  1. 獲取到 While 陳述句節點,然后使用 path.getAllPrevSiblings() 方法獲取其前面的所有兄弟節點,遍歷每個兄弟節點,找到與 switch() 里面陣列的變數名相同的節點,然后再取節點的值進行后續處理;
  2. 直接取 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/qita/469818.html

標籤:其他

上一篇:C++進階-2-STL初識(容器、演算法、迭代器等)

下一篇:實用的業務邏輯漏洞

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more