成功從來沒有捷徑。如果你只關注CVE/NVD的動態以及google專家泄露的POC,那你只是一個腳本小子。能夠自己寫有效POC,那就證明你已經是一名安全專家了。今天我需要復習一下glibc中內存的相關知識,以鞏固我對堆溢出的理解和分析。帶着以下問題去閱讀本章:
- dlmalloc – General purpose allocator
- ptmalloc2 – glibc
- jemalloc – FreeBSD and Firefox
- tcmalloc – Google
- libumem – Solaris
我們以glibc為例探討堆的運行機制,主要是因為服務器絕大部分都和glibc有關,研究glibc有廣泛意義。
系統調用:malloc本身需要調用brk或mmap完成內存分配操作
線程:ptmalloc2的前身是dlmalloc,它們最大的區別是ptmalloc2支持線程,它提升了內存分配的效率。在dlmalloc中,如果有2個線程同時調用 malloc,只有一個線程可以進入關鍵區,線程之間共享同一個freelist數據結構。在ptmaloc2中,每一個線程都擁有單獨的堆區段,也就意味着每個線程都有自己的freelist結構體。沒有線程之間的共享和爭用,性能自然提高不少。Per thread arena用來特指為每個線程維護獨立的堆區段和freelist結構體的方式。
1 /* Per thread arena example. */ 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <pthread.h> 5 #include <unistd.h> 6 #include <sys/types.h> 7 8 void* threadFunc(void* arg) { 9 printf("Before malloc in thread 1\n"); 10 getchar(); 11 char* addr = (char*) malloc(1000); 12 printf("After malloc and before free in thread 1\n"); 13 getchar(); 14 free(addr); 15 printf("After free in thread 1\n"); 16 getchar(); 17 } 18 19 int main() { 20 pthread_t t1; 21 void* s; 22 int ret; 23 char* addr; 24 25 printf("Welcome to per thread arena example::%d\n",getpid()); 26 printf("Before malloc in main thread\n"); 27 getchar(); 28 addr = (char*) malloc(1000); 29 printf("After malloc and before free in main thread\n"); 30 getchar(); 31 free(addr); 32 printf("After free in main thread\n"); 33 getchar(); 34 ret = pthread_create(&t1, NULL, threadFunc, NULL); 35 if(ret) 36 { 37 printf("Thread creation error\n"); 38 return -1; 39 } 40 ret = pthread_join(t1, &s); 41 if(ret) 42 { 43 printf("Thread join error\n"); 44 return -1; 45 } 46 return 0; 47 }
分析:主線程在malloc調用之前,沒有任何堆區和棧區被分配
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread Welcome to per thread arena example::6501 Before malloc in main thread ... sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ cat /proc/6501/maps 08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread b7e05000-b7e07000 rw-p 00000000 00:00 0 ... sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$
主線程在調用malloc之后,從下圖中我們可以看出堆區域被分配在0804b000-0806c000區域,這是通過調用brk調整內存中止點來建立堆。此外,盡管申請了1000字節,但分配了132KB的堆內存。這個連續區域被稱為Arena。主線程建立的就稱為Main Arena。未來分配內存的請求會持續使用Arena區域直到用盡。如果用盡,可以調整內存中止點來擴大Top trunk。相似的,也可以相應的收縮以防止top chunk有太多的空間。(Top trunk是Arena最頂部的chunk)
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread Welcome to per thread arena example::6501 Before malloc in main thread After malloc and before free in main thread ... sploitfun@sploitfun-VirtualBox:~/lsploits/hof/ptmalloc.ppt/mthread$ cat /proc/6501/maps 08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804b000-0806c000 rw-p 00000000 00:00 0 [heap] b7e05000-b7e07000 rw-p 00000000 00:00 0 ... sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$
主線程 Free之后,內存並未歸還給OS,而是交由glibc malloc管理,放在Main Arena的bin中。(freelist數據結構體就是bin)之后所有的空間申請,都會在bin中尋求滿足。無法滿足時才再次向內核獲得空間。
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread Welcome to per thread arena example::6501 Before malloc in main thread After malloc and before free in main thread After free in main thread ... sploitfun@sploitfun-VirtualBox:~/lsploits/hof/ptmalloc.ppt/mthread$ cat /proc/6501/maps 08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804b000-0806c000 rw-p 00000000 00:00 0 [heap] b7e05000-b7e07000 rw-p 00000000 00:00 0 ... sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$
在調用thread1中malloc之前,thread1的堆區域並未建立,但線程棧已建立。
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread Welcome to per thread arena example::6501 Before malloc in main thread After malloc and before free in main thread After free in main thread Before malloc in thread 1 ... sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ cat /proc/6501/maps 08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804b000-0806c000 rw-p 00000000 00:00 0 [heap] b7604000-b7605000 ---p 00000000 00:00 0 b7605000-b7e07000 rw-p 00000000 00:00 0 [stack:6594] ... sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$
在thread1中malloc調用之后,線程堆區段建立了。位於b7500000-b7521000,大小132KB。這顯示和主線程不同,線程malloc調用的是mmap系統調用,而非sbrk。盡管用戶請求1000字節,1M的堆內存被映射到了進程地址空間。但只有132KB被設置為可讀寫權限,並被設置為該線程的堆空間。這個連續的內存空間是Thread Arena。
當用戶內存請求大小超過128KB時,不論請求是從主線程還是子線程,內存分配都是由mmap系統調用來完成的。
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread Welcome to per thread arena example::6501 Before malloc in main thread After malloc and before free in main thread After free in main thread Before malloc in thread 1 After malloc and before free in thread 1 ... sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ cat /proc/6501/maps 08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804b000-0806c000 rw-p 00000000 00:00 0 [heap] b7500000-b7521000 rw-p 00000000 00:00 0 b7521000-b7600000 ---p 00000000 00:00 0 b7604000-b7605000 ---p 00000000 00:00 0 b7605000-b7e07000 rw-p 00000000 00:00 0 [stack:6594] ... sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$
Thread1在free之后,被分配的內存區並未交還給操作系統,而是歸還給glicbc分配器,實際上它交給了線程Arena bin.
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread Welcome to per thread arena example::6501 Before malloc in main thread After malloc and before free in main thread After free in main thread Before malloc in thread 1 After malloc and before free in thread 1 After free in thread 1 ... sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ cat /proc/6501/maps 08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804b000-0806c000 rw-p 00000000 00:00 0 [heap] b7500000-b7521000 rw-p 00000000 00:00 0 b7521000-b7600000 ---p 00000000 00:00 0 b7604000-b7605000 ---p 00000000 00:00 0 b7605000-b7e07000 rw-p 00000000 00:00 0 [stack:6594] ... sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$
Arena:
在上面的例子中,主線程對應的是Main Arena,子線程對應的是Thread Arena。那線程和Arena是否是一一對應的呢?不是。實際上線程數可以多於核數,因此,讓一個線程擁有一個Arena有些奢侈。應用程序Arena的數量是和核數相關的,具體如下:
For 32 bit systems: Number of arena = 2 * number of cores. For 64 bit systems: Number of arena = 8 * number of cores.
Multiple Arena:
舉例說明:一個單核32位系統有4個線程(1主3子)。那4個線程只有2個Arena。Glibc內存分配器確保Multiple Arena在線程之間共享
- 主線程首次調用malloc時一定是創建了Main Arena
- 當子線程1和子線程2首次調用malloc時,會給它們都建立一個新的Arena。此時Arena和線程是一一對應的
- 當子線程3首次調用 malloc,此時會計算Arena的限制。已超出Arena數量限制,要重用Main Arena, Thread1 Arena或Thread2 Arena
- 重用:
- 遍歷存在的Arena,找到一個后試圖去lock它
- 成功lock,比如是Main Arena,給用戶返回Arena
- 沒有空閑的Arena,就排隊等待
- 當Thread3二次調用 malloc時,malloc將使用最近訪問的Arena(可能是main arena)。如果main arena是空閑的就使用它,如果忙時就Block等待。
多堆:
Heap_info: 堆頭。一個線程Arena擁有多個堆,每個堆有它自己的頭。之所以有多個堆,是因為開始的時候只有一個堆,但隨着堆區空間用盡,新堆會由mmap重新建立,而且地址空間是不連續的,新舊堆無法合並
malloc_state: Arena Header - 一個線程Arena有多個堆,但那些堆只有一個Arena頭。Arena頭包含了bins,top chunk和last remainder chunk等信息
malloc_chunk: Chunk Header - 一個堆被分為很多chunks,多個用戶請求導致多個chunk。每個chunk有它自己的頭部信息
注意:
- Main Arena沒有多個堆,因此沒有heap_info結構。當main arena空間耗盡,sbrk的堆區被延展
- 和線程Arena不同,Main Arena的Arena頭並非由sbrk調用而產生的堆區的一部分。它是全局變量,存在於libc.so的數據區
Chunk的類型:
- Allocated chunk
- Free chunk
- Top chunk
- Last Remainder chunk
Allocated Trunk:
prev_size: 前一個chunk為空閑區,則該區域包含前一區域的大小。如果非空閑,則該域包含前一區域的用戶數據
size: 被分配空間的大小 。后三比特域包含標志位
- PREV_INUSE (P) – 前一個chunk是否被占用
- IS_MMAPPED (M) – 是否是mmap分配
- NON_MAIN_ARENA (N) – 是否屬於thread arena
注意:
- 對於allocated chunk, 其他域如 fd, bk不被使用. 這里只存儲用戶數據
- 用戶請求的內存空間包含了malloc_chunk信息,因此實際使用的空間會小於用戶請求大小。
Free Trunk:
prev_size: 兩個空閑區不能毗鄰,當兩個chunk空閑豕毗鄰,則會合並為一個空閑區。因此通常前一個chunk是非空閑的,prev_size是前一個chunk的用戶數據
size: 空間大小
fd: Forward pointer – 同一bin中的下一個chunk(非物理空間)
bk: Backward pointer – 同一bin中的前一個chunk(非物理空間)
Bins: 根據大小不同,有如下bin
- Fast bin
- Unsorted bin
- Small bin
- Large bin
fastbinsY: 這個array是fastbin列表
bins: 共有126 個bins
- Bin 1 – Unsorted bin
- Bin 2 to Bin 63 – Small bin
- Bin 64 to Bin 126 – Large bin
Fast Bin: 大小在16~80字節之間.
- 數量 – 10
- 每個fastbin有一個空閑chunk的單鏈表. 之所以用單鏈表是因為在鏈表中沒有刪除操作。添加和刪除都在表的頂部 – LIFO.
- Chunk大小 – 8字節對齊
- 首個fastbin包含16字節的binlist, 第2個fastbin包含24 bytes的binlist,以此類推
- 同一fastbin中的chunk大小是一致的
- 在malloc初始化時, 最大的fast bin 大小設置為64比特,而非80比特.
- 不合並 – 毗鄰chunk不合並. 不合並會導致碎片,但效率提高
- malloc(fast chunk) –
- 初始態 fast bin max size 和 fast bin indices 為空,因此盡管用戶請求fast chunk,是small bin code提供服務而非fast bin code。
- 之后當fastbin不為空,fast bin index通過計算激活相應的binlist
- 激活后的binlist中可以給用戶提供內存
- free(fast chunk) –
- 計算Fast bin index以激活相應binlist
- 釋放后的chunk被放入剛才激活的binlist 中
Unsorted Bin: 當small chunk 或 large chunk被釋放,不是將其歸還給相應的bin中,而是添加至unsorted bin。這對性能有所提升
- 數量 – 1
- 循環雙鏈表
- Chunk 大小 – 大小無限制
Small Bin:大小小於512字節的塊稱為小塊。small bins在內存分配和釋放方面比large bins快(但比fast bins慢)。
- 數量– 62
- 每個small bin都包含一個循環的空閑塊的雙向鏈接列表(又稱垃圾箱列表)。使用雙鏈表是因為在小垃圾箱鏈接的中間可能會發生塊移除的操作。FIFO。
- 塊大小 – 8字節對齊:
- 小bin包含大小為8個字節的塊的binlist。first small bin包含大小為16個字節的塊,second small bin包含大小為24個字節的塊,依此類推……
- small bin 內的塊大小相同
- 合並– 兩個空閑的chunk不能彼此相鄰,將它們合並為一個空閑的塊。合並消除了外部碎片,但它放慢了速度!!
- malloc(small chunk)–
- 初始,所有small bin都將為NULL,盡管用戶請求一個small chunk, unsorted bin code 會為其服務,而不是smll bin code
- 同樣,第一次調用malloc的過程中,將初始化malloc_state中發現的small bin和large bin數據結構(bin),即,bin指向自身,表示它們為空。
- 稍后,當small bin不為空時,將刪除其對應的binlist 中的last chunk並將其返回給用戶。
- free (small chunk) –
- 釋放該塊時,請檢查其上一個或下一個塊是否空閑,如果有,則將它們從各自的鏈接列表中取消鏈接,然后將新合並的塊添加到未排序的bin鏈接列表的開頭
Large Bin:大小大於512的塊稱為大塊。存放大塊的垃圾箱稱為大垃圾箱。大存儲區在內存分配和釋放方面比小存儲區慢。
垃圾箱數量– 63
每個大垃圾箱都包含一個循環的空閑塊的雙向鏈接列表(又稱垃圾箱)。使用雙鏈表是因為在大倉中,可以在任何位置(前,中或后)添加和刪除塊。
在這63個垃圾箱中:
32個bin包含大小為64個字節的塊的binlist。即)第一個大容器(Bin 65)包含大小為512字節至568字節的塊的binlist,第二個大容器(Bin 66)包含大小為576字節至632字節的塊的binlist,依此類推…
16個bin包含大小為512字節的塊的binlist。
8個bin包含大小為4096字節的塊的binlist。
4個bin包含大小為32768字節的塊的binlist。
2個bin包含大小為262144個字節的塊的binlist。
1箱包含一塊剩余的大小。
與小垃圾箱不同,大垃圾箱中的塊大小不相同。因此,它們以降序存儲。最大的塊存儲在前端,而最小的塊存儲在其binlist的后端。
合並–兩個空閑的塊不能彼此相鄰,將它們合並為一個空閑的塊。
malloc(大塊)–
最初,所有大容器都將為NULL,因此即使用戶請求了大塊而不是大容器代碼,下一個最大的容器代碼也會嘗試為其服務。
同樣在第一次調用malloc的過程中,將初始化malloc_state中發現的小bin和大bin數據結構(bin),即,bin指向自身,表示它們為空。
稍后,當大容器為非空時,如果最大的塊大小(在其Binlist中)大於用戶請求的大小,則將Binlist從后端移動到前端,以找到大小接近/等於用戶請求的大小的合適塊。一旦找到,該塊將分為兩個塊
用戶塊(具有用戶請求的大小)–返回給用戶。
剩余塊(剩余大小)–添加到未排序的垃圾箱。
如果最大的塊大小(在其binlist中)小於用戶請求的大小,請嘗試使用下一個最大的(非空)容器來滿足用戶請求。下一個最大的bin代碼掃描binmap,以查找不為空的下一個最大的bin,如果找到任何這樣的bin,則從該binlist中檢索合適的塊並將其拆分並返回給用戶。如果找不到,請嘗試使用頂部塊滿足用戶請求。
free(大塊)–其過程類似於free(小塊)
TOP Chunk: Arena頂部的chunk稱為top chunk. 它不屬於任何bin。它在當用戶需求無法滿足時使用。如果top chunk size 比用戶請求大小大,那top chunk被分為兩個
- 用戶塊
- 剩下的塊
剩下的塊成為新的top chunk。如果top chunk 大小小於用戶請求大小,則top chunk調用sbrk(Main arena)或mmap(thread arena)系統調用進行延展
Last Remainder Chunk: 即最近一次切割后剩下的那個chunk. Last remainder chunk 可幫助提升性能。連續的small chunk請求可能會導致分配的位置相近。
在很多arena的chunk中,哪個能夠成為last reminder chunk?
當一個用戶請求small chunk,small bin和unsorted bin都無法滿足,就會掃描binmaps進而找尋next largest bin. 正如較早提及的,找到the next largest bin,它將會分為2個chunk,user chunk返回給用戶,remainder chunk 添加至unsorted bin. 除此之外,它成為最新的last remainder chunk.