如何通過(guò)CVE-2015-7547(GLIBC getaddrinfo)漏洞繞過(guò)ASLR
0x01 前言
2016年2月16日,Google披露了一個(gè)重要的緩沖區(qū)溢出漏洞,該漏洞在GLIBC庫(kù)中的getaddrinfo函數(shù)中觸發(fā)。同時(shí)他們還提供了一份PoC?;诖耍诒疚闹?,我們將展示如何通過(guò)CVE-2015-7547繞過(guò)ASLR。
0x02 漏洞描述
getaddrinfo()函數(shù)的作用是通過(guò)查詢(xún)DNS服務(wù)將主機(jī)名和服務(wù)解析為addrinfo結(jié)構(gòu)體。
在getaddrinfo()函數(shù)實(shí)現(xiàn)中,使用了alloca()函數(shù)(在堆棧上分配緩沖區(qū))對(duì)DNS進(jìn)行響應(yīng)。開(kāi)始的時(shí)候,該函數(shù)首先分配一段棧空間用于DNS響應(yīng), 如果響應(yīng)時(shí)間過(guò)長(zhǎng),它將重新分配一個(gè)堆緩沖區(qū)用于響應(yīng)。但由于更新代碼將緩沖區(qū)為新分配的堆緩沖區(qū)后,舊的堆棧緩沖區(qū)未及時(shí)釋放,仍舊再使用。 這個(gè)懸空指針便造成了一個(gè)經(jīng)典的緩沖區(qū)溢出。
ASLR? ASLR!
在上述情況下,可以通過(guò)這個(gè)漏洞覆蓋getaddrinfo()函數(shù)的返回地址,但是我們應(yīng)該將返回地址覆蓋到哪里呢?在啟用ASLR的系統(tǒng)中,模塊地址是隨機(jī)的。因此,攻擊者不能將攻擊流地址設(shè)置為預(yù)先設(shè)定的地址。
fork()
fork()是Linux中創(chuàng)建新進(jìn)程的方法。一個(gè)典型的fork使用方法如下圖所示:
fork出的子進(jìn)程和父進(jìn)程使用的相同的指令段,他們只有pid不相同,而pid是fork函數(shù)返回給子進(jìn)程的。和windows下的代碼復(fù)用的區(qū)別在于,這里意味著子進(jìn)程與其父進(jìn)程共享許多特性--具有相同的寄存器狀態(tài),堆棧和內(nèi)存布局。
0x03 程序流程示例
考慮一個(gè)服務(wù)器應(yīng)用程序,其運(yùn)行模式如下:
1. 客戶(hù)端遠(yuǎn)程連接到應(yīng)用程序。
2. 應(yīng)用程序自己fork一個(gè)子進(jìn)程用于響應(yīng)客戶(hù)端請(qǐng)求
3. 在處理客戶(hù)端請(qǐng)求的過(guò)程中,子進(jìn)程使用"getaddrinfo()"函數(shù)解析主機(jī)名。 同時(shí),它向其DNS服務(wù)器發(fā)送DNS請(qǐng)求。
4. DNS服務(wù)器對(duì)DNS請(qǐng)求做出一個(gè)合法響應(yīng)。
5. 子進(jìn)程啟動(dòng)與已被解析的主機(jī)的連接。
每次主進(jìn)程進(jìn)行響應(yīng)處理時(shí),它都會(huì)自己fork一個(gè)子進(jìn)程。根據(jù)前面的描述,這意味著所有子進(jìn)程將共享相同的內(nèi)存布局--包括加載模塊的地址。 這種場(chǎng)景對(duì)于許多服務(wù)(例如HTTP代理,電子郵件服務(wù)器或DNS服務(wù)器)是非常常見(jiàn)的。
0x04 攻擊流程示例
在實(shí)施攻擊的過(guò)程中,我們假設(shè)攻擊者具有能夠響應(yīng)受害者的任意DNS請(qǐng)求的能力。實(shí)現(xiàn)這種情形完全可以通過(guò)ARP欺騙、DNS欺騙完成。攻擊場(chǎng)景如下圖:
1. 一個(gè)攻擊者構(gòu)造一個(gè)請(qǐng)求發(fā)送給受害服務(wù)器
2. 為了響應(yīng)攻擊者的請(qǐng)求,受害服務(wù)器的守護(hù)進(jìn)程fork出一個(gè)子進(jìn)程
3. 子進(jìn)程處理請(qǐng)求時(shí),發(fā)起一個(gè)DNS請(qǐng)求
4. 攻擊者回復(fù)一個(gè)惡意的DNS響應(yīng),該響應(yīng)將子進(jìn)程的返回地址覆蓋,在這里,我們將其設(shè)置未0x12121212
5. 攻擊者獲得子進(jìn)程用connect()函數(shù)發(fā)起的TCP回連
如果0x12121212確實(shí)是getaddrinfo()的正確返回地址,那么該進(jìn)程將正常運(yùn)行,并通過(guò)connect()發(fā)起tcp連接。
如果不是這種情況,并且攻擊者將返回地址寫(xiě)為其他任何地址,則應(yīng)用程序?qū)⒂捎趦?nèi)存段錯(cuò)誤或執(zhí)行無(wú)效的指令而崩潰。
這種方式可以作為判斷一個(gè)地址是否為getaddrinfo()的返回地址的一種方法,原因在于如果地址正確,那么一個(gè)TCP連接將會(huì)成功建立。由于模塊的基址在不同的子進(jìn)程中是沒(méi)有隨機(jī)化(前面提到的公用內(nèi)存布局),于是這個(gè)地址在所有的子進(jìn)程中可以通用。一個(gè)攻擊者可以使用這種方式去遍歷每一個(gè)可能的地址,知道正確建立TCP連接而獲得正確的地址。
然而,采用這種方式進(jìn)行基址定位需要猜解2的64次方的數(shù)量的地址,這并沒(méi)有太大的現(xiàn)實(shí)意義。
逐字節(jié)的逼近
不過(guò),攻擊者可以每次只覆蓋一個(gè)字節(jié)。 例如,假設(shè)getaddrinfo的返回地址為0x00007fff01020304:
我們首先只覆蓋getaddrinfo()函數(shù)的返回地址最低有效位(LSB)的一字節(jié)。這里用0x00進(jìn)行覆蓋。由于在假設(shè)中g(shù)etaddrinfo()的返回地址為0x00007fff01020304,將最低位覆蓋為0x00,那么這里返回地址就會(huì)變?yōu)?x00007fff01020300,由于該地址是非法地址,函數(shù)返回后程序就會(huì)崩潰。于是我們繼續(xù)重復(fù)上述操作,并且每次重復(fù)是LSB只加1(即第一次0x00,第二次0x01,第三次0x02,...),當(dāng)我們將LSB增加到0x04時(shí),getaddrinfo函數(shù)的返回地址為正確的返回地址0x00007fff01020304,此時(shí)程序不會(huì)崩潰,建立tcp連接。于是最低位的值便確定了。
接下來(lái),我們重復(fù)上述整個(gè)操作,通過(guò)覆蓋返回地址的兩個(gè)字節(jié)(0x04 0x00)來(lái)枚舉下一個(gè)字節(jié),我們將返回地址的第一個(gè)字節(jié)設(shè)置為剛剛猜解出來(lái)的正確字節(jié)(0x04),于是我們只需要采取相同辦法猜解第二個(gè)字節(jié)即可。猜解成功的標(biāo)志和第一個(gè)字節(jié)一樣,建立正確的連接。
接下來(lái)是第三個(gè)字節(jié),第四個(gè)字節(jié)。。。
通過(guò)這種逐字節(jié)逼近的方法,我們最多只需要進(jìn)行8*2^8次(每個(gè)字節(jié)最多2^8次猜解,總共8字節(jié))嘗試便可以得到正確的返回地址,這種方式在幾秒內(nèi)便可得到結(jié)果。
0x05 查找可利用的應(yīng)用程序
http://codesearch.debian.net是一個(gè)包含超過(guò)18,000 Debian包的索引的網(wǎng)站。我們通過(guò)該網(wǎng)站查找所有調(diào)用 fork()和getaddrinfo()函數(shù)的應(yīng)用程序(這些程序都是有可能進(jìn)行利用的),發(fā)現(xiàn)超過(guò)1300個(gè)潛在的可利用的應(yīng)用程序。 然后,進(jìn)一步的我們需要檢查每個(gè)應(yīng)用程序的源代碼,檢查其流程是否適合我們的需要。
0x06 Tinyproxy
Tinyproxy是Linux下一個(gè)小型的http代理軟件,通過(guò)審計(jì),發(fā)現(xiàn)該應(yīng)用程序的執(zhí)行流程符合上述分析的執(zhí)行流程。當(dāng)它在響應(yīng)HTTP連接請(qǐng)求時(shí),會(huì)fork出一個(gè)子進(jìn)程,然后調(diào)用getaddrinfo()函數(shù)來(lái)檢索所請(qǐng)求的網(wǎng)站的IP地址。 然后使用connect()函數(shù)連接該主機(jī)獲取網(wǎng)站內(nèi)容。
1. 堆棧任意指針泄露
在下面的代碼塊中我們遇到了第一崩潰點(diǎn):
rbx寄存器首先被覆蓋,然后執(zhí)行"mov BYTE PTR [rbx],sil"指令,該指令可釋放對(duì)rbx指向的地址的指針。 rbx原先是指向棧上的,也就是說(shuō),如果我們采用逐字節(jié)逼近的方法,枚舉其值使得該程序在堆棧上泄漏一個(gè)地址。
下圖(output of /proc/PID/maps)顯示了堆棧的邊界。 正如圖中所示,它的初始大小總是大于0x1000字節(jié)。
寄存器rbx指向的地址必須是可寫(xiě)的,否則會(huì)引發(fā)分段錯(cuò)誤導(dǎo)致程序崩潰。然而它的缺陷在于,無(wú)論在哪個(gè)地址寫(xiě)入“sil”值,只要它是可寫(xiě)地址,程序流將正確地繼續(xù),這意味著對(duì)于rbx的低12位設(shè)置什么值根本無(wú)關(guān)緊要,因?yàn)橛捎诙褩5木壒仕偸强勺x可寫(xiě)的。
所以我們只要在堆棧范圍內(nèi)泄露了任意一個(gè)指針。當(dāng)我們必須精確定位時(shí),堆棧變量指向的地址并不會(huì)對(duì)程序流產(chǎn)生什么影響。
2. 泄露棧基址
由于應(yīng)用程序的流程總是相同的,所以棧的大小總是相同的。 這意味著我們可以依賴(lài)于?;返竭@些變量,結(jié)構(gòu)體和緩沖區(qū)的偏移量進(jìn)行定位。而且在這樣的情況下,我們往往依賴(lài)于這樣的常量偏移。所以首先應(yīng)該得到?;贰?/p>
由于我們已經(jīng)得到一個(gè)在??臻g內(nèi)的地址,所以泄露?;繁让杜e任意地址更簡(jiǎn)單。而且我們知道它擁有兩個(gè)屬性,一是?;放c頁(yè)面邊界(0x1000)對(duì)齊,二是?;穼⑹菞:竺娴牡谝粋€(gè)不可讀的地址。
讓我們假設(shè)堆棧基址在0x00007fffed008000。我們利用已經(jīng)泄漏的任意堆棧地址,并將其對(duì)齊到頁(yè)邊界得到一個(gè)新的對(duì)齊地址,例如0x00007fffed000140對(duì)齊到0x00007fffed000000。然后,我們枚舉堆棧基址,從這個(gè)對(duì)齊的地址開(kāi)始進(jìn)行覆蓋,并在每次嘗試之后遞增0x1000(頁(yè)大小)。在我們發(fā)送請(qǐng)求之后,等待一段時(shí)間,并檢查服務(wù)器是否嘗試連接到我們解析的IP。如果是,這意味著我們還沒(méi)有達(dá)到堆棧基地。如果發(fā)生超時(shí),說(shuō)明服務(wù)器發(fā)生崩潰,我們達(dá)到了目標(biāo),獲得了堆?;贰?/p>
3. 堆棧偏移
在從getaddrinfo返回之前,程序會(huì)執(zhí)行以下檢查:
注意以紅色突出顯示的塊。如果我們到達(dá)它并且傳遞一個(gè)無(wú)效的堆指針作為參數(shù),應(yīng)用程序崩潰,因?yàn)樗噲D釋放(free()函數(shù))一個(gè)無(wú)效的堆塊。如果要繞過(guò)這個(gè)free()函數(shù),r14和rdi必須相等。 r14指向原來(lái)的__alloca()函數(shù)堆棧緩沖區(qū)。 由于堆棧基址先前泄漏,并且__alloca()緩沖區(qū)與堆?;返钠屏繎?yīng)該是常量,因此我們不應(yīng)該遇到任何問(wèn)題。 然而,我們發(fā)現(xiàn)偏移在每次運(yùn)行時(shí)略有不同。 為什么?
這涉及到內(nèi)核代碼對(duì)ASLR的處理問(wèn)題,如下linux內(nèi)核代碼所示:
- /arch/x86/kernel/process.c
觀(guān)察上述內(nèi)核代碼,可以看到,如果ASLR被啟用,每次堆棧分配時(shí)SP(堆棧指針)將減少一個(gè)隨機(jī)數(shù)。這意味著在每次不同的運(yùn)行時(shí),在rsp和堆棧之間將存在一個(gè)隨機(jī)增量。
幸運(yùn)的是,這個(gè)增量非常小。我們可以輕松地枚舉這個(gè)隨機(jī)偏移。
看看上面的IDA代碼片段,我們可以發(fā)現(xiàn),如果rdi等于r14,程序?qū)⒉粫?huì)運(yùn)行到釋放rdi的那個(gè)分支。 因此,我們可以使用我們之前得到的堆?;?,結(jié)合預(yù)先計(jì)算(即,如果該值于?;穼?duì)齊程序?qū)⒎祷?),然后嘗試所有其他2^9種可能性,便可得到此增量。
4. 泄漏LIBC模塊地址
這部分的實(shí)現(xiàn)是超級(jí)簡(jiǎn)單的,因?yàn)槲覀兛梢允褂们懊嫣岬降募夹g(shù)(byte-by-byte approach 逐字節(jié)逼近)來(lái)枚舉返回地址的每個(gè)字節(jié)從而得到它。
5. 代碼執(zhí)行
剩下要做的就是構(gòu)造一個(gè)ROP鏈,這是非常容易和直接。我們知道system()函數(shù)在libc的基址偏移的某個(gè)確定的位置,所以我們只需設(shè)置它的參數(shù),并使用ROP調(diào)用它。
0x07 結(jié)論
在這項(xiàng)研究中,我們使用了Linux創(chuàng)建進(jìn)程時(shí)的特性來(lái)繞過(guò)ASLR。這種技術(shù)同時(shí)也可以用于其他內(nèi)存損壞漏洞的利用。因此,用戶(hù)應(yīng)始終嘗試通過(guò)及時(shí)部署軟件修補(bǔ)程序和更新來(lái)保護(hù)服務(wù)器。以保證我們?cè)跐撛谕{行動(dòng)者行動(dòng)前領(lǐng)先一步。