【逆向学习记录】堆分配中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搞定了,其他的也就差不多了,接下来就进行简单的堆漏洞利用了

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