malloc 中的 consolidate
在 malloc 的流程中有几处调用到了这个函数,它的主要作用是遍历 fastbin 链表,将其中的堆块进行合并,插入到 unsorted bin 当中。
Update: 2020-11-02
1 |
|
consolidate 的逻辑还算简单,主要是对 fastbin 进行操作,首先要判断分配区是否已经初始化,如果是,会逐个 fastbin 链表进行遍历,如果链表为空,就转移到下一个链表,如果不空,就依次取出其中的 chunk,首先判断是否能够进行后向合并,然后判断是否能进行前向合并,最后判断能否与 topchunk 合并,在合并的过程中,要考虑到合并出来的堆块是 smallbin 还是 largebin,以便于用不同的方式插入 unsortedbin。参考下图
sYSMALLOc
这个函数会在所有缓冲区和 topchunk 都不能满足分配要求时调用,主要功能是向操作系统申请新的内存。
1 |
|
首先是各种变量的定义,它们的功能在注释里面写的比较清楚。
1 |
|
接着判断一下 HAVE_MMAP 宏定义是否为 1,引用 malloc.c 的注释如下
1 | Define HAVE_MMAP as true to optionally make malloc() use mmap() to |
选项是可以自定义的,这是因为有些操作系统并不支持 mmap,又或者用户不希望程序使用 mmap 向系统申请空间,就可以将这个选项关闭。
然后会判断是否能通过 mmap 分配内存,需要满足 2 个条件,一是 nb 大于等于 mmap 的分配阈值,二是当前进程通过 mmap 方式分配的内存总量小于最大值,这两个最大值分别是
1 | DEFAULT_MMAP_THRESHOLD 128 * 1024 |
不知道为什么,第二个判断似乎总能成功,有了解的大哥请告诉我
如果上面两个条件都能满足,就会进入到 mmap 的分配流程,首先要重新计算 size,引用 malloc.c 的注释如下
1 | Round up size to nearest page. For mmapped chunks, the overhead |
重新计算 size 的原因有两个,一是 mmap 分配的内存块必须页对齐,二是 mmap 分配出来的堆块没有下一个堆块的 prev_size 空间可以复用,所以需要重新计算 size。
计算 size 之后会执行下面的代码
1 | tried_mmap = true; |
首先要判断 size 是否小于 nb,如果出现这种情况,说明 size 发生了溢出,不会继续分配内存,否则进入到主要的 mmap 逻辑。 直接调用 mmap 函数尝试分配一个堆块,分配成功就不需要进一步检查对齐的问题了,因为 mmap 的堆块本来就是页对齐的。
之后会将堆块的 IS_MMAPED 标志位置 1,表示它是通过 mmap 方式分配出来的。之后就是一些更新全局变量的操作,并且返回堆块。
如果在 mmap 这一支不能成功分配堆块,有 3 种情况,一是 nb 大于 mmap 的分配阈值,二是 size 发生溢出,不能通过 mmap 分配,三是 mmap 分配失败。
如果发生上面的三种情况,会进入到下一个阶段,即拓展 top chunk
1 | /* Record incoming configuration of top */ |
首先保存旧的 top chunk 信息,然后判断 top chunk 是否合法,当程序执行到这里,top chunk 有两种情况,一是没有被初始化,size 等于 0,二是已经被初始化,但是 size 不能满足 nb,这使就要求 size 大于 MINSIZE ,并且 prev_inuse 标志位为 1,并且 top chunk 的尾部是对齐的。
最下面是一个比较重要的判断,如果没有开启 ATOMIC_FASTBINS,当程序执行到这里时,所有的 fastbin chunk 一定是通过 consolidate 合并过了,如果还有 fastbin chunk 残留在链表中,说明发生了一些错误。
1 | if (av != &main_arena) { // 当前分配区不是主分配区 |
如果当前线程的分配区不是主分配区,会先尝试拓展当前的 heap,通过调整 sub_heap 的边界尝试获取更多的空间,如果失败,就会尝试新建一个 heap(使用 brk() 拓展) ,在新建 heap 的代码中,和 pwn 题有关系的就是 __int_free(av, old_top),这句代码,因为它会将旧的 top chunk 加入到 unsortedbin 中,这也是 house of orange 以及一系列衍生攻击的基础。
注意! 下面的代码从 sYSMALLOc 尾部取得
1 | /* if (av != &main_arena) */ |
拓展出新的 heap 之后就可以切割出满足要求的堆块了。
如果拓展 topchunk 或者是 分配新的 heap 都失败了,就会检查是否尝试过 mmap,如果没有,就会跳转到 mmap 部分尝试映射一块内存。
如果当前分配区属于主分配区,那么情况要复杂很多,不过最后的效果是差不多的,而且这段代码和 pwn 题关联并不是很大,想要详细了解的同学可以自行分析一下 glibc 的源代码。
sYSMALLOc 的执行流程可以参考下面这张图片(省略了在主分配区的情况)
malloc 几个细节
malloc 还有几个细节,在这里稍作解释。
- 物理内存分配时间。上一篇文章提到调用 malloc 并不会立即在物理页面上创建相应的堆块,而是当用户真正使用这块空间的时候才会创建,这是由于 malloc 给程序分配的是虚拟地址,当程序访问这个虚拟地址的时候,MMU 发现虚拟地址没有对应的物理内存,触发页中断,这时候硬件会陷入内核处理中断,操作系统负责分配物理内存,MMU 再将虚拟地址绑定到物理内存上面,完成内存分配操作。
- MMU: 英文 Memory Management Unit 内存管理单元,这个模块一般都被集成在 CPU 中,CPU 发出的是虚拟地址(例如调试的时候经常看到的 0x400000 等),正常情况下这个如果使用这个地址去寻找内存的话是找不到的,需要先经过 MMU 将虚拟地址转换成物理内存地址才能正常寻址。
简单理解 brk 与 mmap:
这两个属于底层的系统调用,malloc 就是依靠它们完成内存分配的,brk 在原有的 heap 基础上进行拓展,而 mmap 尝试在堆栈之间找一块内存。
以下图为例,这是一个简单的内存模型
如果用户申请了一块比较大的内存,当前 top chunk 大小不足以容纳(并且没有超过 mmap 的阈值),就会调用 sYSMALLOc 尝试拓展当前 heap。这里要介绍一个指针 _edate,它被定义在 glibc 中,指向当前进程数据段的最高地址,当拓展 heap 的时候,会调 brk 这个系统调用,例如连续分配两个大小为 0x7800 的堆块,调用 brk 之后如图
所以 brk 的大致原理就是抬高 _edata 指针,来拓展当前的 heap。
当用户申请的 chunk 大小超过 mmap 阈值,会直接调用 mmap 来分配堆块,例如申请一个大小为 0x22000 的堆块,mmap 之后如图
free 的时候也是一样,如果 free mmap 的区域,ptmalloc 会直接将内存返回给系统,但是当 free chunk1 的时候,由于 _edata 指针指向数据段的最高地址,而最高地址还有一个 chunk2 正在被使用,这时不能收缩堆。而当 chunk2 也被 free 之后,1 2 两个 chunk 会自动合并,_edata 指针也就可以收缩了,chunk1 和 2 组成的一块内存会被归还操作系统。
free
前面分析了 malloc 以及几个附加函数,下面就要进入到 free 函数了。
操作系统的内存不是无限的,程序使用 malloc 向操作系统申请内存,不能一直占据不放,当这些内存不再使用的时候,需要使用 free 函数进行释放,以保证整个系统的正常运行。
简单的理解一下,free 函数所执行的操作其实可以视为 malloc 函数的逆过程,malloc 从缓冲区或者操作系统中拿取空间,而 free 则负责将这些空间放回缓冲区或者操作系统。
public_fREe
此函数是 free 的外壳函数,所处理的问题和多线程有关,free 的核心函数是 __int_free
1 | void |
__int_free
和 malloc 一样,free 函数的核心代码是 __int_free,可以在 malloc.c 中找到。
函数开头首先定义了一些局部变量,摘录如下
1 | static void |
依旧是那些很常用的变量,这里就不详细介绍了。
__int_free 函数主要有两个参数,一是指向 arena 的指针 av,二是传入的 chunk 指针,代码首先使用 chunksize 计算出当前 chunk 的大小,然后是一些安全检查
1 | /* Little security check which won't hurt performance: the |
这段代码检查了 chunk 的 size 是否发生了溢出,堆块的地址要对齐,并且堆块的 size 不能小于最小值。
接下来会首先判断当前 chunk 是否属于 fastbin,因为 fastbin 是分配速度最快的缓冲区
1 | /* |
这部分代码比较简单,实现的功能是将属于 fastbin 的 chunk 插入到 fastbin 对应的链表中去,当 free 一个属于 fastbin 的 chunk 时,会首先检查这个 chunk 的下一个 chunk 的 size 是否正确,这也就是为什么在一些 pwn 题中伪造 fastbin chunk 的时候要一并伪造好下一个 chunk 的结构。
接着会获取 chunk 在 fastbin 中的 index,并且检查这条链表是否合法,如果链表不为空会判断第一个堆块是否属于当前链表,如果不是,说明发生了异常。另外还会判断正要插入的 chunk 是不是已经是当前链表的第一个堆块,如果是的话,说明发生了 double free(注意:这里仅仅检查了链表的第一个堆块,而没有遍历所有堆块,这就是 fastbin double free 能够成功的关键原因!)。
如果上面这些检查都没有问题,就会把 chunk 插入到对应的 fastbin 链表中,注意 fastbin 是从头部插入,而 malloc 也是从头部取出。另外,代码中涉及到了 ATOMIC_FASTBIN 优化的问题,它的机制之前已经简要介绍过,利用 lock-free 基础实现对 fastbin 链表的操作,我们这里就不去深究了。
上面的流程可以用这张图片表示
如果待 free 的堆块不属于 fastbin ,那么就会进入到下一段逻辑,代码如下
1 | /* |
上面一段代码比较长,但是实现的功能很简单,如果代码能够执行到这里,说明用户想要 free 的 chunk 大小不属于 fastbin,此时 ptmalloc 会分别判断当前 chunk 物理相邻的堆块是否空闲,如果是,就会调用 unlink 将这些堆块从它们的链表中卸下,然后和当前 chunk 合并,如果和 top chunk 紧邻,就会和 top chunk 进行合并。
当 chunk 的合并完成之后,合并出来的 chunk 很有可能会很大(之前已经申请和释放了许多 chunk),如果这个 chunk 大小超出了 FASTBIN_CONSOLIDATION_THRESHOLD 阈值,并且 fastbin 中还有堆块,那就会调用 consolidate 合并 fastbin 中的 chunk。
在合并操作完成之后,还会判断当前分配区是不是主分配区,如果是,并且 top chunk 的大小已经超过了 heap 的收缩阈值,就调用 sYSTRIm 收缩 heap,不是主分配区,也会使用 heap_trim 来收缩 sub_heap.
以上是待 free chunk 不是由 mmap 分配的情况,如果是由 mmap 分配的堆块,会调用函数 munmap_chunk
1 | else { |
这个函数主要用来回收 mmap 分配的空间。
结合这张图片理解
sYSTRIm
这个函数可以视为 sYSMALLOc 函数的逆过程,负责将内存返还给操作系统。
摘录 ptmalloc 的注释如下
1 | sYSTRIm is an inverse of sorts to sYSMALLOc. It gives memory back |
函数的代码如下
1 |
|
这个函数的流程也是比较简单的,就不再赘述了。
Unlink
unlink 是由一个独立的宏实现的,它的主要功能就是将一个空闲的 chunk 从它的链表中卸下来,以便于和其他 chunk 进行合并,Unlink 代码如下
1 | /* Take a chunk off a bin list */ |
其中最为关键的代码如下
1 | FD->bk = BK; \ |
利用 unlink 的攻击实际上利用的就是这两句代码,如果我们能够控制某个 chunk 的 bk 和 fd,并且能够绕过一些验证,那么就可以完成一次 unlink 攻击,但是这不是本文讨论的重点,网络上有很多大牛详细解析了 unlink 攻击的原理,大家可以自行搜索学习。
总结一下
至此我们通过 glibc 的源代码大致分析了一下 ptmalloc 中的 malloc 和 free 两个常用函数,由于堆溢出经常出现在一些 CTF 比赛中,所以从根源上理解这些东西对于我们深入学习和掌握堆溢出是很有帮助的,ptmalloc 非常注重性能,但是所谓有得必有失,注重性能的代价就是安全性不高,和 windows 的堆管理策略相比可谓小巫见大巫了。
ptmalloc 中还有几个与内存分配相关的函数如 realloc 等,但是它们的核心思想都是依托现有的缓冲区,尽量减少向操作系统申请内存的次数,以达到提高程序执行效率的目的。
Reference
华庭 《Glibc 内存管理 Ptmalloc2 源代码分析》
cloudburst ptmalloc flow chart
http://www.cnblogs.com/zhaoyl/p/3820852.html
- 本文作者: Catalpa
- 本文链接: https://wzt.ac.cn/2019/03/15/glibc_analyse2/
-
版权声明:
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。