Dance In Heap(一):淺析堆的申請釋放及相應保護機制


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。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM