【逆向學習記錄】堆分配中chunk&bins

1 概述

前段時間各種出差,導致堆的學習中斷,
今日有空,趕緊總結一下,學習堆的過程中,開始的時候,有個很難理解的東西,那個東西就是malloc,涉及到malloc就會涉及到chunk,實話說chunk這個東西的學習,確實經歷了不少時間,總結一下,防止忘記
參考
Linux堆內存管理深入分析-上這篇文章詳細講了chunk的進化過程,這裏就不在深入查看了,深入淺出,是學習堆溢出基礎的佳作
Linux堆內存管理深入分析-下這篇文章詳細講了bin的結構,並且被我大量摘抄過來了
這裏,根據自己的理解進行總結,因爲bins是比較宏觀的東西,有了bins的概念,理解起來容易點,因此就從bins開始學習了,

2 bins–大量摘抄的參考文章2

將參考文章的那個圖稍微修訂一下:
在這裏插入圖片描述

2.1 fastbin

在內存分配和釋放過程中,fast bin是所有bin中操作速度最快的。下面詳細介紹fast bin的一些特性:

  1. fast bin的個數——10個

  2. 每個fast bin都是一個單鏈表(只使用fd指針)。爲什麼使用單鏈表呢?因爲在fast bin中無論是添加還是移除fast chunk,都是對“鏈表尾”進行操作,而不會對某個中間的fast chunk進行操作。更具體點就是LIFO(後入先出)算法:添加操作(free內存)就是將新的fast chunk加入鏈表尾刪除操作(malloc內存)就是將鏈表尾部的fast chunk刪除。需要注意的是,爲了實現LIFO算法,fastbinsY數組中每個fastbin元素均指向了該鏈表的rear end(尾結點),而尾結點通過其fd指針指向前一個結點,依次類推。
    在這裏插入圖片描述

  3. chunk size:10個fast bin中所包含的fast chunk size是按照步進8字節排列的,即第一個fast bin中所有fast chunk size均爲16字節,第二個fast bin中爲24字節,依次類推。在進行malloc初始化的時候,最大的fast chunk size被設置爲80字節(chunk unused size爲64字節),因此默認情況下大小爲16到80字節的chunk被分類到fast chunk。詳情如圖2-1所示。

  4. 不會對free chunk進行合併操作。鑑於設計fast bin的初衷就是進行快速的小內存分配和釋放,因此係統將屬於fast bin的chunk的P(未使用標誌位)總是設置爲1(allocated),這樣即使當fast bin中有某個chunk同一個free chunk相鄰的時候,系統也不會進行自動合併操作,而是保留兩者。雖然這樣做可能會造成額外的碎片化問題,但瑕不掩瑜。

  5. malloc(fast chunk)操作:即用戶通過malloc請求的大小屬於fast chunk的大小範圍(注意:用戶請求size加上16字節就是實際內存chunk size)。在初始化的時候fast bin支持的最大內存大小以及所有fast bin鏈表都是空的,所以當最開始使用malloc申請內存的時候,即使申請的內存大小屬於fast chunk的內存大小(即16到80字節),它也不會交由fast bin來處理,而是向下傳遞交由small bin來處理,如果small bin也爲空的話就交給unsorted bin處理

/* Maximum size of memory handled in fastbins.  */
static INTERNAL_SIZE_T global_max_fast;
/* offset 2 to use otherwise unindexable first 2 bins */
/*這裏SIZE_SZ就是sizeof(size_t),在32位系統爲4,64位爲8,fastbin_index就是根據要malloc的size來快速計算該size應該屬於哪一個fast bin,即該fast bin的索引。因爲fast bin中chunk是從16字節開始的,所有這裏以8字節爲單位(32位系統爲例)有減2*8 = 16的操作!*/
#define fastbin_index(sz) \
  ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
/* The maximum fastbin request size we support */
#define MAX_FAST_SIZE     (80 * SIZE_SZ / 4)
#define NFASTBINS  (fastbin_index (request2size (MAX_FAST_SIZE)) + 1)

那麼fast bin 是在哪?怎麼進行初始化的呢?當我們第一次調用malloc(fast bin)的時候,系統執行_int_malloc函數,該函數首先會發現當前fast bin爲空,就轉交給small bin處理,進而又發現small bin 也爲空,就調用malloc_consolidate函數對malloc_state結構體進行初始化,malloc_consolidate函數主要完成以下幾個功能:

a. 首先判斷當前malloc_state結構體中的fast bin是否爲空,如果爲空就說明整個malloc_state都沒有完成初始化,需要對malloc_state進行初始化。

b. malloc_state的初始化操作由函數malloc_init_state(av)完成,該函數先初始化除fast bin之外的所有的bins(構建雙鏈表,詳情見後文small bins介紹),再初始化fast bins。
然後當再次執行malloc(fast chunk)函數的時候,此時fast bin相關數據不爲空了,就開始使用fast bin(見下面代碼中的※1部分):

static void *
_int_malloc (mstate av, size_t bytes)
{  ……
  /*
     If the size qualifies as a fastbin, first check corresponding bin.
     This code is safe to execute even if av is not yet initialized, so we
     can try it without checking, which saves some time on this fast path.
   */
   //第一次執行malloc(fast chunk)時這裏判斷爲false,因爲此時get_max_fast ()爲0
   if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
    {1 idx = fastbin_index (nb);
      mfastbinptr *fb = &fastbin (av, idx);
      mchunkptr pp = *fb;
      do 
      {
          victim = pp;
          if (victim == NULL)
            break;
      }2 while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))!= victim);
      if (victim != 0)
      {
          if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
          {
              errstr = "malloc(): memory corruption (fast)";
          errout:
              malloc_printerr (check_action, errstr, chunk2mem (victim));
              return NULL;
          }
          check_remalloced_chunk (av, victim, nb);
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
      }
  }

得到第一個來自於fast bin的chunk之後,系統就將該chunk從對應的fast bin中移除,並將其地址返回給用戶,見上面代碼※2處。

  1. free(fast chunk)操作:這個操作很簡單,主要分爲兩步:先通過chunksize函數根據傳入的地址指針獲取該指針對應的chunk的大小;然後根據這個chunk大小獲取該chunk所屬的fast bin,然後再將此chunk添加到該fast bin的鏈尾即可。整個操作都是在_int_free函數中完成。

2.2 Unsorted bin

當釋放較小或較大的chunk的時候,如果系統沒有將它們添加到對應的bins中(爲什麼,在什麼情況下會發生這種事情呢?詳情見後文),系統就將這些chunk添加到unsorted bin中。爲什麼要這麼做呢?這主要是爲了讓“glibc malloc機制”能夠有第二次機會重新利用最近釋放的chunk(第一次機會就是fast bin機制)。利用unsorted bin,可以加快內存的分配和釋放操作,因爲整個操作都不再需要花費額外的時間去查找合適的bin了。

Unsorted bin的特性如下:

  1. unsorted bin的個數: 1個。unsorted bin是一個由free chunks組成的循環雙鏈表。
  2. chunk size: 在unsorted bin中,對chunk的大小並沒有限制,任何大小的chunk都可以歸屬到unsorted bin中。這就是前言說的特例了,不過特例並非僅僅這一個(參考文章2)。

2.3 Small bin

小於512字節的chunk稱之爲small chunk,small bin就是用於管理small chunk的。就內存的分配和釋放速度而言,small bin比large bin快,但比fast bin慢。

Small bin的特性如下:

  1. small bin個數:62個。每個small bin也是一個由對應free chunk組成的循環雙鏈表。同時Small bin採用FIFO(先入先出)算法:內存釋放操作就將新釋放的chunk添加到鏈表的front end(前端),分配操作就從鏈表的rear end(尾端)中獲取chunk。

  2. chunk size: 同一個small bin中所有chunk大小是不一樣的,且第一個small bin中chunk大小爲16字節,後續每個small bin中chunk的大小依次增加8字節,即最後一個small bin的chunk爲16 + 62 * 8 = 512字節。

  3. 合併操作:相鄰的free chunk需要進行合併操作,即合併成一個大的free chunk。具體操作見下文free(small chunk)介紹。

  4. malloc(small chunk)操作:類似於fast bins,最初所有的small bin都是空的,因此在對這些small bin完成初始化之前,即使用戶請求的內存大小屬於small chunk也不會交由small bin進行處理,而是交由unsorted bin處理,如果unsorted bin也不能處理的話,glibc malloc就依次遍歷後續的所有bins,找出第一個滿足要求的bin,如果所有的bin都不滿足的話,就轉而使用top chunk,如果top chunk大小不夠,那麼就擴充top chunk,這樣就一定能滿足需求了(《Linux堆內存管理深入分析-上》在Top Chunk)。注意遍歷後續bins以及之後的操作同樣被large bin所使用,因此,將這部分內容放到large bin的malloc操作中加以介紹。
    那麼glibc malloc是如何初始化這些bins的呢?因爲這些bin屬於malloc_state結構體,所以在初始化malloc_state的時候就會對這些bin進行初始化,代碼如下:

malloc_init_state (mstate av)
{
  int i;
  mbinptr bin;
  /* Establish circular links for normal bins */
  for (i = 1; i < NBINS; ++i)
  {
      bin = bin_at (av, i);
      bin->fd = bin->bk = bin;
  }
……
}

注意在malloc源碼中,將bins數組中的第一個成員索引值設置爲了1,而不是我們常用的0(在bin_at宏中,自動將i進行了減1處理…)。從上面代碼可以看出在初始化的時候glibc malloc將所有bin的指針都指向了自己——這就代表這些bin都是空的。
過後,當再次調用malloc(small chunk)的時候,如果該chunk size對應的small bin不爲空,就從該small bin鏈表中取得small chunk,否則就需要交給unsorted bin及之後的邏輯來處理了。

  1. free(small chunk):當釋放small chunk的時候,先檢查該chunk相鄰的chunk是否爲free,如果是的話就進行合併操作:將這些chunks合併成新的chunk,然後將它們從small bin中移除,最後將新的chunk添加到unsorted bin中。

2.4 Large bin

大於512字節的chunk稱之爲large chunk,large bin就是用於管理這些large chunk的。
Large bin的特性如下:

  1. large bin的數量:63個。Large bin類似於small bin,只是需要注意兩點:一是同一個large bin中每個chunk的大小可以不一樣,但必須處於某個給定的範圍(特例2) ;二是large chunk可以添加、刪除在large bin的任何一個位置。

  2. chunk size:在這63個large bins中,前32個large bin依次以64字節步長爲間隔,即第一個large bin中chunk size爲512~575字節,第二個large bin中chunk size爲576 ~ 639字節。緊隨其後的16個large bin依次以512字節步長爲間隔;之後的8個bin以步長4096爲間隔;再之後的4個bin以32768字節爲間隔;之後的2個bin以262144字節爲間隔;剩下的chunk就放在最後一個large bin中。鑑於同一個large bin中每個chunk的大小不一定相同,因此爲了加快內存分配和釋放的速度,就將同一個large bin中的所有chunk按照chunk size進行從大到小的排列:最大的chunk放在鏈表的front end,最小的chunk放在rear end。

  3. 合併操作:類似於small bin。

  4. malloc(large chunk)操作:初始化完成之前的操作類似於small bin,這裏主要討論large bins初始化完成之後的操作。首先確定用戶請求的大小屬於哪一個large bin,然後判斷該large bin中最大的chunk的size是否大於用戶請求的size(只需要對比鏈表中front end的size即可)。如果大於,就從rear end開始遍歷該large bin,找到第一個size相等或接近的chunk,分配給用戶。如果該chunk大於用戶請求的size的話,就將該chunk拆分爲兩個chunk:前者返回給用戶,且size等同於用戶請求的size;剩餘的部分做爲一個新的chunk添加到unsorted bin中。如果該large bin中最大的chunk的size小於用戶請求的size的話,那麼就依次查看後續的large bin中是否有滿足需求的chunk,不過需要注意的是鑑於bin的個數較多(不同bin中的chunk極有可能在不同的內存頁中),如果按照上一段中介紹的方法進行遍歷的話(即遍歷每個bin中的chunk),就可能會發生多次內存頁中斷操作,進而嚴重影響檢索速度,所以glibc malloc設計了Binmap結構體來幫助提高bin-by-bin檢索的速度。Binmap記錄了各個bin中是否爲空,通過bitmap可以避免檢索一些空的bin。如果通過binmap找到了下一個非空的large bin的話,就按照上一段中的方法分配chunk,否則就使用top chunk來分配合適的內存。

  5. Free(large chunk):類似於small chunk。

2.5 彙總

在這裏插入圖片描述

3 chunk-大量摘抄的參考文章1

3.1 chunk的基本概念

在上圖中的bins中的一個一個節點就是chunk,那麼chunk到底是什麼呢?關於chunk的發展史,直接查看參考文章1,這裏直接把結構體拿過來

struct malloc_chunk {
  /* #define INTERNAL_SIZE_T size_t */
  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. 這兩個指針只在free chunk中存在*/
  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;
};

在glibc malloc中將整個堆內存空間分成了連續的、大小不一的chunk,即對於堆內存管理而言chunk就是最小操作單位。Chunk總共分爲4類:1)allocated chunk; 2)free chunk; 3)top chunk; 4)Last remainder chunk。從本質上來說,所有類型的chunk都是內存中一塊連續的區域,只是通過該區域中特定位置的某些標識符加以區分。爲了簡便,我們先將這4類chunk簡化爲2類:allocated chunk以及free chunk,前者表示已經分配給用戶使用的chunk,後者表示未使用的chunk。

3.2 chunk中的標誌位

PREV_INUSE ( P ):表示前一個chunk是否爲allocated
--------P = 1:pre chunk allocated
--------P = 0:pre chunk free
IS_MMAPPED( M ):表示當前chunk是否是通過mmap系統調用產生的。
NON_MAIN_ARENA(N):表示當前chunk是否是thread arena。

當前glibc malloc free chunk格式在這裏插入圖片描述
當前glibc malloc allocated chunk格式:在這裏插入圖片描述

3.3 Top Chunk

當一個chunk處於一個arena的最頂部(即最高內存地址處)的時候,就稱之爲top chunk。**該chunk並不屬於任何bin,而是在系統當前的所有free chunk(無論那種bin)都無法滿足用戶請求的內存大小的時候,將此chunk當做一個應急消防員,分配給用戶使用。**如果top chunk的大小比用戶請求的大小要大的話,就將該top chunk分作兩部分:1)用戶請求的chunk;2)剩餘的部分成爲新的top chunk。否則,就需要擴展heap或分配新的heap了——在main arena中通過sbrk擴展heap,而在thread arena中通過mmap分配新的heap。

3.4 Last Remainder Chunk

對於Last remainder chunk,我們主要有兩個問題:1)它是怎麼產生的;2)它的作用是什麼?

先回答第一個問題。對small bin的malloc機制的介紹中當用戶請求的是一個small chunk,且該請求無法被small bin、unsorted bin滿足的時候,就通過binmaps遍歷bin查找最合適的chunk,如果該chunk有剩餘部分的話,就將該剩餘部分變成一個新的chunk加入到unsorted bin中,另外,再將該新的chunk變成新的last remainder chunk。

然後回答第二個問題。此類型的chunk用於提高連續malloc(small chunk)的效率,主要是提高內存分配的局部性。那麼具體是怎麼提高局部性的呢?舉例說明。當用戶請求一個small chunk,且該請求無法被small bin滿足,那麼就轉而交由unsorted bin處理。同時,假設當前unsorted bin中只有一個chunk的話——就是last remainder chunk,那麼就將該chunk分成兩部分:前者分配給用戶,剩下的部分放到unsorted bin中,併成爲新的last remainder chunk。這樣就保證了連續malloc(small chunk)中,各個small chunk在內存分佈中是相鄰的,即提高了內存分配的局部性。

4 fastbin案例

4.1 測試源碼1

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(int argc, char* argv[])
{
    char *p1 = NULL;
    char *p2 = NULL;
    char *p3 = NULL;
    p1 = (char *)malloc(sizeof(char) * 50);
    p2 = (char *)malloc(sizeof(char) * 60);
    p3 = (char *)malloc(sizeof(char) * 40);
    memset(p1,'A',50);
    memset(p2,'B',60);
    memset(p3,'C',40);
    printf("p1:%s\n",p1);
    printf("p2:%s\n",p2);
    printf("p3:%s\n",p3);
    free(p1);
    free(p2);
    free(p3);
    return 0;
}

4.1.1 malloc之後

在完成memset完成之後進行斷點,查看目前堆的現狀:
在這裏插入圖片描述
查看當前內存情況
在這裏插入圖片描述
第一個chunk長度總共是65(0x41),在源碼中分配的長度是50,
第二個chunk長度總共是81(0x51),在源碼中分配的長度是60,
第三個chunk長度總共是49(0x31),在源碼中分配的長度是40,

通過上述的分配,可以簡單看到一個規則:如果申請的內存大小除以16餘數大於8,就不會再使用padding,避免覆蓋size的內容,如果小於8,就會使用padding,作爲payload的一部分
比如:50 %16 = 2 < 8,可以直接與下一個chunk共用一段內存
而:60%16 = 12 > 8 不能和下一個chunk共用一段內存。
同時,查看malloc之後的賦值情況如下,每個值都與對應的chunk起始地址相差16字節
在這裏插入圖片描述
因此,三段chunk最終的分佈如下圖爲:
在這裏插入圖片描述

4.1.2 Free之後

第一個free,free(p1)
因爲fastbin是單鏈表因此將fd賦值,但是此時位第一個chunk,因此
fd = 0x0
在這裏插入圖片描述
然後依次free(p2),free(3)得到的結果,竟然fd都是0x0
在這裏插入圖片描述
猜測是分配字節的問題,因爲fastbin是以8個字節作爲步長的,因此猜測,當前的結果應該是如此佈局
在這裏插入圖片描述

4.2 測試的源碼2

因此修改源碼爲,將p1,p2的大小相近

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(int argc, char* argv[])
{
    char *p1 = NULL;
    char *p2 = NULL;
    char *p3 = NULL;
    p1 = (char *)malloc(sizeof(char) * 50);
    p2 = (char *)malloc(sizeof(char) * 52);
    p3 = (char *)malloc(sizeof(char) * 40);
    memset(p1,'A',50);
    memset(p2,'B',52);
    memset(p3,'C',40);
    printf("p1:%s\n",p1);
    printf("p2:%s\n",p2);
    printf("p3:%s\n",p3);
    free(p1);
    free(p2);
    free(p3);
    return 0;
}

4.2.1 malloc之後

在這裏插入圖片描述
allocated 的堆棧圖:
在這裏插入圖片描述

4.2.2 free之後

修改之後,chunk2的fd的內容變爲:0x602000,爲第一個chunk的地址
在這裏插入圖片描述
在這裏插入圖片描述
堆棧圖如下,注意此時的指針大小爲8個字節
在這裏插入圖片描述
因此此時的佈局爲:
在這裏插入圖片描述

4.3 案例總結:

以上案例充分驗證了fastbin的所有特性
1,malloc的時候,fd指針和bk指針是沒用的,裏面都是數據,應該叫padding段
2,fastbin中size的最後一位始終爲1,這就是爲什麼分配的時候明明是64,但是計算的時候爲65的原因
3,fastbin的步長爲8遞進
4,fastbin爲單向鏈表,free的時候,只是用fd指針,即後向指針,bk指針的值是無效的
5,fastbin在free的時候,相鄰的free chunk不會合並掉
6,fastbin中向量指針永遠指向最後的chunk,因此fd名義上是next,但是感覺上before,但是實際的鏈表是下面這樣的,要銘記下圖,新增的free chunk 插入的位置都是頭指針的位置
在這裏插入圖片描述

5 總結

上面基本把fastbin的特點都已經說明白了,關於其他的bins,就變成了雙向鏈表,並且free的過程中,有可能會進行合併,這點需要自己一點一點,不過已經把fastbin搞定了,其他的也就差不多了,接下來就進行簡單的堆漏洞利用了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章