堆深入学习记录1 | 青训营

191 阅读11分钟

此篇文章是我参与 #青训营笔记创作活动 的第7篇笔记

主要是简单记录一下自己在深入学习堆时遇到的一些小要点,

只是个人的一个学习记录,水平不高还望大佬们包容。

堆的概念

在程序运行过程中,堆可以提供动态分配的内存,允许程序申请大小未知的内存。堆其实就是程序虚拟地址空间的一块连续的线性区域,它由低地址向高地址方向增长。我们一般称管理堆的那部分程序为堆管理器。(from wiki


堆的实现

  • dlmalloc : Genral purpose allocator

  • jemalloc : Freebsd and Firefox

  • tcmalloc : Google

  • libumen : Solaris

  • ptmalloc2 : glibc

    • 堆的实现以ptmalloc2中堆的实现为主

    • 在 glibc-2.3.x. 之后,glibc 中集成了ptmalloc2,可以下载glibc源码查看ptmalloc

    • Index of /gnu/glibc

    • 查看glibc版本

      • # shell:
        ldd --version
        # 输出:
        ldd (Ubuntu GLIBC 2.35-0ubuntu3.1) 2.35
        
      • image-20230801184545837

堆管理器

堆管理器位于程序与内核之间,主要做两件事:

  • 响应用户申请内存的请求 (请求堆

    • 为了保持内存管理的高效性,内核一般都会预先分配很大的一块连续的内存(虚拟内存),即 top _chunk

      • 只有当真正访问一个地址的时候,系统才会在虚拟内存和物理页面的映射关系。
      • 所以这里的内存其实只是虚拟内存。只有当用户使用到相应的内存时,系统才会真正分配物理内存给用户使用。
    • 只有当出现了堆空间不足的情况,堆管理器才会再次与操作系统进行交互

      • 新创建一个top_chunk,并且把原来的top_chunk分配到unsorted_bin 里面
  • 管理用户所释放的内存 (释放堆

    • 用户释放的内存并不是直接返还给操作系统,而是由堆管理器进行管理,除了fastbin以外大都由bins来管理
    • 这些释放的内存可以用来响应用户新申请的内存的请求。

系统所调用的函数

malloc和free在动态申请或释放内存时,主要是调用(s)brk和mmap,unmmap函数实现的。

  1. (s)brk函数机制
# include <stdio.h>
# include <unistd.h>
# icclude <sys/types.h>
int main()
{
 void *cuur_bkr,*tmp_brk = NULL;
 printf("%d\n",getid());
 tm_brk = curr_brk = sbrk(0);//给当前程序一个brk
 printf("%p\n",curr_brk);
 getchar();
brk(curr_brk+4096);//设置结尾位置,即分配了4096字节的堆块
 curr_brk=sbrk(0);
 printf("%p\n",curr_brk);
 getchar();
 brk(tmp_brk);
 curr_brk=sbrk(0);
 printf("%p\n",curr_brk);
 getchar();
 return 0;
}
  • 初始时,堆的起始地址start_brk以及堆的当前末尾brk指向同一地址。根据是否开启ALSR,两者的具体位置会有所不同。

    • 不开启ASMR时,start_brk以及brk会指向data/bss段的结尾。
    • 开启ASMR时,start_brk以及brk也会指向同一位置,只是这个位置是在data/bss段结尾后的随机偏移处。
    • sbrk创建的chunk紧邻数据段
  1. mmap函数机制
  • malloc会使用mmap来创建独立的匿名映射段。
  • 匿名映射的目的主要是可以申请以0填充的内存,并且这块内存仅被调用进程所使用,这块内存为系统随机分配。
  • munmap用于释放内存。
  • mmap创建的chunk紧邻libc

多线程支持

  • 在原来的dlmalloc实现中,当两个线程同时要申请内存时,只有一个线程可以进入临界区申请内存,而另外一个线程必须等待直到临界区中不再有线程。
  • 这是因为所有的线程共享一个堆。
  • 在glibc和ptmalloc实现中,支持了多线程的快速访问,在新的实现中,所有的线程共享多个堆。

详见 wiki


堆的微观结构

先补充两句,个人认为从总体上看可以把堆的数据结构分为微观结构和宏观结构:

  • 微观结构主要是malloc_chunk的数据与结构:

    • prev_size
    • size
    • fd,bk
    • fd_nextsize, bk_nextsize
  • 宏观结构包括堆块的宏观信息,可以使我们更好地了解glibc的堆管理实现,宏观结构分为以下几个部分:

    • arena & main_arena:可以看作是堆块的管理器,但其实就是堆内存本身

    • bin:用链表结构管理被free的malloc_chunk(堆块)

    • top_chunk:处于一个arena的最顶部(即最高内存地址处)、不属于任何bin的一片内存

    • malloc_state:管理 arena 的核心结构,包含堆的状态信息、bins 链表等

      • main arena 对应的 malloc state 结构存储在 glibc 全局变量中
      • 其他线程 arena 对应的 malloc_state 存储在 arena 本身中

SIZE_SZ 与 size_t

先补充一个小知识,关于堆中结构我们常常会看到SIZE_SZsize_t,但实际上,在一般场景里他们两个的值是一样的,即在32位系统中是8字节,在64位系统中是16字节。

下载glibc源码查看ptmalloc可以看到:

#ifndef INTERNAL_SIZE_T
#define INTERNAL_SIZE_T size_t
#endif
/* The corresponding word size. */
#define SIZE_SZ (sizeof (INTERNAL_SIZE_T))
  • INTERNAL_SIZE_T:size_t,为32/64位整数(8字节/16字节)
  • SIZE_SZ:同size_t,8字节/16字节

malloc_chunk

struct malloc_chunk {
​
  INTERNAL_SIZE_T      prev_size;  /* 如果前面一个堆块是空闲的则表示前一个堆块的大小,否则无意义  */
  INTERNAL_SIZE_T      size;       /* 当前chunk的大小,由于对齐的原因所以低三位作为flag*//*######################
 1.真正的内存从这里开始分配
 2.malloc之后这些指针没有用,这时存放的是数据
 3.只有在free之后才有效。
########################*/
    
  struct malloc_chunk* fd;         /* 当chunk空闲时才有意义,记录后一个空闲chunk的地址 */
  struct malloc_chunk* bk; /* 同上,记录前一个空闲chunk的地址  */
    
  /* Only used for large blocks: pointer to next larger size. */
  struct malloc_chunk* fd_nextsize; /*当前chunk为largebin时才有意义,指向比当前chunk大的第一个空闲chunk */
  struct malloc_chunk* bk_nextsize;/*指向比当前chunk小的第一个空闲堆块*/
};

每个字段的具体的解释如下(from wiki and zhihu



prev_size

  • 如果该 chunk 的物理相邻的前一地址 chunk(两个指针的地址差值为前一 chunk 大小)是空闲的话,那该字段记录的是前一个 chunk 的大小 (包括 chunk 头)。

  • 否则,该字段可以用来存储物理相邻的前一个 chunk 的数据。这里的前一 chunk 指的是较低地址的 chunk

    • 当申请的内存大小对 2*SIZE_SZ 取余之后小于等于 size_t 的话就可以用它的下一个 chunk 的 prev_size
    • 比如:64位下 malloc(0x55), 0x58 mod 0x10 还差 0x8 <= 0x8字节,那他就可以用后面一个 chunk 的 prev_size,最后加上 chunk header 大小是 0x60(所以下图为6行)
    • 还是 64 位下,如果大小是 0x59 的话 mod 0x10之后还差 0x9 > 0x8字节,那就不够用了,只能多申请一块,最后加上 chunk header 用了 0x70(所以下图为7行)
    • image-20230807000723376.png
  • malloc(0x58)会分配0x60的内存

  • malloc(0x59)会分配0x70的内存


size

  • 该 chunk 的大小,大小必须是 2 * SIZE_SZ 的整数倍。

    • SIZE_SZ 在32位系统下是 32位 即4个字节大小,在64位系统下是 64位 即8个字节大小

    • 所以64位chunk的size必须是16字节对齐,32位chunk的size必须是8 字节对齐

    • 64位 低4位没用 11110000

    • 32位 低3位没用 11111000

    • 关于16字节对齐:

      • 1字节为8位,即2个16进制位(如 0xff ),所以16字节为32个16进制位
      • 在gdb中体现为 0x0011223344556677 0x8899aabbccddeeff 为16字节
  • NON_MAIN_ARENA,A:倒数第三位表示当前chunk属于主分配区(0)还是非主分配区(1)

  • IS_MAPPED,M:倒数第二位表示当前chunk是从mmap(1)[多线程]分配的,还是从brk(0)[子线程]分配的

  • PREV_INUSE, P:最低位表示前一个 chunk 块是否被分配。

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

一些基础的攻击思路:

  • chunk1的数据有效区域覆盖到chunk2的prev_size位,并且chunk2的size位的prev_inuse被覆盖为0。系统认为chunk2之前的chunk1已经未在使用了。
  • 当free(chunk2)的时候,系统会将chunk2与chunk2中prev_size大小的空间合并到bins。
  • 我们可以通过改变chunk2的prev_size的内容,操纵向前合并的大小。
  • 造成的问题:overlap(堆块重叠),chunk1被释放了,但是我们可以操纵修改它(堆利用的核心思想),从而修改bins链的内容,泄露其中的地址。
  • 形成的攻击:fastbin ---> fd ---> main_arena ---> 分配新的堆块,我们通过修改chunk1的fd内容,达到分配任意内存的目的,造成fastbin attack
  • 最小堆原则 : malloc(0)会分配0x20的空间,prev_size + size + 数据对齐的0x10字节

fd,bk

  • chunk 处于分配状态时,从 fd 字段开始是用户的数据。chunk 空闲时,会被添加到对应的空闲管理链表中,其字段的含义如下
  • fd 指向下一个(非物理相邻)空闲的 chunk
  • bk 指向上一个(非物理相邻)空闲的 chunk
  • 通过 fd 和 bk 可以将空闲的 chunk 块加入到空闲的 chunk 块链表进行统一管理

fd_nextsize, bk_nextsize

  • 也是只有 chunk 空闲的时候才使用,不过其用于较大的 chunk(large chunk)。
  • fd_nextsize 指向前一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
  • bk_nextsize 指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
  • 一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适 chunk 时挨个遍历。

一些细节

一个已经分配的 chunk 的样子如下。我们称前两个字段称为 chunk header,后面的部分称为 user data。每次 malloc 申请得到的内存指针,其实指向 user data 的起始处。

当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size 域无效,所以下一个 chunk 的该部分也可以被当前 chunk 使用。这就是 chunk 中的空间复用

# 使用中的chunk结构
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of previous chunk, if unallocated (P clear)  | # 前一个堆未被使用时可以知道其大小
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of chunk, in bytes                     |A|M|P| # 重点在P位
  mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             User data starts here...                          .
        .                                                               . # 没有fd和bk指针
        .             (malloc_usable_size() bytes)                      .
next    .                                                               |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             (size of chunk, but used for application data)    | # 由于本chunk正在使用,
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ # 所以占用了下一个chunk的prev_size位
        |             Size of next chunk, in bytes                |A|0|1| # 下一个chunk的size位
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        
# 未使用的chunk结构
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of previous chunk, if unallocated (P clear)  |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`head:' |             Size of chunk, in bytes                     |A|0|P|
  mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Forward pointer to next chunk in list             | # fd指针(chunk块链表的下一个空闲块)
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Back pointer to previous chunk in list            | # bk指针
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Unused space (may be 0 bytes long)                . # 空内存,不被使用
        .                                                               . # 不过值并不一定为0
 next   .                                                               |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`foot:' |             Size of chunk, in bytes                           | # 上一个chunk的大小
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of next chunk, in bytes                |A|0|0| # P位 置0
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • 当我们向malloc申请一片内存区域时,这块内存区域就会在内存管理器 ptmalloc 以malloc_chunk结构体来表示,而且当我们释放这个堆块时,这个堆的数据结构依然是同一个,只是表现形式不同。

  • malloc 函数返回的是一个指针,一个对应大小字节的内存块的指针。

  • 关于n的一些异常情况:

    • 当 n=0 时,返回当前系统允许的堆的最小内存块。(一般分配0x20的空间,prev_size + size + 数据对齐的0x10字节)
    • 当 n 为负数时,由于在大多数系统上,size_t 是无符号数(这一点很重要) ,所以程序就会申请很大的内存空间,但通常来说都会失败,因为系统没有那么多的内存可以分配。
  • 如果一个 chunk 处于 free 状态,那么会有两个位置记录其相应的大小

    • 该chunk自身的 size 字段
    • 下一个chunk的prev_size字段

附录

学习与参考链接:

堆相关数据结构 - CTF Wiki (ctf-wiki.org)

二进制安全之堆溢出(系列)——堆基础 & 结构(一)

二进制安全之堆溢出(系列)——堆基础 & 结构(二)

二进制安全之堆溢出(系列)——堆基础 & 结构(三)

二进制安全之堆溢出(系列)——堆基础 & 结构(四)

堆相关知识 (yuque.com)

malloc内存管理总结_g_malloc是怎么进行内存管理的