estools 輔助反混淆 Javascript

wyzsk發表於2020-08-19
作者: 0xCC · 2015/08/07 10:26

0x00 前言


Javascript 作為一種執行在客戶端的指令碼語言,其原始碼對使用者來說是完全可見的。但不是每一個 js 開發者都希望自己的程式碼能被直接閱讀,比如惡意軟體的製造者們。為了增加程式碼分析的難度,混淆(obfuscate)工具被應用到了許多惡意軟體(如 0day 掛馬、跨站攻擊等)當中。分析人員為了掀開惡意軟體的面紗,首先就得對指令碼進行反混淆(deobfuscate)處理。

本文將介紹一些常見的混淆手段和 estools 進行靜態程式碼分析的入門。

0x01 常見混淆手段


加密

這類混淆的關鍵思想在於將需要執行的程式碼進行一次編碼,在執行的時候還原出瀏覽器可執行的合法的指令碼,然後執行之。看上去和可執行檔案的加殼有那麼點類似。Javascript 提供了將字串當做程式碼執行(evaluate)的能力,可以透過Function 構造器evalsetTimeoutsetInterval將字串傳遞給 js 引擎進行解析執行。最常見的是base62 編碼——其最明顯的特徵是生成的程式碼以eval(function(p,a,c,k,e,r))開頭。

base62 編碼的 Javascript

無論程式碼如何進行變形,其最終都要呼叫一次 eval 等函式。解密的方法不需要對其演算法做任何分析,只需要簡單地找到這個最終的呼叫,改為 console.log 或者其他方式,將程式解碼後的結果按照字串輸出即可。自動化的實現方式已經有許多文章介紹過,此處就不再贅述。

隱寫術

嚴格說這不能稱之為混淆,只是將 js 程式碼隱藏到了特定的介質當中。如透過最低有效位(LSB)演算法嵌入到圖片的 RGB 通道、隱藏在圖片 EXIF 後設資料、隱藏在 HTML 空白字元等。

比如這個聳人聽聞的議題:[一張圖片黑掉你:在圖片中嵌入惡意程式]PPT放出來一看,正是使用了最低有效位平面演算法。結合 HTML5 的 canvas 或者處理二進位制資料的 TypeArray,指令碼可以抽取出載體中隱藏的資料(如程式碼)。

最低有效位

隱寫的方式同樣需要解碼程式和動態執行,所以破解的方式和前者相同,在瀏覽器上下文中劫持替換關鍵函式呼叫的行為,改為文字輸出即可得到載體中隱藏的程式碼。

複雜化表示式

程式碼混淆不一定會呼叫 eval,也可以透過在程式碼中填充無效的指令來增加程式碼複雜度,極大地降低可讀性。Javascript 中存在許多稱得上喪心病狂的特性,這些特性組合起來,可以把原本簡單的字面量(Literal)、成員訪問(MemberExpression)、函式呼叫(CallExpression)等程式碼片段變得難以閱讀。

Js 中的字面量有字串、數字、正規表示式

下面簡單舉一個例子。

  • 訪問一個物件的成員有兩種方法——點運算子和下標運算子。呼叫 window 的 eval 方法,既可以寫成 window.eval(),也可以 window['eval']

  • 為了讓程式碼更變態一些,混淆器選用第二種寫法,然後再在字串字面量上做文章。先把字串拆成幾個部分:'e' + 'v' + 'al'

  • 這樣看上去還是很明顯,再利用一個數字進位制轉換的技巧:14..toString(15) + 31..toString(32) + 0xf1.toString(22)

  • 一不做二不休,把數字也展開:(0b1110).toString(4<<2) + (' '.charCodeAt() - 1).toString(Math.log(0x100000000) / Math.log(2)) + 0xf1.toString(11 << 1)

  • 最後的效果:window[(2*7).toString(4<<2) + (' '.charCodeAt() - 1).toString(Math.log(0x100000000) / Math.log(2)) + 0xf1.toString(11 << 1)]('alert(1)')

在 js 中可以找到許多這樣互逆的運算,透過使用隨機生成的方式將其組合使用,可以把簡單的表示式無限複雜化。

0x02 靜態分析實現


解析和變換程式碼

本文對 Javascript 實現反混淆的思路是模擬執行程式碼中可預測結果的部分,編寫一個簡單的指令碼執行引擎,只執行符合某些預定規則的程式碼塊,最後將計算結果替換掉原本冗長的程式碼,實現表示式的簡化。

如果對指令碼引擎直譯器的原理有初步瞭解的話,可以知道直譯器在為了“讀懂”程式碼,會對原始碼進行詞法分析、語法分析,將程式碼的字串轉換為抽象語法樹(Abstract Syntax Tree, AST)的資料形式。

如這段程式碼:

var a = 42; var b = 5; function addA(d) { return a + d; } var c = addA(2) + b;

對應的語法樹如圖:

抽象語法樹

(由 JointJS的線上工具生成)

不考慮 JIT 技術,直譯器可以從語法樹的根節點開始,使用深度優先遍歷整棵樹的所有節點,根據節點上分析出來的指令逐個執行,直到指令碼結束返回結果。

透過 js 程式碼生成抽象語法樹的工具很多,如壓縮器 UglifyJS 帶的 parser,還有本文使用的 esprima

esprima 提供的介面很簡單:

​ var ast = require('esprima').parse(code)

另外 Esprima 提供了一個線上工具,可以把任意(合法的)Javascript 程式碼解析成為 AST 並輸出: http://esprima.org/demo/parse.html

再結合 estools 的幾個輔助庫即可對 js 進行靜態程式碼分析:

  • escope Javascript 作用域分析工具

  • esutil 輔助函式庫,檢查語法樹節點是否滿足某些條件

  • estraverse語法樹遍歷輔助庫,介面有一點類似 SAX 方式解析 XML

  • esrecurse 另一個語法樹遍歷工具,使用遞迴

  • esquery 使用 css 選擇器的語法從語法樹中提取符合條件的節點

  • escodegen與 esprima 功能互逆,將語法樹還原為程式碼

專案中使用的遍歷工具是 estraverse。其提供了兩個靜態方法,estraverse.traverseestraverse.replace。前者單純遍歷 AST 的節點,透過返回值控制是否繼續遍歷到葉子節點;而 replace 方法則可以在遍歷的過程中直接修改 AST,實現程式碼重構功能。具體的用法可以參考其官方文件,或者本文附帶的示例程式碼。

規則設計

從實際遇到的程式碼入手。最近在研究一些 XSS 蠕蟲的時候遇到了類似如下程式碼混淆:

程式碼樣本

觀察其程式碼風格,發現這個混淆器做了這幾件事:

  • 字串字面量混淆:首先提取全部的字串,在全域性作用域建立一個字串陣列,同時跳脫字元增大閱讀難度,然後將字串出現的地方替換成為陣列元素的引用

  • 變數名混淆:不同於壓縮器的縮短命名,此處使用了下劃線加數字的格式,變數之間區分度很低,相比單個字母更難以閱讀

  • 成員運算子混淆:將點運算子替換為字串下標形式,然後對字串進行混淆

  • 刪除多餘的空白字元:減小檔案體積,這是所有壓縮器都會做的事

經過搜尋,這樣的程式碼很有可能是透過 javascriptobfuscator.com的免費版生成的。其中免費版可以使用的三個選項(Encode Strings / Strings / Replace Names)也印證了前面觀察到的現象。

這些變換中,變數名混淆是不可逆的。要是可以智慧給變數命名的工具也不錯,比如這個 jsnice 網站提供了一個線上工具,可以分析變數具體作用自動重新命名。就算不能做到十全十美,實在不行就用人工的方式,使用 IDE(如 WebStorm)的程式碼重構功能,結合程式碼行為分析進行手工重新命名還原。

再看字串的處理。由於字串將會被提取到一個全域性的陣列,在語法樹中可以觀察到這樣的特徵: 在全域性作用域下,出現一個 VariableDeclarator,其 init 屬性為 ArrayExpression,而且所有元素都是 Literal ——這說明這個陣列所有元素都是常量。簡單地將其求值,與變數名(識別符號)關聯起來。注意,此處為了簡化處理,並沒有考慮變數名作用域鏈的問題。在 js 中,作用域鏈上存在變數名的優先順序,比如全域性上的變數名是可以被區域性變數重新定義的。如果混淆器再變態一點,在不同的作用域上使用相同的變數名,反混淆器又沒有處理作用域的情況,將會導致解出來的程式碼出錯。

在測試程式中我設定瞭如下的替換規則:

  • 全域性變數宣告的字串陣列,在程式碼中直接使用數字下標引用其值

  • 結果確定的一連串二元運算,如 1 * 2 + 3 / 4 - 6 % 5

  • 正規表示式字面量的 source,字串字面量的 length

  • 完全由字串常量組成的陣列,其join / reverse / slice 等方法的返回值

  • 字串常量的 substr / charAt 等方法的返回值

  • decodeURIComponent 等全域性函式,其所有引數為常量的,替換為其返回值

  • 結果為常數的數學函式呼叫,如 Math.sin(3.14)

至於縮排的還原,這是 escodegen 自帶的功能。在呼叫 escodegen.generate 方法生成程式碼的時候使用預設的配置(忽略第二個引數)即可。

DEMO 程式

這個反混淆器的原型放在 GitHub 上:https://github.com/ChiChou/etacsufbo

執行環境和使用方法參考倉庫的 README。

從  YOU MIGHT NOT NEED JQUERY上摘抄了一段程式碼,放入 javascriptobfuscator.com 測試混淆:

jsobfuscate.com 混淆樣例

將混淆結果https://github.com/ChiChou/etacsufbo/blob/master/tests/cases/jsobfuscator.com.js進行解開,結果如下:

6-deobfuscated

雖然變數名可讀性依舊很差,但已經可以大體看出程式碼的行為了。

演示程式目前存在大量侷限性,只能算一個半自動的輔助工具,還有許多沒有實現的功能。

一些混淆器會對字串字面量進行更復雜的保護,將字串轉換為 f(x) 的形式,其中 f 函式為一個解密函式,引數 x 為密文的字串。也有原地生成一個匿名函式,返回值為字串的。這種方式通常使用的函式表示式具有上下文無關的特性——其返回值只與函式的輸入有關,與當前程式碼所處的上下文(比如類的成員、DOM 中取到的值)無關。如以下程式碼片段中的 xor 函式:

var xor = function(str, a, b) {

return String.fromCharCode.apply(null, str.split('').map(function(c, i) { var ascii = c.charCodeAt(0); return ascii ^ (i % 2 ? a : b); })); };

如何判斷某個函式是否具有這樣的特性呢?首先一些庫函式可以確定符合,如 btoa,escape,String.fromCharCode 等,只要輸入值是常量,返回值就是固定的。建立一個這樣的內建函式白名單,接著遍歷函式表示式的 AST,若該函式參與計算的引數均沒有來自外部上下文,且其所有 CallExpression 的 callee 在函式白名單內,那麼透過遞迴的方式可以確認一個函式是否滿足條件。

還有的混淆器會給變數建立大量的引用例項,也就是給同一個物件使用了多個別名,閱讀起來非常具有干擾性。可以派出 escope 工具對變數識別符號進行資料流分析,替換為所指向的正確值。還有利用數學的恆等式進行混淆的。如宣告一個變數 a,若 a 為 Number,則表示式 a-aa * 0 均恆為 0。但如果 a 滿足 isNaN(a),則表示式返回 NaN。要清理這類程式碼,同樣需要藉助資料流分析的方法。

目前還沒有見到使用扁平化流程跳轉實現的 js 混淆樣本,筆者認為可能跟 js 語言本身的使用場景和特點有關。一般 js 的代都是偏業務型的,不會有太複雜的流程控制或者演算法,混淆起來效果不一定理想。

0x03 結束語


Javascript 的確是一門神奇的語言,經常可以遇到一些讓人驚訝的奇技淫巧。解密保護過的程式碼也是有趣的事情。據說幾大科技巨頭在醞釀給瀏覽器應用設計一款通用的位元組碼標準——WebAssembly。一旦這個設想得以實現,程式碼保護將可以引入真正意義上的“加殼”或者虛擬機器保護,對抗技術又將提升到一個新的臺階。

演示專案程式碼託管在 GitHub:https://github.com/ChiChou/etacsufbo

0x04 參考資料


  1. http://tobyho.com/2013/12/02/fun-with-esprima/
  2. https://github.com/estree/estree/blob/master/spec.md
  3. https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Parser_API
  4. http://jointjs.com/demos/javascript-ast
本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章