0×00 前面的話
在內存中,堆是一個很有趣的地方,因為它可以由用戶去直接的進行分配與銷毀,所以也產生了一些很有趣、奇思妙想的漏洞,像unlink漏洞、House系列漏洞等等。但是在學習的過程中,我們很容易難以理解那些介紹的比較模糊的概念,比如 unsortedbin 在某些條件下會放回 smallbin 或 largebin 中,那到底是什么時候?也會對一些大佬構造的 payload 犯迷糊,為什么這里多了一個chunk,為什么這個字節要填充…,大佬們往往不對這些細節做過多的解釋,但是這可難為了我們初學堆利用的新兵,所以,我想寫幾篇文章,將堆的運作機制,例如一些基本的概念,malloc機制、free機制、保護機制,和利用方法結合起來說一下,讓大家能夠對堆這一塊有個較為清楚的認識,少走一些彎路。首先呢,我想在這篇文章中較為細致的介紹一下堆中的一些情況,剩下的有機會的話我會一並寫成一個系列。
這篇文章主要分為四個部分:
0x01 chunk 簡介 0x02 bin 簡介 0x03 malloc 機制 0x04 free 機制
這些內容相對比較重要,如果看完還覺得不夠的,推薦大家去讀一下華庭老師的《glibc內存管理ptmalloc源代碼分析》。
0×01 chunk 簡介
首先先說一下堆是如何分配的,在內存中,堆(低地址到高地址,屬性RW)有兩種分配方式(與malloc申請chunk做區分):
mmap: 當申請的size大於128kb時,由mmap分配 brk: 當申請的size小於128kb時,由brk分配,第一次分配132KB(main arena),第二次在brk下分配,不夠則執行系統調用,向系統申請
在內存中進行堆的管理時,系統基本是以 chunk 作為基本單位,chunk的結構在源碼中有定義
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; };
INTERNAL_SIZE_T 即 size_t
#ifndef INTERNAL_SIZE_T #define INTERNAL_SIZE_T size_t #endif
我們可以打印一下本機的 sizeof(size_t),這個長度可以說是一個基准單位
#include<stdio.h> int main() { printf("sizeof(size_t) is %d\n",sizeof(size_t)); return 0; }
這個結構不再多談,相關的介紹網上很多,主要提一下結構體中最后兩個指針 fd_nextsize 和 bk_nextsize,這兩個指針只在 largebin 中使用,其他情況下為 NULL。我們可以根據 chunk 的狀態將其分為三種(allocated chunk、free chunk、top chunk):
allocated chunk:
chunk header:
prev_size(當上一塊是free狀態時,存儲該chunk的size,否則被上一塊chunk使用)
size(該chunk大小(包括chunk header),某位3 bits為標志位) 0bit表示上一chunk是否free 1bit表示該chunk是否由mmap分配 2bit表示該chunk是否屬於main arena data: free chunk: chunk header: prev_size: size: fd:指向 bin 中的next chunk bk:指向 bin 中的last chunk(bin中先進的為last,后進的為next) fd_nextsize: bk_nextsize: top chunk:brk中未分配的頂端chunk chunk header: prev_size: size:
其中在 free chunk中有一種特殊的chunk(last remainder chunk):
last remainder chunk:從free chunk中malloc時,如果該chunk足夠大,那么將其分為兩部分,未分配的放到last remainder中並交由 unsorted bin 管理。
重點強調一下:這里的上一塊表示在內存的堆中連續的chunk的上一塊,區別bin中的前后關系。另外 chunk 的前后關系只有在bin中是使用fd、bk指針標識的,在內存中連續的chunk則通過 prev_size 和 size 來尋找前后 chunk,當然,這也就造成了漏洞。
由於chunk會在幾種狀態之間切換,當其為free chunk時,最少需要4*sizeof(size_t)的空間,所以有最小分配大小。
並且由於prev_size的復用,所以實際申請的大小為 max(2sizeof(size_t)(chunk_header)-sizeof(size_t)(prev_size)+申請大小,最小分配大小),而且 chunk的size是按照 2sizeof(size_t)對齊的,也就是說當你申請一個不是 2*sizeof(size_t)整倍數的空間時, malloc 返回的 size 有會對齊,大於實際申請的空間。
另外提一下,當 malloc 一個chunk后,實際返回用戶的地址為chunk除去chunk header后的地址,而在bin中存儲的是chunk的地址,也就是說
p = malloc(0x40); // 假設chunk的地址為 0xdeadbeef,則返回給用戶的地址是 0xdeadbeef+sizeof(chunk header) free(p) //將p釋放掉后,保存在bin中的地址為 0xdeadbeef
0×02 bin簡介
bin在內存中用來管理free chunk,bin為帶有頭結點(鏈表頭部不是chunk)的鏈表數組,根據特點,將bin分為四種,分別為(fastbin、unsortedbin、smallbin、largebin):
fastbin:
根據chunk大小維護多個單向鏈表
sizeof(chunk) < 64(bytes) 下一chunk(內存中)的free標志位不取消,顯示其仍在使用 后進先出(類似棧),先free的先被malloc 擁有維護固定大小chunk的10個鏈表 unsortedbin: 雙向循環鏈表 不排序 暫時存儲free后的chunk,一段時間后會將chunk放入對應的bin中(詳見0x02) 只有一個鏈表 smallbin: 雙向循環鏈表 sizeof(chunk) < 512 (bytes) 先進先出(類似隊列) 16,24...64,72...508 bytes(62個鏈表) largebin: 雙向循環鏈表 sizeof(chunk) >= 512 (bytes) free chunk中多兩個指針分別指向前后的large chunk 63個鏈表:0-31(512+64*i) 32-48(2496+512*i) ... 鏈表中chunk大小不固定,先大后小
這其中 fastbin 像是cache,用來實現快速的chunk分配,其中的chunk size大小與smallbin中的有重復(只是說大小,chunk並不重復)
unsortedbin 功能也是作為cache,盡量減少搜索合適chunk的時間。
這四個bin中,除了fastbin,其他三個都是維護雙向循環鏈表,並且由一個長度為128 size_t的數組bins維護,bins結構如下:
| NULL | unsortbin | smallbin | largebin | NULL |
|---|---|---|---|---|
| 0 | 1 | 2-63 | 64-126 | 127 |
0×03 malloc機制
malloc功能主要由 _int_malloc() 函數實現,原型如下:
static Void_t* _int_malloc(mstate av,size_t bytes)
當接收到申請的內存大小后,我們看一下malloc的申請過程。
長度位於 fastbin 時:
1.根據大小獲得fastbin的index 2.根據index獲取fastbin中鏈表的頭指針 如果頭指針為 NULL,轉去smallbin 3.將頭指針的下一個chunk地址作為鏈表頭指針 4.分配的chunk保持inuse狀態,避免被合並 5.返回除去chunk_header的地址 長度位於 smallbin 時: 1.根據大小獲得smallbin的index 2.根據index獲取smallbin中雙向循環鏈表的頭指針 3.將鏈表最后一個chunk賦值給victim 4.if(victim == 表頭) 鏈表為空,不從smallbin中分配 else if(victim == 0) 鏈表未初始化,將fastbin中的chunk合並 else 取出victim,設置inuse 5.檢查victim是否為main_arena,設置標志位 6.返回除去chunk_header的地址 長度位於 largebin 時: 1.根據大小獲得largebin的index 2.將fastbin中chunk合並,加入到unsortbin中
留意一點:系統實際分配的內存地址與返回的地址是不同的,返回的地址直接指向了除去 chunk header 的地址。
當然,我們注意到上面的分配過程並沒有完成,當 smallbin 中沒有 chunk 或者 smallbin 未初始化時,並沒有返回分配結果,這種情況下的chunk分配將在后面與largebin的分配一起處理
unsortedbin: 1.反向遍歷unsortedbin,檢查 2*size_t<chunk_size<內存總分配量 2.unsortedbin的特殊分配: 如果前一步smallbin分配未完成 並且 unsortedbin中只有一個chunk 並且該chunk為 last remainder chunk 並且該chunk大小 >(所需大小+最小分配大小) 則切分一塊分配 3.如果請求大小正好等於當前遍歷chunk的大小,則直接分配 4.繼續遍歷,將合適大小的chunk加入到smallbin中,向前插入作為鏈表的第一個chunk。(smallbin中每個鏈表中chunk大小相同) 5.將合適大小的chunk加入到largebin中,插入到合適的位置(largebin中每個鏈表chunk由大到小排列) largebin: 1.反向遍歷largebin,由下到上查找,找到合適大小后切分 切分后大小<最小分配大小,返回整個chunk,會略大於申請大小 切分后大小>最小分配大小,加入 unsortedbin。 2.未找到,index+1,繼續尋找
如果這之后還未找到合適的chunk,那么就會使用top chunk進行分配,還是沒有的話,如果在多線程環境中,fastbin可能會有新的chunk,再次執行合並,並向unsortedbin中重復上面,還是沒有的話,就只能向系統申請了。
以上就是malloc分配的全經過。
幾個malloc檢查:
1.從fastbin中取出chunk后,檢查size是否屬於fastbin 2.從smallbin中除去chunk后,檢查victim->bk->fd == victim 3.從unsortbin取chunk時,要檢查2*size_t<chunk_size<內存總分配量 4.從largebin取chunk時,切分后的chunk要加入unsortedbin,需要檢查 unsortedbin的第一個chunk的bk是否指向unsortedbin
0×04 free機制
1.首先使用 chunksize(p) 宏獲取p的size
#define PREV_INUSE 0x1 #define IS_MMAPPED 0x2 #define NON_MAIN_ARENA 0x4 #define SIZE_BITS (PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA) #define chunksize(p) ((p)->size & ~(SIZE_BITS))
也就是直接屏蔽了控制位信息,不過不要緊,chunk的分配是 2*sizeof(size_t) 對齊的,所以屏蔽低三位對大小無影響
2.安全檢查:
chunk的指針地址不能溢出
chunk 的大小 >= MINSIZE(最小分配大小),並且檢查地址是否對齊
3.大小為fastbin的情況(不改變inuse位)
1).檢查下一個chunk的size:2*size_t<chunk_size<內存總分配量 2).double free檢查: 檢查當前free的chunk是否與fastbin中的第一個chunk相同,相同則報錯
簡單的小例子
#include <stdio.h> #include <stdlib.h> int main() { char *a=malloc(24); char *b=malloc(24); free(a); free(a); }
報錯
#include <stdio.h> #include <stdlib.h> int main() { char *a=malloc(24); char *b=malloc(24); free(a); free(b); free(a); }
沒問題
4.其他情況
1).檢查下一個chunk的size:2*size_t<chunk_size<內存總分配量 如果當前 chunk 為 sbrk()分配,那么它相鄰的下一塊 chunk 超過了分配區的地址,會報錯 2).double free檢查: 檢查當前free的chunk是否為top chunk,是則報錯 根據下一塊的inuse標識檢查當前free的chunk是否已被free 3) unlink合並: 檢查前后chunk是否free,然后向后(top chunk方向)合並,並改變對應的inuse標志位 unlink檢查: I.當前chunk的size是否等於下一chunk的prev_size II.P->bk->fd == P && P->bk->fd == P 如果合並后 chunk_size > 64bytes,則調用函數合並fastbin中的chunk到unsortedbin中 將合並后的chunk加入unsortedbin 4) unsortedbin檢查 需要檢查 unsortedbin的第一個chunk的bk是否指向unsortedbin
我們可以看到,針對free的檢查主要是下一塊的size和inuse位,另外fastbin的檢查可以用來做double free。
0×05
以上就是對堆的情況所做的一些介紹,了解堆的保護機制后,我們便可以在攻擊時想辦法進行繞過,從而構造出那些光怪陸離的payload。
