文/談心
內核中的數據競爭可能導致許多有害行為。最嚴重的后果是數據競爭導致內存污染最終可能造成非法權限提升,例如CVE-2016-8655, CVE-2017-2636, and CVE-2017-17712。目前的技術在內核數據競爭漏洞的檢測與防護方面都存在一定的局限性,其原因是內核中的數據競爭漏洞受到一些系統不確定性行為的影響,比如線程的調度與同步機制。因此與普通的漏洞相比,要檢測這類漏洞除了需要控制流和數據流信息以外,還需要精準地執行信息。
在該項工作中,本文提出并設計了針對內核中的數據競爭類型漏洞的模糊測試(Fuzzing)工具Razzer。本文的思路是引導Fuzzing工具去盡可能地執行到存在數據競爭漏洞的代碼。這包含了兩種技術:一是通過靜態分析來定位潛在存在數據競爭的代碼;二是一種確定性的線程交錯技術,來控制線程調度,以提供精確的并行執行信息,降低不確定性。本工作并沒有去解決同步機制對多線程fuzzing的影響。
為了更好地檢測數據競爭類型漏洞,文中對這類問題給出了一個明確的定義。
如果目標程序內兩條內存訪問的指令滿足以下3個條件,就是數據競爭:1. 訪問的內存地址相同;2. 至少其中一條指令是對內存的寫;3. 兩條指令可以并發執行。
同時,數據競爭并不都是漏洞,有一些數據競爭可能是開發者有意設計的,有一些數據競爭行為可能產生非預期的行為,這些才是數據競爭漏洞。文中還引入了一些標注,以便后續的說明:
RacePair_{cand}:可能滿足上面三個條件的RacePair
- RacePair_{true}:已確定滿足上面三個條件的RacePair,是RacePair_{cand}的子集
- RacePair_{benign}:屬于預期內的數據競爭
- RacePair_{harm}:非預期的數據競爭
本文給出了一個具體的例子CVE-2017-2636來具體說明,相關代碼如圖1所示。
一個用戶態程序的兩個線程A、B,分別調用了系統函數ioctl和write,這兩個系統函數會由內核來執行。在參數滿足一定條件的情況下,線程A和B都會去訪問同一個內存地址n_hdlc->tbuf,滿足條件一;其中第440行是對內存的寫,第216行是對內存的讀,滿足條件二;這兩處訪存可以并行執行,滿足條件三,因此這是一組RacePair_{true}。而且在這個例子中,程序的行為會由于兩條指令執行先后的不同而變化。當第216行先于440行執行,n_hdlc->buf會被壓入free_list兩次,在后續的代碼中,free_list中的元素會被依次釋放(第269行)。因此最終會導致double free的問題。這一組指令是一組RacePair_{harm}。

圖1 代碼示例
本文把檢測內核中的數據競爭漏洞拆分成了兩個設計需求。
1. 找到一個執行RacePair_{cand}的程序。即找到一個多線程的用戶態程序,每個線程能夠在內核態分別執行到RaceRair_{cand}的指令。
2. 找到一個線程執行序列,使得這RacePair_{cand}的指令能并行執行。
需求1把問題做了簡化,并不去考慮并行執行的問題,就不用考慮線程調度對分析的影響。需求2主要是去尋找一個交錯執行的線程調度方案,使得RacePair_{cand}的指令能并行執行。現在的大部分工具都是只針對上述某一個需求的,而且都存在一定的局限性。

圖2 工具架構
對于需求1,Google的內核Fuzz工具Syzkaller會隨機生成一系列的系統調用,隨機組合成多線程程序,然后運行。但不會考慮線程調度對程序運行的影響。而且純隨機的生成系統調用使得其效率較低。
對于需求2,有一些關注線程交錯執行隨機化地工作,比如 SKI和PCT 算法。這些工具對于給定的一個程序,會去探索所有可能的線程交錯執行序列來執行這個程序。但其缺點是這些工具的算法隨機性都比較強,效率不高。因為在這個研究課題的場景下,我們只需要關心RacePair_{cand}指令的線程調度情況,其他指令的執行順序不是我們所關注的。
下面介紹本工作的設計思路。
Razzer結合使用了靜態分析和動態分析的方法。先通過靜態分析得到內核中潛在的存在數據競爭的代碼RacePair_{cand}。之后會進行兩階段的動態分析,第一階段進行單線程Fuzz,找到一個能執行到RacePair_{cand}的用戶態程序。然后按照算法將這個程序轉化為一個多線程程序(滿足條件一)。第二階段是多線程Fuzz。會尋找特定的線程交錯,使得在執行多線程程序時能并行執行RacePair_{cand}。如果找到了,則獲得了一個RacePair_{true}。Razzer還會檢測內核是否出現了錯誤,如果RacePair_{true}在程序后續執行過程中,導致了內核錯誤,則得到一個RacePair_{harm}。整個工具的架構如圖2所示。

圖3 轉化算法
以下是一些設計上的細節問題:
1. 靜態分析:文中使用了Point_to分析來尋找內核中對同一個結構體的內存訪問。但傳統的Point_to分析具有誤報率高,復雜度高的缺陷。對于靜態分析的結果,Razzer會通過后續的動態分析來確認,以避免誤報。在性能上,本文基于一定的insight,對內核代碼進行了分部分分析,以減小分析的代碼量。
2. 線程調度:待Fuzz的內核運行在虛擬化的環境中,為了控制虛擬CPU的調度,作者修改了虛擬環境的Hypervisor,增加了以下功能:為每個虛擬CPU設置斷點,精確地控制在恢復執行時哪個線程的訪存語句先執行,不同的執行順序會導致后續是否存在錯誤行為,新的Hypervisor給Razzer提供了準確控制CPU調度的能力。
3. 多線程Fuzz:這一步的關鍵是將單線程Fuzz輸出的一個單線程程序Pst,轉化為一個多線程版本Pmt。在轉換過程中還會進行一些插樁,與Hypervisor協作控制程序的調度。轉化算法如圖3所示。
當Pmt中的RacePair_{cand}指令都觸發斷點時,Razzer會檢查訪存指令的訪問地址是否相同,如果相同,則判定為RacePair_{true}。可以注意到在Pmt的最后加入了一些隨機的syscall,這是為了使數據競爭如果造成了惡性后果,程序會報錯。當檢測到一個RacePair_{true},會把結果反饋回生成算法,保持前面的代碼不變,只修改后續隨機添加的syscall,進行新的Fuzz。如果其中某個Pmt使kernel報錯,則是一個RacePair_{harm}。
Razzer最終發現了30個惡性的數據競爭漏洞。其中有16個已經被確認。數據競爭引起的漏洞往往難以復現,更難以找到原因并修復,Razzer生成的漏洞報告極大幫助了開發者對漏洞進行修復。此外作者還測量了工具的性能開銷,并與最新的Fuzzing工具進行了比較。在可接受的額外開銷下,能夠更有效率地Fuzz這一類漏洞。
Razzer是一個專門Fuzz內核中數據競爭漏洞的工具,其亮點有兩個:第一是通過靜態分析得到一些RacePair_{cand},再用動態分析確認,即降低誤報又減少了搜索空間。第二通過算法與工具的結合,給Fuzz工具提供了比較準確的線程并行執行狀態,解決了Fuzz多線程程序的重大挑戰,值得借鑒。