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


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

1、什么是堆?

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

我們都知道棧的從高內存向低內存擴展的,而堆是相反的,它是由低內存向高內存擴展的

image-20210808210831151

堆管理器的作用,充當一個中間人的作用。管理從操作系統中申請來的物理內存,如果有用戶需要,就提供給他。

2、了解堆管理器

image-20210808211146557

注意:linux使用glibc

image-20210808211341106

這里有兩種申請內存的系統調用:

  1. brk
  2. mmap

第一種brk,是將heap下方的data段(bss屬於data段),向上擴展申請的內存。

第二種mmap,其實下圖中的shared libraries叫做mmap區域,也就是內存映射。如果使用這種方式申請內存,那么就在這塊區域內開辟新的內存空間。

image-20210808211540839

主線程可以用brk和mmap,如果主線程申請的空間過大,那么會使用mmap;如果申請的空間比較小,那么就會再data段上向上擴展一段空間

子線程只能使用mmap段

image-20210808212344994

malloc就是向堆管理器申請一塊內存空間

free就是將申請來的內存空間歸還給堆管理器

用戶使用malloc向堆管理器要內存,堆管理器通過brk和mmap向操作系統要內存

3、堆管理器的操作方式

首先了解三個關鍵詞:

  • arena
  • chunk
  • bin

image-20210809093642970

堆管理器可以與用戶的內存交易發生於arena中

可以理解為堆管理器向操作系統批發來的有冗余的內存庫存

每一個線程中都有一個arena分配區,每一個分配區都有一個控制結構

image-20210809102451987

image-20210809112554397

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指針

image-20210810150106366

2.free_chunk

  • 再認識free chunk中最常見的幾種
    • small bin、unsorted bin
    • 這兩種結構如下圖所示

image-20210819183332506

  • 如果下面的這個chunk被free了,並且標志位P=0(也就是上一個chunk是free chunk),那么會變成這樣的一個大的free chunk

image-20210819183540244

  • large bin free chunk 的結構

image-20210819184229246

  • fast bin free chunk的結構

image-20210819184249187

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中的實現

image-20210819185057958

chunk的結構體如上圖,但是我們發現其實除了large bin free chunk之外,其他的chunk都沒有用結構體中的所有變量

首先來看一個程序

image-20210819193117008

我們申請了一個0x100空間大小的heap,用空指針prt指向malloc返回的地址,然后再通過free()函數釋放這段空間

我們用gcc編譯一下,得到了一個a.out的elf文件

image-20210819193151885

我們使用gdb對這個elf文件進行調試

我們執行到malloc執行完畢的時候查看vmmap

image-20210819193632034

我們可以看到兩個細節:

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

    image-20210809093642970

    我們知道操作系統會將內存分配給堆管理器,然后堆管理器再調用給用戶。

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

    就像堆管理器向操作系統批發了一大塊內存空間,然后再對用戶進行一小份一小份的售賣。

    所以我們這里看到的0x21000大小的區域其實是操作系統給堆管理器的(也就是我們上面說的top chunk),然后我們的第二次調用malloc就從這一大份的內存空間中給出

  2. 第二個細節:我們發現我們申請的heap區域是在data段的高地址處,這也印證了我們剛剛說的如果主線程申請的內存區域比較小,那么是通過brk的方式在data段的高地址申請一塊區域

一個小插曲:

我們想知道在x64下,能最小分配的堆空間是多大呢?

我們繼續在剛剛的gdb調試中,輸入fastbin

image-20210819194945012

我們最小的chunk被free掉之后就會放入fastbin中,可以看到最小的fastbin是0x20的大小,為什么是0x20的大小呢?

image-20210819195313316

首先在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大小空間

image-20210819200821121

這個時候我們又有一個問題了,我們是通過空指針prt當再malloc的返回值,那么我們的ptr指針在哪里呢?其實我們pte指針是指向0x100這個數據段的,而並非prev size這個chunk的開頭部分

image-20210819201315827

我們再回到調試,輸入heap觀察堆,可以發現我們申請的0x100大小的空間其實是0x111,這是為什么呢?(其他的heap、chunk區域可能是程序的緩沖區之類的)

image-20210819201715463

這個0x111其實是0x100+0x10+0x1得來的

0x10就是prev size+size的大小

0x1其實是size最后的3bit中的P=1

然后我們再來看ptr這個指針,我們剛剛說了ptr這個malloc返回的指針處在size之后的data段開頭

image-20210819202810184

我們申請的0x100大小的heap的addr是0x55555555559290,而我們ptr這個指針指向的地址是0x555555552a0,我們發現其實是heap的addr+0x10,也就是在pre size和size之后,印證了我們剛剛的結論

再來一個小插曲:

這個插曲是關於prev size的覆用

首先說一個結論,我們申請0xn0大小的空間和申請0xn8大小的空間,堆管理器給我們的內存是一樣的,為什么呢?

image-20210819203914622

因為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如下圖所示,我們可以從中看出邏輯鏈表的結構特點

image-20210820182404990

  • 邏輯鏈表的好處是什么呢?如果我們想要再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字節。
  • image-20210820191116173
  • 這里有一個小插曲:為什么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因為僅僅是單獨的一個,所以結構如下圖
  • 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大小都一樣
  • image-20210820193617664

4.large bin

  • bin[64]~bin[126]

  • 63個循環雙向鏈表

  • 先進先出(FIFO)的工作特性

  • 管理大於504 bytes的free chunks(32位下)

  • large bin被分為了6組,每組bin能夠容納的chunk按順序排成了等差數列,如下圖所示

  • image-20210820193544659

  • large bin為了加快檢索速度,fd_nextsize和bk_nextsize指針用於指向第一個與自己不同大小的chunk。所以只有在加入了大小不同的chunk時,這兩個指針才會被修改。

內存申請和釋放

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


免責聲明!

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



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