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