1. 程式人生 > 其它 >【pwn】學pwn日記(堆結構學習)

【pwn】學pwn日記(堆結構學習)

【pwn】學pwn日記(堆結構學習)

1、什麼是堆?

堆是下圖中綠色的部分,而它上面的橙色部分則是堆管理器

我們都知道棧的從高記憶體向低記憶體擴充套件的,而堆是相反的,它是由低記憶體向高記憶體擴充套件的

堆管理器的作用,充當一箇中間人的作用。管理從作業系統中申請來的實體記憶體,如果有使用者需要,就提供給他。

2、瞭解堆管理器

注意:linux使用glibc

這裡有兩種申請記憶體的系統呼叫:

  1. brk
  2. 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種:

  1. alloced_chunk

  2. free_chunk

  3. top chunk

  4. 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

我們可以看到兩個細節:

  1. 第一個細節:雖然我們申請的是0x100大小的heap,但是這裡第一次申請卻有0x21000大小的區域。為什麼會申請這麼大的空間呢?這個就與我們剛剛瞭解到的arena有關了

    我們知道作業系統會將記憶體分配給堆管理器,然後堆管理器再呼叫給使用者。

    這個過程我們可以怎麼理解呢?

    就像堆管理器向作業系統批發了一大塊記憶體空間,然後再對使用者進行一小份一小份的售賣。

    所以我們這裡看到的0x21000大小的區域其實是作業系統給堆管理器的(也就是我們上面說的top chunk),然後我們的第二次呼叫malloc就從這一大份的記憶體空間中給出

  2. 第二個細節:我們發現我們申請的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中的連結串列有兩種:

    1. 物理連結串列
    2. 邏輯連結串列
    • 物理連結串列就是每一個prev size記錄了前面一個free chunk的大小,從而可以指向上一個prev size,形成了一個物理連結串列。這種連結串列是物理層面上的相鄰
    • 而邏輯連結串列不是物理層面的互相連在一起,而是通過chunk中的指標來連線,比如fastbin就是由fd連到下一個prev size,然後按照這樣的結構延續下去的一個結構。邏輯連結串列就是將同類型的chunk通過指標連線在一起。
  • 在bin中我們一般都是討論邏輯連結串列

  • fastbins如下圖所示,我們可以從中看出邏輯連結串列的結構特點

  • 邏輯連結串列的好處是什麼呢?如果我們想要再free之後重新申請一塊區域,這個時候在bins中就會尋找適配的bin來還原記憶體空間。而這些空間恰好是被邏輯連結串列連在一起的,這樣就可以提供剛好合適的記憶體空間給使用者,不會造成浪費

  • bin有兩種結構:雙向連結串列和單向連結串列,除了fastbin是單向連結串列,其餘的bin都是雙向連結串列

  • 我們的bin中有兩個bin陣列:

    1. fastbinsY:裝有NFASTBINS個fast bin,NFASTBINS一般是7
    2. 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時,這兩個指標才會被修改。

記憶體申請和釋放

這一塊等到學到了再補上吧