【pwn】學pwn日記(堆結構學習)
1、什么是堆?
堆是下圖中綠色的部分,而它上面的橙色部分則是堆管理器
我們都知道棧的從高內存向低內存擴展的,而堆是相反的,它是由低內存向高內存擴展的
堆管理器的作用,充當一個中間人的作用。管理從操作系統中申請來的物理內存,如果有用戶需要,就提供給他。
2、了解堆管理器
注意:linux使用glibc
這里有兩種申請內存的系統調用:
- brk
- mmap
第一種brk,是將heap下方的data段(bss屬於data段),向上擴展申請的內存。
第二種mmap,其實下圖中的shared libraries叫做mmap區域,也就是內存映射。如果使用這種方式申請內存,那么就在這塊區域內開辟新的內存空間。
主線程可以用brk和mmap,如果主線程申請的空間過大,那么會使用mmap;如果申請的空間比較小,那么就會再data段上向上擴展一段空間
子線程只能使用mmap段
malloc就是向堆管理器申請一塊內存空間
free就是將申請來的內存空間歸還給堆管理器
用戶使用malloc向堆管理器要內存,堆管理器通過brk和mmap向操作系統要內存
3、堆管理器的操作方式
首先了解三個關鍵詞:
- arena
- chunk
- bin
堆管理器可以與用戶的內存交易發生於arena中
可以理解為堆管理器向操作系統批發來的有冗余的內存庫存
每一個線程中都有一個arena分配區,每一個分配區都有一個控制結構
chunk是內存分配的最小單位,也是我們malloc過來的內存
chunk的size控制字段的最后三位分別是A、M、P
A代表是否是主線程arena中分配的內存
M代表這段區域是否是MMAP的
P用於標識上一個chunk的狀態。當它為1時,表示上一個chunk處於釋放狀態,否則表示上一個chunk處於使用狀態
我們來了解malloc_chunk各個成員的功能
- prev_size:如果上一個chunk處於釋放狀態,用於表示其大小。否則作為上一個chunk的一個部分,用於保存上一個chunk的數據
- size:表示當前size的大小,根據規定必須是2*SIZE_SZ的整數倍。默認情況下,SIZE_SZ在64位系統下是8字節,32位下是4字節。受到內存對齊的影響,最后3個比特位被用作狀態標識,從高到低分別表示
- NON_MAIN_ARENA:用於標識當前堆是否不屬於主線程,1 表示不屬於,0 表示屬於。
- IS_MAPPED:用於標識一個chunk是否是從mmap()函數得到的。如果用戶申請一個相當大的內存,malloc會通過mmap分配一個映射段
- PREV_INUSE:用於標識上一個chunk的狀態。當它為0時,表示上一個chunk處於釋放狀態,否則表示上一個chunk處於使用狀態
- fd和bk:僅在當前chunk處於釋放狀態有效。chunk被釋放后會加入相應的bin鏈表中,此時fd和bk指向該chunk在鏈表的下一個和上一個free chunk(不一定時物理相連的)。如果當前chunk處於使用狀態,那么這兩個字段是無效的,都是用戶使用的空間
- fd_nextsize和bk_nextsize:與fd和bk相似,僅在處於釋放狀態時有效,否則就是用戶使用的空間。不同的是,它們僅僅用於large bin,分別指向前后第一個和當前chunk大小不同的chunk
4、各種chunk的結構
chunk有4種:
-
alloced_chunk
-
free_chunk
-
top chunk
-
ast_remainder chunk
1.alloced_chunk
- 首先認識alloced chunk結構,alloced chunk就是處於使用狀態的chunk,即pre_size和size組成的chunk header和后面供用戶使用的user data。malloc函數返回給用戶的實際上是指向用戶數據的mem指針
2.free_chunk
- 再認識free chunk中最常見的幾種
- small bin、unsorted bin
- 這兩種結構如下圖所示
- 如果下面的這個chunk被free了,並且標志位P=0(也就是上一個chunk是free chunk),那么會變成這樣的一個大的free chunk
- large bin free chunk 的結構
- fast bin free chunk的結構
3.top chunk
- 我們再來看top chunk
- 在整個堆初始化后,會被當成一個free chunk,稱為top chunk,每次用戶申請內存的時候,如果bins中沒有合適的chunk,malloc就會從top chunk中進行划分,如果top chunk的大小不夠,那么會調用brk()擴展堆的大小,然后從新生成的top chunk中進行切分。
4.last remainder chunk
- 再看last_remainder chunk
- 首先我們需要知道用戶申請內存的過程,在底層是如何實現的
- 首先,如果申請的內存小於64bytes,在fastbin中查找並給出
- 如果申請大於64bytes,那么在unsorted bin中查找
- 如果unsorted bin中沒有適合申請內存大小的bin段,那么unsorted bin進行遍歷合並一部分free chunk,在這些合並后的chunk中找合適的
- 如果還沒找到那么就向top chunk在申請一些內存
- 如果top chunk的內存都不夠,如果僅僅比top chunk大一點,那么向操作系統要一點,通過brk()的方式擴展top chunk的空間
- 如果比top chunk大了很多很多,那么通過mmap()的方式映射一塊內存給和用戶
- 說了這么多過程,last remainder chunk在哪里出現了呢?
- 其實在第二步就出現了,因為glibc的特性,在unsorted bin中查詢到了比用戶申請的內存大的chunk段,malloc就會返回這一段的size之后的指針。而如果我們的這段內存其實比用戶申請的大了那么一點,多出來的就會變成我們的last remainder chunk,然后這一部分再在prev size中又進入了unsoorted bin中
5、chunk在glibc中的實現
chunk的結構體如上圖,但是我們發現其實除了large bin free chunk之外,其他的chunk都沒有用結構體中的所有變量
首先來看一個程序
我們申請了一個0x100空間大小的heap,用空指針prt指向malloc返回的地址,然后再通過free()函數釋放這段空間
我們用gcc編譯一下,得到了一個a.out的elf文件
我們使用gdb對這個elf文件進行調試
我們執行到malloc執行完畢的時候查看vmmap
我們可以看到兩個細節:
-
第一個細節:雖然我們申請的是0x100大小的heap,但是這里第一次申請卻有0x21000大小的區域。為什么會申請這么大的空間呢?這個就與我們剛剛了解到的arena有關了
我們知道操作系統會將內存分配給堆管理器,然后堆管理器再調用給用戶。
這個過程我們可以怎么理解呢?
就像堆管理器向操作系統批發了一大塊內存空間,然后再對用戶進行一小份一小份的售賣。
所以我們這里看到的0x21000大小的區域其實是操作系統給堆管理器的(也就是我們上面說的top chunk),然后我們的第二次調用malloc就從這一大份的內存空間中給出
-
第二個細節:我們發現我們申請的heap區域是在data段的高地址處,這也印證了我們剛剛說的如果主線程申請的內存區域比較小,那么是通過brk的方式在data段的高地址申請一塊區域
一個小插曲:
我們想知道在x64下,能最小分配的堆空間是多大呢?
我們繼續在剛剛的gdb調試中,輸入fastbin
我們最小的chunk被free掉之后就會放入fastbin中,可以看到最小的fastbin是0x20的大小,為什么是0x20的大小呢?
首先在x64下,一個地址的內存大小就為0x8,那么我們的一個最小的chunk,就像上圖一樣,用pre size記錄上一個chunk大小,用size記錄自己的大小,size下面是一個fd,在下面是data,所以如果要最小的話,一共是4個0x8,那么就是0x20的大小
那么同理,在x86下,一個地址的內存大小為0x4,所以就是上面的圖從中砍了一半,剩下左半部分是有效的,那么最小的堆在x86中就是0x10的大小
回到主線:
我們在test.c中使用malloc申請的是0x100的大小空間,但是實際上,堆管理器會給我們0x110的chunk,這多出來的0x10實際上就是prev size和size的大小,我們能夠使用的data段就是這個0x100大小空間
這個時候我們又有一個問題了,我們是通過空指針prt當再malloc的返回值,那么我們的ptr指針在哪里呢?其實我們pte指針是指向0x100這個數據段的,而並非prev size這個chunk的開頭部分
我們再回到調試,輸入heap觀察堆,可以發現我們申請的0x100大小的空間其實是0x111,這是為什么呢?(其他的heap、chunk區域可能是程序的緩沖區之類的)
這個0x111其實是0x100+0x10+0x1得來的
0x10就是prev size+size的大小
0x1其實是size最后的3bit中的P=1
然后我們再來看ptr這個指針,我們剛剛說了ptr這個malloc返回的指針處在size之后的data段開頭
我們申請的0x100大小的heap的addr是0x55555555559290,而我們ptr這個指針指向的地址是0x555555552a0,我們發現其實是heap的addr+0x10,也就是在pre size和size之后,印證了我們剛剛的結論
再來一個小插曲:
這個插曲是關於prev size的覆用
首先說一個結論,我們申請0xn0大小的空間和申請0xn8大小的空間,堆管理器給我們的內存是一樣的,為什么呢?
因為prev size的作用是記錄相鄰的低地址的free chunk的大小,而如果prev size上面是一個malloced chunk,那么prev size就沒有作用了,這個時候堆管理器體現出了節省內存的思想,將prev size進行覆寫,從而獲得0x8的內存大小
6、bin和鏈表
-
bin是什么?在英文中,bin是垃圾桶的意思,就如字面意思一樣,bin是管理堆的回收。
-
bin管理arena中空閑的chunk的結構,並且以數組的形式存在,數組元素為相應大小的chunk鏈表的鏈表頭。bin存在於arena的malloc_state中
-
在chunk被釋放的時候,glibc會將它們重新組織起來,構成不同的bin鏈表。當用戶再次申請的時候,就會從其中尋找合適的chunk返回給用戶。
-
不同大小區間的chunk被划分到不同的bin中,再加上一種特殊的fast bin,一共是4種:fast bin、small bin、large bin、unsorted bin
-
關於chunk中的鏈表有兩種:
- 物理鏈表
- 邏輯鏈表
- 物理鏈表就是每一個prev size記錄了前面一個free chunk的大小,從而可以指向上一個prev size,形成了一個物理鏈表。這種鏈表是物理層面上的相鄰
- 而邏輯鏈表不是物理層面的互相連在一起,而是通過chunk中的指針來連接,比如fastbin就是由fd連到下一個prev size,然后按照這樣的結構延續下去的一個結構。邏輯鏈表就是將同類型的chunk通過指針連接在一起。
-
在bin中我們一般都是討論邏輯鏈表
-
fastbins如下圖所示,我們可以從中看出邏輯鏈表的結構特點
-
邏輯鏈表的好處是什么呢?如果我們想要再free之后重新申請一塊區域,這個時候在bins中就會尋找適配的bin來還原內存空間。而這些空間恰好是被邏輯鏈表連在一起的,這樣就可以提供剛好合適的內存空間給用戶,不會造成浪費
-
bin有兩種結構:雙向鏈表和單向鏈表,除了fastbin是單向鏈表,其余的bin都是雙向鏈表
-
我們的bin中有兩個bin數組:
- fastbinsY:裝有NFASTBINS個fast bin,NFASTBINS一般是7
- bins:是一個bin數組,一共有126個bin,按順序分別是:
- bin[1]是unsorted bin
- bin[2]~bin[63]是small bin
- bin[64]~bin[126]是large bin
1.fastbin
- 除了fastbin的結構是單項鏈表,其他的bin都是雙向鏈表。因為fastbin只有一個fd指針。
- fastbin的工作方式是后進先出。
- fastbin的P永遠是1,因為就如同字面的fast意思一樣,為了更快的釋放和分配。這樣就避免了fastbin被合並。也就是這樣讓它有了fast的屬性
- 那么我們為什么需要fastbin這種東西呢?
- 因為fastbin的范圍是從最小的0x20開始,有7個,也就是到0x80。我們的程序經常性的頻繁的會申請一些小空間,如果一些很小的空間都需要被堆管理器頻繁的接手,那就會變得非常麻煩,並且消耗資源。這就猶如我們在銀行頻繁的存入5塊錢,然后下一秒又取出3塊錢,又存1塊錢,然后又取出10塊錢。為了避免這樣的情況出現,就有了fastbin的單鏈表。
- 並且這也是為什么fastbin的工作方式是LIFO(后進先出),因為需要快速的管理小的內存空間。也是為什么P永遠為1。
- fastbin管理16、24、32、40、48、56、64bytes的free chunks(32位下默認)
- 按照fastbinsY數組里從小到大的順序,序號為0的fast bin中容納的chunk大小為4*SIZE_SZ字節,隨着序號增加,所容納的chunk遞增2*SIZE_SZ字節。
- 這里有一個小插曲:為什么fastbins中有bk指針?
- 因為fastbin管理16~64bytes的free chunks,而smallbin管理16~504bytes的free chunks(32位下)
- 並且如果unsotred bin在自己遍歷的過程中,可能會將fastbin變為smallbin。
- 在fastbin中,bk這個域沒有任何用處
2.unsorted bin
在實踐中,一個被釋放的chunk常常很快就會被重新使用,所以將其先放入unsorted bin中,可以加快分配的速度。
- unsorted bin僅僅占用一個,也就沒有bins的說法,所以是bin[1]
- unsorted bin管理剛剛釋放還未分類的chunk(這也就是為什么叫unsorted bin)
- 我們可以unsorted bin視為空閑的chunk回歸其所屬bin之前的緩沖區
- 然后unsorted bin因為僅僅是單獨的一個,所以結構如下圖
- 當malloc了一個在large bin范圍之內的chunk,並且在unsorted bin中沒有找到滿足用戶要求的空間大小的free chunk,這個時候unsorted bin就會開始遍歷進行可以合並的chunk進行合並(物理結構上相鄰的兩個或者多個free chunk),合並完成了就會把合並完成后從bin放入相對應的bins中
3.small bin
small bin使用頻率介於fast bin和large bin之間。剛剛也提到了在unsorted bin 遍歷的時候,fast bin可以變為small bin。
- bin[2]~bin[63]
- 62個循環雙向鏈表
- 先進先出(FIFO)的工作特性
- 管理16、24、32、40、....、504 bytes的free chunks(32位下)
- 每個鏈表中存儲的chunk大小都一樣
4.large bin
-
bin[64]~bin[126]
-
63個循環雙向鏈表
-
先進先出(FIFO)的工作特性
-
管理大於504 bytes的free chunks(32位下)
-
large bin被分為了6組,每組bin能夠容納的chunk按順序排成了等差數列,如下圖所示
-
-
large bin為了加快檢索速度,fd_nextsize和bk_nextsize指針用於指向第一個與自己不同大小的chunk。所以只有在加入了大小不同的chunk時,這兩個指針才會被修改。
內存申請和釋放
這一塊等到學到了再補上吧