一、瀏覽器模糊測試簡介
在當前整個計算機網絡環境中,web瀏覽器是應用最廣泛的軟件之一,針對瀏覽器發起的攻擊層出不窮。近年來,瀏覽器安全事件頻發,給人們帶來嚴重的損失。而Fuzzing的自動化屬性以及其表現出的強大性能使得其能高效應用于瀏覽器漏洞發現領域。目前針對于瀏覽器的fuzz技術主要分為兩個類別:基于生成的和基于變異的模糊測試技術。
基于生成的模糊測試技術是最早應用于瀏覽器模糊測試的技術。它主要是通過手工構建或者是嘗試使用一些自動化方法,像上下文無關語法、深度學習的方法構建語法庫模板,以此來生成測試用例。該類方法雖然能夠使得生成的測試用例具有較高的語法正確性,但一方面語法庫的構建成本較高,另一方面該方法很難產生能過夠觸及深層代碼漏洞的復雜輸入。
基于變異的方法,在初始階段主要是采用一種隨機變異的方法來根據現有輸入變異生成一些子代碼片段,以此組合來生成新的測試用例。但是由于瀏覽器解析測試代碼時對代碼的語法和語義上下文極其敏感,因而逐漸衍生出語法感知和語義感知突變技術。目前這兩類技術的研究重點主要在于對瀏覽器的JavaScript引擎的模糊測試,基礎思想都是首先將JS代碼轉換為語法樹AST,再在語法樹上進行相關變異操作。
本文所介紹的工具DIE就是一種使用語法和語義突變技術對JavaScript引擎進行模糊測試的工具。同時,該工具的特點在于:在變異過程中,會保留原始種子輸入的有利性質和條件。這些希望保留的優選屬性稱為aspect。同時對其他部分進行變異,以便可以發現類似的或新的錯誤。
二、工具基本原理概要
01 動機
DIE設計保留屬性變異的前提在于,它認為當一個POC能造成漏洞時,與POC具有相同的結構輸入就有很大可能造成新的漏洞,并且通過在原有POC基礎上進行保留變異,生成的新的代碼就可以在原有的基礎上進入更深層次的代碼片段,具有更高的質量。
下圖所示為兩個相似漏洞的實例。首先這兩個代碼共有的前提條件有:①循環調用opt()函數,以此來啟用JIT;②中都用不同的參數再次調用opt()函數;③則是定義了opt()函數內部的訪問順序。對比這兩個POC,可以看到如果(a)被作為輸入語料庫,我們需要保留①-③中的某些屬性,再添加④就可以發現新的漏洞。

因此DIE工具提出一種方面保持變異——在生成新的測試用例時,隨機地保留了原始種子輸入的有利性質和條件。另一方面保持變異包括兩種變異策略:即結構保持和類型保持。結構保持突變尊重結構方面,如循環或分支;而類型保持突變則保持每個句法元素的類型突變,可以有效降低測試用例在執行過程中的語法和語義錯誤。該工具是2020年發表在S&P期刊上。
論文源碼https://github.com/sslab-gatech/DIE
論文鏈接https://ieeexplore.ieee.org/abstract/
document/9519403
02 基礎架構
DIE工具是在AFL的基礎上構建的。主要變化是在AFL的基礎上引入預處理階段,并將AFL傳統變異更改為在語法樹上進行相關屬性的保留變異。如下圖所示為DIE工具的架構設計,主要分為3個模塊:預處理、種子變異和fuzz執行。
首先①DIE預處理所有原始種子文件,通過動態\靜態分析構建各種子文件所對應的類型化AST。②在fuzz的主循環中選擇一個語料庫中的測試用例及其類型AST。③DIE通過修改\插入新節點變異類型AST,在該過程中保持其類型和結構信息。④將變異后的類型AST轉換回JavaScript文件和執行fuzz。⑤DIE記錄運行時覆蓋反饋信息決定新文件將被保存。并且記錄運行時產生的崩潰信息。

2.1 類型AST構建
類型AST是在傳統AST的基礎上,為每個AST節點拓展出其類型和相關綁定信息。
類型AST的基本構建過程如下:首先DIE會調用babel工具中的各種api接口將JS代碼解析為傳統的AST,然后DIE會在引用標識符(即變量名、函數名等)的語句前對種子文件進行插樁,插樁代碼會跳轉到一個類型解析函數,解析得到標識符的類型。如下圖所示。

通過上述靜態分析我們可以得到AST中標識符等葉節點的基本類型,然后在AST的基礎上,DIE參考ECMA-262標準從下到上靜態地推斷出其他AST節點的類型。ECMA-262規定了在特定表達式或內置API中使用的參數的類型。此外,DIE同樣記錄自定義函數的參數和返回值的類型,以便在新構建的AST節點中進行合法調用。為了完整起見,DIE還為沒有定義值類型的語句標注了相應的描述性類型,如if語句、函數聲明等。
2.2 變異類型AST
對于給定的輸入,DIE以方面保留的方式改變類型AST,在變異過程中,DIE特別避免刪除整個if語句、循環語句和自定義函數定義,這在一定程度上保留了現有JS代碼的結構,這是結構保留變異的基本思想。具體的變異過程如下圖偽代碼所示,具體變異策略主要分為以下兩種:
01
變異一個類型子樹:DIE隨機選擇一個沒有結構作用的子樹AST。然后,子AST被替換為由具有相同類型的構建器構建的新AST(偽代碼第5-6行所示)。
02
插入新語句或者新的變量:DIE定位語句塊(例如,if語句的主體、函數或程序主體部分),并在塊內隨機選擇一個代碼點進行插入。接下來,DIE使用在該點聲明的現有變量生成一個新的表達式語句,或者是生成插入一個新的變量聲明(偽代碼10-15行)。
當選擇突變方法時,DIE優選sub-AST突變和新語句插入。DIE僅在長時間沒有發現新的代碼路徑時才向輸入中引入新的變量。


三、工具運行
01 運行環境與依賴下載
環境:Ubuntu 18.04
相關依賴下載安裝:npm;nodejs;
radis-server;clang;AFL等
具體操作流程詳情可見源碼README文檔;
02 目標JS引擎插樁
(1)下載引擎(以ChakraCore引擎為例):
進入engines目錄:cd ~/DIE/engines
運行命令:
./download-engine.sh ch 1.11.24
./build-ch.sh 1.11.24
(2)修改proxy.py文件:
修改代碼:
line8: this_is_chakra = True
line9: this_is_v8 = False
注釋代碼:
line114: new_cmdline = rewrite(new_cmdline)
(3)利用AFL對引擎插樁:
./build-ch-cov.sh 1.11.24
03 DIE服務端配置
(1)語料庫準備:
項目運行的初始預料來源于已設定的種子庫中,里面存儲了針對于常見的四個JS引擎(ChakraCore/jsc/v8/
firefox)常見的一些POC文件。運行命令如下所示:
cd ~/DIE/
git clone https://github.com/sslab-gatech/DIE-corpus.git
python3 ./fuzz/scripts/make_initial_corpus.py ./DIE-corpus ./corpus
(2)與darid-server建立連接隧道
./fuzz/scripts/redis.py
(3)使用種子庫運行
./fuzz/scripts/populate.sh [target binary path] [path of DIE-corpus dir] [target js engine (ch/jsc/v8/ffx)]
示例:
./fuzz/scripts/populate.sh ./engines/chakracore-1.11.24/out/Debug/ch ./DIE-corpus/ ch
(4)查看新創建的會話corpus
tmux attach -t corpus

04 DIE客戶端配置
(1)執行測試:
./fuzz/scripts/run.sh [target binary path] [path of DIE-corpus dir] [target js engine (ch/jsc/v8/ffx)]
示例:
./fuzz/scripts/run.sh ./engines/chakracore-1.11.24/out/Debug/ch ./DIE-corpus/ ch
(2)查看創建的新會話fuzzer
tmux attach -t fuzzer

四、代碼分析
01 類型AST構建:
DIE在具體實現過程中主要借助于babel工具中的各種api將JS代碼轉換為AST,并在AST上實現遍歷和修改操作。
(1)首先依賴@babel/parser將JS解析為AST;要實現類型化AST,我們只需在原始Node結構中引入一個新的字段,該字段存儲推斷的類型,如下圖所示。

(2)依賴接口@babel/traverse遍歷節點路徑解析得到類型AST,在解析過程中得到結點的類型。類型是由@babel/types定義的,像二進制表達式、邏輯表達式、數組類型、函數聲明、數字、字符串等類型,如果不在這些@babel/types類型中則暫時定義為undefined。
02 測試用例變異與插入:
該部分代碼位于DIE/fuzz/TS/base/estestcase.ts,主要包括了對測試用例結構的相關聲明和變異、插入策略的實現。
(1)預變異premutate()
整體思想為調用this.nodes.set(path.node, number)結構判斷是否對節點進行變異。當number數值為0時意味著跳過該結點跳過變異,當為-1時意味著它的子節點也會跳過變異。同時會對一些結點的變異類型設置一些傾向,具體變異傾向的類型詳情可見代碼DIE/fuzz/TS/base/espreferrnces.ts。以下為針對于各類型的結點的預變異處理:
1)當類型為函數時,不會變異整體函數且對所有函數的參數列表不變異;
a.
當其為函數聲明的時候,像function test(){},不去變異函數的名稱id;
b.
當其為Object或class方法的時候,這時如果它的關鍵字是一個變量或者聲明不對其進行變異;

2)當是變量聲明時,不對變量名稱進行變異;

3)當是全局的函數調用,像main()不進行變異;
4)當是標記語句(case 1:等)、break語句不進行變異;
5)當是catch(e)語句時不對里面的異常e進行變異;
6)當是類或者結構體這種時,不對鍵值key進行變異;
7)當是函數調用表達式時,像a.b(c),傾向于這個結點變異為complexPreference:數組或新表達式、變量、標記符、或者ObjectLike類型。

8)當是賦值表達式時,左邊參數變異的傾向是數組表達式、新的表達式、標志符等;
9)當是賦值匹配時,不對其和左邊表達式進行變異;
10)當是更新表達式,像a++,變異的傾向是:數組或新表達式、標記符、內建程序;
11)當是for ..in 或者for...of結構時,不對其變異;
12)當函數參數是rest參數時,變異時這個結構本身不變,它的名稱可以進行變異。
(2)預插入preInsert()
如果結點類型 是程序主體或者是代碼塊語句{}時,則將該結點,及其路徑的body字段內容成對進行存儲;其中存儲的數組定義為:bodies: Array>;

(3)預訪問preVisit()
按照路徑對其進行遍歷,并進行預變異和預插入的過程;
(4)變異mutate():
首先將節點存儲至位圖,然后隨機設置變異次數,變異次數的范圍在this.MUTATE_MIN范圍內。然后在變異次數范圍內,隨機選取位圖內的結點,調用函數MUTATOR.mutate()進行變異。然后調用函數applyChange()應用變異得到的改變,具體操作是用新結點替換舊結點或者是新的操作符替換舊的操作符。

1)變異函數MUTATOR.mutate():位于DIE/fuzz/Ts/
base/esmutator.ts
首先設置變異改變類型:0表示對結點進行變異;1表示對操作符進行變異,并設置新、舊節點兩個參數。針對于不同類型的結點的變異策略如下:
a.
對于代碼塊結構、函數結構、if和loop結構、函數參數、對象屬性不進行變異;


b.
對于表達式變異:
具體實現函數為mutateExpOp函數,其位于DIE/fuzz/TS/base/engine/exp.ts中,主要包括對二進制、邏輯、更新和一元運算這四類表達式的變異操作,具體操作思想為:修改替換表達式中的操作符。
①二進制表達式:
代碼中定義了二進制表達式的3類操作符,如下圖所示:

然后調用函數mutateOp()對二進制表達式進行變異操作:首先通過解析表達式得到該表達式結點的操作符、左結點和右節點;然后根據操作符類型進行以下變異修改:
如果操作符不等于“+”或者操作符等于“+”且其左右操作數均為數字類型,則新的變異的操作符為任意NumArithOps。
如果操作符是比較類型:如果左右操作數都是數字類型則其操作符變異為任意CompOps。如果左右操作數不是全為數字類型,則新操作符為任意EqualOps.
②邏輯表達式:如果操作符為“&&”,則更改為“||”,否則新操作符為“&&”;
③一元表達式:原操作符為“+”,新操作符變異為“-”;原操作符為“-”則變為“+”;
④更新表達式:如果原操作符為“++”,則更改為“--”,否則替換后的新操作符為“++”;
c.
對于語句statement:
基本思想為生成新的statement語句來代替舊的語句來完成變異;其中新statement的構建依賴于函數build(),位于DIE/fuzz/TS/base/engine/statement.ts。

build()函數基本思想如下所示:
①DIE中生成新的語句主要是指生成以下四類語句:生成Objbased語句、生成新的變量聲明語句、生成構造器語句、生成原型對象prototype語句。然后這四種語句對應編碼為0,1,2,3。并將這四個數按照一定的個數占比存儲至weights中。然后從weights中隨機選取值來生成對應編碼類型的語句。如下圖代碼所示,可以看出生成Objbased語句占比更多。


②genObjBasedStmt():根據結點的類型,選擇生成相對應類型的語句。如下圖代碼所示主要包括以下類型。各類型語句的構建函數在這里不做過多詳細介紹,可在代碼中索引進行查看。
數字類型語句,包括賦值表達式、更新表達式和元表達式;
字符串類型語句,包括賦值表達式、內置對象屬性獲取、內置對象方法調用;
數組構建;
類型數組構建,主要是指一些固定類型的數組,像Int8Array、float32Arrray等;
函數調用語句構建(借助于bazel實現);
內置語句構建,包括內置屬性和內置函數;

③genNewVar():構建新的變量聲明語句,該部分功能主要依賴于@babel/types中的變量構建函數實現;
④genConstructorStmt():構造內置函數調用語句,與genObjBasedStmt()中的內置函數構建類似;
⑤genProtoTypeStmt():暫時不生成該類語句。
(5)插入insert():
插入所需要的語句其生成同上述變異中statement的生成相同。插入的基本思想是不在reutrn語句后面插入;如果是結點是程序體program,則在program下的body字段前插入語句。如果是結點是塊代碼語句,在塊代碼段內部開頭插入語句。實現過程如下圖代碼所示:

03 原生AFL修改部分:
DIE用自己的突變引擎取代了AFL的二進制輸入突變器。在代碼中具體表現為:在afl-fuzz.c中,將主循環中的函數fuzz_one()替換為自定義的fuzz_js()函數。
(1)fuzz_js()函數首先調用自定義的generate_js函數,該函數的作用是:生成經過變異或者插入操作的測試用例,生成的測試用例存儲至fuzz_input_dir中。

generate_js函數:
該函數的主要功能是執行代碼文件esfuzz.js。

esfuzz.ts代碼路徑為DIE/fuzz/TS/esfuzz.ts,主要功能就是對輸入的JS代碼文件執行變異和插入操作。

(2)然后fuzz_js函數調用自定義的fuzz_dir函數。fuzz_dir函數的基本流程是按順序遍歷fuzz_input_dir種子文件,再調用AFL原生函數common_fuzz_stuff()執行fuzz過程。

五、結語
瀏覽器模糊測試主要在于對其各組件的測試,目前重點在對JavaScript引擎的模糊測試。而針對于JavaScript引擎測試的難點又在于對測試用例的生成,相較于傳統變異策略,其需要保持JS本身的語法和語義結構特點,又要嘗試生成高質量復雜的輸入,便于執行到引擎的深層代碼,有助于發現漏洞。本文所介紹的工具DIE就是一方面采用類型保持變異降低了語法和語義錯誤,又采用結構保持變異,保持輸入的種子POC中一些特殊的結構,使得生成的測試用例可以執行到更深的層次。
LemonSec
CNCERT國家工程研究中心
商密君
一顆小胡椒
FreeBuf
安全牛
安全圈
Hacking就是好玩
LemonSec
聚銘網絡
系統安全運維