堆溢出與堆的內存布局有關,要搞明白堆溢出,首先要清楚的是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位置覆蓋為exit@got-12,bk位置覆蓋為shellcode地址。
覆蓋后的堆空間布局情況:
low address +---------------------+ <--first chunk ptr | prev_size | +---------------------+ | size=48 | +---------------------+ <--first | | | allocated | | chunk | +---------------------+ <--second chunk ptr | XXXXXXXXX | +---------------------+ | size=0xfffffffc | +---------------------+ <--second | exit@got-12 | | 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是exit@got的地址-12 4.所以second->fd+12的位置就是exit@got-12 + 12 = exit@got即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執行。