[CTF_Pwn堆漏洞利用] 2.基础知识——堆相关数据结构

[CTF_Pwn堆漏洞利用] 2.基础知识——堆相关数据结构

我一直都计划开个专题,重新整理一下堆的知识点——这一部分我学的实在是太模糊了。

这篇博客的内容主要讲解堆的基础知识,依托于glibc源码进行剖析讲解。本文以glibc2.23版本作为用例,内容集中于通用的堆基础知识,诸如tcache等高版本的堆特性暂不阐述。

该系列持续更新......


chunk


#ifndef INTERNAL_SIZE_T
#define INTERNAL_SIZE_T size_t
#endif
#define SIZE_SZ                (sizeof(INTERNAL_SIZE_T))

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;
};

前两个域是堆块头

prev_size,记录物理相邻的前一个堆块的大小

size记录堆块大小,对齐2*SIZE_SZ

其中size域的最后三位作为标记位(AMP),它们从高到低分别表示

NON_MAIN_ARENA,记录当前 chunk 是否不属于主线程,1 表示不属于,0表示属于。

IS_MAPPED,记录当前 chunk 是否是由 mmap 分配的。

PREV_INUSE,记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。
当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲 chunk 之间的合并。

如果这个chunk是正在使用未被free的chunk,那么它的内部结构就到此为止了,此后便是内存块中存放的数据,当然了,分配到内存可不是如上只有6 * SIZE_SZ这么大,只是内存块的前6 * SIZE_SZ会有特殊用处罢了。

但是如果这个chunk被free了,那么后面的四个指针就会派上用场了

fd,bk,用于管理被释放的堆块,fd指向下一个,bk指向上一个,chunk通过它们和bin构成一个双向链表

    ->      ->      ->      ->      ->            fd
bin    chunk   chunk   chunk   chunk   chunk    
    <-      <-      <-      <-      <-            bk

bin这个东西你可以暂时理解为管理free的堆块的一个链表头

fd_nextsize和bk_nextsize用于大堆块(large bins),用来指向另一个large bin中的第一个空闲堆块(这个放在后面细说)

这里要注意chunk的后面四个指针指向另外某个malloc_chunk结构体的开头,这里是用于内部的管理,但是实际使用时malloc函数返回的指针实际指向chunk开头+2*SIZE_SZ的地方,即从fd域开始。

关于chunk有许多常见的宏,比如:

/*内存块和堆块头指针转换,这里的“mem”就是malloc返回的内存块地址*/
#define chunk2mem(p)   ((void*)((char*)(p) + 2*SIZE_SZ))
#define mem2chunk(mem) ((mchunkptr)((char*)(mem) - 2*SIZE_SZ))

/*最小堆块大小,可以看到堆块至少要包含fd和bk*/
#define MIN_CHUNK_SIZE        (offsetof(struct malloc_chunk, fd_nextsize))

/*这个大概是个掩码,相当于二进制的111*/
#define SIZE_BITS (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
/*根据size域获取堆块大小,屏蔽了后三位*/
#define chunksize(p)         ((p)->size & ~(SIZE_BITS))

(如果我某天又想起来了某些重要的宏,我一定会再回来添加...)


bins


glibc的堆管理系统的实际工作,其实就是管理那些被释放的内存块。这些内存块通过链表的方式串联起来,而所谓的bins正是这些链表的表头。

根据free的chunk的大小和时机,我们将堆块分为如下四类,fast bin,small bin large bin,unsorted bin。

bins数组在malloc_state结构中,至于后面这个结构是什么,我们后面会讲。

下面是一个bins数组,绝大多数bin都在这里,不过fast bin不在这里,他比较特殊。

typedef struct malloc_chunk* mchunkptr;

#define NBINS             128

mchunkptr bins[NBINS * 2 - 2];

可以看到,bins数组就是由很多个chunk*指针组成的。

不同的索引对应不同的bin类型

索引 类型
1 unsorted bin2
1-63 small bin
64-127 large bin

有人可能会异或(疑惑),为什么这里的索引不是从0开始的呢?这里要看取某个bin的动作了,没错,这也是一个宏:

typedef struct malloc_chunk *mbinptr;

/*索引定位对应bin*/
#define bin_at(m, i) \
  (mbinptr) (((char *) &((m)->bins[((i) - 1) * 2]))			      \
             - offsetof (struct malloc_chunk, fd))

/*找后一个bin,实际上就是加上2*SIZE_SZ*/
#define next_bin(b)  ((mbinptr) ((char *) (b) + (sizeof (mchunkptr) << 1)))

m应该填一个malloc_state结构,这个会在后面细说。i则是我们上面说的索引。

也许此时就会有同学发现问题了:似乎这里每个bin占据了bins数组的两个元素,而且这里返回值为mbinptr(即malloc_chunk)!,而且这里还匪夷所思地减了一个2*SIZE_SZ的偏移!这是什么情况呢?其实这里是我们通过malloc_chunk这个类型来记录索引链表,但是记录的过程中只用到了fd和bk这两个指针(统一类型也许是为了方便)。这样,尽管前一个bin的fd和bk对应后一个bin的prev_size和size,但是由于后一个bin并没有用到这两个域,所以不会发生重合,以此我们就实现了空间复用。

为什么要使用fd和bk呢,这大概是为了让bin和chunk更好地融入到一起吧(看看这几个宏)

/* 分别是找离bin最近的和最远的堆块,对应每个bin所占的bins数组的两前一个元素和后一个元素 */
#define first(b)     ((b)->fd)
#define last(b)      ((b)->bk)

是不是和谐多了,这下子bin本身好像也是一个chunk......

接下来我们分类讲讲这四种bin结构


fast bin

先介绍最最特殊的fast bin吧。

fast bin通过单向链表管理(仅仅使用fd指针),采用LIFO原则,每一个新的chunk会插入到链表的头部(靠近bin),且取出时也是优先从头部取出。

fast bin并不在上面说的bins数组中,而是在下面的这个fastbinsY这个结构里面,当然,这个fastbinsY也在malloc_state这个结构中。

下面是fastbinsY的定义和一些相关宏,之前上面讲bins时的一些宏在这里可能不太管用——它们适用于后面三个。

typedef struct malloc_chunk *mfastbinptr;

#define fastbin(ar_ptr, idx) ((ar_ptr)->fastbinsY[idx])

/*根据大小查索引*/
#define fastbin_index(sz) \
  ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)

/*最大fast bin*/
#define MAX_FAST_SIZE     (80 * SIZE_SZ / 4)

#define NFASTBINS  (fastbin_index (request2size (MAX_FAST_SIZE)) + 1)

#define set_max_fast(s) \
  global_max_fast = (((s) == 0)						      \
                     ? SMALLBIN_WIDTH : ((s + SIZE_SZ) & ~MALLOC_ALIGN_MASK))
#define get_max_fast() global_max_fast

#define DEFAULT_MXFAST     (64 * SIZE_SZ / 4)

mfastbinptr fastbinsY[NFASTBINS];

fast bin,bin如其名,特点就是快,这个范围的chunk被释放时不会发生合并,物理相邻的后一个堆块的P为始终为1,后一个堆块的prev_size也不会启用,这就大大减少了维护损耗。

fastbin中的每个堆块大小都是一样的,这里可以看到,堆块大小和索引的关系大致为 size == i * ( 2 * SIZE_SZ )。64位下,就是0x20为0开始,每0x10递增。fastbin最大可以是MAX_FAST_SIZE,但是这只是一个理论的上界限,实际我们可以通过set_max_fast进行一个预设。通常,global_max_fast,会被预设为DEFAULT_MXFAST,在64位下,这个值是0x80。

fastbin中的chunk会被malloc_consolidate函数清空,不过这个是后话了,我们以后再聊。


small bin

small bin,被维护在bins数组当中的索引2-63号。和fast bin一样,链表中每一个堆块大小都是一样的,不过它是采用双向链表进行管理,并且采用FIFO机制,表头插入,表尾取出。

/*对齐方式*/
#define MALLOC_ALIGNMENT       (2 *SIZE_SZ < __alignof__ (long double)      \
                                  ? __alignof__ (long double) : 2 *SIZE_SZ)
/*似乎也是对齐方式*/
#define SMALLBIN_WIDTH    MALLOC_ALIGNMENT
#define SMALLBIN_CORRECTION (MALLOC_ALIGNMENT > 2 * SIZE_SZ)

#define NSMALLBINS         64
#define MIN_LARGE_SIZE    ((NSMALLBINS - SMALLBIN_CORRECTION) * SMALLBIN_WIDTH)

#define in_smallbin_range(sz)  \
  ((unsigned long) (sz) < (unsigned long) MIN_LARGE_SIZE)

#define smallbin_index(sz) \
  ((SMALLBIN_WIDTH == 16 ? (((unsigned) (sz)) >> 4) : (((unsigned) (sz)) >> 3))\
   + SMALLBIN_CORRECTION)

仔细算一下就能知道MIN_LARGE_SIZE是63*SIZE_SZ,在64位下是1008,从1024开始,就是large bin了

small bin也是从2,也就是2(2SIZE_T)开始的,也许你已经发现了一个问题:这不是和fast bin范围重合了吗?的确如此,但是small bin并不具有fast bin不切割不合并不标记的特点,两者仍有差别,而fast bin中的堆块也可以进到small bin当中。这在后来我们剖析函数源码时会有进一步讲解。


large bin

感觉比较吃力的同学可以先跳过这一节,large bin在堆攻击学习的前期不太会涉及,当然,欠的债迟早要补——和我一样。

从small bin后面(索引64以后)就全是large bin的天下了(63个),这要注意里large bin中的chunk大小并不是完全一致的,但也不是杂乱无章,每一个bin中chunk大小一定是在一定的区间范围内。

这63个bin被分为了6组,每组内的相邻的bin之间范围的范围构成一个等差数列。

数量 公差
1 32 64B
2 16 512B
3 8 4096B
4 4 32768B
5 2 262144B
6 1 不限制
#define largebin_index_32(sz)                                                \
 (((((unsigned long) (sz)) >> 6) <= 38) ?  56 + (((unsigned long) (sz)) >> 6) :\
  ((((unsigned long) (sz)) >> 9) <= 20) ?  91 + (((unsigned long) (sz)) >> 9) :\
  ((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :\
  ((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :\
  ((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :\
  126)

#define largebin_index_32_big(sz)                                            \
 (((((unsigned long) (sz)) >> 6) <= 45) ?  49 + (((unsigned long) (sz)) >> 6) :\
  ((((unsigned long) (sz)) >> 9) <= 20) ?  91 + (((unsigned long) (sz)) >> 9) :\
  ((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :\
  ((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :\
  ((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :\
  126)

// XXX It remains to be seen whether it is good to keep the widths of
// XXX the buckets the same or whether it should be scaled by a factor
// XXX of two as well.
#define largebin_index_64(sz)                                                \
 (((((unsigned long) (sz)) >> 6) <= 48) ?  48 + (((unsigned long) (sz)) >> 6) :\
  ((((unsigned long) (sz)) >> 9) <= 20) ?  91 + (((unsigned long) (sz)) >> 9) :\
  ((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :\
  ((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :\
  ((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :\
  126)

#define largebin_index(sz) \
 (SIZE_SZ == 8 ? largebin_index_64 (sz)                                     \
  : MALLOC_ALIGNMENT == 16 ? largebin_index_32_big (sz)                     \
  : largebin_index_32 (sz))

比较复杂对吧。

fd和bk指针,在large bin中仍然启用,并且和small bin一样用来你链接大小相同的chunk,但是我们上面讲了,large bin中的堆块大小是不相同的,为了便于管理,large bin启用了fd_nextsize和bk_nextsize这两个域,在相同大小的chunk各自通过fd和bk串联成双向链表后,这些链表头部的chunk也会依次通过fd_nextsize和bk_nextsize串联起来。而bin的fd和bk则会分别指向最大的链表和最小的链表。

具体的串联方式见下图(转自博客ptmalloc代码浅析2(small bin/large bin结构图)),这里不详细介绍了,后面会单独分析large bin的管理机制,到时候我们再细说
图片.png


unsorted bin

这个在bins最开头的一项,索引为1。就像它的名字一样,里面的chunk的未整理的,大小不一致。

这个bin你可以理解为是一个缓冲区,任何要放入small bin或者large bin的chunk都要先放进unsorted bin里面,再经过后续的一些整理进入unsorted bin。

unsorted bin和small bin很相似,除了里面储存的chunk大小范围不一样之外,其余的特点大致都相同(FIFO,双向链表管理等等)

这里要介绍一个宏unlink,用来把chunk(P)从它所处的双向链表中取出,在unsorted bin中比较常见,而且针对于unsorted bin chunk也有一个unlink的混淆攻击手段。

#define unlink(AV, P, BK, FD)                                             \

最后再介绍一个通用的宏,用来查找某个大小对应的bin索引

#define bin_index(sz) \
  ((in_smallbin_range (sz)) ? smallbin_index (sz) : largebin_index (sz))

微观结构——杂项


top chunk

堆段内存分配,其实就相当于切蛋糕,分配出去的是小蛋糕,切的就是top chunk。

top chunk在main arena中可以通过sbrk扩展,在thread heap中通过mmap映射。

top chunk的P位永远是1,否则之前分配的chunk就应该被合并进来。


last reminder

有的时候我们在malloc时会从一个已经free掉的堆块中切一个合适堆块出来,剩下的东西就是last reminder

被切出来的last reminder会被扔进unsorted bin


宏观结构


arena

这一块我也说不明白,其实不懂的话基础的堆题不受影响。

arena在线程第一次申请内存时创建,不是每个线程都有对应的arena

需要注意的一点,main_arena(主线程的arena)保存在libc段中。


malloc_state

记录了arena申请的内存信息

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

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

  /* 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.  Access to this field is serialized
     by free_list_lock in arena.c.  */
  struct malloc_state *next_free;

  /* Number of threads attached to this arena.  0 if the arena is on
     the free list.  Access to this field is serialized by
     free_list_lock in arena.c.  */
  INTERNAL_SIZE_T attached_threads;

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

里面有很多我们之前提到的结构(bins等)

宏观结构可能在前期不是很重要,对于其结构稍作了解即可,前期学习的重点要放在malloc和free的那一堆繁琐却不严密的检查机制上。

posted @ 2023-01-11 11:16  Jmp·Cliff  阅读(89)  评论(0编辑  收藏  举报