【讀書筆記】ndss2018_K-Miner_ Uncovering Memory Corruption in Linux

論文筆記

題目 :K-Miner: Uncovering Memory Corruption in Linux
出處:NDSS 2018
作者:David Gens, Simon Schmitt, Simon Schmitt and Ahmad-Reza Sadeghi
單位:CYSEC/Technische Universitat Darmstadt, Universität of Duisburg-Essen
原文http://wp.internetsociety.org/ndss/wp-content/uploads/sites/25/2018/02/ndss2018_05A-1_Gens_paper.pdf
作者研究方向:kernel- userland isolation, chip- and firmware security, static analysis and formal verification
相關材料
Slides
Video
SourceCode
HomePage
Lab

一、背景

    目前,已經有很多種在運行時保護系統的方法(例如:控制流完整性CFI等),運行時保護系統並不能消除造成內存崩潰(memory corruption)的根本原因。然而,與運行時相對應的是靜態源碼分析,但是在目前的靜態分析框架中,存在很多不完善的地方,例如,這種方法只能限制在局部的、過程內的檢查,或者是隻能基於單個源文件的檢查(這些限制都是由於路徑爆炸造成的),這對於代碼量龐大的工程(例如:Linux內核)來說,是根本行不通的。
    爲了克服以上的這些不足,作者在這篇文章中提出了一種新的、基於LLVM的靜態源碼分析框架:K-Miner,它能夠分析大規模的內核源碼(例如:Linux內核),採用大規模指針分析(scalable pointer analysis)、過程間分析(inter-procedural analysis)和全局-上下文敏感分析(global, context-sensitive analysis)等技術,可以系統的挖掘幾種不同類型的內存崩潰漏洞(包括dangling pointers, user-after-free, double-free 和 double-lock)。

二、面臨的挑戰

    靜態分析方法必須考慮所有可能的路徑和狀態,這就導致路徑爆炸問題,但是,剪枝或者是忽略某些部分(路徑或狀態等)都會導致分析結果不可靠。這就限制了靜態源碼分析只能是基於局部的過程內分析,或者是基於單個文件的分析。
    而在本文中,作者爲了處理上述問題,採用的方法是:基於系統調用接口來分割內核代碼,每個系統調用作爲一個執行(分析)路徑的入口,這樣就有效的解決路徑爆炸問題,使得K-Miner可以分析複雜的內核代碼,並可以執行過程間的數據流(inter-procedural data-flow analysis)分析。
    但是,分割內核代碼也不是一件簡單的事情,因爲在內核中,有很多的全局數據結構被頻繁的使用,以及每個系統調用之間的同步問題,全局內存狀態(context)問題,複雜的指針關係和別名問題等。
    具體來說,K-Miner面臨四個挑戰:
1. 處理全局狀態問題:
    在靜態分析過程中,要想執行過程間分析,就必須考慮全局的數據結構,因此,局部的指針訪問(例如函數內部)可能會影響全局的數據結構,因爲局部指針可能只是一個別名(alias)而已。本文中使用的是稀疏程序表示(sparse program representation)技術,並結合控制流分析和數值流分析等技術來解決全局狀態問題。
2. 處理代碼量太大問題:
    目前的Linux內核代碼超過240萬行,對於如此巨大的代碼量,對它直接進行靜態分析是及其困難的,因此,作者通過分割內核代碼的方式,按照系統調用接口把它進行劃分,這樣就可以極大的減少單次分析的代碼量,使K-Miner分析Linux內核成爲可能。
3. 處理誤報率太高的問題:
    粗粒度的近似分析程序行爲,出現高誤報率是不可避免的,而開發者沒有這麼多的時間去手工分析大量的誤報結果,因此,減少誤報率是提高程序可用性的重要基礎。作者利用儘可能多的信息,並精心設計每次的分析過程(individual analysis passes)來減少誤報率,並在生成報告之前,對分析結果進行淨化(sanitize)、去重(deduplicate)和過濾(filter),最大限度的減少誤報率。
4. 多路分析問題:
    爲了儘可能多的檢測造成內存崩潰的bug,K-Miner必須能夠同步每次的分析結果(individual pass),並且利用已知的結果去分析未知的結果,提高分析的準確性和精確性。爲了提高分析效率,還必須重用LLVM產生的中間結果(IR),因此,IR產生的結果首先被保存到磁盤中,然後在後序的分析中重新導入。

# **三、關鍵技術之數據流分析**     靜態分析的通用方法是把程序和一些預編譯的屬性(pre-compiled properties)作爲輸入,對於給定的屬性,去查找所有可能的路徑。而這裏的屬性包括:活性分析(liveness analysis)、死碼分析(dead-code analysis)、類型狀態分析(typestate analysis)和空值分析(nullness analysis)。     在圖1中,子圖a是一個簡單的示例代碼,通過分析子圖c的指針分配圖PAG(Pointer Assignment Graph)我們可以知道,存在一條路徑,使得變量b被賦值爲空,因此會產生一個對應的空值報告。

    此外,在靜態分析中,另外一個被使用的數據結構是:過程間控制流圖ICFG(Inter-procedural Control-Flow Graph),如子圖b所示,它是一個全局的控制流圖,可以被用來執行路徑敏感性分析(path-sensitive analysis)。最後子圖d是一個數值流圖VFG(Value-Flow Graph),該圖作爲污點分析技術的輔助圖,可以用來跟蹤單個的內存對象。

四、前提假設

    本文提出的靜態分析框架基於以下三個前提假設:
- 攻擊值可以控制用戶空間的進程,並且可以調用任何的系統調用。
- 操作系統獨立於用戶進程,例如,通過虛擬內存機制或者是劃分不同權限級別等,經典的是x86架構和ARM架構。
- 攻擊者不能插入惡意代碼到內核中(內核是被簽名的)。

# **五、K-Miner運行機制**

    如圖2所示,K-Miner是基於LLVM和過程間靜態數值流分析(SVF)之上靜態源碼分析框架,的主要的工作流程分爲三步:

  1. K-Miner首先接收兩個輸入,一個是內核代碼,另一個是一份配置文件,該配置文件是內核特徵(kernel features)的一個列表,用來告訴前端解析器,哪些內核模塊需要編譯,哪些不需要編譯。編譯器根據這兩個輸入去構建抽象語法樹(AST),並把它轉化爲中間表示語言(IR),作爲後序階段的輸入。
  2. 把上一個階段得到的中間表示語言作爲本階段的輸入,去遍歷配置文件中列出來的每一個系統調用,對於每一個系統調用,K-Miner都生成以下數據結構:一個調用圖(CG)、一個指針分析圖(PAG)、一個數值流圖(VFG)和幾個相關的內部數據結構。有了以上結果,就可以對每一個系統調用進行相應的分析(例如,空指針解引用分析等)。
  3. 對於第二步中的分析結果,如果檢測到有內存漏洞,則在這一步中產生相應的分析報告(報告包含相應的內核版本、配置文件、系統調用以及程序執行路徑等)。

六、K-Miner實現細節

1. Global Context 初始化

    Global Context初始化是分析每一次pass的前提基礎,有效的管理Global Context是進行大規模代碼分析的必備條件,它分爲以下三個部分:
- 內核上下文初始化:由於內核的初始化是通過調用一系列的InitCalls來完成的,所以,作者通過模擬內核中的InitCalls的執行來填充(初始化)內核上下文,並將結果存入文件中,方便後序導入。
- 跟蹤堆的分配:由於在內核中分配內存有多種形式(slab allocator、low-level page-based allocator和various object caches等),因此,K-Miner把這些內存分配函數都標記爲堆分配函數,以便於後序的數值流分析。
- 建立系統調用上下文:在分析每一個系統調用圖的過程中,作者通過收集它們所用到的所有全局變量和函數來建立一個memory context,然後再結合global context,來建立一個更加準確的memory context。

2. 分析每個系統調用的內核代碼

    僅僅以系統調用爲單位來分割內核代碼,雖然能極大的減少每一次分析過程中的代碼量,但是對於作者32G內存的實驗環境,目前依然吃不消,需要進一步的優化。通過分析發現,消耗資源的主要原因是函數指針的劇增,因爲在通常情況下,對於每一個系統調用來說,最樸素的想法是:使得所有的全局變量和函數都可達,這種方法確實會比較安全,但是也很低效,因此,作者使用瞭如下三個方法來改善這種樸素的想法:
- 提高函數調用圖的準確性:在IR層分析所有函數的調用關係圖,以確定某個函數是否可達(例如:通過判斷全局函數是否被局部變量訪問),這就使得對於每一個系統調用,可以得到一個更加簡練的可達函數集合。
- 流敏感(Flow-Sensitive)指針分析:首先通過一個基於閉包(inclusion-based)的指針分析策略來約束上一步的分析結果,然後再結合控制流分析,進一步簡練每一個系統調用的可達函數集合。
- 結合全局內核上下文分析:對於上一步的分析結果,把它結合全局內核上下文,再次分析每一個系統調用,進一步確定它所能到達的全局變量和全局函數,並把缺失的引用包含進來,得到一個更加完整的、更加精確的context。

## **3. 最小化誤報率**     使用靜態分析方法來近似的逼近程序行爲,不可避免的產生誤報情況,而作者在文章中使用Dangling Pointer爲例,分析如何較少誤報率。
  • 精確的數據流分析
        如圖4所示,子圖a模擬了一個具有指針懸空漏洞的系統調用,在add_x函數中,局部指針變量p被賦值給全局指針變量global_p;在remove_x函數中,把全局指針變量global_p置空,但是在do_foo函數中,我們可以看到,只有在滿足特定條件(③)下才執行remove_x函數,因此存在指針懸空的漏洞。
        在子圖b中,local_o和global_o分別表示相應的內存對象,K-Miner通過兩個步驟來查找該bug:第一步,在子圖c中,前向分析local_x這個節點所經過的位置,在這個節點的合法範圍內(do_foo函數內),如果離開這個區域的出口數量大於進入這個區域的入口數量,那麼就存在一個非法的引用。對於local_x,存在一條路徑:local_x->①->②->③->⑤->⑥,在這條路徑的最後一個節點:⑥,離開了local_x的合法範文之內; 第二步,反向遍歷VFG圖,查找對該節點的引用,只要存在這樣的引用,就表明它是一個指針懸空漏洞,在子圖c中可以找到:⑥->⑤->③->②->global_p,即存在一個指針懸空漏洞。此外,由於有了PAG圖,在反向查找的時候就可以避免訪問不屬於當前跟蹤的內存對象的邊(例如:⑤->④)。最後把該bug所經過的路徑信息和相應的節點等保存下來,以便後續處理。
  • 淨化報告
        通過以上的數值流分析之後,作者再次檢查報告中的候選結果,並去除一些在運行時不可能出現的結果(例如:調用函數g,然後從函數f中返回)。此外,對於同一個節點,如果產生重複的報告,則把它們合併爲一個報告,精簡最後的輸出結果。

七、 評估

    作者的測試環境是:
        
CPU:Xeon E5-4650、8核處理器、2.4GHz的主頻
        
內存:32G

## **1. 測試時間評估**

    如表一所示,是對不同版本的Linux內核代碼的測試結果,每個版本的內核都測試了DP(Dangling Pointer)、 UAF(Use After Free)和DF(Double Free)。結果表明,對於每一個系統調用,平均大約需要25分鐘測試時間(第四列的結果)。對於每一個內核版本,對所有系統調用都進行三項測試(DP、UAF、DF),所需時間在70小時到200小時之間。表中的最後三列表示被檢測出來的可能的漏洞數量,括號中的數值表示產生的警告數量。

    圖5給出了測試一部分系統調用所需的時間(這裏只顯示測試時間超過30分鐘的系統調用),圖中的每個柱子被劃分爲四節,每一節的長度代表測試該類型所需要的時間,從上到下依次代表的測試對象是:Double Free Checker、Memory Leak Checker(Use-After-Free Checker)、 Use-After-Return Checker(Dangling Pointer)和Context Handling。其中,對於測試每一個系統調用,主要花費的時間是在Context Handling 上,因爲Contex Handling是進行其它各項分析(例如:數值流分析等)的基礎,是開始測試的第一階段。但是,這裏也有幾個異常情況,分別是:sys_execve、 sys_madvise 和 sys_keyctl,這幾個系統調用的DP測試時間與Context Handling 時間基本相等。

## **2. 內存消耗評估**

    在本文中,由於作者使用分割內核的技術,使得內存使用情況在可以接受的範圍內,即,對於分析每一個系統調用,平均內存使用量在8.7G到13.2G之間,最大值達到26G。而對於測試每一個版本的內核,內存的使用情況如表2所示:

八、 結論

    作者開發了一個新穎的內核代碼檢測工具:K-Miner,該工具是第一個對系統內核進行數值流分析的框架,它對內核進行大規模的、精確的分析,並能夠找到內核中存在的漏洞:CVE-2014-3153(dangling pointer 漏洞)和CVE-2015-8962(double free 漏洞),此外,它還是第一個能夠對具有龐大代碼量的系統內核進行過程間分析的框架,它對不同的漏洞類型提供了不同的passes,目前論文中已經完成的passes有:dangling pointers、 use-after-free和 double free,其它類型的漏洞還在慢慢完善的過程中。

九、 文章解決的問題

    運行時防護措施無法根除bug,而現存的靜態源碼分析方法無法檢測大規模工程,只能基於過程內(intra-procedural)的檢測,或者是基於單個文件的檢測,對於代碼量龐大的工程(例如Linux內核等)無能爲力,甚至對於跨函數或者是跨文件的工程都難以檢測。因此,作者的目標是實現一個能夠檢測Linux內核這種大規模代碼工程的靜態分析工具,但是由於靜態分析工具存在一個致命的缺陷,那就是路徑爆炸問題,爲了解決這個問題,作者使用分割技術,按照系統調用爲單位,對內核進行分割,以解決路徑爆炸問題。因此,作者實現了一個基於LLVM和SVF的、強有力的靜態分析工具:K-Miner。

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