Dowsing for Overflows: A Guided Fuzzer to Find Buffer Boundary Violations

 

目錄

摘要

1.介紹

2.big picture

2.1運行示例

2.2高級概述

3.尋找候選指令

3.1 Building analysis groups

3.2 Conditions guarding analysis groups

3.3 Scoring array accesses

4.Using tainting to find inputs that matter

4.1 Baseline: dynamic taint analysis

4.2 Field shifting to weed out false dependencies

5 Exploring candidate instructions

5.1 Baseline: concrete + symbolic execution

5.2 Phase 1: learning

Discussion

5.3 Phase 2: hunting bugs

Guided fuzzing 

6 Evaluation

7 相關工作

軟件複雜性度量


摘要

Dowser是一種“引導式”模糊器,它結合了污點跟蹤、程序分析和符號執行,以發現隱藏在程序邏輯深處的緩衝區溢出和下溢漏洞。關鍵的思想是,對程序的分析使我們能夠精確地找出程序代碼中要探測的正確區域,以及此探測需要的適當輸入。

直觀地說,對於典型的緩衝區溢出,我們只需要考慮循環中訪問數組的代碼,而不需要考慮程序中所有可能的指令。在找到所有這些候選指令集之後,我們根據對它們包含有趣漏洞的可能性的估計對它們進行排序。然後,我們對最有希望的集合進行進一步的測試。具體地說,我們首先使用污點分析來確定哪些輸入字節影響數組索引,然後使用符號化輸入

符號執行程序。通過不斷地沿着最有可能導致溢出的分支結果引導符號執行,我們能夠檢測出實際程序中的深層bug(比如nginx webserver、inspircd IRC服務器和ffmpeg視頻播放器)。我們發現的兩個bug是之前在ffmpeg和poppler PDF呈現庫中沒有文檔說明的緩衝區溢出。

1.介紹

我們將討論Dowser,一種結合污點跟蹤、程序分析和符號執行的“引導式”模糊器,以發現隱藏在程序邏輯深處的緩衝區溢出錯誤。緩衝區溢出經常出現在最危險的軟件錯誤[12]的前3位中,最近的研究表明這種情況不會很快改變[41,38]。有兩種方法來處理它們。我們要麼使用內存保護程序來加強軟件,當溢出發生時(在運行時)終止程序,要麼在發佈軟件之前跟蹤漏洞(例如,在測試階段)。

內存保護器包括一些常見的解決方案如影子堆棧(當調用的時候將返回地址存儲在另一個地方,當程序返回時比較堆棧中的返回地址和存儲在別的地方的返回地址比較,如果不同將導致崩潰)和canaries[11],以及更復雜的編譯器擴展,如WIT[3]。它們可以有效地防止程序被利用,但它們本身並不能消除溢出錯誤。雖然崩潰比允許使用要好,但是崩潰也是不可取的!

因此,供應商更喜歡事先消除bug,通常會通過模糊測試儘可能多地找到bug。Fuzzers向程序提供無效、意外或隨機數據,以查看它們是否導致程序有崩潰或表現出意外行爲。例如,微軟爲每種產品的每一個不受信任的界面強制使用模糊測試,而他們的模糊測試解決方案自2008年7月24日以來一直運行,總共運行了400多次的[18]。

不幸的是,大多數模糊器的有效性都很差,結果很少超出淺層bug的範圍。大多數fuzzer採用“黑盒”方法,它只關注輸入格式,而忽略測試的軟件目標。Blackbox fuzzing是一種流行且快速的方法,但它忽略了許多相關的代碼路徑,因此也會產生許多bug。黑盒模糊有點像在黑暗中射擊:你必須幸運地擊中任何有趣的東西。

在[18,7,10]中實現的Whitebox fuzzing更有原則。通過符號執行,探索在程序中執行所有可能的執行路徑,從而發現所有可能的bug——儘管這可能需要幾年的時間發展。由於全符號執行速度較慢,且不能擴展到大型程序,因此很難使用它來發現大型程序中的複雜bug[7,10]。在實踐中,目標是首先覆蓋儘可能多的獨特代碼。因此,除非在非常簡單的情況下,否則很難觸發需要程序多次執行相同代碼的bug(比如緩衝區溢出)。

由符號執行提供的最終完整性既是優點也是缺點,在本文中,我們將評估完全相反的策略。我們不是測試所有可能的執行路徑,而是對一小部分代碼區域執行抽查,這些代碼區域看起來可能是緩衝區溢出錯誤的候選區域,然後依次測試每個代碼區域。

這種方法的缺點是,我們以迭代的方式爲每個候選代碼區域執行符號運行。此外,我們只能在可以執行的循環中發現緩衝區溢出。另一方面,通過直接瞄準有希望的代碼區域,我們大大加快了搜索速度,並設法在實際程序中找到了一些複雜的bug,而這些bug對於大多數現有的模糊器來說是很難找到的。

Contributions:我們給自己設定的目標是開發一種有效的模糊器,它可以主動地直接搜索緩衝區溢出。其中的關鍵是,對程序的仔細分析使我們能夠精確地找到探測的正確位置和適當的輸入。主要的貢獻是,我們的fuzzer直接放大這些緩衝區溢出候選項,並在符號執行中探索一種新的“抽樣檢查”方法。

要使這一方法發揮作用,我們需要應對兩項主要挑戰。第一個挑戰是如何控制程序的執行以增加發現漏洞的機會。Whitebox fuzzers“盲目”地試圖執行儘可能多的程序,希望最終能碰到bug。相反,Dowser使用有關目標程序的信息來識別最容易受到緩衝區溢出影響的代碼。

例如,緩衝區溢出(主要)發生在訪問循環中的數組的代碼中。因此,我們尋找這樣的代碼並忽略程序中其餘的大部分指令。此外,Dowser對程序執行靜態分析來對這些訪問進行排序。我們將評估不同的排序函數,但是到目前爲止,最好的一個根據複雜度對數組訪問進行排序。直觀的感覺是,使用卷積指針算法和/或複雜控制流的代碼比直接的數組訪問更容易出現內存錯誤。此外,通過關注這些代碼,Dowser優先處理複雜的bug——通常是靜態分析或隨機模糊無法發現的那種漏洞。這樣做的目的是減少浪費在淺層bug上的時間,這些bug也可以通過現有方法找到。儘管如此,其他排序方式也是可能的,Dowser完全不知道使用哪種排序函數。

我們要解決的第二個挑戰是如何將程序的執行引導到這些“有趣的”代碼區域。作爲基線,我們使用動態符號執行[43]:具體值執行和符號值執行的組合,其中具體的(固定的)輸入從符號執行開始。在Dowser中,我們通過兩個優化來增強動態符號執行。

首先,提出了一種新的路徑選擇算法。正如我們前面看到的,傳統的符號執行的目標是代碼覆蓋—最大化所執行的單個分支的比例[7,18]。相反,我們的目標是所選代碼片段的指針值覆蓋率。當Dowser檢查一個有趣的指針取消引用時,它將沿着可能更改指針值的分支引導符號執行。

其次,我們儘可能減少符號輸入的數量。具體來說,Dowser使用動態污點分析來確定哪些輸入字節影響用於數組訪問的指針。稍後,它只將這些輸入視爲符號。雖然污點分析本身並不新鮮,但是我們引入了新的優化,以達到一組儘可能精確的符號輸入(既不需要太少的符號字節,也不需要太多的符號字節)。

總之,Dowser是一種新的fuzzer,它針對的是那些希望測試其代碼的緩衝區溢位和溢位的供應商。我們在LLVM pass實現了對下行器的分析,而符號執行步驟使用了S2E[10]。最後,Dowser是一個實用的解決方案。它不是針對所有可能的安全bug,而是專門針對緩衝區溢出類(對於代碼注入來說,如果不是最重要的攻擊向量類,也是最重要的攻擊向量類之一)。到目前爲止,Dowser在nginx、ffmpeg和inspircd等複雜程序中發現了一些真正的bug。在現有的符號執行工具中,很難找到它們中的大多數。

假設和大綱:在本文中,我們假設我們有一個測試套件,它允許我們訪問數組。我們無法測試我們無法達到的指令。剩餘部分,我們從一個大的,正在運行的例子開始(第二節)。然後,我們依次討論Dowser的三個主要組成部分:選擇有趣的代碼片段(第三節),使用動態污點分析,以確定哪些輸入影響候選人指令(4節),和我們的方法推動該計劃引發錯誤符號執行期間(第五節)。我們在第六節評估系統,在第七節討論相關的項目。我們在第8節中結束。

2.big picture

Dowser的主要目標是操作指令用來訪問循環中的數組的指針,希望強制緩衝區溢出或不足。

2.1運行示例

nginx中的緩衝區欠運行漏洞

int ngx_http_parse_complex_uri(ngx_http_request_t *r)
{
    state = sw_usual;
    u_char* p = r->uri_start;   // user input
    u_char* u = r->uri.data; // store normalized uri here
    u_char ch = *p++;            // the current character
    while (p <= r->uri_end) {
        switch (state) {
               case sw_usual:
                  if (ch == '/')
                      state = sw_slash; *u++ = ch;
                  else if /* many more options here */
                  ch = *p++; break; 
               case sw_slash:
                  if (ch == '/')
                      *u++ = ch;
                  else if (ch == '.')
                       state = sw_dot; *u++ = ch;
                  else if /* many more options here */
                  ch = *p++; break; 
               case sw_dot:
                  if (ch == '.')
                       state = sw_dot_dot; *u++ = ch;
                  else if /* many more options here */
                  ch = *p++; break; 
               case sw_dot_dot:
                  if (ch == '/')
                      state = sw_slash; u -=4;
                      while (*(u-1) != '/') u--;
                  else if /* many more options here */
                  ch = *p++; break;
           }
     }
 } 

 Nginx是一家網絡服務器公司,在全球最繁忙的100萬個網站中,其市場份額排名第三。在撰寫本文時,它在全球擁有大約2200萬個域名。0.6.38之前的版本有一個特別糟糕的漏洞[1]。當nginx接收到HTTP請求時,解析函數nginx HTTP parse complex uri首先在p=r->uri_start(第4行)中規範化uri路徑,將結果存儲在由u=r->uri.data指向的堆緩衝區中。while-switch實現了一個狀態機,它每次消耗一個字符的輸入,並將其轉換爲u中的規範形式。當提供一個精心設計的路徑時,nginx錯誤地將u的開頭設置爲r->uri.data下面的某個位置。假設uri是“//../foo”。當p達到“/foo”時,u指向(r->uri.data+4),狀態爲sw_dot_dot(第30行)。該例程現在將u減少4(第32行),因此它指向r->uri.data。只要r->uri.data下面的內存不包含字符“/”,u就會進一步減小(第33行),即使它跨越了緩衝區邊界。最後,用戶提供的輸入(“foo”)被複制到u指向的位置。在這種情況下,覆蓋的緩衝區包含一個指向函數的指針,該函數最終將被nginx調用。因此,該漏洞允許攻擊者修改函數指針,並在系統上執行任意程序。這是一個複雜的bug,很難用現有的解決方案找到它。依賴於符號輸入的許多條件語句對於符號執行來說是有問題的,而依賴於輸入的間接跳轉對於靜態分析來說也是一個糟糕的匹配。

在本文中,我們將使用圖1中的函數來說明Dowser是如何工作的。該示例是nginx-0.6.32 web服務器中緩衝區未運行漏洞的簡化版本。一個特別設計的輸入欺騙程序將u指針設置爲緩衝區邊界之外的位置。當該指針稍後用於訪問內存時,它允許攻擊者重寫函數指針,並在系統上執行任意程序。

圖1只顯示了原始函數的一段摘錄,該函數實際上包含大約400行C代碼。它在switch語句中包含許多附加選項,以及一些嵌套的條件if語句。這種複雜性嚴重阻礙了靜態分析工具和符號執行引擎對bug的檢測。例如,當我們將S2E[10]一直引導到易受攻擊的函數,並僅將HTTP消息的7字節長uri路徑設置爲符號時,跟蹤這個有問題的場景花費了60多分鐘。在實踐中,需要一個更可伸縮的解決方案。沒有這些提示,S2E在長達8小時的執行過程中根本沒有發現這個bug。相比之下,Dowser不到5分鐘就找到了。

S2E中分析成本高的主要原因是依賴於(符號)輸入的大量條件分支。對於每個分支,符號執行首先檢查條件或其否定是否可滿足。當兩個分支都可行時,默認行爲是檢查兩個分支。這個過程導致路徑的指數增長。

這個真實的例子顯示了(1)需要將強大而昂貴的符號執行集中在最有趣的情況上,(2)做出明智的分支選擇,(3)最小化符號數據量。

2.2高級概述

圖2展示了總體探測體系結構。首先,它對目標程序執行數據流分析,並對循環1中訪問緩衝區的所有指令進行排序。雖然我們可以用不同的方式對它們進行排序,而且Dowser不知道我們使用的排序函數,但是我們目前的經驗是,對複雜性的估計效果最好。具體地說,我們將計算和複雜條件的等級排列得比簡單條件高。在圖1中,u涉及三個不同的操作,即, u++, u-,和u-=4,在一個循環中的多個指令中。我們將看到,這些複雜的計算將u的取消引用放在nginx中最複雜的指針訪問的前3%中。

在第二步2中,Dowser反覆選擇高級訪問,並選擇測試輸入來執行它們。然後,它使用動態污點分析來確定哪些輸入字節影響候選指令中取消引用的指針。這個想法是,給定的格式輸入,Dowser fuzz(即當作符號),只有那些字段,影響潛在的脆弱的內存訪問,並讓剩下的不變。在圖1中,我們瞭解到將HTTP請求中的uri路徑視爲符號就足夠了。實際上,脆弱函數內部的計算獨立於輸入消息的其餘部分。

接下來的3步,對於計算數組指針所涉及的每個候選指令和輸入字節,Dowser使用符號執行來嘗試將程序推向溢出緩衝區的方向。具體來說,我們符號執行包含候選指令的循環(因此應該對緩衝區溢出進行測試)——只將相關字節作爲符號處理。我們將看到,一種新的路徑選擇算法有助於引導執行快速達到可能的溢出。

最後,我們檢測可能發生的任何溢出。就像在whitebox fuzzers中一樣,我們可以使用任何技術來做到這一點(例如Purify、Valgrind[30]或BinArmor[37])。在我們的工作中,我們使用谷歌的addresssanifier[34] 4。它利用受保護的程序來確保內存訪問指令永遠不會讀或寫所謂的“中毒”紅區。紅色區域是插入到任何兩個堆棧、堆或全局對象之間的內存小區域。由於程序永遠不應該處理它們,因此對它們的訪問表明存在非法行爲。該策略檢測順序緩衝區溢出和溢出,以及一些更復雜的指針損壞bug。這種技術在搜索新bug時非常有用,因爲它還會在靜默故障時觸發,而不僅僅是應用程序崩潰。在nginx的例子中,addresssanifier檢測u指針讀取緩衝區邊界之外的內存時的下溢(第33行)。

我們在第3節解釋了第1步(靜態分析),第4節解釋了第2步(污染分析),第5節解釋了第3步(引導執行)。

3.尋找候選指令

以前的研究表明,從軟件構件中收集的軟件複雜度度量有助於發現易受攻擊的代碼組件[16,44,35,32]。然而,儘管複雜性度量可以作爲有用的指標,但它們也存在精度低或召回值低的問題。此外,目前的大多數方法都是在模塊或文件的粒度上操作的,這對於Dowser中的定向符號執行來說太粗糙了。正如Zimmermann等人觀察到的[44],我們需要利用漏洞的獨特特徵的度量,例如緩衝區溢出或整數溢出。原則上,Dowser可以使用任何能夠對訪問循環緩衝區的指令組進行排序的度量。所以,問題是如何設計一個好的複雜度度量來滿足這個標準?在本節的其餘部分中,我們將介紹這樣一種度量:一種基於啓發式的方法,它是專門爲檢測潛在緩衝區溢出漏洞而設計的。

我們利用了複雜緩衝區溢出背後的一個主要實用原因:程序員很難理解複雜的指針計算。因此,我們主要關注在循環內部實現的“複雜”數組訪問。此外,我們將分析限制在與循環歸納變量一起演化的指針上,即,以訪問數組的(各種)元素。

使用這個度量,Dowser通過評估數組索引(指針)計算中涉及的數據流和控制流的複雜性來對緩衝區訪問進行排序。每個循環的程序時,它首先靜態地確定(1)所涉及的所有指令修改數組指針(我們將稱之爲一個指針的分析組),和(2)保護這個分析組的條件,例如,包含數組索引計算的if或while語句的條件。接下來,它用反映其複雜性的分數給所有這些集合貼上標籤。我們將在3.1、3.2和3.3節中詳細解釋這些步驟。

圖3:圖1中與u指針相關聯的數據流圖和分析組。爲了清晰起見,圖中用僞代碼表示指針算術指令。PHI節點表示從不同的控制流合併數據的位置。方框中的數字表示Dowser分配的點。

3.1 Building analysis groups

假設一個指針p在一個循環中涉及到一個“有趣的”數組訪問指令accp。與accp關聯的分析組AG(accp)收集在循環執行期間影響取消引用指針值的所有指令。

爲了確定AG(accp),我們計算一個過程內的數據流圖,該數據流圖表示循環中的操作,計算accp中取消引用的p的值。然後,我們檢查這個圖是否包含循環。循環表示p在前一個循環迭代中的值影響當前循環迭代中的值,因此p依賴於循環誘導變量。

如前所述,我們的這部分工作構建在LLVM[23]編譯器基礎設施之上。LLVM提供的靜態單賦值(SSA)表單直接轉換爲數據流圖。圖3顯示了一個示例。注意,由於指針u的所有解引用都共享它們的數據流圖,所以它們也形成一個單獨的分析組。因此,當Dowser稍後試圖在這個分析組中找到一個非法的數組訪問時,它會同時測試所有的取消引用——沒有必要單獨考慮它們。

3.2 Conditions guarding analysis groups

與數組指針關聯的數據流可能很簡單,但是由於一些複雜的控件更改,指針的值很難跟隨。因此,探測者等級也控制着流量:影響分析組的條件。假設操作數組指針p的指令由變量var上的條件保護,例如if(var<10){*p++=0;}。如果var的值難以跟蹤,那麼p的值也難以跟蹤。爲了評估var的複雜性,Dowser分析其數據流,並確定分析組AG(var)(如3.1節所述)。此外,我們還遞歸地分析了循環內影響var和p的其他變量的分析組。因此,我們得到了一些分析組,我們將在下一個步驟中對它們進行排序(第3.3節)。

3.3 Scoring array accesses

對於在循環中實現的每個數組訪問,Dowser評估3.1和3.2節中構建的分析組的複雜性。對於每個分析組,它考慮所有指令,併爲它們分配點數。一個積分制的AG分數越多,它就越複雜。數組訪問的總秩由得分的最大值決定。直觀地說,它反映了最複雜的組件。

評分算法應該爲語義相同的代碼提供大致相同的結果。因此,我們強制執行LLVM編譯器中提供的優化(例如,消除常見的子表達式)。這樣,我們就可以最小化編譯器選項所產生的指令量的差異。此外,我們還分析了LLVM代碼生成策略,並定義了一組功能強大的等價規則,這些規則最小化了分配給語法不同但語義等價的代碼的分數的變化。我們在下面突出顯示它們。

表1介紹了所有類型的指令,並討論了它們對最終得分的影響。原則上,除了我們認爲有風險的兩條指令:指針強制轉換和返回指針計算中使用的非指針值的函數之外,數組索引計算中涉及的所有常見指令的順序都是10個點。

每種類型的指令的絕對懲罰不是很重要。但是,我們確保這些點反映了不同代碼片段之間複雜性的差異,而不是給所有數組訪問相同的分數。也就是說,使數組索引複雜化的指令會對得分有貢獻,而使索引非常複雜的指令相對於其他指令的得分也非常高。在第6節中,我們將我們的複雜性排名與備選方案進行比較。

表1:指針算術操作中涉及的指令及其懲罰點的概述。
指令 基本原理/等價規則
數組索引操作
基本索引算法,即加減法 GetElemPtr(按索引增加或減少指針)得分相同。因此,指針上的操作等同於偏移量上的操作。如果一條指令修改了一個未傳遞給下一個循環迭代的值,則該指令得分爲1。 1 or 5
其他索引算術instr。例如,除法、移位或異或 這些指令涉及到比標準的添加或子指針計算更復雜的指針計算,因此,我們對它們進行了更多的懲罰。 10
不同的常量值 用於修改指針的多個常量使其值難以跟隨。跟蹤總是以相同值遞增的指針更容易。 每個值10分
用於訪問結構字段的常量 我們假設編譯器正確地處理對結構的訪問。我們只考慮用於計算數組索引的常量,而不考慮字段的地址。 0
在循環外確定的數值 雖然在循環上下文中它們只是常量,但是編譯器不能預測它們的值。因此,它們很難推理,更容易出錯。 30
返回非指針值的非內聯函數 由於將指針的計算與其使用解耦可能很容易導致錯誤,因此我們將嚴重懲罰這種操作。 500
數據轉移指令 移動(標量或指針)數據不會增加計算的複雜性。 0
高級操作
加載循環外計算的指針 它表示檢索對象的基本指針,或使用內存分配器。我們以同樣的方式對待所有遠程指針——所有的得分都爲0。 0
GetElemPtr 從基和偏移量計算指針的LLVM指令。 1或5
指針類型操作 由於轉換指令經常指示不等同於標準指針操作的操作(上面列出的),因此值得仔細檢查。 100

4.Using tainting to find inputs that matter

一旦Dowser按複雜度對循環中的數組訪問進行了排序,我們將依次檢查它們。通常,只有一小部分的輸入影響的執行一個特定的分析小組,所以我們要尋找一個錯誤僅僅通過修改這部分的輸入,同時保持其他常數(參見第五節)。在當前的部分中,我們說明Dowser識別程序的組件輸入之間的聯繫和不同的分析。注意,這個結果也有利於基於模糊的其他bug發現工具,而不僅僅是Dowser和動態符號執行。

我們將討論重點放在與數組指針引用accp關聯的分析組AG(accp)上。我們假設我們可以獲得一個測試輸入I,它用於測試潛在的易受攻擊的分析組。雖然這可能並不總是正確的,但我們相信這是一個合理的假設。大多數供應商都有測試套件來測試他們的軟件,並且它們通常包含至少一個輸入來測試每個複雜的循環。

4.1 Baseline: dynamic taint analysis

作爲一種基本方法,Dowser對輸入I執行動態污點分析(DTA)[31](用唯一的顏色污染每個輸入字節,並在數據移動和算術操作上傳播顏色)。然後,它記錄在AG(accp)指令中涉及的所有顏色和輸入字節。給定輸入的格式,Dowser將這些字節映射到各個字段。在圖1中,Dowser將uri視爲符號就足夠了。

如上所述,DTA的問題是它完全忽略了隱式流(也稱爲控制依賴關係)[14,21]。這樣的流沒有將受污染的值直接分配給變量——變量將由DTA傳播。相反,變量的值完全由條件下受污染變量的值決定。在圖1中,即使第12行u的值依賴於第11行受污染的字符ch,污染也不會直接流向u,因此DTA不會報告這種依賴關係。隱式流是出了名的難以跟蹤[36,9],但是忽略它們完全降低了我們的準確性。因此,Dowser採用了一種基於Bao等人的[6]工作的解決方案,但是採用了一種新的優化來提高分析的準確性(第4.2節)。

與Bao等人的[6]一樣,Dowser實現了嚴格的控制依賴關係。直觀地說,我們只在信息最豐富(或信息保存)的依賴項上傳播顏色。具體來說,我們需要在受污染的變量和編譯時常量之間進行直接比較。例如,在圖1中,我們將第11行ch的顏色傳播到變量state,並將u傳播到第12行。但是,如果第11行中的條件是“if(ch!= ' / ')”或“if(ch< ' / ')”,我們將保持狀態和u不受污染。由於隱式流不是本文的重點,所以我們希望有興趣的讀者參考[6]瞭解更多細節

4.2 Field shifting to weed out false dependencies

圖4:圖中顯示了Dowser如何打亂輸入,以確定哪些字段真正影響分析組。假設一個解析器逐個提取輸入的字段,並且分析組依賴於字段B和D(分別使用顏色B和D)。處理程序中的顏色顯示後續處理程序嚴格依賴於哪些字段,陰影矩形表示傳播到分析組的顏色。排除在外的顏色被排除在我們的分析之外。

 如上所述,Dowser改進了Bao等人對嚴格控制依賴關係的處理。當輸入格式中的字段順序不固定時,就會出現問題,例如HTTP、SMTP(以及大多數程序的命令行)。來自[6]的方法可能錯誤地認爲字段依賴於到目前爲止提取的所有字段。

例如,lighttpd在循環中讀取新的頭字段,並將它們與各種選項進行比較,大致如下:

while()
{
    if(cmp(field, "Content") == 0)
        ...
    else if(cmp(field, "Range") == 0)
        ...
    else exit (-1);
    field = extract_new_header_field(); 
}

當解析器測試等價性時,隱式流將從一個字段傳播到下一個字段,即使根本沒有真正的依賴關係!最後,最後一個字段似乎依賴於整個頭部。

Dowser通過移動順序不固定的字段來確定哪些選項對分析組中的指令真正重要。參見圖4,假設我使用A,B, C, D和E五個選項運行程序和我們的分析組實際上取決於B和D .一旦消息被處理,我們看到AG並不依賴於E, E可以排除在進一步分析。由於最後觀察到的顏色,D,對AG有直接的影響,它是一個真正的依賴關係。通過對D、A、B、C、E的順序進行循環移位,Dowser只找到與A、B、D對應的顏色。在下一個循環移動之後,Dowser將顏色減少爲B和D。

優化是基於兩個觀察:(1)最後一個傳播到AG的場對AG有直接影響,需要保持,(2)超過這個場的所有場保證對AG沒有影響。通過執行循環移位,並在更新的輸入上運行DTA, Dowser消除了不必要的依賴。

儘管這種優化需要對輸入有一些最小的瞭解,但我們不需要完全理解輸入語法,比如字段的內容或效果。識別順序不固定的字段就足夠了。幸運的是,這些信息對於許多應用程序都是可用的——特別是當供應商測試他們自己的代碼時。

5 Exploring candidate instructions

一旦我們知道了程序輸入的哪一部分影響分析組AG(accp),我們就對這一部分進行模糊處理,並試圖以非法的方式推動程序使用指針p。更嚴格地說,我們將輸入中有趣的組件視爲符號,其餘部分視爲固定的(具體的),並象徵性地執行與AG(accp)關聯的循環。

然而,由於整個循環遍歷的成本在原則上是指數級的,因此循環是符號執行[19]最困難的問題之一。因此,在分析循環時,我們嘗試選擇在上下文中最有希望的路徑。具體來說,Dowser優先選擇顯示可能出現複雜指針算法的路徑。正如我們在第6節中所示,我們的技術顯著地優化了溢出的搜索。

Dowser的循環探測過程有兩個主要階段:學習和bug發現。在學習階段,Dowser爲循環中的每個分支分配一個權值,該權值近似於沿着這個方向的路徑包含新指針引用的概率。權重基於在執行短符號輸入期間觀察到的指針值的變化的統計數據。

接下來,在bug查找階段,Dowser使用第一步中確定的權重來過濾循環中不感興趣的部分,並對重要的路徑進行優先級排序。當與某個分支關聯的權重爲0時,Dowser甚至不會進一步探索它。在脆弱的nginx解析循環中(圖1顯示了其中的一段摘錄),60個分支中只有19個得到非零的值,因此考慮執行。在這個階段,符號輸入代表一個真實的場景,因此它相對較長。因此,使用流行的符號執行工具進行分析將非常昂貴。

在第5.1節中,我們簡要回顧了動態符號執行的一般概念,然後分別在第5.2節和5.3節中討論了這兩個階段。

5.1 Baseline: concrete + symbolic execution

像DART和SAGE[17,18]一樣,Dowser通過結合具體值和符號執行來生成新的測試輸入。這種技術稱爲動態符號執行[33]。它在具體的輸入上運行程序,同時從沿途遇到的條件語句中收集符號約束。爲了測試可選路徑,它系統地否定收集的約束,並檢查新集合是否可滿足。如果是,則生成一個新的輸入。爲了引導該過程,Dowser接受一個測試輸入,該測試輸入執行分析組AG(accp)。

如前所述,應用這種方法的一個挑戰是如何首先選擇要探索的路徑。經典的解決方案是通過回溯[22]來深度優先探索路徑。然而,由於這樣做會導致需要測試的路徑呈指數級增長,研究社區已經提出了各種啓發式方法來將執行引導到未開發的區域。我們將在第7節中討論這些技術。

5.2 Phase 1: learning

學習階段的目的是對依賴於循環l中的符號輸入的所有條件分支的真方向和假方向進行評級。,我們不期望在其他結果中發現的偏差)。因此,我們回答了這樣一個問題:當我們沿着這條路走下去時,我們期望獲得多少收益,而不是另一種選擇。我們把這些信息編碼成權重。

具體來說,權重表示唯一訪問模式的可能性。指針p的訪問模式是循環執行期間p的所有解引用值的序列。在圖1中,當我們用u0表示u的初值時,輸入"//../"觸發指針u的以下訪問模式:(u0, u0+1, u0+2,u0-2,…)

爲了計算權重,我們學習了單個分支的影響。原則上,它們中的每一個都可能(a)直接影響指針的值,(b)是另一個重要分支的先決條件,或者(c)與計算無關。爲了區分這些情況,Dowser分析了短符號輸入的所有可能執行。通過比較一個分支的兩個結果觀察到的p的訪問模式集,它發現哪些分支不影響指針引用的多樣性(即。,無關)。

在第4節中,我們確定了我們需要將測試輸入I的哪一部分進行符號化。我們用IS表示它。在學習階段,Dowser執行循環L。出於性能原因,我們進一步限制符號數據的數量,只生成一小段IS符號。例如,對於圖1,學習階段只生成uri符號的前4個字節(不足以觸發bug),而在bug查找階段擴展到50個符號字節。

算法探測器對一個簡短的符號輸入執行L,並記錄在條件分支語句中所做的決策如何影響指針引用指令。對於執行路徑上的每個分支b,我們保留在此執行期間實現的p的訪問模式AP(p)。我們將其非正式地解釋爲“如果您選擇分支b的true(分別爲false)方向,則期望訪問模式AP(p)(分別爲AP ' (p))”。這個過程爲每個分支語句分別生成兩組訪問模式,分別爲已取分支和未取分支。每個方向的最終權重是該方向唯一的訪問模式的百分比,即,而另一組則沒有觀察到。

上面的描述解釋了學習機制背後的直覺,但是完整的算法更加複雜。問題是,條件分支b可能在執行路徑中被執行多次,並且所有b的實例都可能影響觀察到的訪問模式。

直觀地說,爲了考慮到這一點,我們不將訪問模式與b上的單個決策關聯起來(true或false)。相反,每次執行b時,我們還保留之前爲b選擇的方向。因此,如果遵循b的真(分別爲假)方向,我們仍然收集“期望”訪問模式,但是我們使用一個前提條件來增強它們。通過這種方式,當我們比較真集和假集來確定b的權重時,我們將基於對訪問模式是如何實現的更深入的理解來獲得分數。

Discussion

對於我們的算法來說,避免錯誤否定是很重要的:我們不應該錯誤地將一個分支標記爲不相關的——這將阻止它在錯誤發現階段被探索。假設instr是一條取消引用指針p的指令。要了解分支直接影響instr,執行它就足夠了。類似地,由於分支保留p的完全訪問模式,所以有關正在執行的instr的信息也會“傳播”到它的所有先決條件。因此,爲了完全避免假陰性,該算法需要對分析組中的指令進行完全覆蓋。我們強調需要執行所有指令,而不是循環中的所有路徑。正如[7]所觀察到的,即使是很短的符號輸入的窮舉執行在實踐中也提供了很好的指令覆蓋。

雖然假陽性也是不受歡迎的,但它們只會導致Dowser在第二階段執行比絕對必要的更多的路徑。由於路徑覆蓋範圍有限,在某些情況下會出現誤報。即便如此,在nginx中,60個分支中只有19個得到非零值,這使得我們可以使用50字節長的符號輸入執行復雜循環。

5.3 Phase 2: hunting bugs

在這個步驟中,Dowser執行一個實際大小的符號輸入,希望找到一個觸發bug的值。Dowser使用來自學習階段的反饋(第5.2節)將其符號執行引導到新的、有趣的指針解引用。我們啓發式的目標是避免不帶來任何新的指針操作指令的執行路徑。因此,Dowser將符號執行的目標從傳統的代碼覆蓋轉移到指針值覆蓋。

Dowser的策略是由權重決定的。作爲基線,執行遵循深度優先探測,當Dowser根據符號輸入選擇分支b的方向時,它遵循以下規則:

  • 如果b的真方向和假方向的權值都爲0,我們不認爲b會影響訪問模式的多樣性。因此,Dowser隨機選擇方向,而不打算檢查其他方向。
  • 如果只有一個方向的權值爲非零,那麼我們期望只有在執行路徑遵循這個方向時才能觀察到唯一的訪問模式,而Dowser傾向於這個方向。
  • 如果b的兩個方向的權值都是非零的,true和false選項都可能帶來唯一的訪問模式。Dowser檢查了兩個方向,並按照它們的權值大小安排它們。

直觀地說,Dowser的符號執行嘗試選擇更有可能導致溢出的路徑。

Guided fuzzing 

這就結束了我們對Dowser架構的描述。總之,Dowser通過以下方法幫助模糊處理:(1)發現“有趣的”數組訪問,(2)確定影響訪問的輸入,(3)智能模糊處理以覆蓋數組。此外,基於指針值覆蓋率和少量符號輸入值的目標選擇過程允許Dowser快速發現bug並擴展到更大的應用程序。此外,數組訪問的排序允許我們放大更復雜的數組訪問。

6 Evaluation

在本節中,我們首先放大圖1中運行的nginx示例,以詳細評估系統的各個組件(第6.1節)。在第6.2節中,我們考慮了七個實際應用程序。基於它們的脆弱性,我們評估了我們的探測機制。最後,我們概述了Dowser檢測到的攻擊。由於Dowser使用“抽查”而不是“代碼覆蓋”方法來檢測bug,所以它必須單獨分析每個複雜分析組,從排名最高的組開始,然後是第二組,以此類推。它們都在運行,直到找到bug或終止。問題是什麼時候應該終止符號執行運行。由於在Dowser中對單個循環的符號執行進行了高度優化,所以我們在不到11分鐘的時間內發現了每個bug,所以我們執行每個符號運行最多15分鐘。

我們的測試平臺是一個Linux 3.1系統,使用Intel(R) Core(TM) i7 CPU, CPU頻率爲2.7GHz,緩存爲4096KB。這個系統有8GB的內存。在我們的實驗中,我們使用了OpenSUSE 12.1安裝。我們運行每個測試多次並給出中值。

7 相關工作

Dowser是一種“引導性”模糊器,它從多個領域汲取知識。在本節中,我們將系統置於現有方法的上下文中。我們從評分函數和代碼片段的選擇開始。接下來,我們討論傳統的模糊。在此基礎上,回顧了模糊處理中動態污染分析的研究現狀,最後討論了白盒模糊處理和符號執行方面的研究現狀。

軟件複雜性度量

許多研究表明,軟件複雜度度量與缺陷密度或安全漏洞呈正相關[29,35,16,44,35,32]。然而,Nagappan等人的[29]認爲沒有一組度量標準適合所有項目,而Zimmermann等人的[44]強調需要利用漏洞的獨特特徵的度量標準,例如緩衝區溢出或整數溢出。所有這些方法考慮發佈後的廣義類缺陷或安全漏洞,並考慮一組通用的測量,例如,基本塊的數量在一個函數的控制流圖,讀取或寫入全局或局部變量的數量,if或while語句的最大嵌套級等等。Dowser在這方面很不一樣,據我們所知,他是第一個這樣的人。我們只關注一小部分安全漏洞,即,緩衝區溢出,因此我們的評分函數是定製的,以反映指針操作指令的複雜性。

傳統的fuzz

軟件模糊測試開始於90年代,Miller等人[25]描述了他們如何向(UNIX)實用程序提供隨機輸入,並設法使25-33%的目標程序崩潰。沿着相同路線的更高級的模糊器,如Spike[39]和snze[5],會故意生成格式錯誤的輸入,而針對更深層錯誤的後期模糊器通常基於輸入語法(如Kaksonen[20]和[40])。DeMott[13]提供了一個關於fuzz測試工具的調查。正如Godefroid等人所觀察到的[18],傳統的模糊器是有用的,但通常只能找到淺層的bug。

DTA在模糊測試中的應用

BuzzFuzz[15]使用DTA來定位影響庫調用中使用的值的種子輸入文件的區域。他們特別選擇庫調用,因爲它們通常由不同的人開發,而不是調用程序的作者,而且常常缺乏對API的完美描述。Buzzfuzz根本不使用符號執行,而是使用DTA來確保它們保持正確的輸入格式。與Dowser不同的是,它完全忽略了隱式流,所以它永遠找不到nginx中的bug(圖1)。很難評估哪些庫調用是重要的,並且需要更仔細的檢查,而Dowser則顯式地選擇複雜的代碼片段。

TaintScope[42]與此類似,它還使用DTA選擇影響安全敏感點(例如,系統/庫調用)的輸入種子字段。此外,TaintScope能夠識別和繞過校驗和檢查。與Buzzfuzz一樣,它與Dowser的不同之處在於它忽略了隱式流,只假設庫調用是有趣的點。與BuzzFuzz不同,TaintScope在二進制級別而不是源代碼上運行。

Symbolic-execution-based fuzz

近年來,人們對白盒模糊、靜態符號執行、動態符號執行和約束求解等方面的研究引起了極大的興趣。例如EXE[8]、KLEE[7]、CUTE[33]、DART[17]、SAGE[18]以及Moser等人的作品[28]。例如,微軟的SAGE從格式良好的輸入開始,象徵性地執行測試中的程序,試圖遍歷程序的所有可行執行路徑。在此過程中,它使用AppVerifier檢查安全屬性。所有這些系統都用符號值替換(一些)程序輸入,在程序跟蹤上收集輸入約束,並生成新的輸入,以執行程序中的不同路徑。它們非常強大,可以詳細地分析程序,但是很難使它們具有一定規模(特別是如果您想研究許多基於循環的數組訪問)。問題是路徑的數量增長非常快。

Zesti[24]採用了一種不同的方法,並符號執行現有的迴歸測試。直觀地說,它檢查它們是否可以通過稍微修改測試輸入來觸發脆弱的條件。這種技術可以更好地擴展,並且對於在現有測試套件附近的路徑中發現bug非常有用。它不適合遠離這些路徑的bug。例如,執行圖1中脆弱循環的通用輸入的uri形式爲“//{任意字符}”,觸發bug的最短輸入是“//../”。當使用“//abc”時,[24]找不到bug——因爲它不是爲這個場景設計的。相反,它需要一個更接近漏洞條件的輸入,例如,“//..”{一個任意字符}”。對於Dowser,一般的輸入就足夠了。

SmartFuzz[27]關注整數bug。它使用符號執行來構造測試用例,這些測試用例觸發算術溢出、非保值寬度轉換或危險的有符號/無符號轉換。相反,Dowser的目標是更常見(也更難找到)的緩衝區溢出情況。最後,Babi ' c等人的[4]將符號執行引導到靜態分析檢測到的潛在易受攻擊的程序點。然而,所提出的過程間上下文和流敏感靜態分析方法不能很好地應用於實際程序,實驗結果只包含了很短的跟蹤。

8 結論

Dowser是一種有指導意義的模糊器,它結合了靜態分析、動態污染分析和符號執行來發現程序邏輯深處的緩衝區溢出漏洞。它首先確定“有趣的”數組訪問,即。,最可能包含緩衝區溢出的訪問。它按照複雜性對這些訪問進行了排序——如果需要,允許安全專家關注複雜的bug。接下來,它使用污點分析來確定哪些輸入影響這些數組訪問,並且只模糊這些字節。具體地說,它在隨後的符號執行中(僅)使這些字節具有符號性。在可能的情況下,Dowser的符號執行引擎選擇最有可能導致溢出的路徑。每個三個步驟包含論文本身的貢獻(如數組訪問的排名,隱式流處理污染分析,和象徵性執行基於指針的值覆蓋),但整體的貢獻是一個新的、實用和完整的模糊方法,擴展到真實的應用程序和複雜的缺陷,很難或不可能找到與現有技術。此外,Dowser提出了一種新的“抽查”方法來發現實際軟件中的緩衝區溢出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章