堆入门

翻了下笔记,翻到篇之前写的 比较基础的、简洁的 Glibc 内存管理 - ptmalloc2 机制玉门学习笔记,这是刚开始学堆的时候写的,难免会有些描述不当的地方。

学堆的话,有空必须得研读一下源码。但是前期学习可以先不研究得太过细、过于理念,这样会卡很久的进度,进步缓慢。我是由简入繁,了解了基础概念后,按 fast bin -> unsorted bin -> tcache -> small bin -> large bin 这个顺序去做题来学习堆利用的。

堆的定义及组成*

堆空间是一段动态分配的向下增长的内存,即从低地址向高地址增长。

堆结构的一些常见的成员

start_brk指针:指向堆空间的起始地址,即低地址处。对应的 sbrk() 函数

brk指针:指向堆空间的末尾地址,即高地址处。

mmap:malloc 会使用 mmap 来创建独立的匿名映射段。

main_arena:表示堆内存空间本身,本质上是指向 malloc_state 的指针。

malloc_state:管理 arena 的核心结构(包括堆的状态信息,bins 链表),main_arena 对应的 malloc_state 结构存储在 glibc 全局变量中,其他线程 arena 对应的 malloc_state 存储在其 arena 本身。

chunk:通常指由 malloc 申请的内存,被 free 后会被加入到相应的空闲管理列表中。

bin:顾名思义,垃圾桶。即用来管理空闲内存块,通常用链表结构进行组织,被释放掉的堆块要么与 top chunk 合并,要么进 bin 链。

当程序第一次向操作系统申请内存空间时,操作系统会分配一块很大的内存给程序,以减少内核态与用户态的切换,提高了程序的运行效率。这一整块连续的内存区域就是 arena

在这段空间中需要重点关注的是,一部分是已经分配给用户使用的空间,剩下的一部分是 top chunk,当用户继续申请空间的时候,会尽量从 top chunk 中切割出来,这样就避免了频繁与操作系统交互。

top chunk 是处于当前 arena 物理地址最高的 chunk,且不会被划入任意一个 bin 数组中。

堆空间分配机制

malloc_chunk

image-20230427224048680

(s)brk 函数:增加brk的大小,即将指针上下移。

mmap 函数:在已有的堆空间中开辟 chunk。

unmmap 函数:用于回收申请了过大的 chunk。

free_chunk

free 就是 将 chunk 从用户手中取过来,交给 glibc 管理。被 free 的 chunk 的信息会被按一定规则放到一个链表中,用户申请时会先从 bin 中寻找。

总的来说,即释放指针指向的内存块(有可能是 malloc 的内存块,也有可能是 realloc 的内存块),进行相应的入链操作(改 chunk 头的状态位,通过修改内存单元的数据来记录指针指向下一个 free_chunk)。

malloc/free 流程图

这张图很细,忘了在哪偷来的了。

ptmalloc

ARENA 机制

malloc_state*

malloc_state 并不是 heap segment 的一部分,而是一个全局变量,存储在 libc.so 的数据段,即 glibc 的全局变量。(即 0x7f... 的位置,由此引发联想,要是泄了 malloc_state 结构体中的某个成员的地址,是不是就相当于知道了 libc 呢?事实正是如此,这也会是以后做题时的一种常用思路

这个结构体是用于管理 main_arena 的,其中包括堆状态信息,bins 链表等。

malloc_state 结构体源码:

struct malloc_state
{
  /* Serialize access.  */
  mutex_t mutex;//(相当于多线程的互斥锁)

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

  /* Fastbins */
  mfastbinptr fastbinsY[NFASTBINS];// 一个数组,里面的元素是各条不同大小的 fastbin 链的首地址

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

  /* 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];// 以bit为单位的数组,共128bit,16个字节,对应128个bin,对于这里面的128bit,为0表示bin没有空闲块,为1表示有空闲块。通过四个int大小的空间可以找出不同index的bin中是否有空闲块。这个在某些时候会用到。

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

main_arena

全局变量,被分配在 libc.data 段上。

static struct malloc_state main_arena =
{
  .mutex = _LIBC_LOCK_INITIALIZER,
  .next = &main_arena,
  .attached_threads = 1
};

main_arena 实际上是 malloc_state 结构体的一个实例,即理解为指向 malloc_state 结构体的一个指针

main_arena 是指向主线程使用的 malloc_state 结构体的指针,即主线程的堆管理器。而 malloc_state 结构体则是堆管理器的核心数据结构,它保存了所有分配和释放的信息。

malloc_state 结构体中定义了许多和堆管理有关的成员变量,例如 top, last_remainder,bins 等,它们在堆管理器中扮演着重要的角色。

main_arena --> malloc_state [ top chunk, bins... ]

image-20230403165335945


(补充)thread_arena

当一个线程只有一个堆段时,如图,被分为3部分:

heap_info:表示该堆段的信息,并且该结构体的 ar_ptr 成员指针指向于该堆段所属的 thread_arena
malloc_state:该堆段的 arena 并且顶部指向于最顶端的 malloc_chunk
chunk块:就是该堆段所存储的区域

img

当一个线程含有多个堆段时,相对于一个堆段,做了如下改变:

heap_info:因为有多个堆段,所以每个堆段都有自己的 heap_info, 并且两个堆段在内存中不是物理相邻的,因此第二个 heap_info 的上一个指针指向于第一个 heap_info 的 ar_ptr 成员,而第一个 heap_info 结构体的 ar_ptr 成员指向了 malloc_state,这样就构成了一个单链表,方便后续管理。
malloc_state:虽然有多个堆段,但是只有一个 thread_arena。

img

CHUNK

本质上是由 malloc 申请的一块内存。

(物理相邻的上一个 chunk 指的是较低地址的 chunk。

freed chunk 会被加入到对应的 bins 链表。但无论 chunk 的大小如何,处于分配还是释放状态,使用的是一个相同的数据结构。

但注意 used chunk 和 freed chunk 的表现形式会有所不同。

malloc_chunk 结构体*

/*
  This struct declaration is misleading (but accurate and necessary).
  It declares a "view" into memory allowing access to necessary
  fields at known offsets from a given base. See explanation below.
*/
struct malloc_chunk {
// --> chunk
  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */

// When this chunk is used, the following memory is loaded with user data.
// --> mem
  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

(只有在物理相邻的前一个 chunk 为 freed 时使用,作用是为了方便在堆块被释放时进行合并操作。

若某 chunk 的物理相邻的前一个 chunk状态是 used,这时候此 chunk 的 prev_size 域无效,即对于此 chunk 来说是不使用的。该字段可以用来存储物理相邻的前一个 chunk 的数据。(这就是 chunk 中的空间复用

如果该 chunk 的物理相邻的前一个 chunk(两个指针所指向的地址 差值为前一个 chunk 的大小)状态是 freed,那该字段记录的是物理相邻的前一个 chunk 的大小 (包括 chunk 头)。

btw,堆的第一个 chunk 所记录的 prev_inuse 位默认为 1,因为该 chunk 在堆的初始状态下是作为 top chunk 使用的,而 top chunk 是一个已分配的 chunk,因此其 prev_inuse 位应该设置为 1。

size*

记录当前 chunk 的字节大小。

size 大小必须是 2 * SIZE_SZ 的整数倍。32 位系统中,SIZE_SZ 是 4;64 位系统中,SIZE_SZ 是 8。即 SIZE_SZ 为字的大小。

由上可知 size 的大小必为 8 的整数倍,该字段的低三个比特位将对 chunk 的大小没有影响,于是用来作标志位 A|M|P,它们从高位到低位分别表示:

  • NON_MAIN_ARENA,记录当前 chunk 是否不属于主线程(不由 main_arena 管理),1 表示不属于,0 表示属于。
  • IS_MAPPED,记录当前 chunk 是否是由 mmap 分配的,1 表示是,0 表示该 chunk 非 mmap 来的(或许是由 ptmalloc 切割等
  • PREV_INUSE,记录物理相邻的前一个 chunk 块是否被分配。1 表示 used,0 表示 freed。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址,这也方便进行空闲 chunk 之间的合并。

other pointer

fd、bk、fd_nextsize、bk_nextsize

这几个指针只有在当 chunk 被释放了,入链后才会被赋予。

BIN*

[堆利用入门]bin & top chunk & last remainder chunk
bin 数组是用来管理已分配然后被回收的内存,即被 free 的 chunk 将被记录在 bin 链表,可以是 单向/双向循环 链表。

fd --> 位于当前链表中的下一个 chunk

bk --> 位于当前链表中的上一个 chunk

  • total bins (136)

    • fast bins (10),单向链表,LIFO
    • tcache,单向链表,LIFO
    • bins (126)
      • unsorted bin (1),双向链表,FIFO(因为添加/取走 bin 链上的 chunk 时,是通过 bin 头,即 bin_at(1) 的 bk 指针来操作的)
      • small bins (62),双向链表,FIFO,与 fast bin 的分组策略相同
      • large bins (63),双向链表,比较特殊,还有 fd_nextsize, bk_nextsize 指针

无论是先进先出,还是后进先出,之所以有这样的不同,我认为很本质的一个区别在于单向链表和双向链表的区别,ptmalloc 基本是根据 bin 头存储的指针来寻找可使用的 chunk,双向链表的话 bin 头的 bk 指针是能指向链尾的那个 chunk 的,至于单向链表,自然也就是找链表头的 chunk 最方便了。

fastbinsY[ ]

fast bin 链共有 7 条 [0x20, 0x30, 0x40 ... 0x80]

/* The maximum fastbin request size we support */
#define MAX_FAST_SIZE     (80 * SIZE_SZ / 4)
#define NFASTBINS  (fastbin_index (request2size (MAX_FAST_SIZE)) + 1)

fastbins 对应的数据结构在 malloc_state 中

/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];

tcache

tcache 是 glibc 2.26 (ubuntu 17.10) 之后引入的一种技术,目的是提升堆管理的性能。但提升性能的同时舍弃了很多安全检查,也因此有了很多新的利用方式。

主要是这两个结构体,tcache_entrytcache_perthread_struct。tcache 是由结构体 tcache_perthread_struct 管理的,指向这个结构体的指针存在 TLS 中,是各个线程独有的。

Tcache 全名为 Thread Local Caching,它为每个线程创建一个缓存,里面包含了一些小堆块。无须对 arena 上锁既可以使用,所以采用这种机制后分配算法有不错的性能提升。每个线程默认使用 64 个单链表结构的 bins,每个 bins 最多存放 7个 chunk,64 位机器 16 字节递增,从 0x20 到 0x410,也就是说位于这个范围大小的 chunk 释放后都会先行存入到 tcache bin 当中。对于每个 tcache bin 单链表,它和 fast bin 一样都是先进后出,而且 prev_inuse 标记位都不会被清除,所以被释放链入 tcache bin 中的 chunk 即使和 top chunk 相邻也不会被合并。

另外 tcache 机制出现后,每次产生堆都会先产生一个 0x250 大小的堆块,该堆块位于堆的开头,用于记录 64 个 bins 的地址(注意了,这些地址是指向堆块的数据部分,不是直接指向 chunk 头)以及每条 bins 链中的 chunk 数量。在这个 0x250 大小的堆块中,前 0x40 个字节用于记录每条 bins 链中 chunk 数量,每个字节对应一条链中 tcache bin 的数量,从 0x20 开始到 0x410 结束,刚好 64 条链,然后剩下的每 8 字节记录一条 tcache bin 链的开头地址,也是从 0x20 开始到 0x410 结束。还有一点值得注意的是,tcache bin 中的 fd 指针是指向 malloc 返回的地址,也就是用户数据部分,而不是像 fast bin 单链表那样 fd 指针指向 chunk 头。

tcache_entry 结构体如下:

typedef struct tcache_entry
{
  struct tcache_entry *next;
} tcache_entry;

tcache_entry 用于链接空闲的 chunk 结构体,其中 next 指针指向下一个大小相同的chunk。这里需要注意的是 next 指向 chunk 的 data 部分,这和 fastbin 有一些不同,fastbin 的 fd 指向的是下一个 chunk 的头指针。tcache_entry 会复用空闲 chunk 的 data 部分。

tcache_perthread_struct 结构体如下:

typedef struct tcache_perthread_struct
{
  char counts[TCACHE_MAX_BINS];
  tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

# define TCACHE_MAX_BINS                64

static __thread tcache_perthread_struct *tcache = NULL;

tcache_perthread_struct 是用来管理 tcache 链表的,这个结构体位于 heap 段的起始位置,size 大小为 0x251。每一个 thread 都会维护一个 tcache_perthread_struct 结构体,一共有 TCACHE_MAX_BINS 个计数器和 TCACHE_MAX_BINS 项 tcache_entry。

其中:

  • tcache_entry 用单向链表的方式链接了相同大小的处于空闲状态(free 后)的 chunk
  • counts 记录了 tcache_entry 链上空闲 chunk 的数目,每条链上最多可以有 7 个 chunk,即只用 3 bits 即可记录,而一个计数器是 8 bits。

tcache 有很多,分别为 [0x20, 0x30, 0x40 ... 0x410],每条 tcache 链最多收 7 个chunks。

bins[ ]

ptmalloc 将 unsorted bin, small bins, large bins 维护在同一个数组,即 bins 数组。

这些 bins 对应的数据结构在 malloc_state 中,bin 链共有128条

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

为什么在 arena 中,bins 数组的大小为 NBINS * 2 - 2 ?
因为每个 bin 链在 bins 数组中存储的是一个 fd 指针和一个 bk 指针,即两个指向 chunk 的指针,所以要 NBINS * 2。
又因为数组 bins 中下标为 0 1 的指针是不使用的,所以要减去 2。

例: bins[2] 为 unsorted bin 链的 fd 成员,bin[3] 为其 bk 成员

所以说 bin 的下标和我们所说的第几个 bin 并不是一致的,在这里,bins[2] 和 bins[3] 索引第一个 bin。

概念

放到一起了解、对比一下。

fast bin(bins的高速缓存区

fast bin 是 bin 的高速缓冲区,大约有10个定长队列。

当用户释放一块不大于 max_fast(默认值64)的 chunk(即小内存)的时候,会默认会被放到 fast bin 链表上。

unsorted bin(bins的一个缓存区

用户释放的内存大于 max_fast 的 chunk、由 fast bin 合并后的 chunk 以及分割其他 free chunk 产生的剩余 free chunk,都会进入 unsorted bin 里。

主要是为了让 "glibc malloc 机制" 能够有第二次机会重新利用最近释放的chunk(第一次机会就是fast bin机制)。利用unsorted bin,可以加快内存的分配和释放操作,因为整个操作都不再需要花费额外的时间去查找合适的bin了。

small bin & large bin(真正用来放置chunk的

small bin 和 large bin 是真正用来放置 chunk 双向链表的。small bin 中每条相邻 bin 链之间相差8个字节,large bin 中同一组 bin 下的相邻 bin 链中 “能容纳的 chunk 的最大值” 的公差一致。

tcache

glibc 2.26 (ubuntu 17.10) 之后引入的一种技术,目的是提升堆管理的性能,但提升性能的同时舍弃了很多安全检查,也因此有了很多新的利用方式。

类似于 fast bin 一样的东西,每条链上最多可以有 7 个 chunk,free 的时候只有当 tcache 满了才放入 fast bin, unsorted bin, small bin, large bin... malloc 的时候也会优先去 tcache 找。

虽然 fast bin 的操作速度比其他 bin 更快,但是使用 fast bin 存在一些限制与保护机制。首先,fast bin 不能存储超过一个特定大小的chunk,这意味着大多数 chunk 都需要分配到其他的 bin 中,从而增加了搜索 bin 的时间。其次,由于 fast bin 是一个全局的数据结构,因此在高并发环境下,访问和更新 fast bin 可能会导致锁竞争,从而导致性能瓶颈。

为了解决这些问题,从 glibc2.26 开始,ptmalloc 引入了 tcache bin,tcache 是一个线程本地的缓存,用于存储最近释放的chunk,这些 chunk 可以直接在 tcache 中分配,而不需要搜索其他 bin,从而提高了分配的速度。tcache 的大小是可调整的,可以根据系统的负载情况进行优化。

总之,tcache bin 是为了提高分配速度和减少锁竞争而引入的,它与 fast bin 并不矛盾,而是互补的。

总结

10个 fast bin 链 + (bins 链中)索引1为 unsorted bin,2-63 为 small bin,64-126 为 large bin,共 136 bins。

就是说,fast bins 和 small bins 中每条 bin 链中的 chunk 的大小都是相等的,且两条相邻 bin 链中的 chunk 大小差值相等。

large bins 中,一条链包含一个范围内的 chunk。此外,这 63 条 bin 链被分成了 6 组,同组 bin 中,两条相邻 bin 链中各自能容纳的 chunk 的最大值之间的公差一致。例如在组一内公差为 64B,则第一条 bin 链中能容纳的 chunk (范围在512B-568B) 的最大值为 568B,第二条 bin 链中能容纳的 chunk (范围在576B-632B) 的最大值为 568+64B ...

large bin 链上的 chunks 是按大小排列的,从链头 (bin[] 处) 到链尾,沿着各个 chunk 的 fd 指针,chunks 从大到小依次排序。一整条 bin 链的结构如下

1

基础函数

malloc

用户请求 chunk 时会进行堆空间分配,具体会从哪里切割 chunk,会按以下顺序判断。

刚开始我会对这些感到疑惑:进行 malloc 操作时,是按什么顺序去 bin 链中找 chunk 来使用,以及对 chunk 有怎样的脱链操作?如果找不到合适的 chunk,会怎么分配内存来使用?对 chunk 进行 free 操作的机制的具体细节是怎么样的,进哪个链表?怎么判断的?

image-20230403162812532

简单来说,先由小到大依次检查不同的 bin 中是否有相应的空闲块可以满足用户请求的内存,如没有则考虑从 top chunk 中划分。如果 top chunk 不够内存空间的话,堆分配器会考虑用 brk() 函数扩展,进行内存块申请,使 top chunk 变大。

具体分配说明参见下列引用内容:

1、获取分配区的锁,为了防止多个线程同时访问同一个分配区,在进行分配之前需要取得分配区域的锁。线程先查看线程私有实例中是否已经存在一个分配区,如果存在尝试对该分配区加锁,如果加锁成功,使用该分配区分配内存,否则,该线程搜索分配区循环链表试图获得一个空闲(没有加锁)的分配区。如果所有的分配区都已经加锁,那么ptmalloc会开辟一个新的分配区,把该分配区加入到全局分配区循环链表和线程的私有实例中并加锁,然后使用该分配区进行分配操作。开辟出来的新分配区一定为非主分配区,因为主分配区是从父进程那里继承来的。开辟非主分配区时会调用 mmap() 创建一个 sub-heap,并设置好 top chunk。
2、将用户的请求大小转换为实际需要分配的chunk空间大小。
3、判断所需分配chunk的大小是否满足 chunk_size <= max_fast (max_fast 默认为 64B),如果是的话,则转下一步,否则跳到第5步。
4、首先尝试在 fast bins 中取一个所需大小的 chunk 分配给用户。如果可以找到,则分配结束。否则转到下一步。
5、判断所需大小是否处在 small bins 中,即判断 chunk_size < 512B 是否成立。如果 chunk 大小处在 small bins 中,则转下一步,否则转到第7步。
6、根据所需分配的 chunk 的大小,找到具体所在的某个 small bin,从该 bin 的尾部摘取一个恰好满足大小的 chunk,若成功,则分配结束,否则,转到下一步。
7、到了这一步,说明需要分配的是一块大的内存,或者 small bins 中找不到合适的 chunk。于是,ptmalloc 首先会遍历 fast bins 中的 chunk,将相邻的 chunk 进行合并,并链接到 unsorted bin 中,然后遍历 unsorted bin 中的 chunk,如果 unsorted bin 只有一个 chunk,并且这个 chunk 在上次分配时被使用过,并且所需分配的 chunk 大小属于 small bins,并且 chunk 的大小大于等于需要分配的大小,这种情况下就直接将该 chunk 进行切割,分配结束,否则将根据 chunk 的空间大小将其放入 small bins 或是 large bins 中,遍历完成后,转入下一步。
8、到了这一步,说明需要分配的是一块大的内存,或者 small bins 和 unsorted bin 中都找不到合适的 chunk,并且 fast bins 和 unsorted bin 中所有的 chunk 都清除干净了。从 large bins 中按照 “smallest-first,best-fit” 原则,找一个合适的 chunk,从中划分一块所需大小的 chunk,并将剩下的部分链接回到 bins 中。若操作成功,则分配结束,否则转到下一步。
9、如果搜索 fast bins 和 bins 都没有找到合适的 chunk,那么就需要操作 top chunk 来进行分配了。判断 top chunk 大小是否满足所需 chunk 的大小,如果是,则从 top chunk 中分出一块来。否则转到下一步。
10、到了这一步,说明 top chunk 也不能满足分配要求,所以,于是就有了两个选择: 如果是主分配区,调用 sbrk(),增加 top chunk 大小;如果是非主分配区,调用 mmap 来分配一个新的 sub-heap,增加 top chunk 大小;或者使用 mmap() 来直接分配。在这里,需要依靠 chunk 的大小来决定到底使用哪种方法。判断所需分配的 chunk 大小是否大于等于 mmap 分配阈值,如果是的话,则转下一步,调用 mmap 分配,否则跳到第12步,增加 top chunk 的大小。
11、使用 mmap 系统调用为程序的内存空间映射一块 chunk_size align 4kB 大小的空间。 然后将内存指针返回给用户。
12、判断是否为第一次调用 malloc,若是主分配区,则需要进行一次初始化工作,分配一块大小为 (chunk_size + 128KB) align 4KB 大小的空间作为初始的heap。若已经初始化过了,主分配区则调用 sbrk() 增加heap空间,分主分配区则在 top chunk 中切割出一个 chunk,使之满足分配需求,并将内存指针返回给用户。

free

被释放的 chunk 不会马上归还给操作系统,而是通过 ptmalloc 的规则来管理这些块,看是与 top chunk 合并,还是是进入 bin 数组。

首先会过一系列的检查机制,都合格的话判断当前 bin 是不是在 fast bin 范围内,在的话就插入到相应 fast bin 头部。

如果不是的话就……(待补充)

在 free chunk 时,还会先检查其前后是否有相邻的 freed chunk,这是通过判断 PREV_INUSE 标志位来实现的。如果有,则从 bin 数组中将该 freed chunk 拿回,这是通过 prev_size 域来寻得前一个 chunk 的,与其进行合并后再继续处理。

malloc_consolidate

有点像内存碎片整理。malloc_consolidate 是 malloc 的一个子函数,用于合并已释放的内存块,以减少内存碎片。

ptmalloc 的机制会在分配 large bin 之前对堆中碎片 bin 进行合并,以便减少堆中的碎片。

realloc

比较特殊的机制,做题时有时会用这个函数的特性去调整栈,使满足 ogg 的使用条件。

用于从 bin 双向链表删除某个空闲块。

fast bins 没有 unlink 机制,它们利用已知的堆块大小或地址来进行分配和释放操作,以避免频繁调用 unlink 操作,这就是为什么漏洞会经常出现在它们身上的原因。

malloc_printerr

在 glibc malloc 时检测到错误的时候,会调用 malloc_printerr 函数。

check*

当时学习的时候遇到一点记一点,肯定是不全的

2.23

  • 将一个 chunk 链入一条 fast bin 链时,会去检查链头指向的 chunk 是否与该 chunk 一致。(old_double_free_check)

  • 众所周知,在进行 malloc 操作时,如果 fast bins、small bins 链中有满足需求的 chunk 可以使用,malloc 就会在相关的 bins 链中寻找可用的 freed chunk 来使用,但是取走一个 freed chunk 是有检测机制的。
    要对 fast bin 链中的 chunk 进行脱链操作时,会去检查该 chunk 的 size 是否属于当前所属的 fast bin 链,并检测该 chunk 的 PREV_INUSE 为是否为1,为1才可以通过检测。我们选取的攻击目标地址的偏移 size 成员数值的 NON_MAIN_ARENA, IS_MAPPED, PREV_INUSE 位都要为1,比如当前 fastbin 链管理的 freed chunk 大小为 0x70,而伪造的 size 成员处的数值为 0x71、0x72 这样的数值不能够符合要求的,但 0x7f 这样的地址就可以满足需求,可以将堆块申请到 malloc_hook - 0x23 的地方。

    如果此时我们没有数值为 0x7f 这样的地址来让我们构造,因为比如要打 free_hook 的话 free_hook 往上的内存数据中也不一定存在 0x7f 的字节数据,有以下几种思路

    1、借助 unsortedbin attack ,利用 unsortedbin attack 向我们的目标地址处写入一个 0x7f... 的数值(见文章:https://blog.csdn.net/qq_41453285/article/details/99329694)

    2、可以通过改写 main_arena 中的 top_chunk 的地址,让 top_chunk 落到 free_hook 上方指定的位置 ,然后不断向 top_chunk 申请 chunk,最终可以分配到包含 free_hook 的堆块,从而可以改写 __free_hook 的值

    3、2.27 后可考虑直接 tcache bin attack

    4、先修改 global_max_fast 为大值,这样向 heap 申请的块都将按照 fastbin 来处理,然后就可以在 free_hook 很上方的位置去寻找 0x7f 来构造。

2.27/2.28

  • 主要是tcache double free,tcache 还完全没有任何检查,只需要free(c1), free(c1)就可以构造一个环出来。

  • unlink 的时候,检查使用 prev_size 字段找到的堆块的 size 是否与 prev_size 一致。(2.28 及以前的版本没有该 check

2.31

2.35

  • 异或 fd + 大随机数key(类似canary

malloc_hook/free_hook

在 malloc/free 函数被调用前执行的函数,即先执行一些特定的操作。

全局变量。通常被用来实现内存泄漏检查、内存跟踪、垃圾回收、安全性检查等功能。

常用宏

指针

mchunkptr 指向当前堆内存块的指针

Top Chunk

程序第一次进行 malloc 的时候,heap 会被分为两块,一块给用户,剩下的那块就是 top chunk。其实,所谓的 top chunk 就是处于当前堆的物理地址最高的 chunk。这个 chunk 不属于任何一个 bin,它的作用在于当所有的 bin 都无法满足用户请求的大小时,如果其大小不小于指定的大小,就进行分配,并将剩下的部分作为新的 top chunk。否则,就对 heap 进行扩展后再进行分配。在 main arena 中通过 sbrk 扩展 heap,而在 thread arena 中通过 mmap 分配新的 heap。

需要注意的是,top chunk 的 prev_inuse 比特位始终为 1,否则其前面的 chunk 就会被合并到 top chunk 中。

last remainder chunk

在用户使用 malloc 请求分配内存时,ptmalloc2 找到的 chunk 可能并不和申请的内存大小一致,这时候就将分割之后的剩余部分称之为 last remainder chunk ,unsorted bin 也会存这一块。top chunk 分割剩下的部分不会作为 last remainer

posted @ 2023-10-20 11:48  ve1kcon  阅读(148)  评论(0编辑  收藏  举报