堆
堆(heap)是一种数据结构,在程序运行的过程中,堆可以提供动态分配的内存,允许程序申请大小未知的内存。
堆是程序虚拟地址空间中的一块连续的线性区域,它由低地址向高地址方向增长(和栈的增长方向相反),管理堆的程序也称为堆管理器。
目前 Linux 标准发行版中使用的堆分配器是 Glibc 中的堆分配器:ptmalloc2
堆的基本操作是分配和回收,ptmalloc2 主要通过 malloc() 和 free() 函数来分配和释放内存块(漏洞的主要产生地)。
原文源自(https://www.uf4te.cn/posts/463ab4ed.html)根据自己理解等做了小部分调整以及补充。
堆的基本操作
malloc(未初始化仅分配)
函数定义:
void *malloc(size_t size)其中:
size是内存块的大小,以字节为单位
malloc() 的作用是分配所需的内存空间(不会对内存空间进行初始化,这点与calloc不同),并返回一个指向它的指针;如果请求失败,则返回 NULL
- 当
size = 0时,返回当前系统允许的堆的最小内存块 - 当
size为负数时,由于在大多数系统上,size_t 是无符号数(这一点非常重要),所以程序就会申请很大的内存空间,但通常来说都会失败,因为系统没有那么多的内存可以分配
以一个简单的例子来看看 malloc() 函数和堆:
1 |
|
通过 GDB 调试可以看到,在执行 malloc() 函数前,程序的地址空间里是没有堆的:
执行 malloc() 函数后:
可见程序中最开始是没有堆这部分空间的,在用户通过 malloc() 申请内存后才会出现,并且会一次性申请很大空间的堆段(0x555555559000 ~ 0x55555557a000)
注意:新版本的 Glibc 对堆结构的管理有些区别,以上图是在 Glibc 2.37 的 Kali Linux 2024.1 中进行的测试
而在 Glibc 2.23 的 Ubuntu 16.04 中是这样的:
calloc(初始化分配)
函数定义:
void *calloc(size_t nitems, size_t size)其中:
nitems为要被分配的元素个数;size为元素的大小
calloc() 在功能上与 malloc() 几乎相同,最大区别在于 calloc() 申请内存空间后会将其全部初始化为 0
与malloc函数相似,使用 calloc() 函数时需要注意,如果分配的内存块过大,可能会导致内存不足的问题
realloc
函数定义:
void *realloc(void *ptr, size_t size)其中:
ptr是一个指向要重新分配内存的内存块的指针;size是内存块的新的大小,以字节为单位
realloc() 的作用是调整之前通过 malloc() 或 calloc() 所分配的 ptr 所指向的内存块的大小,并返回一个指向重新分配大小的内存的指针;如果请求失败,则返回 NULL
- 如果
ptr为空指针,则会分配一个新的内存块,且函数返回一个指向它的指针,相当于malloc() - 如果
size = 0,且ptr指向一个已存在的内存块,则ptr所指向的内存块会被释放,并返回一个空指针,相当于free()
另外,针对重新申请的大小与之前申请内存的大小的关系,又有三种不同的情况:
如果重新申请的大小 > 之前申请内存的大小,且当前内存段后面有需要的内存空间,则直接扩展这段内存空间,
realloc()将返回原指针如果重新申请的大小 > 之前申请内存的大小,且当前内存段后面的空闲空间不够,那么就使用堆中的第一个能够满足这一要求的内存块,将目前的数据复制到新的位置,并将原来的数据块释放掉,返回新的内存块地址,相当于
free() + malloc()**如果 chunk 与 top chunk 相邻,直接扩展这个 chunk 到新 size 大小 **
如果 chunk 与 top chunk 不相邻,相当于 free(ptr),malloc(new_size)
如果重新申请的大小 < 之前申请内存的大小,堆块会直接缩小,被削减的内存会释放,这里的释放与
free()不同- 如果相差不足以容得下一个最小 chunk(64 位下 32 个字节,32 位下 16 个字节),则保持不变
- 如果相差可以容得下一个最小 chunk,则切割原 chunk 为两部分,free 掉后一部分
free
函数定义:
void free(void *ptr)其中:
ptr是一个指向要释放内存的内存块的指针
free() 的作用是释放之前通过 malloc()、calloc() 或 realloc() 所分配的内存空间,该函数不返回任何值
如果传递的参数
ptr是一个空指针,则无任何效果当参数
ptr已经被释放之后,再次释放会出现乱七八糟的效果(Double Free堆漏洞)当释放很大的内存空间时,程序会将这些内存空间还给系统,以便于减小程序所使用的内存空间(被
mallopt禁用的情况下除外)备注: mallopt函数可以控制 内存分配的函数 形如int mallopt(int param,int value)
param 的取值可以为
M_CHECK_ACTION
M_MMAP_MAX
M_MMAP_THRESHOLD
M_MXFAST(从glibc2.3起)
M_PERTURB(从glibc2.4起)
M_TOP_PAD
M_TRIM_THRESHOLD
value是以 字节为单位的。
还是以上面的例子来看,执行 free() 之后堆段并不会消失:
但是堆中的内容发生了变化:
我们申请的空间变成了 Free chunk
注意:新版本的 Glibc 对堆结构的管理有些区别,上图是在 Glibc 2.37 的 Kali Linux 2024.1 中进行的测试
而在 Glibc 2.23 的 Ubuntu 16.04 中是这样的:
[
](https://blog-markdown-1317553172.cos.ap-nanjing.myqcloud.com/CTF - PWN_堆与堆溢出7.png)
通过 free() 释放的堆块不会立刻被回收,它们会变成 Free chunk 并加上了一种 xxx bin 的名字,例如上图 Glibc 2.23 中的 fastbins(fast bin)
通常来说,当堆块释放后,如果与另一个被释放的堆块或者 top chunk 相邻,则这些空间会被合并(但是 fast bin 是个特例,不会轻易合并)
内存分配背后的系统调用
无论是
malloc函数还是free函数,我们动态申请和释放内存时,都经常会使用,但是它们并不是真正与系统交互的函数这些函数背后的系统调用主要是
brk函数以及mmap函数
brk是将 DATA 数据段的最高地址指针_edata往高地址推(_edata指向数据段的最高地址)mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存
brk 和 mmap 这两种方式分配的都是虚拟内存,没有分配物理内存
在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系
malloc小于128k(0x20000字节)的内存时,使用brk分配内存malloc大于等于128k(0x20000字节)的内存时,使用mmap分配内存,在堆和栈之间找一块空闲内存分配
第一次执行 malloc 可能出现的系统调用如下:
[](https://blog-markdown-1317553172.cos.ap-nanjing.myqcloud.com/CTF - PWN_堆与堆溢出19.png)
注意:
brk会直接拓展原来的堆,mmap会单独映射一块内存
mmap分配的内存与 libc 基地址之前存在固定的偏移,因此可以推算出 libc 的基地址
brk
对于堆的操作,操作系统提供了
brk函数,Glibc 库提供了sbrk函数,我们可以通过增加brk的大小来向操作系统申请内存
初始时,堆的起始地址 start_brk 以及堆的当前末尾 brk 指向同一地址。根据是否开启 ASLR,两者的具体位置会有所不同:
- 不开启 ASLR 保护时,
start_brk以及brk会指向 DATA/BSS 段的结尾。 - 开启 ASLR 保护时,
start_brk以及brk也会指向同一位置,只是这个位置是在 DATA/BSS 段结尾后的随机偏移处
[](https://blog-markdown-1317553172.cos.ap-nanjing.myqcloud.com/CTF - PWN_堆与堆溢出18.png)
mmap
malloc会使用mmap来创建独立的匿名映射段。匿名映射的目的主要是可以申请以 0 填充的内存,并且这块内存仅被调用进程所使用
- 在执行
mmap之前,只有.so文件的mmap段 - 执行
mmap之后,我们申请的内存与已经存在的内存段结合在了一起,构成了新的mmap段
堆的结构
微观结构
malloc_chunk
chunk也叫块,在内存中表示的意思就是一块内存,这块内存在ptmalloc2内部用malloc_chunk结构体来表示在程序的执行过程中,我们称由
malloc()申请的内存为chunk,chunk也是堆的最小操作单元参考文章:堆相关数据结构 - CTF Wiki
[](https://blog-markdown-1317553172.cos.ap-nanjing.myqcloud.com/CTF - PWN_堆与堆溢出15.png)
malloc_chunk 的结构体定义如下:
1 | /* |
一些参数的解释:
prev_size- 如果该
chunk的物理相邻的前一地址chunk(两个指针的地址差值为前一个chunk的大小)是空闲的话,那么prev_size记录的是前一个chunk的大小(包括chunk头) - 否则,
prev_size可以用来存储物理相邻的前一个chunk的数据。这里的前一个chunk指的是较低地址的chunk
- 如果该
sizesize表示该chunk的大小,大小必须是2 * SIZE_SZ的整数倍。如果申请的内存大小不是2 * SIZE_SZ的整数倍,会被转换成满足大小的最小的2 * SIZE_SZ的倍数- 32 位系统中,
SIZE_SZ是 4;64 位系统中,SIZE_SZ是 8。 该字段的低三个比特位对chunk的大小没有影响,它们从高到低分别表示 - 一般来说,堆中第一个被分配的内存块的
size字段的P位都会被设置为 1,以便于防止访问前面的非法内存;当一个chunk的size的P位为 0 时,我们能通过prev_size字段来获取上一个chunk的大小以及地址。这也方便进行空闲chunk之间的合并
| 参数 | 意义 | |
|---|---|---|
(A)NON_MAIN_ARENA |
记录当前 chunk 是否不属于主线程,1 表示不属于,0 表示属于 |
|
(M)IS_MAPPED |
记录当前 chunk 是否是由 mmap 分配的 |
|
(P)PREV_INUSE |
记录前一个 chunk 块是否被分配,0 表示空闲,1 表示使用中 |
fd、bkchunk处于分配状态时,从fd字段开始是用户的数据。chunk空闲时,会被添加到对应的空闲管理链表中- 通过
fd和bk可以将空闲的chunk块加入到空闲的chunk块链表进行统一管理
| 参数 | 意义 |
|---|---|
fd |
指向下一个(非物理相邻)空闲的 chunk |
bk |
指向上一个(非物理相邻)空闲的 chunk |
fd_nextsize、bk_nextsize- 只有
chunk空闲的时候才使用,不过其用于较大的chunk(large chunk) - 一般空闲的
large chunk在fd的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适 chunk 时挨个遍历
- 只有
| 参数 | 意义 |
|---|---|
fd_nextsize |
指向前一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针 |
bk_nextsize |
指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针 |
注意:
无论一个 chunk 的大小如何,处于分配状态还是释放状态,它们都使用一个统一的结构
虽然在分配状态和释放状态下,
chunk都是同一个数据结构,但是它们的表现形式是不一样的
chunk处于分配状态(Allocated chunk):

前两个字段称为 chunk header,后面的部分称为 user data
每次 malloc 申请得到的内存指针,其实指向 user data 的起始处
chunk中的空间复用:当一个
chunk处于使用状态时,它的下一个chunk的prev_size域无效,所以下一个chunk的该部分也可以被当前chunk使用
chunk处于释放状态(Freed chunk)(可能是循环双向链表,也可能是单向链表):

如果一个 chunk 处于 free 状态,那么会有两个位置记录其相应的大小:
- 该
chunk本身的size字段会记录 - 该
chunk后面的一个chunk会记录
堆管理器会通过
prev_size字段以及size字段合并两个物理相邻的空闲chunk块
top chunk
程序第一次进行
malloc的时候,heap会被分为两块,一块给用户,剩下的那块就是top chunk,简而言之,**top chunk就是处于当前堆的物理地址最高的chunk**
top chunk不属于任何一个bin,它的作用在于:
- 当所有的
bin都无法满足用户请求的大小时,如果top chunk不小于用户请求的大小,就从top chunk中进行分配,并将剩下的部分作为新的top chunk- 否则,就对
heap进行扩展后再进行分配(在main arena中通过sbrk扩展heap,而在thread arena中通过mmap分配新的heap)
- 初始情况下,可以将
unsorted chunk作为top chunk top chunk的PREV_INUSE位始终为 1(否则其前面的chunk就会被合并到top chunk中)
last remainder chunk
在用户使用
malloc请求分配内存时,ptmalloc2找到的chunk可能并不和申请的内存大小一致,这时候就将分割之后的剩余部分称之为last remainder chunk
unsorted bin也会存这一块top chunk分割剩下的部分不会作为last remainder
宏观结构
arena
无论是主线程还是新创建的线程,在第一次申请内存时,都会有独立的
arena,arena就是用来管理线程中的这些堆的,也可以理解为堆管理器所持有的内存池
- 一个线程只有一个
arnea,并且这些线程的arnea都是独立的不是相同的
但也不是每一个线程都会有对应的 arena,对于不同系统,arena 数量的约束如下:
1 | txtFor 32 bit systems: |
因为每个系统的核数是有限的,当线程数大于核数的二倍(超线程技术)时,就必然有线程处于等待状态,所以没有必要为每个线程分配一个 arena
- 主线程的
arnea称为main_arena,子线程的arnea称为thread_arena
[](https://blog-markdown-1317553172.cos.ap-nanjing.myqcloud.com/CTF - PWN_堆与堆溢出16.png)
- 主线程无论一开始
malloc多少空间,只要size < 128KB,kernel都会分配132KB具有读写权限的heap segment,这部分称为main_arena
[](https://blog-markdown-1317553172.cos.ap-nanjing.myqcloud.com/CTF - PWN_堆与堆溢出17.png)
例如这张图中:
[](https://blog-markdown-1317553172.cos.ap-nanjing.myqcloud.com/CTF - PWN_堆与堆溢出2.png)
1 | heap segment` 地址为 `0x555555559000 ~ 0x55555557a000`,具有 `rw` 权限,总共:`(0x55555557a000 - 0x555555559000)B / 1024 = 132KB |
注意:
main_arena并不在申请的heap中,而是一个全局变量,在libc.so的数据段中
后续申请的内存会一直从这个 arena 中获取,直到空间不足
当 arena 空间不足时,它可以通过增加 brk 的方式来增加堆的空间;类似地,arena 也可以通过减小 brk 来缩小自己的空间
即使将所有 main_arena 所分配出去的内存块 free 完,也不会立即还给 kernel,而是交由 Glibc 来管理。当后面程序再次申请内存时,在 Glibc 中管理的内存充足的情况下,Glibc 就会根据堆分配的算法来给程序分配相应的内存
heap_info
程序刚开始执行时,每个线程是没有
heap区域的。当其申请内存时,就需要heap_info这个结构来记录对应的信息当该
heap的资源被使用完后,就必须得再次申请内存了。此外,一般申请的heap是不连续的,因此需要记录不同heap之间的链接结构
heap_info这个数据结构是专门为从Memory Mapping Segment处申请的内存准备的,即为非主线程准备的- 主线程可以通过
sbrk()函数扩展program break location获得(直到触及Memory Mapping Segment),只有一个heap,没有heap_info数据结构
heap_info 的主要结构如下:
1 |
|
该结构主要是描述堆的基本信息,包括:
- 堆对应的
arena的地址 - 由于一个线程申请一个堆之后,可能会使用完,之后就必须得再次申请。因此,一个线程可能会有多个堆。
prev即记录了上一个heap_info的地址。这里可以看到每个堆的heap_info是通过单向链表进行链接的 size表示当前堆的大小pad确保分配的空间是按照MALLOC_ALIGN_MASK + 1对齐的
malloc_state
malloc_state结构用于管理堆,记录每个arena当前申请的内存的具体状态,例如:是否有空闲chunk,空闲chunk的大小等等
- 无论是
thread_arena还是main_arena,它们都只有一个malloc state结构 - 由于
thread的arena可能有多个,malloc state结构会在最新申请的arena中
malloc_state 的结构如下:
1 | struct malloc_state { |
libc_lock_define(, mutex)
该变量用于控制程序串行访问同一个分配区,当一个线程获取了分配区之后,其它线程要想访问该分配区,就必须等待该线程分配完成后才能够使用。flagsflags记录了分配区的一些标志,比如bit0记录了分配区是否有fast bin chunk,bit1标识分配区是否能返回连续的虚拟地址空间。具体如下:
1 | /* |
fastbinsY[NFASTBINS]
存放每个fast chunk链表头部的指针top
指向分配区的top chunklast_reminder
最新的chunk分割之后剩下的那部分bins
用于存储unstored bin,small bin和large bin的chunk链表binmapptmalloc2用 1 个bit来标识某一个bin中是否包含空闲chunk
注意:
main_arena的malloc_state并不是heap segment的一部分,而是一个全局变量,存储在libc.so的数据段
bin 的种类
Glibc 为了让
malloc可以更快找到合适大小的chunk,用户free释放掉的chunk不会马上归还给系统,而是将该chunk根据大小加入到合适的bin中当用户再一次通过
malloc请求分配内存时,ptmalloc2会试图在空闲的chunk中挑选一块合适的空间给用户,这样可以避免频繁的系统调用,降低内存分配的开销
bin的中文意思为垃圾桶,就像要删除的文件会先放入 Windows 的回收站一样不会立即删除,很生动形象了
ptmalloc2 会根据空闲的 chunk 的大小以及使用状态,将 chunk 初步放入相应的 bin 中,bin 的种类主要分为:
fast binsmall binlarge binunsorted bintcache
Glibc 提供了两个数组:fastbinsY[] 和 bins[] 用来存放这些 bin
具体来说,可分为:
- 10 个
fast bin,存储在fastbinsY[]中 - 1 个
unsorted bin,存储在bins[1]中 - 62 个
small bin,存储在bins[2]至bins[63]中 - 63 个
large bin,存储在bins[64]至bins[126]中
其中虽然定义了 bins[128],但是 bins[0] 和 bins[127] 其实是不存在的
chunk 在 bin 上以链表的形式存放:(fast bin 是单链表,其他的 bin 都是双链表)
[](https://blog-markdown-1317553172.cos.ap-nanjing.myqcloud.com/CTF - PWN_堆与堆溢出10.png)
fast bin
fast bin非常像高速缓存 cache,为了减少一些较小的chunk在合并、分割以及中间检查的过程中的开销,ptmalloc2中专门设计了fast bin,对应的变量就是malloc state中的fastbinsY[]数组,用于提高小内存分配效率
fast bin存储在fastbinsY[]处,是 10 个单链表(最后 3 个链表保留未使用)对于高速缓存(Cache):
它是一种用于存储和快速访问数据的硬件或软件组件。它的主要目的是提高数据的访问速度,减少延迟。以下是高速缓存的一些关键概念和功能:
原理:高速缓存利用局部性原理,即程序在访问某些数据后,很可能会在不久的将来再次访问相同的数据。基于这一原理,缓存存储了近期使用的数据。
时间局部性(Temporal Locality):
当某个数据被访问后,近期内再次访问该数据的可能性很高。比如,在循环结构中,程序可能会多次访问同一块数据。因此,缓存会保留近期使用过的数据,以便快速响应后续的访问请求。
空间局部性(Spatial Locality):
当某个数据被访问时,临近地址的数据在不久的将来也可能会被访问。例如,在数组或连续内存区域中,访问一个元素后,往往会访问它周围的元素。因此,缓存通常会预取一块数据,而不仅仅是单个数据项,从而提高数据访问的效率。
例子
时间局部性:在一个简单的循环中,访问同一个数组的元素多次。
空间局部性:访问数组时,程序会依次访问相邻的元素。
高速缓存的设计
基于局部性原理,高速缓存的设计考虑了以下几个方面:
缓存行(Cache Line):缓存通常以固定大小的数据块存储,常见的大小是32字节或64字节,这样可以有效利用空间局部性。
预取机制:一些高级的缓存系统会根据访问模式预测将来可能需要的数据,并提前将其加载到缓存中,以减少访问延迟。
替换策略:如前所述,使用LRU或LFU等策略,确保缓存中保留高概率再次使用的数据。
通过利用局部性原理,高速缓存能够显著提高系统性能,降低内存访问的延迟。
类型:
指令缓存:存储CPU执行的指令。
数据缓存:存储数据处理所需的数据。
fast bin的chunk大小(含chunk头部)为:16 ~ 64字节(64 位为32 ~ 128字节)- 相邻
bin存放的大小相差 8 字节(64 位为 16 字节) - 采取
LIFO策略(最近释放的chunk会更早地被分配) chunk的PREV_INUSE位(下一个物理相邻的chunk的P位)总为 1,释放到fastbin的chunk不会被清除PREV_INUSE标志位
[](https://blog-markdown-1317553172.cos.ap-nanjing.myqcloud.com/CTF - PWN_堆与堆溢出11.png)
如果遇到以下两种情况,ptmalloc2 会首先判断 fast bin 中相应的 bin 中是否有对应大小的空闲块,如果有的话,就会直接从这个 bin 中获取 chunk;如果没有的话,ptmalloc2 才会做接下来的一系列操作:
- 在 32 位系统中(
SIZE_SZ = 4),用户需要的chunk大小 < 64 字节 - 在 64 位系统中(
SIZE_SZ = 8),用户需要的chunk大小 < 128 字节
关于 fast bin 的大小定义如下:
1 |
|
在 32 位系统中,fast bin 默认支持最大的 chunk 的数据空间大小为 64 字节:
1 | DEFAULT_MXFAST = (64 * 4 / 4) = 64 |
但是其可以支持的 chunk 的数据空间最大为 80 字节:
1 | MAX_FAST_SIZE = (80 * 4 / 4) = 80 |
fast bin 最多可以支持的 bin 的个数为 10 个,在 32 位系统中,用户数据空间从第 8 字节开始一直到第 80 字节(不包括 prev_size 和 size 字段的 8 字节)
注意:
fast bin 中的
chunk的PREV_INUSE位(下一个物理相邻的 chunk 的 P 位)始终被置为 1,因此它们不会和其它被释放的chunk合并,这也是为什么前面说 fast bin 是个特例,不会轻易合并但是,当释放的
chunk与该chunk相邻的空闲chunk合并后的大小 >FASTBIN_CONSOLIDATION_THRESHOLD时,说明内存碎片较多,此时就需要把fast bin中的chunk都进行合并,以减少内存碎片对系统的影响
unsorted bin
unsorted bin非常像缓冲区 buffer,可以视为空闲chunk回归其所属bin之前的缓冲区大小超过
fast bin阈值的chunk被释放时会加入到这里,这使得ptmalloc2可以复用最近释放的chunk,从而提升效率
unsorted bin处于bins[1]处,因此unsorted bin只有 1 个双向循环链表unsorted bin中的空闲chunk处于乱序状态- **
unsorted bin在使用的过程中,采用的遍历顺序是FIFO**(插入的时候插入到 unsorted bin 的头部,取出的时候从链表尾获取) - 在
malloc分配时,如果在fast bin、small bin中找不到对应大小的chunk,就会尝试从unsorted bin中寻找chunk。如果取出来的chunk大小刚好满足,就会直接返回给用户;如果在unsorted bin中没有合适的chunk,就会把unsorted bin中的所有chunk分别加入到所属的bin中,然后再在bin中分配合适的chunk
[](https://blog-markdown-1317553172.cos.ap-nanjing.myqcloud.com/CTF - PWN_堆与堆溢出14.png)
当
free的chunk大小 >= 144 字节时,为了效率,Glibc 并不会马上将chunk放到相对应的bin中,而会先放到unsorted bin下次
malloc时会先查找unsorted bin中是否有合适的chunk,找不到才会去对应的bin中寻找,此时会顺便把unsorted bin的chunk放到对应的bin中,但small bin除外,为了效率,反⽽先从small bin找
small bin
1 chunk size` 小于 `0x200` 字节(64 位为 `0x400` 字节)的 `chunk` 叫做 `small chunk`,而 `small bin` 存放的就是这些 `small chunk
small bin存储在bins[2]至bins[63]处,是 62 个双向循环链表(每个链表都有链表头结点,这样可以方便对于链表内部结点的管理)fast bin的chunk大小(含chunk头部)为:16 ~ 496字节(64 位为32 ~ 1008字节)- 相邻
bin存放的大小相差 8 字节(64 位为 16 字节) - 每个链表中存储的
chunk大小都一致 - 采取
FIFO策略(最近释放的chunk会被最后分配),这点和fast bin相反 - 同样与
fast bin相反的是:相邻的空闲chunk会被合并
[](https://blog-markdown-1317553172.cos.ap-nanjing.myqcloud.com/CTF - PWN_堆与堆溢出12.png)
small bin 中每个 chunk 的大小与其所在的 bin 的 index 的关系为:
1 | chunk_size = 2 * SIZE_SZ * index |
small bin 的大小再分成 62 个 bin,大小从 16 字节(64 位为 32 字节)开始,每次固定增加 8 字节(64 位为 16 字节):
| 下标 index | SIZE_SZ=4(32 位) | SIZE_SZ=8(64 位) |
|---|---|---|
| 2 | 16 | 32 |
| 3 | 24 | 48 |
| 4 | 32 | 64 |
| 5 | 40 | 80 |
x |
2 * 4 * x |
2 * 8 * x |
| 63 | 504 | 1008 |
注意:
fast bin中的chunk是有可能被放到small bin中去的
large bin
1 large bin` 存放的是大于等于 `0x200` 字节(64 位为 `0x400` 字节)的 `chunk
large bin存储在bins[64]至bins[126]处,是 63 个双向循环链表- 每个 bin 中的 chunk 的大小不一致(按大小降序排列)
- 采取
FIFO策略 - 插入和删除可以发生在任意位置
- 相邻空闲
chunk会被合并
1 | large bin` 的 `freed chunk` 会多两个指针 `fd_nextsize`、`bk_nextsize`,分别指向前一块和后一块 `large chunk |
[](https://blog-markdown-1317553172.cos.ap-nanjing.myqcloud.com/CTF - PWN_堆与堆溢出13.png)
large bin 的大小再分成 63 个 bin,但大小不再是固定大小增加,而是按照公差分为 6 组:
| 组 | bin 的数量 | 公差 |
|---|---|---|
| 1 | 32 | 0x40 |
| 2 | 16 | 0x200 |
| 3 | 8 | 0x1000 |
| 4 | 4 | 0x8000 |
| 5 | 2 | 0x40000 |
| 6 | 1 | 不限制,大小和 large bin 剩余的大小相同 |
tcache
tcache是 libc2.26(Ubuntu 17.10)之后引进的一种新机制,类似于fast bin一样的东西,目的是提升堆管理的性能,但提升性能的同时舍弃了很多安全检查,也因此有了很多新的利用方式
- 每条链上最多可以有 7 个
chunk malloc的时候优先去tcache找free的时候当tcache满了才放入fastbin或unsorted bin
基本工作方式:
malloc时,会先malloc一块内存用来存放tcache_perthread_structfree内存,且size小于small bin size时- 先放到对应的
tcache中,直到tcache被填满(默认是 7 个) tcache被填满之后,再次free的内存和之前一样被放到fast bin或者unsorted bin中tcache中的chunk不会合并(不取消PREV_INUSE位)
- 先放到对应的
malloc内存,且size在tcache范围内- 先从
tcache取chunk,直到tcache为空 tcache为空后,从bin中找tcache为空时,如果fast bin、small bin、unsorted bin中有size符合的chunk,会先把fast bin、small bin、unsorted bin中的chunk放到tcache中,直到填满;之后再从tcache中取;因此chunk在bin中和tcache中的顺序会反过来
- 先从