【pwn】學pwn日記(堆結構學習)
【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時,這兩個指標才會被修改。
記憶體申請和釋放
這一塊等到學到了再補上吧