Linux堆記憶體管理深入分析(下半部)

wyzsk發表於2020-08-19
作者: 阿里移動安全 · 2016/05/23 15:46

作者:[email protected]

0x00 前言回顧


上一篇文章中,詳細介紹了堆記憶體管理中涉及到的基本概念以及相互關係,同時也著重介紹了堆中chunk分配和釋放策略中使用到的隱式連結串列技術。透過前面的介紹,我們知道使用隱式連結串列來管理記憶體chunk總會涉及到記憶體的遍歷,效率極低。對此glibc malloc引入了顯示連結串列技術來提高堆記憶體分配和釋放的效率。

所謂的顯示連結串列就是我們在資料結構中常用的連結串列,而連結串列本質上就是將一些屬性相同的“結點”串聯起來,方便管理。在glibc malloc中這些連結串列統稱為bin,連結串列中的“結點”就是各個chunk,結點的共同屬性就是:1)均為free chunk;2)同一個連結串列中各個chunk的大小相等(有一個特例,詳情見後文)。

0x01 bin介紹


如前文所述,bin是一種記錄free chunk的連結串列資料結構。系統針對不同大小的free chunk,將bin分為了4類:1) Fast bin; 2) Unsorted bin; 3) Small bin; 4) Large bin。

在glibc中用於記錄bin的資料結構有兩種,分別如下所示:

  • fastbinsY: 這是一個陣列,用於記錄所有的fast bins;
  • bins: 這也是一個陣列,用於記錄除fast bins之外的所有bins。事實上,一共有126個bins,分別是:
    • bin 1 為unsorted bin;
    • bin 2 到63為small bin;
    • bin 64到126為large bin。

其中具體資料結構定義如下:

#!c
struct malloc_state
{
  ……
  /* Fastbins */
  mfastbinptr fastbinsY[NFASTBINS];
  ……
  /* Normal bins packed as described above */
  mchunkptr bins[NBINS * 2 - 2];  // #define NBINS    128
  ……
};
這裡mfastbinptr的定義:typedef struct malloc_chunk *mfastbinptr;
mchunkptr的定義:typedef struct malloc_chunk* mchunkptr;

畫圖更直觀:

圖1-1 bins分類

那麼處於bins中個各個free chunk是如何連結在一起的呢?回顧malloc_chunk的資料結構:

#!c
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;         /* 這兩個指標只在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;
};

其中的fd和bk指標就是指向當前chunk所屬的連結串列中forward或者backward chunk。

0x02 Fast bin


既然有fast bin,那就肯定有fast chunk——chunk size為1680位元組的chunk就叫做fast chunk。為了便於後文描述,這裡對chunk大小做如下約定:

1) 只要說到chunk size,那麼就表示該malloc_chunk的實際整體大小;

2) 而說到chunk unused size,就表示該malloc_chunk中刨除諸如prev_size, size, fd和bk這類輔助成員之後的實際可用的大小。因此,對free chunk而言,其實際可用大小總是比實際整體大小少16位元組

在記憶體分配和釋放過程中,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指標指向前一個結點,依次類推,如圖2-1所示。

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,這樣即使當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處理:

#!c
/* 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部分):

#!c
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處。

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

在main arena中Fast bins(即陣列fastbinsY)的整體操作示意圖如下圖所示:

圖2-1 fast bin示意圖

0x03 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中。這就是前言說的特例了,不過特例並非僅僅這一個,後文會介紹。

0x04 Small bin


小於512位元組的chunk稱之為small chunk,small bin就是用於管理small chunk的。就記憶體的分配和釋放速度而言,small bin比larger 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,這樣就一定能滿足需求了(還記得上一篇文章中在Top Chunk中留下的問題麼?答案就在這裡)。注意遍歷後續bins以及之後的操作同樣被large bin所使用,因此,將這部分內容放到large bin的malloc操作中加以介紹。

那麼glibc malloc是如何初始化這些bins的呢?因為這些bin屬於malloc_state結構體,所以在初始化malloc_state的時候就會對這些bin進行初始化,程式碼如下:

#!c
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及之後的邏輯來處理了。

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

0x05 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的任何一個位置。

在這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。

2) 合併操作:類似於small bin。

3) 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來分配合適的記憶體。

4) Free(large chunk):類似於small chunk。

瞭解上面知識之後,再結合下圖5-1,就不難理解各類bins的處理邏輯了:

0x06 總結


至此glibc malloc中涉及到的所有顯示連結串列技術已經介紹完畢。鑑於篇幅和精力有限,本文沒能詳細介紹完所有的技術細節,但是我相信帶著這些知識點再去研究glibc malloc的話,定能起到事半功倍的效果。

另外,就我個人所瞭解到的基於堆溢位攻擊而言,掌握以上知識,已經足夠理解絕大部分堆溢位攻擊技術了。因此,後面的文章將會結合這些知識詳細介紹各個攻擊技術的實現原理。

老規矩:如有錯誤,歡迎斧正!

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章