透過灰盒Fuzzing技術來發現Mac OS X安全漏洞

wyzsk發表於2020-08-19
作者: vvun91e0n · 2015/07/17 9:30

透過灰盒Fuzzing技術來發現Mac OS X安全漏洞

翻譯-原文地址:security.di.unimi.it/~joystick/pubs/eurosec14.pdf

注:部分不太重要的沒有翻譯。

0x01 前言


核心是所有作業系統的核心,它的安全性非常重要。任何地方一個漏洞,都足以危害整個系統的安全。非特權使用者如果找到這樣的漏洞可以輕鬆的使整個系統崩潰,也或者取得管理員許可權。可見,核心對攻擊者來說更具吸引力,核心漏洞的數量也在以一個不安的趨勢在上升。因為太複雜。在核心層挖掘漏洞是一件讓人畏懼的事情。的確,現代核心是非常的複雜還含有很多子系統,有些是第三方開發的。通常,第三方開發的核心擴充套件元件沒有核心本身那樣安全。因為第三方元件不是開源的,也缺乏足夠的測試。另外,核心有太多的使用者資料介面。系統呼叫,檔案系統和網路連線等允許使用者提交資料到重要的程式碼路徑。如果在其中找到一個bug,足以威脅到系統安全。測試使用者層到核心的介面十分重要,因為它能夠實現提權攻擊。這是現在大多數攻擊者想做的。

Windows和Linux系統的核心介面已經被深入的分析,並且有很多工具用來進行檢測。但在Mac OS X上,核心提供給擴充套件的介面互動方法相對來說還沒有被廣泛深入的分析。另外,OS X系統的市場份額正在穩步提升,它將吸引更多的網路犯罪分子的注意力。本文中,我們闡述了一個自動在核心擴充套件中發現漏洞的framework。LynxFuzzer的設計和實現。其可以自動載入Mac OS X的核心元件。OS X核心擴充套件稱作Kext。使用蘋果公司提供的IOKit framework來進行開發。LynxFuzze使用灰盒fuzzing技術動態的生成測試資料。其機理是自動從核心擴充套件中提取資料,並使用它們來動態生成輸入資料。進一步說,我們在LynxFuzzer中實現了3種不同的fuzzing引擎:簡單資料引擎的是根據核心擴充套件定義來產生偽隨機輸入資料。突變引擎是根據嗅探到的合法資料來進行變異來生成輸入資料。最後一個是進化引擎,根據進化演算法,使用程式碼覆蓋率來作為主要的驗證特徵。

我們決定在開源的,透過硬體輔助的虛擬化核心偵錯程式HyperDbg的基礎上開發LynxFuzzer。主要是因為Mac OS X在現有的虛擬化軟體中執行會遇到很多困難。實際上蘋果公司一向以其定製化硬體出名。並不能被普通的偵錯程式模擬。這個缺點阻礙了LynxFuzzer測試一些OS X上的驅動。另外,一個硬體輔助環境可以保證在系統崩潰時,收集到關於機器的動態資訊。總的來說我們的工作做出了一下幾點成果:

我們設計了LynxFuzzer來自動發現核心擴充套件的漏洞,比如動態的載入核心模組。我們解決了幾個實際困難來實現這一有效的fuzzing系統。

我們發明了一些有效的透明的技術來自動生成fuzzing輸入,從而減少輸入的搜尋範圍,提高對Mac OS X使用者態和核心互動介面fuzzing的效率。

我們擴充套件了一種fuzzing系統。並且在一些擴充套件上做了實踐。我們的實驗發現了6個bug。在我們分析的17個擴充套件之中。其中兩個已經在OS X 10.9種被修補。其CVE編號為CVE-2013-5166和CVE-2013-5192。

0x01 IOKit基礎


在這一節中,我們將要簡要介紹IOKit framework。這是理解LynxFuzzer的基礎。

Mac OS X是蘋果公司開發的作業系統,用於蘋果電腦。在它的眾多元件中,IOKit是對我們來說是特別重要的。IOKit是一個系統框架,庫,工具和其他資源的集合。用來在OS X上開發裝置驅動。它提供了一個物件導向的程式設計環境,和一些使得開發核心元件更加簡單,使用者體驗更好的抽象物件。在下面結合我們的工作來做一些闡述。

任何一個OS的基本元件就是使用者態與核心空間的互動通訊元件。Windows和Linux透過系統呼叫system call和特殊的虛擬檔案(例如:/dev/urandom)。IOKit支援這兩種技術,但是還新增了一種新奇的更加複雜的機制,叫做裝置介面(DevieceInterface)。

enter image description here

為了實現這種機制,核心擴充套件定義了一系列的可以在使用者態被呼叫的方法。這些方法返回的引數的數量和資料的型別是被限制的。這些方法的列表和引數的限制都都存貯在被極度重要的結構體裡面:這就是分派表(dispatch table)。每一個kext都可以定義一個或多個分派表。每個表都是一組結構體。每一個都包含了一個函式指標,允許的輸入值和輸出值,還有該方法允許接收或返回的值的數量和大小。如果輸入結構體的大小不需要在輸入前檢驗,可以將大小在表中宣告為0xffffffff,然後由接收函式進行檢驗。任何驅動都可以擁有一個以上的分派表。IOKit允許擴充套件在同一時刻提供給使用者空間程式以多個介面。每個kext都必須為每一個介面定義一個UserClient的子類。這些子類的例項隨後與kext一起載入到核心記憶體(圖2)。每一個UserClient物件都包含一個與它提供介面對應的分派表。

enter image description here

使用者態程式可以透過IoConnectCallMethod()來呼叫kext的方法,前提是這個方法已經在kext分派表中。當然,使用者態程式需要必須先找到目標例項。想解釋這一過程,我們首先需要介紹一個IOKit抽象類:IOService。每一個IOKit裝置驅動都是繼承於它的物件,一個kext可以同時擁有不同的IOService物件。舉例說明,具有代表性的就是同時有幾個USB裝置連線到電腦,每個都需要它自己的驅動。這些驅動都被包含在IOUSBFamily kext中,每一個都是一個特殊的IOService的子類。當一個使用者態程式想要和一個裝置互動時,像前面提到的一樣,它會與IOKit建立一個mach連線,然後尋找合適的服務來適應裝置。這個過程叫做Device Matching。

如果找到服務成功,互動通道也成功建立,使用者態程式就會使用IoConnectCallMethod()來呼叫目標方法。在真正執行目標函式之前,程式會將控制權交給IOKit framework,由IOKit framework執行一系列操作。首先,它查詢UserClient物件的分派表入口。入口地址隨後被傳送給externalMethod()函式,同時還有其他被執行呼叫kext方法所允許的引數。只有引數符合分派表要求的情況下,方法才會被呼叫,不然就會被阻止。

相對於一般的機制,如ioctl,整個IOKit輸入控制機制提供了一個保護層。引數檢查會在由使用者層進入驅動層之前進行。顯而易見,這些約束都使得fuzzing工作變得更加複雜。使用完全隨機的引數大小來對kext的函式進行fuzz幾乎是無效的。絕大多數的呼叫請求都會被IOKit檢查並丟棄掉。我們在下一節會看到,我們fuzzer的重要一個特性就是能自動從目標中提取到引數限制,之後動態適應限制,讓fuzzing更加的高效。

0x02 LynxFuzzer


我們fuzzer的目的是在能被使用者態程式呼叫到的kext code中發現bugs。一個可以從使用者態激發的bug可以讓非特權使用者崩潰掉整個系統的。也甚至是執行任意核心程式碼。從而達到提權攻擊。所以,我們決定將我們的注意力集中到裝置介面跨界機制(DeviceInterface boundary-crossing mechanism),因為它是OS X核心擴充套件機制中使用者層和核心互動的標準。

在上一節中,需要指出的是在呼叫一個kext方法時,很多的約束必須得到考慮。對於每個kext來說是不同的。知道分派表中的約束能減小我們fuzzing工作量。提高fuzzing的效率。所以我們設計的LynxFuzzer能夠全自動的提取資訊,然後自動的進行fuzz。當然,我們的fuzzing基礎設計的自動化功能不止於此。事實上,我們能提取到在使用者態和核心態元件非人工互動的合法有效輸入向量。這些輸入可以經過精心使用來加強我們的fuzzing策略。

enter image description here

LynxFuzzer的基本結構可以從圖3中看到,框架擁有兩個主要的元件:一個在使用者態,有4個子元件構成。另一個構建在偵錯程式分析框架上。圖3中還說明了LynxFuzzer內部元件的主要互動行為。tracer和偵錯程式(hypervisor)互動得到目標分派表。一旦發現,偵錯程式就從核心記憶體中取回分派表的地址,返回給tracer,tracer將其儲存到data manager中以待後面使用。sniffer使用這些資訊來攔截非人工的IoConnectCall()呼叫,收集到一些有效的輸入。最後fuzzer元件開始使用自定義的引數來呼叫目標方法。等待最後的異常。fuzzer可以使用之前存貯的資料生成新的輸入資料或者使用覆蓋突變方法來生成輸入,這取決於fuzzer引擎的選擇。

2.1 Tracer


tracer是LynxFuzzer的第一個執行的元件,它的任務是找到fuzz的目標。舉例,它必須確定目標的哪個方法是能夠被呼叫的。這些資訊包含在目標kext的分派表中。然而,定位一個kext的分派表並不簡單,因為IOKIt使用很多抽象層來對使用者層隱藏資訊。我們的解決方案是:任何時候使用者態程式呼叫IoConnectCallMethod(),IOKit將會呼叫它的externalMethod()函式,我們對此進行監視。透過以下方法來實現,LynxFuzzer偵錯程式對externalMethod()函式下一個斷點,截斷對它的呼叫。一旦這個陷阱(trap)被設定好,tracer就對目標kext發起一個請求,其selector引數為0。當偵錯程式截斷externalMethod(),就提取分派表的基地址。然後dump整個分派表,將它返回給tracer。最終,tracer存貯分派表到data maanger,分享給其它元件使用。

分派表的大小事先是不知道的。也不在被截斷函式的引數裡面。為了解決這個問題,LynxFuzzer分析分派表的結構推斷出其擁有多少個入口,然後dump下來。事實上,每個表的入口由一個指標,該指標必須在目標的記憶體範圍內。和4個連續的整數,其中兩個必須在0-15之間。

2.2 Sniffer


除了IOKit的檢查,kext自己可以實現對輸入的約束。所以LynxFuzzer包含了一個sniffer元件。該元件用來截斷目標方法的執行,提取到它們的引數。為了實現這個我們再次利用LynxFuzzer的偵錯程式,它可以透明的無縫的截斷我們感興趣的函式,透過檢查目標kext的記憶體來dump它的引數。

特別的,偵錯程式對於externalMethod()函式來說是透明的。該函式的引數包含了足夠的資訊來獲取有效的輸入。事實上,偵錯程式用dispatch引數來區分出是哪一個kext是這次截斷的目標。用selector引數來確定是哪一個方法被呼叫。IOExternalMethodArguments結構中包含了真正的傳遞進來的引數。這種結構中還包含了引數的數量和大小。它們都將會被儲存到data manager。

2.3 Fuzzer


enter image description here

fuzzer是LynxFuzzer的主要元件。當tracer盒sniffer獲取了足夠的輔助資訊可以進行fuzz一個kext時,fuzzer生成對kext方法的測試集,然後透過IOKit裝置介面來呼叫方法。圖4顯示了該元件的結構:一個請求生成器,一組fuzzing引擎和一個監視器。 請求生成器是一個通用的元件:它必須獨立於目標kext和選擇的fuzzing引擎來運轉。在一次典型的執行中,它從fuzzing引擎中接收到測試資料之後,檢查資料是否符合分派表中宣告目標方法的要求,適當的調整以符合最後在kext中執行目標方法的IoConncetCallMethod()函式. 如果測試資料沒有導致崩潰,kext發出一個回應,由monitor來接收。monitor根據接收到的放回資料和當前的fuzzing引擎來決定是否繼續使用當前的引擎。所以後一個測試基於前一個測試反饋的。突變引擎和進化引擎都會使用該模式。

LynxFuzzer實現了基於會話的fuzzing:我們不多餘操作,只需要發出尋找bug的請求。但是我們要從每個fuzzing會話開始記錄每一個請求。這種方法很常見,特別是在fuzzing基於狀態的網路協議的時候,在這種情景下也是適用的。事實上kext也擁有一種狀態。這種狀態會隨著大量不同的fuzzing請求而變化,直到進入非正常的狀態,一個bug被觸發。出於這種原因,使用基於會話的記錄,代替單個的請求會極大的保證一個bug的可重現性。記錄fuzzer和目標kext之間的互動會話,每一個請求都被存貯在data manager中,最終fuzzer對輸入資料的生成也是由影響的。LynxFuzzer三種不同引擎的細節我們馬上給出。

2.3.1 隨機引擎(Generation Engine)

這是最簡單快速的引擎。它的生成過程可以簡述如下,第一步,它生成可以包含輸入資料到目標方法的資料結構。第二步,生成偽隨機資料來填充這個結構體。最後,將資料透過IoConnectCallMethod()傳送給目標。如果系統沒有崩潰就繼續反覆這一過程。

2.3.2 突變引擎(Mutation Engine)

這種fuzzing方法遵循一個原則就是站在前者資料的對立面:每一個新的輸入資料都是從sniffer元件收集到的有效輸入資料變化生成。fuzzing的過程也比較簡單:使用不同的突變函式將sniffer收集到的有效輸入資料進行變異,然後使用突變後的資料進行請求。如果系統沒有崩潰,monitor會檢查kext給出的返回值。儘可能的將會導致kext返回error的輸入值排除在下一個突變生成之外。這極大的提高了fuzzer的效率。特別是在輸入結構時可變大小的時候,因為它逐步去除了那些在目標方法中被檢查非法的輸入。這個引擎使用的突變函式有:位翻轉,位元組翻轉,位元組交換和大小改變。

2.3.3 進化引擎(Evolution Engine)

進化引擎試圖突破其他引擎的限制。儘量少的使用偽隨機。利用進化演算法來生成新的輸入資料。

任何進化演算法的核心就是適應函式(fitness function)。它定義了生成器使用的最佳元素。在LynxFuzzer裡,我們開發了兩種適應函式:一個測量輸入向量的程式碼的覆蓋率。另一個測量輸入和最佳輸入向量(導致崩潰的輸入)的差距。在第一種情況中,我們極力去生成一組輸入向量來給我們最佳的程式碼覆蓋率。第二種情況在我們想在定製一個給定向量(比如一個會觸發bug的向量)的時很有用。

程式碼覆蓋率分析。我們的程式碼覆蓋率分析方法如下:在開始呼叫kext方法之前,fuzzer元件告知偵錯程式kext的程式碼範圍。偵錯程式將相應程式碼記憶體段的可執行屬性在EPT(Extended Page Tables)入口中移除。這樣只要kext執行相應頁的程式碼時就會觸發一個EPT違規。偵錯程式跟蹤到導致違規的指令。為了繼續,偵錯程式重新將不可執行夜標誌為可執行,同時讓程式單步執行。當偵錯程式由於除錯異常而重新獲得控制權,它再將這個可執行許可權移除,所以下一個指令還是會產生執行違規。當被fuzz的方法返回,fuzzer發出呼叫來解除跟蹤。偵錯程式存貯收集到的資訊到fuzzer的一塊空間中,讓使用者空間的元件來計算相應呼叫的程式碼覆蓋率。

enter image description here

0x03 實驗評估


本節介紹下我們測試LynxFuzzer效率的的實驗。我們測試了17個同的核心擴充套件。找到了6個bug。其中2個已經在OS X 10.9中得到了修復。已經被蘋果公司定義為CVE-2013-5166和CVE-2013-5192。剩下的4個還沒有被修復。也許會在以後版本中修復。

所有的的實驗都是在安裝來Mac OS X 10.8.2系統的蘋果電腦(Intel i5 CPU 12G RAM)上進行的。由於蘋果核心的安全機制。我們找到的漏洞沒有一個可以簡單溢位進行提權的攻擊的。

評價fuzzer效率的的其中一個指標就是程式碼覆蓋率水平。這個指標也許不是那麼的絕對:一個fuzzer也許程式碼覆蓋率達到100%也獲取不了一個bug。但是通常都會報告這一指標,所以我們統計了LynxFuzzer的程式碼覆蓋率。

雖然我們的偵錯程式可以輕鬆的追蹤每一條指令。但是給出一個精確的覆蓋率還是相當不容易的。我們透過靜態動態混合分析技術來估算出可以有分派表達到的程式碼總量。首先,我們靜態的計算匯出方法的指令數。塞選出所有控制轉移指令CTI(control transfer instrction)。然後,對於每個CTI,如果跳轉目標是同一個kext的另一個方法,我們就將其統計到總數中。

不幸的是這還是有不足之處。由於核心的面相物件特性,kext包含很多間接CTI,無法靜態跟蹤。對於這類指令,我們採用動態分析:我們改進了LynxFuzzer程式碼覆蓋率分析模型,讓它dump每一條kext中每一條CTI指令的目標。如果目標在靜態分析中沒有被檢測到,我們還是將這個指令計算到總數中。

表1顯示了一部分程式碼覆蓋率的實驗結果。在覆蓋率欄我們顯示了3種不同的覆蓋率百分比:匯出方法的指令數,混合分析的指令數,kext中指令總數。

我們還估算了在3.3中描述的程式碼覆蓋率方法的開銷。為此,我們在10個不同kext上執行隨機引擎fuzz每個方法。將程式碼覆蓋率統計模組分別開啟和關閉,統計kext每秒可以處理的請求數。出於精確性,我們在每個模組上進行了10次重複。結果取平均值。平均開銷是3.45x,最優和最差分別是1.73x和5.99x。表2給出了細節。我們可以看到,我們為獲取高精度,在沒有最佳化目標時付出了較大的代價,但是相對其他技術來說也算是很低了。

enter image description here

最後,評估了引擎的效率,特別指出,透過不同配置的我們的引擎都能用於發現bug,但是基於程式碼覆蓋率的進化引擎是最快的,隨機引擎最慢。

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章