最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

AST技術(shù)還原JavaScript混淆代碼(圖文解析代碼解密)

 更新時(shí)間:2025年08月30日 11:19:41   作者:K哥爬蟲  
AST(抽象語法樹)技術(shù)通過解析代碼為樹狀結(jié)構(gòu)(節(jié)點(diǎn)=語法元素,邊=關(guān)系),實(shí)現(xiàn)智能反混淆,該技術(shù)突破混淆層,還原代碼原始邏輯,是分析惡意腳本和安全審計(jì)的核心手段,本文實(shí)例講解AST技術(shù)還原JavaScript混淆代碼(圖文解析代碼解密)

AST還原JavaScript混淆代碼構(gòu)思

AST(抽象語法樹)技術(shù)通過解析代碼為樹狀結(jié)構(gòu)(節(jié)點(diǎn)=語法元素,邊=關(guān)系),實(shí)現(xiàn)智能反混淆:

  • 標(biāo)準(zhǔn)化解析‌:將混淆代碼轉(zhuǎn)換為結(jié)構(gòu)化AST,剝離格式干擾
  • 模式識別‌:檢測混淆特征(如十六進(jìn)制常量、數(shù)組拆解、控制流平坦化)
  • 語義還原‌:
    • 解析字符串編碼(\x68\x65\x6c\x6c\x6f → "hello")
    • 合并碎片化數(shù)組(['a','b']+'cd' → "acd")
    • 重建控制流(粉碎的switch/case → 原始邏輯)
  • 結(jié)構(gòu)優(yōu)化‌:刪除無效代碼、簡化表達(dá)式、還原變量名語義
  • 代碼生成‌:將凈化后的AST輸出為可讀性強(qiáng)的標(biāo)準(zhǔn)代碼

什么是 AST

AST(Abstract Syntax Tree),中文抽象語法樹,簡稱語法樹(Syntax Tree),是源代碼的抽象語法結(jié)構(gòu)的樹狀表現(xiàn)形式,樹上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)。語法樹不是某一種編程語言獨(dú)有的,JavaScript、Python、Java、Golang 等幾乎所有編程語言都有語法樹。

小時(shí)候我們得到一個(gè)玩具,總喜歡把玩具拆解成一個(gè)一個(gè)小零件,然后按照我們自己的想法,把零件重新組裝起來,一個(gè)新玩具就誕生了。而 JavaScript 就像一臺精妙運(yùn)作的機(jī)器,通過 AST 解析,我們也可以像童年時(shí)拆解玩具一樣,深入了解 JavaScript 這臺機(jī)器的各個(gè)零部件,然后重新按照我們自己的意愿來組裝。

AST 的用途很廣,IDE 的語法高亮、代碼檢查、格式化、壓縮、轉(zhuǎn)譯等,都需要先將代碼轉(zhuǎn)化成 AST 再進(jìn)行后續(xù)的操作,ES5 和 ES6 語法差異,為了向后兼容,在實(shí)際應(yīng)用中需要進(jìn)行語法的轉(zhuǎn)換,也會用到 AST。AST 并不是為了逆向而生,但做逆向?qū)W會了 AST,在解混淆時(shí)可以如魚得水。

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

語法樹沒有單一的格式,選擇不同的語言、不同的編譯器,得到的結(jié)果也是不一樣的,在 JavaScript 中,編譯器有 Acorn、Espree、Esprima、Recast、Uglify-JS 等,使用最多的是 Babel,后續(xù)的學(xué)習(xí)也是以 Babel 為例。

AST 在編譯中的位置

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

詞法分析

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

語法分析

語法分析是編譯過程的一個(gè)邏輯階段,語法分析的任務(wù)是在詞法分析的基礎(chǔ)上將單詞序列組合成各類語法短語,比如“程序”,“語句”,“表達(dá)式”等,前面的例子中,isPanda('??') 就會被分析為一條表達(dá)語句 ExpressionStatement,isPanda() 就會被分析成一個(gè)函數(shù)表達(dá)式 CallExpression,??會被分析成一個(gè)變量 Literal 等,眾多語法之間的依賴、嵌套關(guān)系,就構(gòu)成了一個(gè)樹狀結(jié)構(gòu),即 AST 語法樹。

代碼生成

代碼生成是最后一步,將 AST 語法樹轉(zhuǎn)換成可執(zhí)行代碼即可,在轉(zhuǎn)換之前,我們可以直接操作語法樹,進(jìn)行增刪改查等操作,例如,我們可以確定變量的聲明位置、更改變量的值、刪除某些節(jié)點(diǎn)等,我們將語句 isPanda('??') 修改為一個(gè)布爾類型的 Literal:true,語法樹就有如下變化:

Babel 簡介

Babel 是一個(gè) JavaScript 編譯器,也可以說是一個(gè)解析庫,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ù)等,都非常多,本文不一一列舉,在實(shí)際使用過程中,應(yīng)當(dāng)多查詢官方文檔,或者參考文末給出的一些學(xué)習(xí)資料。Babel 的安裝和其他 Node 包一樣,需要哪個(gè)安裝哪個(gè)即可,比如 npm install @babel/core @babel/parser @babel/traverse @babel/generator

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

  • @babel/core:Babel 編譯器本身,提供了 babel 的編譯 API;
  • @babel/parser:將 JavaScript 代碼解析成 AST 語法樹;
  • @babel/traverse:遍歷、修改 AST 語法樹的各個(gè)節(jié)點(diǎn);
  • @babel/generator:將 AST 還原成 JavaScript 代碼;
  • @babel/types:判斷、驗(yàn)證節(jié)點(diǎn)的類型、構(gòu)建新 AST 節(jié)點(diǎn)等。

@babel/core

Babel 編譯器本身,被拆分成了三個(gè)模塊:@babel/parser@babel/traverse、@babel/generator,比如以下方法的導(dǎo)入效果都是一樣的:

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 語法樹,其中主要提供了兩個(gè)方法:

  • parser.parse(code, [{options}]):解析一段 JavaScript 代碼;
  • parser.parseExpression(code, [{options}]):考慮到了性能問題,解析單個(gè) JavaScript 表達(dá)式。

部分可選參數(shù) options

參數(shù)

描述

allowImportExportEverywhere

默認(rèn) importexport 聲明語句只能出現(xiàn)在程序的最頂層,設(shè)置為 true 則在任何地方都可以聲明

allowReturnOutsideFunction

默認(rèn)如果在頂層中使用 return 語句會引起錯(cuò)誤,設(shè)置為 true 就不會報(bào)錯(cuò)

sourceType

默認(rèn)為 script,當(dāng)代碼中含有 importexport 等關(guān)鍵字時(shí)會報(bào)錯(cuò),需要指定為 module

errorRecovery

默認(rèn)如果 babel 發(fā)現(xiàn)一些不正常的代碼就會拋出錯(cuò)誤,設(shè)置為 true 則會在保存解析錯(cuò)誤的同時(shí)繼續(xù)解析代碼,錯(cuò)誤的記錄將被保存在最終生成的 AST 的 errors 屬性中,當(dāng)然如果遇到嚴(yán)重的錯(cuò)誤,依然會終止解析

舉個(gè)例子看得比較清楚:

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/ 解析出來的語法樹是一樣的:

@babel/generator

@babel/generator 可以將 AST 還原成 JavaScript 代碼,提供了一個(gè) 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 得到的是一個(gè)對象,其中的 code 屬性才是最終的 JS 代碼。

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

@babel/traverse

當(dāng)代碼多了,我們不可能像前面那樣挨個(gè)定位并修改,對于相同類型的節(jié)點(diǎn),我們可以直接遍歷所有節(jié)點(diǎn)來進(jìn)行修改,這里就用到了 @babel/traverse,它通常和 visitor 一起使用,visitor 是一個(gè)對象,這個(gè)名字是可以隨意取的,visitor 里可以定義一些方法來過濾節(jié)點(diǎn),這里還是用一個(gè)例子來演示:

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

然后我們聲明了一個(gè) visitor 對象,然后定義對應(yīng)類型的處理方法,traverse 接收兩個(gè)參數(shù),第一個(gè)是 AST 對象,第二個(gè)是 visitor,當(dāng) traverse 遍歷所有節(jié)點(diǎn),遇到節(jié)點(diǎn)類型為 NumericLiteralStringLiteral 時(shí),就會調(diào)用 visitor 中對應(yīng)的處理方法,visitor 中的方法會接收一個(gè)當(dāng)前節(jié)點(diǎn)的 path 對象,該對象的類型是 NodePath,該對象有非常多的屬性,以下介紹幾種最常用的:

屬性

描述

toString()

當(dāng)前路徑的源碼

node

當(dāng)前路徑的節(jié)點(diǎn)

parent

當(dāng)前路徑的父級節(jié)點(diǎn)

parentPath

當(dāng)前路徑的父級路徑

type

當(dāng)前路徑的類型

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

因此在上面的代碼中,path.node.value 就拿到了變量的值,然后我們就可以進(jìn)一步對其進(jìn)行修改了。以上代碼運(yùn)行后,所有數(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!";

如果多個(gè)類型的節(jié)點(diǎn),處理的方式都一樣,那么還可以使用 | 將所有節(jié)點(diǎn)連接成字符串,將同一個(gè)方法應(yīng)用到所有節(jié)點(diǎn):

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é)點(diǎn)的遍歷過程中,進(jìn)入節(jié)點(diǎn)(enter)與退出(exit)節(jié)點(diǎn)都會訪問一次節(jié)點(diǎn),traverse 默認(rèn)在進(jìn)入節(jié)點(diǎn)時(shí)進(jìn)行節(jié)點(diǎn)的處理,如果要在退出節(jié)點(diǎn)時(shí)處理,那么在 visitor 中就必須聲明 exit 方法。

@babel/types

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

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

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

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

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

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

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

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

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

先來處理 id,可以使用 types.identifier() 方法來生成,其源碼為 function identifier(name: string),name 在這里就是 b 了,此時(shí) visitor 代碼就可以這么寫:

然后再來看 init 該如何定義,首先仍然是看 AST 結(jié)構(gòu):

init 為 BinaryExpression 對象,left 左邊是 BinaryExpression,right 右邊是 NumericLiteral,可以用 types.binaryExpression() 方法來生成 init,其源碼如下:

function binaryExpression(
    operator: "+" | "-" | "/" | "%" | "*" | "**" | "&" | "|" | ">>" | ">>>" | "<<" | "^" | "==" | "===" | "!=" | "!==" | "in" | "instanceof" | ">" | "<" | ">=" | "<=",
    left: BabelNodeExpression | BabelNodePrivateName, 
    right: BabelNodeExpression
)

此時(shí) 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 語法樹,查詢對應(yīng)方法應(yīng)該傳入的參數(shù),層層嵌套,直到把所有的節(jié)點(diǎn)都構(gòu)造完畢,最終的 visitor 代碼應(yīng)該是這樣的:

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é)點(diǎn)語句后面加了一句 path.stop(),表示插入完成后立即停止遍歷當(dāng)前節(jié)點(diǎn)和后續(xù)的子節(jié)點(diǎn),添加的新節(jié)點(diǎn)也是 VariableDeclaration,如果不加停止語句的話,就會無限循環(huán)插入下去。

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

常見混淆還原

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

字符串還原

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

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

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

我們發(fā)現(xiàn) Unicode 編碼對應(yīng)的是 raw,而 rawValuevalue 都是正常的,所以我們可以將 raw 替換成 rawValuevalue 即可,需要注意的是引號的問題,本來是 console["log"],你還原后變成了 console[log],自然會報(bào)錯(cuò)的,除了替換值以外,這里直接刪除 extra 節(jié)點(diǎn),或者刪除 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!");

表達(dá)式還原

之前K哥寫過 JSFuck 混淆的還原,其中有介紹 ![] 可表示 false,!![] 或者 !+[] 可表示 true,在一些混淆代碼中,經(jīng)常有這些操作,把簡單的表達(dá)式復(fù)雜化,往往需要執(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 對象進(jìn)行執(zhí)行操作,自動(dòng)計(jì)算出結(jié)果,返回一個(gè)對象,其中的 confident 屬性表示置信度,value 表示計(jì)算結(jié)果,使用 types.valueToNode() 方法創(chuàng)建節(jié)點(diǎn),使用 path.replaceInline() 方法將節(jié)點(diǎn)替換成計(jì)算結(jié)果生成的新節(jié)點(diǎn),替換方法有一下幾種:

  • replaceWith:用一個(gè)節(jié)點(diǎn)替換另一個(gè)節(jié)點(diǎn);
  • replaceWithMultiple:用多個(gè)節(jié)點(diǎn)替換另一個(gè)節(jié)點(diǎn);
  • replaceWithSourceString:將傳入的源碼字符串解析成對應(yīng) Node 后再替換,性能較差,不建議使用;
  • replaceInline:用一個(gè)或多個(gè)節(jié)點(diǎn)替換另一個(gè)節(jié)點(diǎn),相當(dāng)于同時(shí)有了前兩個(gè)函數(shù)的功能。

對應(yīng)的 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";

刪除未使用變量

有時(shí)候代碼里會有一些并沒有使用到的多余變量,刪除這些多余變量有助于更加高效的分析代碼,示例代碼如下:

const a = 1;
const b = a * 2;
const c = 2;
const d = b + 1;
const e = 3;
console.log(d)

刪除多余變量,首先要了解 NodePath 中的 scope,scope 的作用主要是查找標(biāo)識符的作用域、獲取并修改標(biāo)識符的所有引用等,刪除未使用變量主要用到了 scope.getBinding() 方法,傳入的值是當(dāng)前節(jié)點(diǎn)能夠引用到的標(biāo)識符名稱,返回的關(guān)鍵屬性有以下幾個(gè):

  • identifier:標(biāo)識符的 Node 對象;
  • path:標(biāo)識符的 NodePath 對象;
  • constant:標(biāo)識符是否為常量;
  • referenced:標(biāo)識符是否被引用;
  • references:標(biāo)識符被引用的次數(shù);
  • constantViolations:如果標(biāo)識符被修改,則會存放所有修改該標(biāo)識符節(jié)點(diǎn)的 Path 對象;
  • referencePaths:如果標(biāo)識符被引用,則會存放所有引用該標(biāo)識符節(jié)點(diǎn)的 Path 對象。

所以我們可以通過 constantViolations、referencedreferences、referencePaths 多個(gè)參數(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);

        // 如標(biāo)識符被修改過,則不能進(jìn)行刪除動(dòng)作。
        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);

刪除冗余邏輯代碼

有時(shí)候?yàn)榱嗽黾幽嫦螂y度,會有很多嵌套的 if-else 語句,大量判斷為假的冗余邏輯代碼,同樣可以利用 AST 將其刪除掉,只留下判斷為真的,示例代碼如下:

const example = function () {
    let a;
    if (false) {
        a = 1;
    } else {
        if (1) {
            a = 2;
        }
        else {
            a = 3;
        }
    }
    return a;
};

觀察 AST,判斷條件對應(yīng)的是 test 節(jié)點(diǎn),if 對應(yīng)的是 consequent 節(jié)點(diǎn),else 對應(yīng)的是 alternate 節(jié)點(diǎn),如下圖所示:

AST 處理思路以及代碼:

  • 篩選出 BooleanLiteralNumericLiteral 節(jié)點(diǎn),取其對應(yīng)的值,即 path.node.test.value
  • 判斷 value 值為真,則將節(jié)點(diǎn)替換成 consequent 節(jié)點(diǎn)下的內(nèi)容,即 path.node.consequent.body;
  • 判斷 value 值為假,則替換成 alternate 節(jié)點(diǎn)下的內(nèi)容,即 path.node.alternate.body
  • 有的 if 語句可能沒有寫 else,也就沒有 alternate,所以這種情況下判斷 value 值為假,則直接移除該節(jié)點(diǎn),即 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 反控制流平坦化

控制流平坦化是混淆當(dāng)中最常見的,通過 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 還原思路:

  • 獲取控制流原始數(shù)組,將 '3,4,0,5,1,2'['split'](',') 之類的語句轉(zhuǎn)化成 ['3','4','0','5','1','2'] 之類的數(shù)組,得到該數(shù)組之后,也可以選擇把 split 語句對應(yīng)的節(jié)點(diǎn)刪除掉,因?yàn)樽罱K代碼里這條語句就沒用了;
  • 遍歷第一步得到的控制流數(shù)組,依次取出每個(gè)值所對應(yīng)的 case 節(jié)點(diǎn);
  • 定義一個(gè)數(shù)組,儲存每個(gè) case 節(jié)點(diǎn) consequent 數(shù)組里面的內(nèi)容,并刪除 continue 語句對應(yīng)的節(jié)點(diǎn);
  • 遍歷完成后,將第三步的數(shù)組替換掉整個(gè) while 節(jié)點(diǎn),也就是 WhileStatement。

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

  • 獲取到 While 語句節(jié)點(diǎn),然后使用 path.getAllPrevSiblings() 方法獲取其前面的所有兄弟節(jié)點(diǎn),遍歷每個(gè)兄弟節(jié)點(diǎn),找到與 switch() 里面數(shù)組的變量名相同的節(jié)點(diǎn),然后再取節(jié)點(diǎn)的值進(jìn)行后續(xù)處理;
  • 直接取 switch() 里面數(shù)組的變量名,然后使用 scope.getBinding() 方法獲取到它綁定的節(jié)點(diǎn),然后再取這個(gè)節(jié)點(diǎn)的值進(jìn)行后續(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é)點(diǎn)
        let switchNode = path.node.body.body[0];
        // switch 語句內(nèi)的控制流數(shù)組名,本例中是 _0x34e16a
        let arrayName = switchNode.discriminant.object.name;
        // 獲得所有 while 前面的兄弟節(jié)點(diǎn),本例中獲取到的是聲明兩個(gè)變量的節(jié)點(diǎn),即 const _0x34e16a 和 let _0x2eff02
        let prevSiblings = path.getAllPrevSiblings();
        // 定義緩存控制流數(shù)組
        let array = []
        // forEach 方法遍歷所有節(jié)點(diǎn)
        prevSiblings.forEach(pervNode => {
            let {id, init} = pervNode.node.declarations[0];
            // 如果節(jié)點(diǎn) id.name 與 switch 語句內(nèi)的控制流數(shù)組名相同
            if (arrayName === id.name) {
                // 獲取節(jié)點(diǎn)整個(gè)表達(dá)式的參數(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ù)進(jìn)行分割,方法不通用,比如分隔符換成 | 就不行了
                // array = init.callee.object.value.split(',');
            }
            // 前面的兄弟節(jié)點(diǎn)就可以刪除了
            pervNode.remove();
        });

        // 儲存正確順序的控制流語句
        let replace = [];
        // 遍歷控制流數(shù)組,按正確順序取 case 內(nèi)容
        array.forEach(index => {
                let consequent = switchNode.cases[index].consequent;
                // 如果最后一個(gè)節(jié)點(diǎn)是 continue 語句,則刪除 ContinueStatement 節(jié)點(diǎn)
                if (types.isContinueStatement(consequent[consequent.length - 1])) {
                    consequent.pop();
                }
                // concat 方法拼接多個(gè)數(shù)組,即正確順序的 case 內(nèi)容
                replace = replace.concat(consequent);
            }
        );
        // 替換整個(gè) while 節(jié)點(diǎn),兩種方法都可以
        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é)點(diǎn)
        let switchNode = path.node.body.body[0];
        // switch 語句內(nèi)的控制流數(shù)組名,本例中是 _0x34e16a
        let arrayName = switchNode.discriminant.object.name;
        // 獲取控制流數(shù)組綁定的節(jié)點(diǎn)
        let bindingArray = path.scope.getBinding(arrayName);
        // 獲取節(jié)點(diǎn)整個(gè)表達(dá)式的參數(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ù)進(jìn)行分割,方法不通用,比如分隔符換成 | 就不行了
        // let array = init.callee.object.value.split(',');

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

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

traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)

以上代碼運(yùn)行后,原來的 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)的資料其實(shí)不是很多,多看源碼、同時(shí)在線對照可視化的 AST 語法樹,耐心一點(diǎn)兒一層一層分析即可,本文中的案例也只是最基本操作,實(shí)際遇到一些混淆還得視情況進(jìn)行修改,比如需要加一些類型判斷來限制等,后續(xù)K哥會用實(shí)戰(zhàn)來帶領(lǐng)大家進(jìn)一步熟悉解混淆當(dāng)中的其他操作。

到此這篇關(guān)于AST技術(shù)還原JavaScript混淆代碼(圖文解析代碼解密)的文章就介紹到這了,更多相關(guān)AST還原JavaScript混淆代碼內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

您可能感興趣的文章:

相關(guān)文章

  • JS實(shí)現(xiàn)的碰撞檢測與周期移動(dòng)完整示例

    JS實(shí)現(xiàn)的碰撞檢測與周期移動(dòng)完整示例

    這篇文章主要介紹了JS實(shí)現(xiàn)的碰撞檢測與周期移動(dòng),結(jié)合完整實(shí)例形式分析了javascript結(jié)合時(shí)間函數(shù)的頁面元素屬性動(dòng)態(tài)操作及事件響應(yīng)相關(guān)使用技巧,需要的朋友可以參考下
    2019-09-09
  • JavaScript訪問樣式表代碼

    JavaScript訪問樣式表代碼

    在web頁面中,我們經(jīng)常需要通過修改樣式(style)來達(dá)到更好的用戶體驗(yàn),需要的朋友可以參考下。
    2010-10-10
  • JS原型prototype和__proto__用法實(shí)例分析

    JS原型prototype和__proto__用法實(shí)例分析

    這篇文章主要介紹了JS原型prototype和__proto__用法,結(jié)合實(shí)例形式分析了JS原型prototype和__proto__使用方法及相關(guān)操作注意事項(xiàng),需要的朋友可以參考下
    2020-03-03
  • JavaScript實(shí)現(xiàn)文本相似度對比

    JavaScript實(shí)現(xiàn)文本相似度對比

    這篇文章主要介紹了JavaScript實(shí)現(xiàn)文本相似度對比,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下
    2022-06-06
  • JS日期對象簡單操作(獲取當(dāng)前年份、星期、時(shí)間)

    JS日期對象簡單操作(獲取當(dāng)前年份、星期、時(shí)間)

    這篇文章主要介紹了JS日期對象簡單操作,獲取當(dāng)前年份、星期、時(shí)間等代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2016-10-10
  • Webpack框架核心概念(知識點(diǎn)整理)

    Webpack框架核心概念(知識點(diǎn)整理)

    webpack 是一個(gè)現(xiàn)代 JavaScript 應(yīng)用程序的模塊打包器(module bundler)。這篇文章主要介紹了Webpack框架核心概念(知識點(diǎn)整理),需要的朋友可以參考下
    2017-12-12
  • 微信小程序?qū)W習(xí)之自定義滾動(dòng)彈窗

    微信小程序?qū)W習(xí)之自定義滾動(dòng)彈窗

    這篇文章主要給大家介紹了關(guān)于微信小程序?qū)W習(xí)之自定義滾動(dòng)彈窗的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-12-12
  • JavaScript和JQuery的鼠標(biāo)mouse事件冒泡處理

    JavaScript和JQuery的鼠標(biāo)mouse事件冒泡處理

    這篇文章主要介紹了JavaScript和JQuery的鼠標(biāo)mouse事件冒泡處理,本文總結(jié)出了mouse事件的一些定論,并分別給出了JavaScript和JQuery測試代碼,需要的朋友可以參考下
    2015-06-06
  • JS?限時(shí)限次數(shù)點(diǎn)擊按鈕的實(shí)現(xiàn)思路

    JS?限時(shí)限次數(shù)點(diǎn)擊按鈕的實(shí)現(xiàn)思路

    這篇文章主要介紹了JS?限時(shí)限次數(shù)點(diǎn)擊按鈕,實(shí)現(xiàn)方法很簡單需要用一個(gè)變量作為計(jì)數(shù),點(diǎn)擊一次,計(jì)數(shù)加一點(diǎn)擊函數(shù)內(nèi)判斷計(jì)數(shù)變量設(shè)置定時(shí)恢復(fù),對實(shí)例代碼感興趣的朋友一起看看吧
    2022-03-03
  • JavaScript和HTML DOM的區(qū)別與聯(lián)系及Javascript和DOM的關(guān)系

    JavaScript和HTML DOM的區(qū)別與聯(lián)系及Javascript和DOM的關(guān)系

    這篇文章主要介紹了JavaScript和HTML DOM的區(qū)別與聯(lián)系及Javascript和DOM的關(guān)系的相關(guān)資料,需要的朋友可以參考下
    2015-11-11

最新評論

宿州市| 平塘县| 彩票| 日土县| 镇巴县| 来宾市| 南阳市| 石台县| 奉贤区| 沽源县| 黄大仙区| 突泉县| 新源县| 甘泉县| 谷城县| 全椒县| 奇台县| 靖西县| 泰和县| 清原| 蒲城县| 莱西市| 鄂伦春自治旗| 开封市| 鲜城| 博湖县| 合肥市| 通渭县| 嘉定区| 绵阳市| 苗栗市| 泗水县| 贵阳市| 连南| 沂水县| 安岳县| 满城县| 乌兰察布市| 阳信县| 隆安县| 镇远县|