Automatic Exploit Generation:漏洞利用自动化
漏洞利用是二進制安全的核心內容之一。當安全研究員挖掘到一個新的漏洞時,首先要做的事情就是嘗試寫POC和exploit。所謂POC,一般來說就是一個能夠讓程序崩潰的輸入,且能夠證明控制寄存器或者其他違反安全規則的行為。Exploit則條件更嚴格一些,是對漏洞的完整利用,通常以彈出一個shell作為目標。
從程序到漏洞再到利用,這個過程需要對二進制程序(或者加上源代碼)及其運行過程進行非常透徹的分析,往往需要安全研究員花費數日的心血去研究。漏洞挖掘的過程自動化程度已經較高,工業界利用fuzzer可以得到大量讓程序崩潰的輸入,但這些輸入及相關漏洞并不全都是可利用的。從崩潰輸入到利用的過程,需要大量人工分析,如果這個工作也可以自動化,那么將大大節省人的工作量,從而提升安全研究與防護的效率。
漏洞利用自動化在學術界已經不是一個新鮮的話題。有一個專有的名詞來定義這個領域:Automatic Exploit Generation。這個詞來源于2011年NDSS上CMU的David Brumley的一篇論文,這篇論文也可以說是漏洞利用自動化的開山之作。經過了六年的發展,AEG已經是一個比較成熟的課題,也產生了大量非常實用的解決方案,但仍然有很多問題沒有解決,所以依然是二進制安全研究領域的一個熱門方向。
這篇文章簡單介紹漏洞利用自動化的基本理念和方法,主要依據David Brumley 2011年發表的論文。文中所介紹的,是最簡化的條件下漏洞利用問題的建模和求解,因此很多實際問題都沒有考慮。無論如何,作為AEG提出的開山之作,用這篇文章來建立漏洞利用自動化的基本思路,是再好不過的。
0x00 從實際出發
講起棧溢出利用,懂點二進制安全的人肯定非常熟悉。這是所有漏洞利用教程的第一篇,一個簡單的由strcpy引起的棧溢出漏洞,通過覆蓋返回地址,跳轉到shellcode,從而能夠彈出shell。
仔細回想一下手動利用的過程,最關鍵的是以下幾個步驟:
1)定位溢出點,通常用不同長度的輸入去試驗,看什么時候會產生崩潰——嚴格來說,這是漏洞挖掘的過程。
2)定位返回地址的位置,在有源代碼的情況下可以自行分析棧幀的布局,從而計算出返回地址的偏移,或者在無源代碼的情況下用一些工具去試驗,比如metasploit的pattern_create.rb。
3)精心構造shellcode布局,在精確的返回地址的位置上放置我們的shellcode的起始地址,然后把shellcode的位置也安排好。如果一切計算無誤,那么將構造好的輸入傳給漏洞程序,就會觸發漏洞,返回到shellcode的地址并執行shellcode,順利彈出shell。
那么這個過程中,有哪些信息是需要人工去獲取的呢?
1)漏洞位置定位,主要表現為什么樣的輸入會讓程序崩潰觸發漏洞。
2)返回地址定位,應該覆蓋哪一部分為要跳轉的地址。
3)棧的布局,此信息將指導如何精心構造shellcode布局。
4)安排最后的整個輸入的布局,每個部分放在什么位置,要基于上面的信息進行推理。
所以,如果我們要進行漏洞自動利用,那么就必須能夠自動生成上述所有進行利用必需的信息。
0x01 問題定義
我們要做的是一件什么事情?給AEG系統輸入一個程序,經過該系統內部的各種計算和運行,就能夠輸出一個有效的利用輸入。這里的“有效”,姑且定義為能夠彈出shell。2011年的AEG還不具備只依靠二進制程序就能產生利用的功能,所以我們再加一個條件,就是擁有這個二進制程序的源代碼。所以我們要做的是,在僅有源代碼的情況下(AEG系統可自行編譯為二進制),不需要人做任何事情,AEG系統就可以自動生成一個有效的利用。
是不是聽起來很誘人?當然,2011年的AEG還是一個非常簡化的模型,還要有以下幾個條件:①漏洞類型限制為棧溢出或者格式化字符串漏洞;②利用類型限制為覆蓋返回地址的控制流劫持導致彈出shell;③沒有考慮任何系統的安全防護措施,包括棧保護、NX、ASLR等。④必須有程序的源代碼;⑤系統環境限制為基于x86的Linux系統。
我們首先來看一個實際的例子,來描述這個問題。所用的例子是iwconfig程序,有大約3400行C代碼。截取其中的一小段:
我們可以看到,15行有一個經典的strcpy造成的棧緩沖區溢出。如果我們要對這個漏洞進行利用,就需要完成以下4個步驟。
1. 定位漏洞位置。
這一步主要是通過分析源代碼完成,也就是說,我們要找到一個輸入,能夠讓程序順利地執行到漏洞存在的那個點,也就是找到從輸入到漏洞點的路徑。AEG利用符號執行技術完成這件事情。
具體說來,就是iwconfig的這段代碼中有多個條件和循環語句,如果畫出控制流圖,我們可以發現程序路徑是非常非常多的,但是能夠到達15行的strcpy的路徑卻不多。順著路徑main->print_info->get_info,就可以到達15行,檢測到越界內存錯誤,而且可以發現錯誤發生在變量ifr.ifr_name上。這些信息就是我們要用符號執行去檢測的。
2. 獲取程序實際運行時的棧布局及其他信息。
這一步需要二進制程序,就用上一步生成的輸入去運行,并且要得到運行時的信息,例如漏洞函數名稱、溢出點的地址、棧的布局等等,這些信息是生成利用必備的。AEG通過動態二進制分析完成這一過程。
如果我們手動分析這個漏洞,很顯然會得到下圖中的執行時棧布局,這個信息對于我們判斷如何利用起到非常大的作用。比如說,我們可以獲知漏洞函數get_info的返回地址位于棧中的第幾個字節的偏移上。
3. 根據上述信息生成利用。
這就要對利用進行形式化描述,將漏洞利用編程一個形式化驗證問題,用符號執行產生生成利用的約束公式,然后用求解器進行求解。通俗點說,就是我們要把一些條件轉化為形式化的描述,變成路徑條件的約束。
這些條件在例子中包括:1)ifr.ifr_name必須包含shellcode;2)覆蓋的返回地址必須包含shellcode地址。我們把這兩個條件加在路徑的約束上,那么約束求解器就會求解出滿足這兩個條件的答案,也就是能夠生成利用的答案。
4. 對利用進行驗證。
約束求解器求出來的滿足的答案就是我們要的利用輸入。最后AEG會用這個輸入去運行以驗證。如果求解不出來就重新去尋找另一個漏洞嘗試進行利用。
0x02 符號執行
在講解AEG問題的形式化定義之前,我們先來簡單介紹一下符號執行。整個AEG方案的基礎和關鍵就是符號執行,正是因為有了這個技術,我們才能夠將AEG變成一個形式化的問題去研究。
將程序的執行抽象為很多條不同的路徑,其分支在于代碼中的條件跳轉。例如下面這段非常簡單的C程序,在第3行就有一個分支條件,這里的條件會讓路徑分開為2條,一條是條件a>1為true,一條是a>1為false。符號執行就是把程序的執行用符號化的公式進行表示,直到最后才求解出滿足約束的值,從而達到遍歷程序所有約束的目的。
比如說,這個示例程序中,符號執行器會先將輸入a定義為一個符號化的int值,其大小為32字節,這32字節的每一位都是符號化的。然后繼續運行,會分支出兩個條件,一個是a>1為true的,這條路徑的條件約束就是“a>1”,走這條路徑的話,最后輸出的b的約束條件就是“a-1”。同理,另一條路徑的條件約束就是“a<=1”,輸出b的約束條件就是“a+1”。
如果我們想要程序能夠走到兩條路徑,達到100%執行覆蓋率,那很簡單,求解器會隨便取兩個值,一個是大于1的值,另一個是小于等于1的值,這兩個輸入就足夠覆蓋兩條路徑。
如果我們想要b輸出一個特定值,也很簡單。假如我們想要b輸出一個值“2”,那么我們有兩條約束:1)a>1且a-1=2;2)a<=1且a+1=2。1)可計算出a=3,2)可計算出a=0,也就說約束求解器會給出兩個答案:a=0或a=3。又如我們想讓b輸出0,那么兩條約束:1)a>1且a-1=0;2)a<=1且a+1=0。這就只能算出一個答案a=-1。
有了符號執行,我們可以把非常復雜的程序變成如同例子一樣的形式化的約束求解問題,于是可以解決很多以前解決不了的問題,只要我們能夠把這個問題轉化為約束求解問題,進行形式化的建模。當然,用于真實的、大型的程序的符號執行是非常復雜的,而且求解器又是另外一個非常難的研究領域,具體的細節這里不表,知道這些就足夠了。
0x03 形式化建模
現在讓我們回到AEG問題的形式化建模上來。一個漏洞利用的生成,其實主要就是兩個條件:1)有漏洞;2)可以利用。這兩個條件看起來是廢話,但如果細究,其實是包含很多具體的約束條件的。我們先將可利用狀態用兩個布爾謂詞定義:有漏洞的執行路徑謂詞和控制流劫持利用謂詞。你可以把這兩個謂詞理解為“兩個條件”的形式化定義。其中,定義了漏洞存在的條件,定義了控制流劫持的條件。那么,一個有效的利用exploit,就是一個能夠滿足下面這個布爾表達式的輸入:
其實這個式子很好理解。一個輸入,既能滿足觸發漏洞條件,又能滿足利用條件,當然就是一個可用的利用。但是關鍵問題是,這兩個條件如何定義。
首先是不安全路徑謂詞。這個謂詞表示執行的路徑違反了一個安全特性,我們用表示。這個是什么,要根據漏洞的類型去定義。比如說,C程序常見的一些安全問題包括越界讀寫、不安全的格式化字符串等等,所謂的安全特性就是一些安全規則的定義。所以,可以理解為對漏洞的定義和描述,那么找到什么樣的漏洞,取決于安全特性如何定義。在符號執行過程中,就是定義了能夠到達漏洞所在路徑的條件。比如我們實例中展示的那一種緩沖區溢出漏洞,復制的輸入字符串大小顯然超過了緩沖區大小的界限,這就是一個明顯的漏洞條件。
然后是利用謂詞。定義了攻擊者的邏輯,也就是說如果能夠劫持EIP,攻擊者會做什么。例如,如果攻擊者只想讓程序崩潰,謂詞就可以是簡單的“將eip設置為無效地址,在獲取控制之后”。然而我們的目的是彈出shell,那么就會定義:①shellcode必須存在于內存中;②EIP必須要指向shellcode的起始位置。如果滿足了和之前的就是最終的結果。如果不滿足,說明漏洞是不可利用的。
這樣一來,我們就把問題轉換為了將漏洞類型建模為,和將利用類型建模為的過程。那么問題的關鍵就在于,如何定義這兩個條件,尤其是,需要對各種漏洞利用方式非常熟悉。當然這里的利用方式是最簡單的,如果涉及到非常復雜的利用條件,那么又是另外一種情況,這個的定義就不那么容易了。
很好,我們已經將漏洞挖掘和利用的問題轉化為了一個形式化驗證的問題,要做的說白了就是生成一大堆復雜的公式,然后去求解它就好了。那么這復雜的公式怎么生成?具體的約束應該是什么?需要的信息怎么獲取?最終公式如何求解?接下來要做的,就是如何實現。
0x04 系統總覽
整個AEG系統的實現,可以分為六大部分,如圖所示。
1)PRE-PROCESS
第一個步驟就是預先處理,可以用如下公式表示:
其實就是把源代碼分別用GCC和LLVM進行編譯,得到二進制和字節碼表示。其中,其實是一種中間代碼,用于符號執行器進行分析,以找到漏洞條件;而則是用于二進制分析,用來獲取運行時信息,以生成利用條件。
2)SRC-ANALYSIS
第二個步驟就是分析源代碼,可以用如下公式表示:
具體說來就是分析LLVM生成的中間代碼,這是一個靜態分析的過程,可以得到程序的一些靜態信息,比如說buffer創建的時候的最大長度是什么,也就是說通過搜索最大的靜態分配buffer大小來定義max。這個max就是符號化輸入數據的最小長度——這很好理解,要觸發漏洞,顯然必須要給定比buffer更長的輸入,才有可能覆蓋棧中的其他數據造成崩潰。這其實是為下一步符號執行器的分析做準備,這樣可以提升符號執行器的分析效率。
3)BUG-FIND
第三個步驟就是尋找漏洞并定位,可以用如下公式表示:
以作為輸入,以及一個安全特性,會輸出一個元組表示漏洞。是表示漏洞的路徑謂詞,V是源代碼級別的信息,這個信息主要包括關于被檢測到的漏洞的各種信息,例如要被覆蓋的對象的名稱,漏洞函數是什么等等。這些信息V對約束生成以及下一步的利用分析都是有用的。
4)DBA
第四個步驟就是動態二進制分析,可以用如下公式表示:
之前第三步其實我們就可以得到一個能夠造成程序崩潰的證明漏洞的輸入了,就用表示,實際上是一個符號執行器生成并進行求解的約束條件,崩潰輸入就是求出的結果。用這個崩潰輸入去運行二進制程序,并在這個運行過程中進行二進制分析,得到提取出的運行時信息R。這個R信息的獲取,要用到V中定義的源代碼級別信息,如漏洞函數。R信息包括一些利用時必須用到的信息,如有漏洞的緩沖區在棧上的地址,漏洞函數的返回地址,以及漏洞觸發前的棧內存內容等。最終生成利用要依據的主要就是R信息。
5)EXPLOIT-GEN
第五個步驟就是生成利用,可以用如下公式表示:
二進制程序和利用表達式作為輸入,用約束求解器進行求解。如果可以滿足有效利用的條件,就返回一個利用,否則返回表示利用不存在。而且作者還增加一步用去運行二進制,檢查是否達成條件,例如彈出shell,這個是否成功的驗證將反饋給約束求解器,以供利用的生成和選擇。
以上就是整個AEG系統運行的過程。這個算法思路其實非常清晰,其實就是兩步走:一是用LLVM的中間語言去進行程序分析,找到漏洞,生成漏洞條件 ;二是用能夠觸發的漏洞輸入和GCC編譯的二進制進行二進制程序分析,得到運行時信息,再加上此前的中間語言程序分析,可以得到利用條件 。所以計算兩個條件都滿足的輸入就可以得到利用了。整個系統的關鍵在于兩個條件的定義,其實也就是把漏洞利用問題轉化為形式化驗證問題。LLVM的分析屬于漏洞挖掘領域,的生成才是論文創新的重點,關鍵在于利用條件的定義。在沒有任何保護的情況下,這個條件的定義相當簡單:①能夠控制一塊區域且把shellcode放在這里;②能夠控制eip跳到shellcode。當然,這一切的實現都因為有符號執行這個非常厲害的工具。
了解到AEG系統的整體原理之后,我們再來分別單獨介紹一下漏洞挖掘和利用生成這兩部分的細節究竟是如何實現的。
0x05 基于符號執行的漏洞挖掘
上述所謂生成約束條件的過程,本質上就是利用符號執行挖掘漏洞的過程。自動化漏洞挖掘在工業界通常都是用fuzzer完成的,因為效率比較高,較短的時間內就能得到很多導致崩潰的輸入,然后再手動分析驗證是否可利用就好。但學術界更青睞符號執行,因為相比fuzzing能夠得到更多的語義信息,能夠深入到fuzzer走不到的深層程序路徑,挖到質量更好的漏洞。但是符號執行最大的問題就在于效率,現實世界的程序是非常復雜的,有無數的分支。由于符號執行器的實現通常是在遇到路徑分支的時候會復制出來一個interpreter進行分析,那么在分支不斷產生的過程中,程序的開銷是以指數級增長的,又稱為“路徑爆炸”問題。所以主流的符號執行研究都是基于如何能夠減少路徑爆炸,提升符號執行的可用性。
AEG的作者在這個問題上也提出了幾種面向利用生成的優化方法,用來提升漏洞挖掘的效率。具體說來有這么幾種:①前置條件的符號執行、②路徑優先級選擇、③環境建模。
1)前置條件的符號執行
給約束加上一個前置條件,就能夠縮小原來很大的一個搜索空間。在AEG中,這個前置條件具體說來,就是輸入的長度必須足夠大道能夠觸發漏洞,覆蓋緩沖區,才有可能產生利用。
在這個例子中,input最多42個字節,如果沒有前置條件,那么在第3行的while語句塊中,由于循環產生的分支,完全符號執行需要復制出42個interpreter去進行分析,而且循環里面還套著循環,這就會產生很大的路徑爆炸。
但是,如果加上前置條件,即buf長度要超過20字節才有可能觸發漏洞并且產生利用,那么這樣就將復制的interpreter數量大大減小,能夠提升符號執行的效率。
AEG中還有另一種條件,就是輸入前綴。輸入前綴要根據程序的具體功能來看,比如HTTPGET請求總是以GET開始,那么GET就是相關輸入的前綴;又比如是一個圖片處理程序,處理的格式是PNG,那么所有的PNG文件的標準是前8字節頭部以PNG_H開頭,這就是要給前綴。如果有這個條件,比將輸入所有字節都符號化,顯然效率高了很多。
2)路徑優先級選擇
在上一個方法中,搜索空間縮小了,但仍然有一個路徑選擇的問題,就是復制了那么多條分支路徑,哪一條應該先探索,哪一條應該后探索呢?解決方法就是路徑優先級,所有的路徑都被插入一個優先級隊列,基于路徑的排名進行探索。這里有兩種排序方法。
第一是漏洞路徑優先。這個方法基于的思想是,如果一條路徑上出現了一個小錯誤,那么說明程序員很容易犯錯誤,那么在這條路徑的后面就更有可能有別的漏洞。在作者的實驗中,就發現一條路徑上,先是出現了一個off-by-one的漏洞,雖然沒法直接利用,但說明程序員對邊界并不是很在意。他們繼續分析,果然在這條路徑后面又發現了一個長度相關的緩沖區溢出。
第二就是循環耗盡策略。傳統的符號執行對于循環的處理多是把帶有循環的路徑優先級降低,或者只循環一定次數。但是這種方法是在基于保證覆蓋率的前提下提出的,并不適用于漏洞挖掘。事實上,常出現漏洞的strcpy函數本質上就是一個復制字節的循環,只要沒遇到NULL字節就會一直循環,所以要想發現漏洞,我們就必須保證每一個循環都能夠耗盡。當然,這可能產生非常大的路徑爆炸,這個問題其實不用在意,因為前面的前置條件方法的使用已經解決這個問題了——因為我們知道了能夠覆蓋緩沖區的最小長度,其實就是知道了strcpy的覆蓋長度,也就是它的循環長度。所謂的strcpy循環,其實就是每個字節都判斷一下是不是0。當我知道strcpy長度的時候,就不需要判斷前面的字節是不是0了,所以實際上可以只用一個解釋器,這就不存在路徑爆炸問題了。
3)環境建模
程序在運行中是不斷與運行環境進行交互的,所以就需要系統能夠自動化地對環境進行模擬。AEG對環境建模比較完全,包括文件系統、網絡包、標準輸入、程序參數、環境變量,而且還能夠處理常見的系統函數和庫函數調用。雖然文中沒有詳細解釋,但我想應該是AEG對每個可能的環境變量進行設置與解析,而庫函數則有相應的建模和信息傳遞。
總而言之,上述的三種方法其實都是只有一個目的,就是加快符號執行發現漏洞的效率。另外值得注意的是,采用前置條件的實現是對程序進行了靜態分析,提取出相關的信息,才能夠定義程序的前置條件。
0x06 利用生成與驗證
我們之前講過,利用生成模塊的公式是這樣的:
其中已經在符號執行的過程中得到了,那么下面就介紹如何獲取運行時信息R。
利用生成步驟所需要的信息R是由二進制動態分析實現的,具體方法是插樁。在進行分析前,首先需要輸入三個信息:1)目標二進制,2)導致漏洞的路徑約束,3)漏洞函數和緩沖buffer的名稱。這些信息是上一步的符號執行步驟中獲取的。
經過二進制動態分析之后,能夠輸出以下運行時信息:1)覆蓋的地址&retaddr(我們的實現中就是函數的返回地址,也可以拓展為包括函數指針或者GOT表中的入口);2)要寫入的開始地址bufaddr;3)額外的約束 ,即在漏洞觸發之前的棧內存內容。這里有一個非常重要的內容,就是棧恢復。如果在漏洞函數返回之前,棧上的信息有被使用的話,就會造成程序崩潰或者已構造的部分被覆蓋。舉個例子來說,在下面的代碼中,攻擊者想要利用strcpy的緩沖區溢出,但是ptr參數在返回地址和buf之間,ptr是在棧溢出之后被使用的,那么我們的棧溢出會導致原本的ptr被破壞,程序對ptr解引用時就會出錯終止。所以一個復雜的攻擊必須考慮上述情況,不要覆蓋有效的內存指針。AEG解決的辦法就是在動態二進制分析的時候檢查整個棧空間的內容,把具體的信息μ傳遞給利用生成模塊,其實理解起來很簡單,就是既然這個位置的數不能改,那我就記錄下來,在構造輸入覆蓋的時候讓原本應該是ptr的位置還放置ptr的值就可以了。
有了運行時信息R,我們就可以生成路徑約束 了。具體的算法偽代碼如下:
我們可以看到,算法是對要構造的棧空間內容逐個進行恢復,第2行就是棧恢復,本質上是對棧中內容符號化。第4行的jmp_target =&retaddr - bufaddr + 8是最簡單的棧溢出利用的套路,即確定返回地址跳轉到什么位置。1-5行的內容是檢查是否可以劫持eip這個條件,具體說來就是exp_str[offset]代表返回地址的位置,jmp_target是shellcode放置的位置,要看返回地址能否跳到jmp_target。6-7行是檢查shellcode是否放置在從exp_str[offset]開始的位置。最后就是返回這個約束 的公式。現在我們有了約束,接下來要做的就是求解了。這就是VERIFY步驟,是利用約束求解器對符號公式進行求解。具體如何求解是一個非常復雜的知識,屬于形式化驗證的范疇。目前安全研究使用約束求解器基本都是當黑盒用,其內在原理有一些在形式化驗證方面更加專業的科學家在研究。總而言之,最后約束求解器產生的那個符合條件的輸入,就是能夠產生利用的輸入。VERIFY還會進一步實際執行一下這個輸入,看看是否真的能彈出shell。
0x07 總結
至此我們就介紹完了整個AEG系統的基本原理和實現。這里還需要說一下AEG的工程實現:由C++和Python編寫,其中符號執行器是在KLEE的基礎上加了大約5000行代碼以實現原創的技術和功能;動態二進制分析是用Python編寫,使用了GDB wrapper的接口實現;約束求解器使用了STP。
總結來說,AEG是安全研究領域第一次將“利用自動生成”提出來作為一個研究課題,漏洞利用自動化的研究熱潮也自此拉開序幕。當然,這畢竟是2011年的文章,其模型和條件都十分簡單,有很多內容是沒有考慮的。
我相信讀者讀了這篇文章,一定會想,如果有NX、ASLR、棧保護的話,可以自動繞過嗎?其他漏洞類型和利用類型可以實現嗎?Windows平臺的利用也可以自動化嗎?
可以說,經過了短短六年的發展,AEG已經相當成熟,后續的研究包括Mayhem、Q、Angr、rex等系統,多多少少解決了上述的一些問題,讓漏洞利用自動化的程度越來越高。最近在安全界備受關注的美國DARPA CGC比賽,就是一次全自動的漏洞攻防賽。這場比賽從2013年初賽到2016年8月決賽,取得第一名的正是David Brumley團隊開發的Mayhem系統。關于CGC的團隊和研究,又可以洋洋灑灑寫下許多,這里暫時按下不表。總而言之,AEG已經成為二進制安全領域的一個研究熱點。
本人才疏學淺,是剛剛接觸二進制安全的一個小菜鳥,文章中若有錯誤之處還請各位大牛多多包涵,且能不吝賜教,悉數指出。如果學有余力,未來可能將繼續介紹Mayhem、Q等更加完善的自動利用系統。希望喜歡二進制安全的朋友們能夠有所收獲。
參考文獻:Avgerinos T, Sang K C, Hao B L T, et al. AEG: Automatic ExploitGeneration.[J]. Internet Society, 2011, 57(2).
https://zhuanlan.zhihu.com/p/26690230
總結
以上是生活随笔為你收集整理的Automatic Exploit Generation:漏洞利用自动化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 区块链共识算法Proof-of-Stak
- 下一篇: 【译】Byzantine Fault T