通過靜態(tài)分析檢測二進(jìn)制代碼中的Use-After-Free漏洞
前言
Use-After-Free是一種眾所周知的漏洞類型,經(jīng)常被現(xiàn)代的攻擊代碼所利用(參見Pwn2own 2016)。在研究項(xiàng)目AnaStaSec中,AMOSSYS提供了許多關(guān)于如何靜態(tài)檢測二進(jìn)制代碼中的此類漏洞的介紹。在這篇博文中,我們將向讀者闡述學(xué)術(shù)界在如何檢測這種類型的漏洞方面提出的各種建議。當(dāng)然,他們當(dāng)前的目標(biāo)是定義一種通用方法,這樣的話,我們就可以根據(jù)自己的需求來構(gòu)建相應(yīng)的概念驗(yàn)證工具了。
關(guān)于Use-After-Free(UAF)漏洞
UAF的原理很容易理解。當(dāng)程序嘗試訪問先前已被釋放的內(nèi)存區(qū)時(shí),就會出現(xiàn)“Use-After-Free”漏洞。在這種情況下創(chuàng)建的懸空指針將指向內(nèi)存中已經(jīng)釋放的對象。
舉個(gè)例子,下面的代碼將導(dǎo)致一個(gè)UAF漏洞。如果下面的代碼在運(yùn)行過程中執(zhí)行了if分支語句的話,由于指針ptr指向無效的存儲器區(qū),所以可能發(fā)生不確定的行為。
- char * ptr = malloc(SIZE);
- …
- if (error){
- free(ptr);
- }
- …
- printf("%s", ptr);
圖 1:Use-After-Free示例代碼
換句話說,如果發(fā)生如下所示的3個(gè)步驟,就會出現(xiàn)UAF漏洞:
(1)分配一個(gè)內(nèi)存區(qū)并且讓一個(gè)指針指向它。
(2)內(nèi)存區(qū)被釋放,但原來的指針仍然可用。
(3)使用該指針訪問先前釋放的內(nèi)存區(qū)。
大多數(shù)時(shí)候,UAF漏洞會只會導(dǎo)致信息泄漏,但有的時(shí)候,它還可能導(dǎo)致代碼執(zhí)行——攻擊者對這種情況更感興趣。導(dǎo)致代碼執(zhí)行通常發(fā)生在下列情況下:
- 程序分配了內(nèi)存塊A,后來將其釋放了。
- 攻擊者分配了內(nèi)存塊B,并且該內(nèi)存塊使用的就是之前分配給內(nèi)存塊A的那片內(nèi)存。
- 攻擊者將數(shù)據(jù)寫入內(nèi)存塊B。
- 程序使用之前釋放的內(nèi)存塊A,訪問攻擊者留下的數(shù)據(jù)。
在C++中,當(dāng)類A被釋放后,攻擊者立刻在原來A所在的內(nèi)存區(qū)上建立一個(gè)類B的時(shí)候,就經(jīng)常出現(xiàn)這種漏洞。這樣的話,當(dāng)調(diào)用類A的方法的時(shí)候,實(shí)際上執(zhí)行的是攻擊者加載到類B中的代碼。
現(xiàn)在我們已經(jīng)掌握了UAF的概念,接下來我們將考察安全社區(qū)是如何檢測這種漏洞的。
靜態(tài)和動態(tài)分析的優(yōu)缺點(diǎn)
二進(jìn)制代碼的分析方法主要有兩種:靜態(tài)分析和動態(tài)分析。就目前來說,動態(tài)地分析整個(gè)代碼是非常困難的,因?yàn)橐肷煽梢愿采w所有二進(jìn)制代碼執(zhí)行路徑的輸入的話,絕不是一件容易的事情。因此,當(dāng)我們專注于代碼覆蓋問題時(shí),靜態(tài)分析方法似乎更為適用。
然而,根據(jù)論文[Lee15]和[Cab12]的介紹,與Use-After-Free漏洞檢測有關(guān)的大多數(shù)學(xué)術(shù)論文仍然集中在動態(tài)分析方面。這主要是因?yàn)椤討B(tài)分析方法易于檢測同一指針的副本,也稱為別名。換句話說,使用動態(tài)分析方法時(shí),我們可以直接訪問內(nèi)存中的值,這種能力對于代碼分析來說是非常重要的。如果使用動態(tài)分析的話,我們能夠獲得更高的準(zhǔn)確性,但同時(shí)也會失去一些完整性。
然而,本文將專注于靜態(tài)分析方法。在學(xué)術(shù)界看來,這種方法仍然面臨兩大困難:
1) 最大的困難是如何管理程序中的循環(huán)。實(shí)際上,當(dāng)計(jì)算循環(huán)中待處理的變量的所有可能值時(shí),需要知道循環(huán)將被執(zhí)行多少次。這個(gè)問題通常被稱為停機(jī)問題。在可計(jì)算性理論中,所謂停機(jī)問題就是去判斷程序會最終停下來,還是一直運(yùn)行下去。不幸的是,這個(gè)問題已經(jīng)被證明是無解的。換句話說,沒有通用算法可以在給出所有可能輸入的情況下解決所有可能程序的停止問題,即不存在一個(gè)判定一切程序的程序,因?yàn)檫@個(gè)程序本身也是程序。在這種情況下,為了解決這個(gè)問題,只好借助于靜態(tài)分析工具來進(jìn)行相應(yīng)的簡化了。
2) 另一個(gè)困難在于內(nèi)存的表示方式。一個(gè)簡單的解決方案是維護(hù)一個(gè)大數(shù)組,其中保存指針的內(nèi)存值。然而,這不是看起來那么簡單。例如,一個(gè)內(nèi)存地址可以具有多個(gè)可能的值,或者一些變量可以具有多個(gè)可能的地址。此外,如果有太多可能取值,那么將所有的值都單獨(dú)保存的話是不合理的。因此,必須對這種內(nèi)存表示進(jìn)行一些簡化。
為了降低靜態(tài)分析的復(fù)雜性,一些論文像[Ye14]或像Polyspace或Frama-C這樣的工具,都是在C源代碼級別來分析問題的,因?yàn)檫@個(gè)級別包含了最大程度的信息。但是,人們在分析應(yīng)用程序的時(shí)候,通常是無法訪問源代碼的。
從二進(jìn)制代碼到中間表示
當(dāng)我們進(jìn)行二進(jìn)制分析的時(shí)候,第一步是建立相關(guān)的控制流圖(CFG)。控制流圖是一種有向圖,用來表示程序在執(zhí)行期間可能經(jīng)過的所有路徑。CFG的每個(gè)節(jié)點(diǎn)代表一條指令。由一條邊連接的兩個(gè)節(jié)點(diǎn)表示可以連續(xù)執(zhí)行的兩個(gè)指令。如果一個(gè)節(jié)點(diǎn)具有兩個(gè)延伸到其他節(jié)點(diǎn)的邊,這表明該節(jié)點(diǎn)是一個(gè)條件跳轉(zhuǎn)指令。因此,通過CFG我們可以將一個(gè)二進(jìn)制代碼組織成一個(gè)指令的邏輯序列。在為可執(zhí)行文件建立CFG的時(shí)候,最常見的方法是使用反匯編程序IDA Pro。
當(dāng)處理二進(jìn)制代碼方面,學(xué)術(shù)論文好像都是用相同的方式來處理UAF漏洞的。論文[Gol10]和[Fei14]給出了具體的處理步驟:
事實(shí)表明循環(huán)似乎對Use-After-Free的存在沒有很大的影響。因此,在著手處理二進(jìn)制代碼時(shí),一個(gè)強(qiáng)制性的步驟就是利用第一次迭代展開循環(huán)。就像我們前面剛剛解釋的那樣,這個(gè)步驟可以避免停機(jī)問題。第一次迭代
為了簡化前面提到的內(nèi)存表示問題,我們可以使用中間表示形式(IR),因?yàn)檫@種表示形式可以獨(dú)立于具體的處理器架構(gòu)。例如,x86匯編代碼就過于復(fù)雜,因?yàn)樗刑嗟闹噶睢R粋€(gè)解決辦法是對小型的指令集進(jìn)行分析。使用中間表示形式的時(shí)候,每個(gè)指令都被轉(zhuǎn)換為幾個(gè)原子指令。至于選擇哪種中間表示形式,則取決于分析的類型。在大多數(shù)情況下,我們都會選擇逆向工程中間語言(REIL),但是在一些學(xué)術(shù)文獻(xiàn)中也有使用其他IR的,例如BAP([Bru11])或Bincoa([Bar11])等。
REIL IR只有17種不同的指令,并且每個(gè)指令最多有一個(gè)結(jié)果值。我們可以使用像BinNavi這樣的工具將本機(jī)x86匯編代碼轉(zhuǎn)換為REIL代碼,BinNavi是由Google(以前是Zynamics)開發(fā)的一個(gè)開源工具。BinNavi可以將IDA Pro的數(shù)據(jù)庫文件作為輸入,這一特性給我們帶來了極大的便利。
符號執(zhí)行與抽象解釋
一旦將二進(jìn)制代碼轉(zhuǎn)換為中間表示形式,我們就可以通過兩種方法來分析這些二進(jìn)制代碼的行為了,即抽象解釋([Gol10]和[Fei14])或符號執(zhí)行([Ye14])。
符號執(zhí)行使用符號值作為程序的輸入,將程序的執(zhí)行轉(zhuǎn)變?yōu)橄鄳?yīng)符號表達(dá)式的操作,通過系統(tǒng)地遍歷程序的路徑空間,實(shí)現(xiàn)對程序行為的精確分析。因此,符號執(zhí)行不會使用輸入的實(shí)際值,而是采用抽象化符號的形式來表示程序中的表達(dá)式和變量。因此,這種分析方法不是跟蹤變量的值,而是用代表變量值的符號來生成算術(shù)表達(dá)式,這些表達(dá)式可以用于檢查條件分支等。
另一方面,抽象解釋是基于這樣的思想的——程序的分析可以在一定抽象級別上進(jìn)行。因此,不需要跟蹤每個(gè)變量的精確值,并且語義可以替換為描述指令對變量的影響的抽象語義。例如,變量可以由它們的符號來定義。對于加法指令來說,可以通過檢查操作數(shù)的符號來設(shè)置結(jié)果的符號。因此,如果操作數(shù)的符號是+,那么結(jié)果的符號也是+,但是永遠(yuǎn)不會計(jì)算變量的確切值。除符號之外,我們還可以定義其他各種抽象域。例如,可以通過一個(gè)內(nèi)存位置(全局,堆和棧)上的值區(qū)間來跟蹤變量的值。值集分析(VSA)就是一種基于這種表示方法的分析技術(shù)。
舉例來說,monoREIL框架就是一個(gè)基于REIL IR的VSA引擎。它極大地簡化了VSA算法的開發(fā)工作,使開發(fā)人員能夠在自己的抽象域上執(zhí)行VSA。
分析中間表示形式
下一個(gè)問題是如何通過CFG時(shí)實(shí)現(xiàn)分析算法。同樣,這里也有兩種方式:
- 過程內(nèi)分析,限于當(dāng)前函數(shù)的范圍
- 過程間分析,能夠進(jìn)入子函數(shù)
不用說,程序內(nèi)分析要比程序間分析簡單得多。然而,當(dāng)一個(gè)人想要檢測UAF漏洞時(shí),他必須能夠一直跟蹤內(nèi)存塊:從這些內(nèi)存塊的分配到釋放...,所以,有時(shí)候會涉及多個(gè)函數(shù)。
這就是為什么論文[Gol10]提出首先進(jìn)行過程內(nèi)分析,然后將其擴(kuò)展到全局的過程間分析的原因。如圖2所示,對于每個(gè)函數(shù),都創(chuàng)建一個(gè)相應(yīng)的方框。這些方框用來總結(jié)函數(shù)的行為,連接它們的輸出與輸入。因此,當(dāng)將分析擴(kuò)展到過程間分析時(shí),每個(gè)函數(shù)調(diào)用都會被這個(gè)函數(shù)的過程內(nèi)分析的結(jié)果代替。這種方法的主要優(yōu)點(diǎn)是函數(shù)只需要分析一次即可,即使它們被調(diào)用了很多次也是如此。此外,在進(jìn)行過程內(nèi)分析時(shí),即使非常小的代碼塊也不會放過,因此這種方法是非常精確的。
圖2:由諸多過程內(nèi)分析合并而成的過程間分析
此外,在論文[Fei14]中還提出了另一種解決方案。第二種方法(如圖3所示)會將被調(diào)用函數(shù)內(nèi)聯(lián)到調(diào)用函數(shù)中。因此,函數(shù)調(diào)用不再是一個(gè)問題。雖然該解決方案更加容易實(shí)現(xiàn),但是它有一個(gè)缺點(diǎn),即如果一個(gè)函數(shù)被調(diào)用兩次的話,則該函數(shù)將被分析兩次。因此,該方法更加耗時(shí),對內(nèi)存的需求也更大。
圖3:通過將函數(shù)內(nèi)聯(lián)到單一函數(shù)中的過程間分析
檢測UAF漏洞
在上文中,我們介紹了分析二進(jìn)制代碼語義的不同方法,以及遍歷控制流圖的各種方法。下面,我們開始介紹如何檢測UAF模式。首先讓我們UAF的定義,我們知道UAF是通過兩個(gè)不同的事件來進(jìn)行刻畫的:
- 創(chuàng)建一個(gè)懸空指針,
- 訪問該指針指向的內(nèi)存。
為了檢測這種模式,論文[Fei14]跟蹤所有已經(jīng)釋放的內(nèi)存堆區(qū)域,并且在每次使用指針時(shí)都會檢查它是否指向這些已經(jīng)釋放的內(nèi)存區(qū)。
下面,讓我們拿下面的偽代碼為例進(jìn)行說明。注意,為了簡單起見,該示例沒有提供復(fù)雜的CFG。事實(shí)上,CFG的處理方法取決于所選擇的分析方法及其實(shí)現(xiàn)...這個(gè)例子的目的,只是展示一種通過分析代碼檢測Use-After-Free的方法。
1. malloc(A);
2. malloc(B);
3. use(A);
4. free(A);
5. use(A);
上面的偽代碼分配了兩個(gè)內(nèi)存塊,并且可以通過名稱A和B來引用這兩個(gè)內(nèi)存塊。然后,訪問(Use(A))了一次內(nèi)存塊A之后,接著釋放(Free(A))該內(nèi)存塊。之后,又再次訪問了該內(nèi)存塊。
通過定義兩個(gè)域(一組分配的堆元素和一組釋放的堆元素),可以在每個(gè)指令處更新這些集合,并檢查它訪問的內(nèi)存是否屬于已分配的內(nèi)存塊集合,具體如圖4所示。
圖 4:通過域檢測機(jī)制挖掘Use-After-Free漏洞
當(dāng)內(nèi)存塊A被再次訪問時(shí),它已經(jīng)在上一步中被注冊為已釋放的內(nèi)存塊,因此分析程序就會發(fā)出警報(bào):檢測到Use-After-Free漏洞。
在論文[Ye14]中,它提出的另一種檢測目標(biāo)模式的方法,但是它使用的是簡單狀態(tài)機(jī)。該方法的思路是,在分配內(nèi)存之后,指向該內(nèi)存塊的指針被設(shè)置為“分配”狀態(tài),并且該狀態(tài)在相應(yīng)的內(nèi)存塊未被釋放之前保持不變。當(dāng)內(nèi)存塊被釋放時(shí),它們就會轉(zhuǎn)換為“釋放”狀態(tài)。當(dāng)處于釋放狀態(tài)的指針被使用的時(shí)候,就會導(dǎo)致“Use-After-Free”狀態(tài)。然而,如果指針及其別名被刪除并且不再引用內(nèi)存塊的話,則它們就是無害的,這時(shí)就會進(jìn)入“結(jié)束”狀態(tài)。這個(gè)簡單的狀態(tài)機(jī)如圖5所示。
圖5:用于檢測Use-After-Free漏洞的簡單狀態(tài)機(jī)
此外,在論文[Gol10]中也提出一個(gè)不同的解決方案,但是它使用的工具是圖論。在這篇論文中,作者會對使用指針的語句進(jìn)行檢查,看看它是在釋放內(nèi)存的語句之后還是之前。如果是之后的話,就檢測出了UAF漏洞。
圖6:具有潛在Use-After-Free漏洞的圖
在任何情況下,只要檢測到懸空指針,分析的最后階段必須通過提取導(dǎo)致該指針的子圖來表征UAF漏洞。這個(gè)子圖必須包含所有必要的元素,來讓人類手動檢查,以避免真陽性。
總結(jié)
我們在這里介紹了幾種通過基于靜態(tài)分析的二進(jìn)制代碼Use-After-Free漏洞檢測方法。同時(shí),我們也對這種分析觸發(fā)器的不同問題進(jìn)行了詳細(xì)的介紹,閱讀之后您就不難理解為什么在檢測這種bug的時(shí)候沒有簡單的解決方案了。
我們在開展這項(xiàng)工作中還發(fā)現(xiàn),只有少數(shù)研究人員將其成果作為開源項(xiàng)目發(fā)布。Veribag團(tuán)隊(duì)的Josselin Feist開發(fā)的GUEB項(xiàng)目就是其中之一。如果對這個(gè)課題感興趣的話,我們鼓勵你訪問他的Github。