亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

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

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

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

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

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

什么是 AST

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

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

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

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

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

AST 在編譯中的位置

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

詞法分析

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

語(yǔ)法分析

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

代碼生成

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

Babel 簡(jiǎn)介

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

Babel 包含的各種功能包、API、各方法可選參數(shù)等,都非常多,本文不一一列舉,在實(shí)際使用過(guò)程中,應(yīng)當(dāng)多查詢(xún)官方文檔,或者參考文末給出的一些學(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 語(yǔ)法樹(shù);
  • @babel/traverse:遍歷、修改 AST 語(yǔ)法樹(shù)的各個(gè)節(jié)點(diǎn);
  • @babel/generator:將 AST 還原成 JavaScript 代碼;
  • @babel/types:判斷、驗(yàn)證節(jié)點(diǎn)的類(lèi)型、構(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 語(yǔ)法樹(shù),其中主要提供了兩個(gè)方法:

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

部分可選參數(shù) options

參數(shù)

描述

allowImportExportEverywhere

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

allowReturnOutsideFunction

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

sourceType

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

errorRecovery

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

舉個(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 語(yǔ)法樹(shù),這和在線(xiàn)網(wǎng)站 https://astexplorer.net/ 解析出來(lái)的語(yǔ)法樹(shù)是一樣的:

@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

嘗試在輸出代碼中使用與源代碼中相同的行號(hào)

接著前面的例子,原代碼是 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;,變量名和值都成功更改了,由于加了壓縮處理,等號(hào)左右兩邊的空格也沒(méi)了。

代碼里 {minified: true} 演示了如何添加可選參數(shù),這里表示壓縮輸出代碼,generate 得到的 result 得到的是一個(gè)對(duì)象,其中的 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è)定位并修改,對(duì)于相同類(lèi)型的節(jié)點(diǎn),我們可以直接遍歷所有節(jié)點(diǎn)來(lái)進(jìn)行修改,這里就用到了 @babel/traverse,它通常和 visitor 一起使用,visitor 是一個(gè)對(duì)象,這個(gè)名字是可以隨意取的,visitor 里可以定義一些方法來(lái)過(guò)濾節(jié)點(diǎn),這里還是用一個(gè)例子來(lái)演示:

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ù)字也有字符串,我們?cè)?AST 中可以看到對(duì)應(yīng)的類(lèi)型為 NumericLiteralStringLiteral

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

屬性

描述

toString()

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

node

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

parent

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

parentPath

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

type

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

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

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

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

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

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

visitor 對(duì)象有多種寫(xiě)法,以下幾種寫(xiě)法的效果都是一樣的:

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!"
        }
    }
}

以上幾種寫(xiě)法中有用到了 enter 方法,在節(jié)點(diǎn)的遍歷過(guò)程中,進(jìn)入節(jié)點(diǎn)(enter)與退出(exit)節(jié)點(diǎn)都會(huì)訪(fǎng)問(wè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;,就可以通過(guò) @babel/types 來(lái)實(shí)現(xiàn)。

首先觀察一下 AST 語(yǔ)法樹(shù),原語(yǔ)句只有一個(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 中各種方法名稱(chēng)和我們?cè)?AST 中看到的是一樣的,只不過(guò)首字母是小寫(xiě)的,所以我們不需要知道所有方法的情況下,也能大致推斷其方法名,只知道這個(gè)方法還不行,還得知道傳入的參數(shù)是什么,可以查文檔,不過(guò)K哥這里推薦直接看源碼,非常清晰明了,以 Pycharm 為例,按住 Ctrl 鍵,再點(diǎn)擊方法名,就進(jìn)到源碼里了:

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

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

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

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

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

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

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

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

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

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

此時(shí) visitor 代碼就可以這么寫(xiě):

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

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

常見(jiàn)混淆還原

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

字符串還原

文章開(kāi)頭的圖中舉了個(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 編碼對(duì)應(yīng)的是 raw,而 rawValuevalue 都是正常的,所以我們可以將 raw 替換成 rawValuevalue 即可,需要注意的是引號(hào)的問(wèn)題,本來(lái)是 console["log"],你還原后變成了 console[log],自然會(huì)報(bào)錯(cuò)的,除了替換值以外,這里直接刪除 extra 節(jié)點(diǎn),或者刪除 raw 值也是可以的,所以以下幾種寫(xiě)法都可以還原代碼:

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哥寫(xiě)過(guò) JSFuck 混淆的還原,其中有介紹 ![] 可表示 false,!![] 或者 !+[] 可表示 true,在一些混淆代碼中,經(jīng)常有這些操作,把簡(jiǎn)單的表達(dá)式復(fù)雜化,往往需要執(zhí)行一下語(yǔ)句,才能得到真正的結(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í)行語(yǔ)句,我們需要了解 path.evaluate() 方法,該方法會(huì)對(duì) path 對(duì)象進(jìn)行執(zhí)行操作,自動(dòng)計(jì)算出結(jié)果,返回一個(gè)對(duì)象,其中的 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:將傳入的源碼字符串解析成對(duì)應(yīng) Node 后再替換,性能較差,不建議使用;
  • replaceInline:用一個(gè)或多個(gè)節(jié)點(diǎn)替換另一個(gè)節(jié)點(diǎn),相當(dāng)于同時(shí)有了前兩個(gè)函數(shù)的功能。

對(duì)應(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í)候代碼里會(huì)有一些并沒(méi)有使用到的多余變量,刪除這些多余變量有助于更加高效的分析代碼,示例代碼如下:

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

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

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

所以我們可以通過(guò) constantViolationsreferenced、references、referencePaths 多個(gè)參數(shù)來(lái)判斷變量是否可以被刪除,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)識(shí)符被修改過(guò),則不能進(jìn)行刪除動(dòng)作。
        if (!binding || binding.constantViolations.length > 0) {
            return;
        }

        // 未被引用
        if (!binding.referenced) {
            path.remove();
        }

        // 被引用次數(shù)為0
        // if (binding.references === 0) {
        //     path.remove();
        // }

        // 長(zhǎng)度為0,變量沒(méi)有被引用過(guò)
        // 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度,會(huì)有很多嵌套的 if-else 語(yǔ)句,大量判斷為假的冗余邏輯代碼,同樣可以利用 AST 將其刪除掉,只留下判斷為真的,示例代碼如下:

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

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

AST 處理思路以及代碼:

  • 篩選出 BooleanLiteralNumericLiteral 節(jié)點(diǎn),取其對(duì)應(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 語(yǔ)句可能沒(méi)有寫(xiě) else,也就沒(méi)有 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)中最常見(jiàn)的,通過(guò) if-else 或者 while-switch-case 語(yǔ)句分解步驟,示例代碼:

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

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

  • 獲取到 While 語(yǔ)句節(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 處理代碼就有兩種寫(xiě)法,方法一:(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 語(yǔ)句內(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 語(yǔ)句內(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'](',') 語(yǔ)句
                array = object[property](argument)
                // 也可以直接取參數(shù)進(jìn)行分割,方法不通用,比如分隔符換成 | 就不行了
                // array = init.callee.object.value.split(',');
            }
            // 前面的兄弟節(jié)點(diǎn)就可以刪除了
            pervNode.remove();
        });

        // 儲(chǔ)存正確順序的控制流語(yǔ)句
        let replace = [];
        // 遍歷控制流數(shù)組,按正確順序取 case 內(nèi)容
        array.forEach(index => {
                let consequent = switchNode.cases[index].consequent;
                // 如果最后一個(gè)節(jié)點(diǎn)是 continue 語(yǔ)句,則刪除 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 語(yǔ)句內(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'](',') 語(yǔ)句
        let array = object[property](argument)
        // 也可以直接取參數(shù)進(jìn)行分割,方法不通用,比如分隔符換成 | 就不行了
        // let array = init.callee.object.value.split(',');

        // switch 語(yǔ)句內(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();

        // 儲(chǔ)存正確順序的控制流語(yǔ)句
        let replace = [];
        // 遍歷控制流數(shù)組,按正確順序取 case 內(nèi)容
        array.forEach(index => {
                let consequent = switchNode.cases[index].consequent;
                // 如果最后一個(gè)節(jié)點(diǎn)是 continue 語(yǔ)句,則刪除 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)行后,原來(lái)的 switch-case 控制流就被還原了,變成了按順序一行一行的代碼,更加簡(jiǎn)潔明了:

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

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

相關(guān)文章

最新評(píng)論