pwn学习笔记(6)--堆基础概述
glibc堆概述:
1.内存管理与堆:
概述:
内存管理是堆计算机的内存资源进行管理,这要求在程序请求时能够动态分配内存的一部分,并在程序不需要时释放分配的内存。CTF竞赛中常见的ptmalloc2就是glibc实现的内存管理机制,它继承了dlmalloc,并提供了对多线程的支持。
堆是程序虚拟内存中由低地址向高地址增长的线性区域。一般只有当用户向操作系统申请内存时,这片区域才会被内核分配出来,并且处于效率和页对齐的考虑,通常会分配相当大的连续内存。程序再次申请时便会从这片内存中分配,直到堆空间不能满足时才会再次增长。堆的位置一般在BSS段高地址处。
brk()和sbrk():
堆的属性是可读可写的,大小通过brk()和sbrk()函数进行控制。在堆未初始化时,program_break指向BSS段的末尾,通过调用brk()和sbrk()来移动program_break使得堆增长。在堆初始化时,如果开启了ASLR,则堆的起始地址start_brk会在BSS段之后的随机位移出,如果没有开启,则start_brk会紧接着BSS段。
两个函数相关内容如下:
1 |
|
brk()函数的参数是一个指针,用于设置program_break指向的位置。sbrk()函数的参数increment(可以是负值)用于与program_break相加来调整program_break的值。成功执行后brk()函数会返回 0 ,sbrk()函数会返回上一次program_break值(可以设置参数increment为0来获得当前program_break的值)。
mmap()和unmmap():
当用户申请内存过大时,ptmalloc2会选择通过mmap()函数创建匿名映射段供用户使用,并通过unmmap()函数回收。
glibc中的堆:
通常来说,系统中的堆指的是主线程中main_arena所管理的区域。但glibc会同时维持多个区域来供多线程使用,每个线程都有属于自己的内存(成为arena),这些连续的内存也可以成为堆。
glibc的想法是:当用户申请堆块时,从堆中按顺序分配堆块交给用户,用户保存指向这些堆块的指针;当用户释放堆块时,glibc会将释放的堆块组织成链表;当两块相堆块都为释放状态时将之合并成一个新的堆块;由此解决内存碎片的问题。用户正在使用中的堆块叫作allocated chunk,被释放的堆块叫做free chunk,由free chunk组成的链表叫bin。我们称呼当前chunk低地址处相邻的chunk为上一个(后面的)chunk,高地址相邻处的chunk为下一个(前面的)chunk。为了方便管理,glibc 将不同大小范围的chunk组织成不同的bin 。如fast bin 、small bin 、large bin 等,在这些链表中的chunk分别叫作fast chunk 、small chunk 和large chunk 。
2.重要概念和结构体:
arena:
arena 包含一片或数片连续的内存,堆块将会从这片区域划分给用户。主线程的 arena 被称为 main_arena,它包含 start_brk 和 brk 之间的这片连续内存。
主线程的 arena 只有堆,子线程的 arena 可以有数片连续内存。如果主线程的堆大小不够分的话可以通过brk()调用来扩展,但是子线程分配的映射段大小是固定的,不可以扩展,所以子线程分配出来的一段映射段不够用的话就需要再次使用mmap()来分配新的内存。
heap_info结构体:
如之前所说,子线程的 arena 可以有多片连续内存,这些内存被称为heap。每一个 heap 都有自己的heap header 。其定义如下,heap header 是通过链表相连接的,并且 heap header 里面保存了指向其他所属的 arena 的指针。
1 | typedef struct _heap_info |
malloc_state结构体:
每个线程只有一个 arena header,里面保存了bin、top chunk 等信息。主线程的 main_arena 保存在libc.so 的数据段里,其他线程的arena则保存在给该 arena 分配的heap里面。malloc_state定义如下:
1 | struct malloc_state |
malloc_chunk结构体:
chunk是glibc管理内存的基本单位,整个堆在初始化后会被当成一个free chunk,称之为top chunk ,每次用户请求内存时,如果bins 中没有合适的 chunk ,malloc就会从 top chunk 中进行划分,如果 top chunk 的大小不够,则调用brk()扩展堆的大小,然后从新生成的 top chunk 中进行切分。用户释放内存时,glibc 会先根据情况将释放的 chunk 与其他相邻的 free chunk 合并,然后加入合适的bin中。
下图展示了堆块申请和释放的过程。首先,用户连续申请了三个堆块 A、B、C,此时释放 chunk B ,由于它与 top chunk 不相邻,所以会被放入bin中,成为一个 free chunk。现在再次申请一个与B相同大小的堆块,则 malloc 将从 bin 中取出 chunk B ,回到一开始的状态,bin 的表头也会指向null。。但如果用户连续释放 chunk A 和 chunk B ,由于他们相邻且都是 free chunk ,那么就会被合并成一个大的chunk放入bin中。
对以上知识有了简单的印象之后,就可以看看chunk的信息是怎么被glibc记录的,下面是malloc chunk的结构体定义:
1 | struct malloc_chunk { |
在默认情况下,INTERNAL_SIZE_T 的大小在64位系统下是8字节,32位系统下是4字节。以下是关于malloc_chunk的各个成员的功能:
prev_size, 如果该 chunk 的物理相邻的前一地址 chunk(两个指针的地址差值为前一 chunk 大小)是空闲的话,那该字段记录的是前一个 chunk 的大小 (包括 chunk 头)。否则,该字段可以用来存储物理相邻的前一个 chunk 的数据。这里的前一 chunk 指的是较低地址的 chunk 。
size
,该 chunk 的大小,大小必须是 2 * SIZE_SZ 的整数倍。如果申请的内存大小不是 2 * SIZE_SZ 的整数倍,会被转换满足大小的最小的 2 * SIZE_SZ 的倍数。32 位系统中,SIZE_SZ 是 4;64 位系统中,SIZE_SZ 是 8。 该字段的低三个比特位对 chunk 的大小没有影响,它们从高到低分别表示
- NON_MAIN_ARENA,记录当前 chunk 是否不属于主线程,1 表示不属于,0 表示属于。
- IS_MAPPED,记录当前 chunk 是否是由 mmap 分配的。
- PREV_INUSE,记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲 chunk 之间的合并。
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 的起始处,也就是mem处的地址。
当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size 域无效,所以下一个 chunk 的该部分也可以被当前 chunk 使用。这就是 chunk 中的空间复用。
1 | chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
被释放的 chunk 被记录在链表中(可能是循环双向链表,也可能是单向链表)。具体结构如下:
1 | chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
3.各类bin介绍:
chunk 被释放时,glibc 会将它们重新组织起来,构成不同的 bin 链表,当用户再次申请时,就从中寻找合适的 chunk 返回用户。不同大小区间 chunk 被划分到不同的 bin 中,再加上一种特殊的 bin,一共有四种:Fast bin 、Samll bin 、 Large bin 、和 Unsorted bin 。这些bin记录在 malloc_state 结构中。
- fastbinsY :这是一个 bin 数组,里面有 NFASTBINS 个 fast bin。
- bins:也是一个 bin 数组,一共有126个 bin ,按顺序分别是:
- bin 1 为unsorted bin
- bin 2到 bin 63 为 small bin
- bin 64到 bin 126 为large bin
fast bin :
在实践中,程序申请和释放的堆块往往都比较小,所以glibc对这类 bin使用单链表结构,并采用LIFO(后进先出)的分配策略。为了加快速度,fast bin 里的 chunk 不会进行合并操作,所以下一个 chunk 的 PRV_INUSE 始终标记为1,使其处于使用状态。同一个 fast bin 里 chunk 大小相同,并且在 fastbinY 数组里按照从小到大的顺序排列,序号为 0 的fast bin 中容纳的 chunk 大小为 4SIZE_SZ 字节,随着序号增加,所容纳的 chunk 递增 2SIZE_SZ 字节。
unsorted bin:
一定大小的 chunk 被释放时,在进入 small bin 或者 large bin 之前,会先加入 unsorted bin 。在实践中,一个释放的 chunk 常常很快就会被重新使用,所以将其先加入 unsorted bin 可以加快分配的速度。 unsorted bin 使用双链表结构,并采用FIFO(先进先出)的分配策略。与fastbinY 不同,unsorted bin 中的 chunk 大小可能是不同的,并且由于是双向链表结构,一个 bin 会占用 bins 的两个元素。
small bin:
同一个 small bin 里 chunk 的大小相同,采用双链表结构,使用频率介于 fast bin 和 large bin 之间。small bin 在 bins 里居第2到63位,共62个。根据排序,每个 small bin 的大小为2SIZE_SZinx(idx表示bins数组的下标)。在64位系统下,最小的 small chunk 为2*8*2=32 字节,最大的 small chunk 为 2*8*63=1008 字节。由于 small bin 和 fast bin 有重合的部分,所以这些 chunk 在某些情况下会被加入 small bin 中。
large bin:
large bin 在 bins 里居 64 到 126 位,共 63 个,被分成了 6 组,每组 bin 所能容纳的chunk按顺序排成等差数列,公差分别如下:
1 | 32 bins of size 64 |
32位系统下第一个 large bin 的 chunk 最小为512字节,第二个large bin的额 chunk 最小为 512+64字节(处于[512,512+64]之间的chunk都属于第一个large bin),以此类推。64位系统也是一样的,第一个 large bin 的chunk最小为1024字节,第二个large bin 的chunk 最小为 1024+64字节(处于[1024,1024+64]之间的chunk都属于第一个large bin)以此类推。
large bin 也是采用双链表结构,里面的chunk 从头节点的fd指针开始,按大小顺序进行排列。为了加快检索速度,fd_nextsize 和 bk_nextsize 指针用于指向第一个与自己大小不同的chunk时,所以也只有在加入了大小不同的chunk时,这两个指针才会被修改。
参考资料:
《CTF竞赛权威指南 pwn 篇》