ptmalloc堆內存管理

背景介紹

Linux的棧內存管理相信大家都已經很熟悉了,針對棧內存的攻擊也是比較常見的。然而對於堆內存的管理機制可能不太熟悉,針對堆內存的攻擊也是比較困難的,所以我通過閱讀各種資料以及Glibc的相關源碼,對Glibc下的堆內存管理機制有了一定的瞭解,故在此記錄下學習心得。

首先不同平臺的堆內存管理機制是不一樣的,我現在主要是針對Glibc的堆內存管理機制進行分析,Glibc的堆內存管理機制叫做ptmalloc。其他的一些比較流行的管理機制有:
1. jemalloc(FreeBSD, Firefox, Android)
2. ptmalloc(Glibc)
3. tcmalloc(Google)
4. libumem(Solaris)

Chunk structure

在Glibc的堆內存管理中,chunk是堆內存分配的基本的單位,它表示堆內存中連續的內存單元。比方說我們通過malloc(8)申請一個連續的8字節內存,則Glibc會分配我們一個大小爲8(chunk size + previous size)+8(payload)大小的chunk。chunk分爲allocated chunk和freed chunk。chunk structure在Glibc的定義如下所示:

struct malloc_chunk {

  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. */
  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;
};

從上面的結構我們可以看出,如果一個chunk爲allocated chunk,則它需要分配prev_size和size域,其中prev_size和size(表示整個chunk的大小,包括sizeof(prev_size)+sizeof(size)+sizeof(payload))用來在進行free()操作的時候將與該chunk空間上相鄰的freed chunk進行合併,減少了堆內存空間的碎片化(具體怎麼合併的在下面會具體介紹)。如果一個chunk爲freed chunk的話,其相對於allocated chunk來說又多了兩個域–fd和bk指針。因爲freed chunk是通過多個鏈表結構將所有的freed chunk鏈接了起來,這樣便於malloc函數快速找到合適大小的freed chunk,並且該這些表是雙向鏈表(fastbins除外)。

所以對於malloced chunk來說,具體的內存區域如下所示:

chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             prev_size:Size of previous chunk, if allocated  | |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of chunk, in bytes                       |M|P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             User data starts here...                          .
        .                                                               .
        .             (malloc_usable_size() bytes)                      .
        .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of chunk                                     |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

在size域,有兩個標誌位,一個爲M,一個爲P。M表示該chunk是否是allocated chunk,P表示與該chunk空間上相鄰的之前的chunk是否是freed chunk,如果該標識爲0,表示previous chunk爲freed chunk,則prev_size域表示previous chunk的大小; 否則如果previous chunk爲allocated chunk,則記錄previous chunk的大小就沒有意義,此時就將prev_size域當做previous chunk的payload的一部分。

對於freed chunk來說,具體的內存區域如下所示:

 chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of previous chunk                            |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    `head:' |             Size of chunk, in bytes                         |P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Forward pointer to next chunk in list             |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Back pointer to previous chunk in list            |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Unused space (may be 0 bytes long)                .
        .                                                               .
        .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    `foot:' |             Size of chunk, in bytes                           |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

對於freed chunk來說,就多了兩個域:fd和bk,這兩個域分別指向該鏈表中的前一個元素和後一個元素。

Bins

在Glibc的堆內存管理中,bin是將一個個freed chunk鏈接起來的鏈表,而bins就是存儲這些鏈表的一維數組。每一個bin都是雙向鏈表。根據freed chunk的大小將其分爲了不同的136個bin,其中有10個爲fastbin, 62個small bin,63個large bin和一個unsorted bin。在malloc_state結構中,就定義了這些bin的數組,具體聲明如下:

struct malloc_state
{
  /* Serialize access.  */
  mutex_t mutex;

  /* Flags (formerly in max_fast).  */
  int flags;

#if THREAD_STATS
  /* Statistics for locking.  Only used if THREAD_STATS is defined.  */
  long stat_lock_direct, stat_lock_loop, stat_lock_wait;
#endif

  /* Fastbins */
  mfastbinptr fastbinsY[NFASTBINS];

  /* Base of the topmost chunk -- not otherwise kept in a bin */
  mchunkptr top;

  /* The remainder from the most recent split of a small request */
  mchunkptr last_remainder;

  /* Normal bins packed as described above */
  mchunkptr bins[NBINS * 2 - 2];

  /* Bitmap of bins */
  unsigned int binmap[BINMAPSIZE];

  /* Linked list */
  struct malloc_state *next;

  /* Linked list for free arenas.  */
  struct malloc_state *next_free;

  /* Memory allocated from the system in this arena.  */
  INTERNAL_SIZE_T system_mem;
  INTERNAL_SIZE_T max_system_mem;
};

Fast Bins

在Glibc的堆內存管理機制中,一共有10個fast bin,每個fast bin鏈表中的所有freed chunk的大小(sizeof(prev_size)+sizeof(size)+sizeof(payload))都是相等的。每個fast bin都是一個單鏈表( )。Fastbins中最小的bin中的chunk大小爲16字節,隨後每個bin都增加8字節,所以最大的bin爲80字節。需要注意的是,fast bin和其它的bin在free處理的時候有很大的不同,fast bin中的chunk的M標誌都爲1,因此在進行free處理的時候不進行freed chunk的合併操作(具體的合併操作在下文會具體介紹)。在每一個fast bin鏈表中,當有新的freed chunk需要插入時,會插入到該鏈表的尾部,刪除也是從尾部刪除,因此形成了一個先入後出(FILO)的策略。

Small Bins

Small bin的個數爲62個,每一個small bin是一些大小相等的freed chunk組成的循環雙向鏈表。當有新的freed chunk加入到該鏈表中,就加入到該鏈表的頭部;如果要從鏈表中刪除一個freed chunk時,則從該鏈表的爲尾部刪除,因此形成了一個先入先出(FIFO)的策略。第一個small bin中的freed chunk的大小都是16 bytes,後面每一個small bin的freed chunk的大小都依次增加8 bytes,因此最後一個small bin的freed chunk的大小爲512 bytes。

Large Bins

與small bin和fast bin不同的是,每一個large bin中的freed chunk的大小不一定相等,其只是表示一個範圍,在前32個large bin中,以64字節爲步長,即第一個large bin中的chunk大小爲512~575字節,第二個large bin中的free chunk大小爲576~639字節。緊隨其後的16個large bin依次以512字節步長爲間隔;之後的8個bin以步長4096爲間隔;再之後的4個bin以32768字節爲間隔;之後的2個bin以262144字節爲間隔;剩下的chunk就放在最後一個large bin中。

Unsorted Bin

只有一個unsorted sin,其主要存儲兩種chunk。一種是在malloc()操作中由於要分配的大小比freed chunk的大小要小,所以需要將該freed chunk進行分割,返回與要分配的大小相符的chunk,剩餘的freed chunk則加入unsorted bin中;另一種是在free()操作之後,會返回一個新的freed chunk,該freed chunk(不在fast bin範圍的chunk)則加入unsorted bin中。設置這一個bin的主要目的是扮演一個緩存層的角色以加快分配和釋放的操作。

Top chunk

有一個特殊的chunk沒有在以上的bins中,那就是top chunk,top chunk可以看做是heap的一個邊界,當所有的bin中的chunk大小都不符合所請求的大小時,就從該chunk中進行分配,如果top chunk的大小大於所請求的大小時,則將top chunk分爲兩部分,一個是用戶請求的chunk,剩餘的部分就會成爲一個新的chunk。否則,就需要擴展通過上移top chunk指針來擴展heap的大小(或者通過mmap來分配新的heap)。

Malloc

malloc函數是堆內存管理中最重要的一個函數之一,其在glibc中的包裝函數爲__libc_malloc(size_t)函數,在該函數中主要是進行一些準備性的工作–查找對應的arena結構,然後調用真正的分配內存的函數 _int_malloc(ar_ptr, bytes)。arena在glibc中的結構爲malloc_state結構,主要存儲bins,top chunk等結構。下面我們來看一下主要的分配函數_int_malloc的具體實現。

_int_malloc (mstate av, size_t bytes)

  1. 首先_int_malloc函數根據要請求的內存大小bytes來計算要請求的chunk的大小nb。主要是加上size和prev_size域和對齊的padding。

  2. 得到了要請求的chunk大小之後,首先判斷該chunk的大小是否在fast bins的範圍內,如果在它們的範圍內就在fast bins中找到大小相符的chunk來分配。

    fast bin圖片

  3. 如果要請求的大小不在fast bins範圍之內或者相應的fast bin鏈表爲空,則會判斷其chunk size是否在small bins的範圍之內,找到對應的small bin,取出該small bin尾部的那個chunk進行分配。

    small bin圖片

  4. 如果請求的大小是一個比較大的請求,則將fast bins進行合併。其要做fast bins合作的目的是爲了避免有fast bins所引起的內存碎片化問題。在實際的操作中,程序一般都是分配小的內存或者分配大的內存,而不怎麼會即分配大的內存又分配小的內存。所以這樣的策略在實際的程序中效率還是很理想的。

    fast bin 合併

  5. 由於unsorted bin中存儲最近freed的chunk(包括第4步合併後的fast bins),接下來會遍歷unsorted bin中的所有freed chunk,直到找到freed chunk大小和請求的大小相等的爲止,或者循環了10000次也會停止遍歷unsorted bin。遍歷unsorted bin是唯一一個將freed chunk插入到相應的small bins和large bins的操作。在遍歷的過程中,將當前節點chunk從unsorted bin中刪除,如果chunk大小與要請求的chunk大小正好一致,則將該chunk返回,停止遍歷,否則,將當前節點的chunk放入相應的small bins和large bins中。

    unsorted bin last remainder圖片

    unsorted bin insert圖片

  6. 遍歷了unsorted bin之後仍然沒有找到合適的freed chunk,接下來回從large bins中進行查找,首先從large bin中的chunk大於等於請求的chunk進行查找。

    large bin find

    如果該large bin爲空或者最大的chunk比要請求的chunk的大小還要小,則找到chunk size範圍更大的large bin,進行的操作和剛纔介紹的操作一致。

  7. 如果要請求的大小比large bins中的chunk還要大,則需要查看top chunk的大小了,如果top chunk的大小比要請求的chunk的大小要大,則分割top chunk,將剩餘的chunk繼續作爲top chunk的一部分,否則判斷是否還有fastbins,如果有fastbins,則將fastbins合併。以上各種查詢都沒有找到合適的chunk,則只能調用sysmalloc函數來進行分配。

    top chunk malloc

  8. 至此,malloc的分配的過程已經介紹完了,總結一下其查找過程就是fastbins->smallbins->unsortedbins->largebins->topchunk

Free

free函數也是堆內存管理中最重要的函數之一,堆內存的攻擊也主要是針對free函數的進行的攻擊。在glibc中,__libc_free(void* mem)爲free(void* mem)函數的包裝函數,其主要的功能也是找到malloc_state結構,然後再調用 _int_free(mstate av, mchunkptr p, int have_lock)函數。

  1. 判斷p指向的地址是否在p+chunksize(p)指向的地址之前,如果不符合該規則的話就會拋出錯誤(“free(): invalid pointer”)。

  2. 判斷chunk的大小是否大於MINSIZE或者是不是MALLOC_ALIGNMENT的整數倍,否則拋出錯誤(“free(): invalid size”)

  3. 判斷要free的chunk的大小是否落在了fast bins的範圍內,如果落在了這個範圍內,則將該freed chunk加入到對應的fast bin中。

    free chunk fastbin

  4. 判斷該chunk是否是mmapped,如果不是,就進行各種判斷防止部分針對free操作的攻擊。

    • 判斷要free的chunk是否是top chunk,如果是,則拋出錯誤(“double free or corruption (top)”)
    • 判斷next chunk在內存中是否在arean的範圍內,如果不是,則拋出錯誤(“double free or corruption (out)”)
    • 判斷next chunk的P標誌是否爲真,如果不是,則拋出錯誤(“double free or corruption (!prev)”)
    • 判斷next chunk的大小是否正常範圍之間,如果不是,則拋出錯誤(“free(): invalid next size (normal)”)

    normal free error

    然後將與free的chunk與其相鄰的freed chunk進行合併,合併了之後將它插入到unsorted bin中。

    normal free

參考

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