通過DARPA的CFAR保護軟體免受漏洞利用
今天,我們(Trail of Bits)將要討論我們正在努力解決的一個問題,這個問題是DARPA網路容錯攻擊恢復(CFAR)計劃的一個組成部分:自動保護軟體免受零時差攻擊、記憶體損壞還有許多當前未知的bug。你可能在想為什麼要這麼麻煩,難道不能只使用堆疊保護,CFG或CFI等漏洞利用緩解來編譯程式碼嗎?當然,這些緩解措施是很棒的,但需要原始碼和對原始碼修改後才能構建過程。在許多情況下,更改構建過程或更改程式原始碼是不切實際的。這就是為什麼我們的CFAR解決方案可以保護源在不可用/不可編輯狀態下的二進位制安裝。
CFAR看似非常直觀簡單。系統並行執行軟體的多個版本或“變體”,並通過比較這些變體來識別某些個版本在行為上何時與其他版本不同。這個想法類似於入侵檢測系統,該系統將程式行為與在相同輸入結果上執行的自身變體進行比較,而不是與過去行為的模型進行比較。當系統檢測到行為差異時,它可以推斷出發生了異常且可能是惡意的事情。
像所有DARPA計劃一樣,CFAR是一個龐大而棘手的研究問題。我們只處理它的一小部分。我們和我們的隊友——Galois,Immunant和UCI共同創作了這篇文章,他們每個人都對CFAR專案貢獻了很多細節。
我們非常樂意談論CFAR,不僅因為它是一個棘手的相關問題,也是由於我們的一個工具——McSema,我們團隊基於LLVM的多功能解決方案的一份子。在本文的部分篇幅中,我們將展示McSema一些鮮為人知的特徵,並解釋開發緣由,也許最讓大家感興趣的是,如何使用McSema和UCI多編譯器來強化現成的二進位制檔案以防止被利用。
我們的CFAR團隊
CFAR的總體目標是在不影響核心功能的情況下檢測現有軟體中的故障並從中恢復。我們團隊的職責是生成一組最佳變體,以檢測和減少引發故障的輸入。其他團隊負責專門的執行環境、紅隊,等等。Galois在其關於CFAR的部落格文章有更詳細的描述。
這些變體的行為必須與原始版本完全相同,並能證明對於所有有效輸入都能保持不變。我們的隊友已經開發了轉換,併為具有可用原始碼的程式提供了等效保證。團隊使用Clang / LLVM工具鏈設計了一個基於多編譯器的變體生成解決方案。
McSema的角色
考慮到原始碼可能不適用於專有或較舊的應用程式,我們一直致力於生成二進位制軟體的程式變體。我們團隊的基於原始碼的工具鏈在LLVM中間表示(IR)級別工作。IR級別的轉換和加固程式允許我們在不改變程式原始碼的情況下操作程式結構。使用McSema,我們可以將二進位制程式轉換為LLVM IR,並能重新利用相同的元件讓源級別的二進位制變體生成。
為了能夠準確的翻譯CFAR程式,我們需要彌合機器級語義和程式級語義之間的差距。機器級語義是由單個指令引起的處理器和記憶體狀態的更改。程式級語義(例如,函式,變數,異常和try / catch塊)是表示程式行為的更抽象的概念。McSema被設計成機器級語義的翻譯器(名稱“McSema”源自“機器程式碼語義”)。但是,要準確轉換CFAR所需的變體,McSema也必須恢復程式語義。
我們正在積極努力恢復越來越多的程式語義,並且已經支援許多常見的用例。在下一節中,我們將討論如何處理兩個特別重要的語義:堆疊變數和全域性變數。
堆疊變數
編譯器可以將資料支援函式變數放在任意幾個位置中。程式變數裡最常見的位置是堆疊,這是一個專門用於儲存臨時資訊並且易於被呼叫函式訪問的記憶體區域。編譯器儲存在堆疊中的變數稱為堆疊變數。
int sum_of_squares(int a, int b) { int a2 = a * a; int b2 = b * b; return a2+b2; }
圖1:在原始碼級別和二進位制級別顯示的簡單函式的堆疊變數。在二進位制級別,沒有單個變數的概念,只有大塊記憶體中的位元組。
當攻擊者將bug轉化為漏洞時,他們通常依賴於特定順序的堆疊變數。多編譯器可以通過生成程式變體來減少此類漏洞,其中沒有哪兩個變體會具有相同順序的堆疊變數。我們希望為二進位制檔案啟用堆疊變數重排,但是存在一個問題:在機器程式碼級別沒有堆疊變數的概念(圖1)。相反,堆疊只是一個大的連續記憶體塊。McSema如實的模擬了這種行為,並將程式堆疊視為不可分割的一塊。當然,這會使得堆疊變數無法進行重排。
堆疊可變恢復
將表示堆疊的記憶體塊轉換為單個變數的過程稱為堆疊變數恢復。McSema將堆疊變數恢復分為三個步驟實現。
首先,McSema通過反彙編程式(例如IDA Pro)的啟發式演算法以及基於DWARF(如果存在的話)的除錯資訊,在反彙編期間識別堆疊變數邊界。以前的研究是在沒有這些提示的情況下識別堆疊變數邊界,但我們計劃在將來使用這些提示。其次,McSema嘗試識別程式中,哪些指令在引用哪個堆疊變數。必須準確識別每個參考,否則生成的程式將無法執行。最後,McSema為每個恢復的堆疊變數建立一個LLVM級變數,並重寫指令以引用這些LLVM級變數,而不是先前的單片堆疊塊。
堆疊變數恢復適用於許多功能,但它並不完美。當遇到具有以下特徵的函式時,McSema將預設採用將堆疊視為整體塊的經典行為:
·Varargs功能。使用可變數量引數的函式(如常見的printf函式系列),具有可變大小的堆疊幀。這種差異會導致很難確定哪個指令引用哪個堆疊變數。
· 間接堆疊引用。編譯器還依賴於堆疊變數的預定佈局,並將生成通過不相關變數的地址訪問變數的程式碼。
· 沒有堆疊幀指標。作為優化,堆疊幀指標可以用作通用暫存器。這種優化使我們很難檢測到可能的間接堆疊引用。
堆疊變數恢復是CFG恢復過程的一部分,目前在IDAPython CFG恢復程式碼(在collect_variable.py中)中實現。它可以通過–recover-stack-vars引數呼叫mcsema-disass。有關示例,請參閱 ofollow,noindex" target="_blank">此篇文章 隨附的程式碼“升級和多樣化二進位制”部分對此進行了詳細介紹。
全域性變數
程式中的所有函式都可以訪問全域性變數。由於這些變數與特定函式無關,因此通常將它們放在程式二進位制檔案的特殊部分中(圖2)。與堆疊變數一樣,攻擊者可以利用全域性變數的特定順序。
bool is_admin = false; int set_admin(int uid) { is_admin = 0 == uid; }
圖2:從原始碼級別和機器程式碼級別看到的全域性變數。全域性變數通常放在程式的特殊部分(在本例中為.bss)
與堆疊一樣,McSema將每個資料部分視為一大塊記憶體。堆疊和全域性變數之間的一個主要區別是McSema知道全域性變數的起始位置,因為它們直接從多個位置引用。不過,這並不足以使全域性變數佈局混亂。McSema還需要知道每個變數的結束位置,這更難。目前,我們依靠DWARF除錯資訊來識別全域性變數大小,但期待實現可用於沒有DWARF資訊的二進位制檔案的方法。
目前,全域性變數恢復與正常的CFG恢復(在var_recovery.py中)分開實現。該指令碼建立一個“空”CFG,僅填充全域性變數定義。正常的CFG恢復過程將使用實際控制流圖進一步填充檔案,引用預先填充的全域性變數。我們稍後將展示使用全域性變數恢復的示例。
提升和多樣化二進位制
在本文的其餘部分,我們將通過多編譯器引用生成新程式變體的過程稱為“多樣化”。對於此特定示例,我們將提升和多樣化使用異常處理的簡單C ++應用程式(包括捕獲-all子句)和全域性變數。雖然這只是一個簡單的例子,程式語義恢復意味著可以處理大型實際應用程式:我們的標準測試程式是Apache2 Web伺服器。
首先,讓我們熟悉標準的McSema工作流程(即沒有任何多樣化),即將示例二進位制檔案提升到LLVM IR,然後將該IR編譯回可執行的程式。要開始使用,請構建並安裝McSema。我們在官方McSema README中提供詳細說明。
接下來,使用提供的指令碼(lift.sh)構建並提升程式。需要編輯指令碼以匹配您的McSema安裝。
執行lift.sh後,你應該有兩個程式:example和example-lift,以及一些中間檔案。
示例程式將兩個數字對齊並將結果傳遞給set_admin函式。如果兩個數字都是5,那麼程式將丟擲std :: runtime_error異常。如果數字為0,則全域性變數is_admin設定為true。最後,如果沒有向程式提供兩個數字,那麼它會丟擲std :: out_of_range。
可以通過以下程式呼叫來演示四種不同的情況:
$ ./example Starting example program Index out of range: Supply two arguments, please $ ./example 0 0 Starting example program You are now admin. $ ./example 1 2 Starting example program You are not admin. $ ./example 5 5 Starting example program Runtime error: Lucky number 5
我們可以看到,示例的提升程式,與McSema提升並重新建立的程式完全相同:
$ ./example-lifted Starting example program Index out of range: Supply two arguments, please $ ./example-lifted 0 0 Starting example program You are now admin. $ ./example-lifted 1 2 Starting example program You are not admin. $ ./example-lifted 5 5 Starting example program Runtime error: Lucky number 5
現在,讓我們對提升的示例程式進行多樣化。首先,安裝多編譯器。接著,編輯lift.sh指令碼以指定多編譯器安裝的路徑。
現在是構建多樣化版本的時候了。使用diversify引數(./lift.sh diversify)執行指令碼以生成多樣化的二進位制檔案。 多樣化的示例在二進位制級別上看起來與原始級別不同(圖3),但具有相同的功能:
$ ./example-diverse Starting example program Index out of range: Supply two arguments, please $ ./example-diverse 0 0 Starting example program You are now admin. $ ./example-diverse 1 2 Starting example program You are not admin. $ ./example-diverse 5 5 Starting example program Runtime error: Lucky number 5
圖3:正常提升二進位制(左)及其多樣化等價物(右)。兩個二進位制檔案在功能上是相同的,但在二進位制級別上看起來不同。二進位制多樣化通過防止某些型別的bug變成漏洞來保護軟體
在您最喜愛的反彙編程式中開啟示例- 提升和示例- 多樣化。您的二進位制檔案可能與螢幕截圖中的二進位制檔案不同,但它們應該彼此不同。
讓我們回顧一下我們做了什麼。這真的很神奇。我們首先構建一個使用異常和全域性變數的簡單C ++程式。然後我們將程式翻譯成LLVM bitcode,識別堆疊和全域性變數,並保留基於異常的控制流程。然後我們使用多編譯器對其進行了轉換,並建立了一個新的,多樣化的二進位制檔案,其功能與原始程式相同。
雖然這只是一個小例子,但這種方法可以擴充套件到更大的應用程式,並且提供了一種快速建立多樣化程式的方法,無論是從原始碼還是以前的程式二進位制檔案開始。
結論
我們首先要感謝DARPA,為CFAR和其他偉大的研究專案提供持續的資金,沒有他們這項工作是不可能的完成的。我們還要感謝我們的隊友–Galois,Immunant和UCI,為建立多編譯器,轉換,為變體提供等效保證以及使所有內容協同工作所做的辛勤工作。
我們正積極致力於改善McSema的堆疊和全域性變數恢復。這些更高級別的語義不僅可以創造更多樣化和轉換機會,而且還可以實現更小、更精簡的bitcode,更快的重新編譯二進位制檔案以及更徹底的分析。
我們相信CFAR和類似技術有著光明的未來:每臺機器的可用核心數量不斷增加,安全計算需求也在不斷增加。許多軟體包無法利用這些核心來提高效能,因此很自然的將備用核心用於保障安全性。McSema,多編譯器和其他CFAR技術展示了我們如何將這些額外的核心用於更強大的安全保障。
如果您認為其中一些技術可以應用於您的軟體,請與我們聯絡。我們很樂意聽取您的意見。要了解有關CFAR,多編譯器以及在此計劃下開發的其他技術的更多資訊,請閱讀Galois部落格和Immunant部落格上的隊友部落格文章。
免責宣告
本文所表達的觀點、意見和調查結果均為作者的觀點,不作為國防部或美國政府的官方觀點或政策。