反混淆JavaScript


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.traverse 和estraverse.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

文章轉載自 http://drops.wooyun.org/tips/7713?utm_source=tuicool



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM