Linux 堆溢位原理分析
堆溢位與堆的記憶體佈局有關,要搞明白堆溢位,首先要清楚的是malloc()分配的堆記憶體佈局是什麼樣子,free()操作後又變成什麼樣子。
解決第一個問題:通過malloc()分配的堆記憶體,如何佈局?
上圖就是malloc()分配兩塊記憶體的情形。
其中mem指標指向的是malloc()返回的地址,pre_size與size都是4位元組資料,size存放當前chunk(記憶體塊,本文均不翻譯)大小,pre_size存放上一個chunk大小。
因為malloc實現分配的記憶體空間是8位元組對齊的,所以size的低3位其實沒用,就取其中一位,用來標誌前一個chunk是否被釋放即PREV_INUSE位。當前一chunk釋放,PREV_INUSE位置0,否則置1。
當malloc()分配的空間使用完畢後,將其mem指標傳給free()進行釋放。
解決第二個問題:free()對堆記憶體佈局會產生什麼影響?
上圖的情形是,當前chunk的上一chunk被free()釋放,容易發現,當前chunk的PREV_ISUSE標誌位置0,表示前一chunk已經被釋放。
被釋放的chunk中,原先data的位置的低地址處被填入兩個指標,分別是fd和bk,它們是forward和backward單詞的縮寫,分別表示前一個free chunk和後一個free chunk的地址。這樣所有通過free()釋放的記憶體chunk會組成一個雙向連結串列。也因此一個chunk最小長度為16位元組:2個size和2個指標。
當一個chunk被釋放時,還有一件事情要做,就是檢查相鄰chunk的是否處於釋放狀態,如果相鄰chunk空閒的話,就會進行chunk合併操作。由於每個chunk中都存放了size資訊,所以很容易就找到當前chunk前後chunk的狀態。
free()裡面會呼叫一個unlink巨集來執行合併操作:
#define unlink(P, BK, FD) { \ FD = P->fd; \ BK = P->bk; \ FD->bk = BK; \ BK->fd = FD; \ }
好了,這個巨集就是堆溢位利用的關鍵。仔細閱讀這個巨集其實就是在一個雙向連結串列中刪除一個結點的操作:
P->fd->bk = P->bk P->bk->fd = P->fd
其中P代表當前被刪除結點。
解決最後一個問題:堆溢位如何利用?
首先構造一段堆溢位漏洞程式碼:
int main(void) { char *buff1, *buff2; buff1 = malloc(40); buff2 = malloc(40); gets(buff1); free(buff1); exit(0); }
給出堆空間佈局:
low address +---------------------+ <--first chunk ptr | prev_size | +---------------------+ | size=48 | +---------------------+ <--first | | | allocated | | chunk | +---------------------+ <--second chunk ptr | prev_size | +---------------------+ | size=48 | +---------------------+ <--second | Allocated | | chunk | +---------------------+ high address
現在使用gets函式進行堆溢位,將第2塊chunk的prev_size覆蓋為任意值,size覆蓋為-4即0xfffffffc,fd位置覆蓋為[email protected],bk位置覆蓋為shellcode地址。
覆蓋後的堆空間佈局情況:
low address +---------------------+ <--first chunk ptr | prev_size | +---------------------+ | size=48 | +---------------------+ <--first | | | allocated | | chunk | +---------------------+ <--second chunk ptr | XXXXXXXXX | +---------------------+ | size=0xfffffffc | +---------------------+ <--second | [email protected] | | shellcode地址 | | Allocated | | chunk | +---------------------+ high address
下面看free(buff1)時發生的操作:
1.first空間即buff1被釋放掉
2.檢查上一chunk是否需要合併(這裡否)
3.檢查下一chunk是否需要合併,檢查的方法是檢查下下個chunk的PREV_ISUSE標誌位。即當前chunk加上當前size得到下個chunk,下個chunk加上下個size得到下下個chunk,因為我們設定下個chunk大小為-4,則下個chunk的pre_size位置被認為是下下個chunk的開始,下個size位置是0xfffffffc標誌未置位,被認為是free的需合併。
那麼,這裡合併用到unlink巨集時出問題了,同樣對照上面圖來看:
second->fd->bk=second->bk /* 1.second->bk是shellcode址 2.shellcode的地址被寫進了second->fd+12的位置 3.second->fd是[email protected]的地址-12 4.所以second->fd+12的位置就是[email protected] + 12 = [email protected]即got中存的exit地址 因此exit()函式地址已經被shellcode地址替換 */ second->bk->fd=second->fd
“shellcode的地址被寫進了second->fd+12的位置” 這句話要好好理解,為什麼second->fd->bk是second->fd+12呢? 其實second->fd指向前一chunk頭部,加12是跳過pre_size,size和fd即到達bk位置。
最後程式在執行到exit(0)語句時,由於地址被替換,shellcode執行。