<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          爬蟲進(jìn)階,利用 AST 技術(shù)還原 JavaScript 混淆代碼

          共 8486字,需瀏覽 17分鐘

           ·

          2022-05-23 18:22


          c58da0556ae366486ade86ffa55083c5.webp

          目錄

          文章較長,可作為 AST Babel 入門手冊,強烈建議收藏!

          f3a42cd2af5097bc13be334388f93fd3.webp

          什么是 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 再進(jìn)行后續(xù)的操作,ES5 和 ES6 語法差異,為了向后兼容,在實際應(yīng)用中需要進(jìn)行語法的轉(zhuǎn)換,也會用到 AST。AST 并不是為了逆向而生,但做逆向?qū)W會了 AST,在解混淆時可以如魚得水。

          AST 有一個在線解析網(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 為例。

          19ce187a2d03925e37a25fa1aed9b512.webp

          AST 在編譯中的位置

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

          3c8e3c35019e5e02b46a11519d2d5877.webp

          詞法分析

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

          385b255b88c4ce95d114a01ed93f0e3b.webp

          語法分析

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

          498d7dafa47c47acefd66a35e499ce29.webp

          代碼生成

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

          7808be55a4b2cc9f497e7bac63f10274.webp

          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ù)等,都非常多,本文不一一列舉,在實際使用過程中,應(yīng)當(dāng)多查詢官方文檔,或者參考文末給出的一些學(xué)習(xí)資料。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é)點等。

          5d3f40e1af84711a645e9562cd6e5e2a.webp

          @babel/core

          Babel 編譯器本身,被拆分成了三個模塊:@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 語法樹,其中主要提供了兩個方法:

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

          部分可選參數(shù) options

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

          舉個例子看得比較清楚:

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

          b88b8b96b56bcce43790672545dcd22c.webp

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

          a7dd1d4f95c6f3f819c557f316773794.webp

          @babel/traverse

          當(dāng)代碼多了,我們不可能像前面那樣挨個定位并修改,對于相同類型的節(jié)點,我們可以直接遍歷所有節(jié)點來進(jìn)行修改,這里就用到了 @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 中可以看到對應(yīng)的類型為 NumericLiteralStringLiteral

          d416770dbde7b33c66489474a56b19e4.webp

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

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

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

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

          如果多個類型的節(jié)點,處理的方式都一樣,那么還可以使用 | 將所有節(jié)點連接成字符串,將同一個方法應(yīng)用到所有節(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é)點的遍歷過程中,進(jìn)入節(jié)點(enter)與退出(exit)節(jié)點都會訪問一次節(jié)點,traverse 默認(rèn)在進(jìn)入節(jié)點時進(jìn)行節(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)在增加了一個:

          0bc045b1d2c6a47b19d2cebb82abdcc1.webp

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

          3164431a0615b5b432abdc985d7148ca.webp

          function?variableDeclaration(kind:?"var"?|?"let"?|?"const",?declarations:?Array)

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

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

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

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

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

          259d768349842d8c237e5d6dee47c877.webp

          先來處理 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):

          afd49f5d64ccb859b57bd7d2280b5e55.webp

          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 語法樹,查詢對應(yīng)方法應(yīng)該傳入的參數(shù),層層嵌套,直到把所有的節(jié)點都構(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é)點語句后面加了一句 path.stop(),表示插入完成后立即停止遍歷當(dāng)前節(jié)點和后續(xù)的子節(jié)點,添加的新節(jié)點也是 VariableDeclaration,如果不加停止語句的話,就會無限循環(huán)插入下去。

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

          b7556742f748f7f28c18697b77e2be5e.webp

          常見混淆還原

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

          字符串還原

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

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

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

          dda2639a34786b40e5e22a91fefdfc31.webp

          我們發(fā)現(xiàn) Unicode 編碼對應(yīng)的是 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!");

          表達(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í)行操作,自動計算出結(jié)果,返回一個對象,其中的 confident 屬性表示置信度,value 表示計算結(jié)果,使用 types.valueToNode() 方法創(chuàng)建節(jié)點,使用 path.replaceInline() 方法將節(jié)點替換成計算結(jié)果生成的新節(jié)點,替換方法有一下幾種:

          • replaceWith:用一個節(jié)點替換另一個節(jié)點;
          • replaceWithMultiple:用多個節(jié)點替換另一個節(jié)點;
          • replaceWithSourceString:將傳入的源碼字符串解析成對應(yīng) Node 后再替換,性能較差,不建議使用;
          • replaceInline:用一個或多個節(jié)點替換另一個節(jié)點,相當(dāng)于同時有了前兩個函數(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?<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?`

          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 中的 scopescope 的作用主要是查找標(biāo)識符的作用域、獲取并修改標(biāo)識符的所有引用等,刪除未使用變量主要用到了 scope.getBinding() 方法,傳入的值是當(dāng)前節(jié)點能夠引用到的標(biāo)識符名稱,返回的關(guān)鍵屬性有以下幾個:

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

          所以我們可以通過 constantViolationsreferencedreferencesreferencePaths 多個參數(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)行刪除動作。
          ????????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,判斷條件對應(yīng)的是 test 節(jié)點,if 對應(yīng)的是 consequent 節(jié)點,else 對應(yīng)的是 alternate 節(jié)點,如下圖所示:

          2c0f85dda53bcd67090e17876bd5b8e8.webp

          AST 處理思路以及代碼:

          1. 篩選出 BooleanLiteralNumericLiteral 節(jié)點,取其對應(yīng)的值,即 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 反控制流平坦化

          控制流平坦化是混淆當(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 還原思路:

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

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

          1. 獲取到 While 語句節(jié)點,然后使用 path.getAllPrevSiblings() 方法獲取其前面的所有兄弟節(jié)點,遍歷每個兄弟節(jié)點,找到與 switch() 里面數(shù)組的變量名相同的節(jié)點,然后再取節(jié)點的值進(jìn)行后續(xù)處理;
          2. 直接取 switch() 里面數(shù)組的變量名,然后使用 scope.getBinding() 方法獲取到它綁定的節(jié)點,然后再取這個節(jié)點的值進(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é)點
          ????????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é)點整個表達(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é)點就可以刪除了
          ????????????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é)點整個表達(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é)點
          ????????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);

          參考資料

          本文有參考以下資料,也是比較推薦的在線學(xué)習(xí)資料:

          • Youtube 視頻,Babel 入門:https://www.youtube.com/watch?v=UeVq_U5obnE (作者 Nicolò Ribaudo,視頻中的 PPT 資料可在 K 哥爬蟲公眾號后臺回復(fù) Babel 免費獲取!)
          • 官方手冊 Babel Handbook:https://github.com/jamiebuilds/babel-handbook
          • 非官方 Babel API 中文文檔:https://evilrecluse.top/Babel-traverse-api-doc/

          END

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



          瀏覽 134
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日xxxx| 激情成人五 | 久草视频大香蕉 | 三级网站在线 | 伊人色色影院 |