内存分配器 ptmalloc2梳理

        最近看了一些内存分配器的资料,找了一大堆,感觉读起来都不是很顺畅,今天终于找到了比较好的资料,特来分享。下文是从一篇论文中截取的部分关于ptmalloc2的描述:

2 Glibc Heap 管理机制
        Glibc 的堆分配器是基于 Doug Lea 早期的dlmalloc 分配器发展而来的 ptmalloc2 分配器, 具有速度快、碎片度低、线程安全的特点。本章以当前流行的 glibc 2.19 版本为例 , 说明其堆的分配机制。
2.1 相关结构
2.1.1 堆块结构
        堆块块(Chunk), ptmalloc2 进行分配的基本单位 , 其头部保存着自身的大小等信息 , 在这之后即是用户数据。一共有 3 种类型 , 它们都使用同一种数据结构 (malloc_chunk) 定义。
    

        在 32 位系统中 , 堆块的大小最小为 16 字节 , 对 齐 8 字节 ; 而在 64 位系统中 , 堆块的大小最小为 32 字节 , 对齐 16 字节。在本文之后的讨论中 , 未做特 别说明则默认为 64 位环境。
        具体来说, 用于管理的堆块主要分为 3 种类型。
         1、Allocated Chunk: 已分配块 , 如图 1 所示 , 只 使用 prev_size size 2 个域 , 用来记录上一块大 小以及自身的大小 , 剩余部分则为用户数据。

 

        2、Free Chunk: 被释放块 , 如图 2 所示 , 另外使用 fd bk 2 个域。

 

         3、Top Chunk: 顶块, 位于所有块之后, 保存着未分配的所有内存, 与已分配块使用相同的域。         除此之外, 由于块的大小是对齐的, 使得低位字节不会使用到, glibc 使用 size 域的最低 3 位来存储一些其它信息。相关的掩码信息定义如下:  

        从以上代码定义可以推断, size 域的最低位表示 此块的上一块 ( 表示连续内存中的上一块 )是否在使 用状态 , 如果此位为 0 则表示上一块为被释放的块, 这个时候此块的 PREV_SIZE域保存的是上一块的地址以便在 free 此块时能够找到上一块的地址并进行合并操作。第 2 位表示此块是否由 mmap 分配 , 如果此位为 0 则此块是由 top chunk 分裂得来 , 否则是由mmap 单独分配而来。第 3 位表示此块是否不属于main_arena, 在之后会提到 main_arena 是主线程用于保存堆状态的结构, 如果此位为 0 则表示此块是在主线程中分配的。
 
2.1.2 Bins 结构
        当一个分配块被执行free 操作后 , glibc将其按照 一定规则放入 bin 结构中 , 以便下次分配时能够再次利用。 bin 实际上即是链表结构 , 利用 fd bk 指针 进行组织 , 根据块的大小不同分为不同的 bin 结构。
         Fast Bins: chunk 的指针数组 , 每个元素是一 条单向链表的头部 , 且同一条链表中块的大小相同。 主要保存大小 32 128 字节的块 , 特点是当 free 时 不取消下一块的 PREV_INUSE , 也不检查是否能够进行合并操作, 主要目的是能够最快速地利用较小的内存块。由于是单向链表 , Fast bins 的取用机制是 LIFO (Last In First Out) , 即后释放的块将先被 利用。
         Small Bins: chunk 的指针数组 , 每个元素是一条双向循环链表的头部 , 且同一条链表中块的大小 相同。主要保存大小 32 1024 字节的块。由于是双向链表 , Small Bins 的取用机制是 FIFO (First In First Out) , 即先释放的块会先被利用 , 之后的 Large Bins Unsorted Bins 也是同样的机制。
         Large Bins: chunk 的指针数组 , 每个元素是一条双向循环链表的头部 , 但同一条链表中块的大小不一定相同 , 按照从大到小的顺序排列 , 每个 bin 保存一定大小范围的块。主要保存大小 1024 字节以上的块。
         Unsorted Bins: Small Bins Large Bins 类似是双向循环链表 , 只有一个 bin, 其中保存的块大小不定 , 用于收集刚刚被 free 或从大的块中分裂剩下的块。
2.1.3 Arena 结构
        当前主线程的堆分配状态是由 glibc 中的全局变量 main_arena 保存的 , 这是一个 malloc_state 类型的结构体。而 malloc_state 结构体的部分定义如下 :
我们关心的部分有以下几个域 :
         fastbinsY: 保存 Fast Bins 的数组
         top: 保存 Top Chunk 的地址
         last_remainder: 保存上一次分裂的块
         bins: 其中下标为 1 的元素是 unsorted bin, 之后的 bins 从小到大对应 small bins large bins, 下标为 0 的元素不用。

 

2.2 分配函数
        Malloc 函数为 glibc 的主要分配接口 , 给出需要分配的大小参数 , 返回值为分配得到的用户数据指针。主要的功能由 _int_malloc 函数实现。
        在第一次执行 malloc 函数时 , 系统会使用 brk 系统调用向操作系统扩展程序的数据区 , 此时 glibc 将初始化 top chunk main_arena, 取得 132KB的空间。 如果之后所有的 malloc 操作都可以满足 , 即最后总是能在此 132KB 中找到合适的内存块返回 , 则不再使用系统调用与内核交互。这段时间 , 程序的堆内存由 glibc 管理。否则 , 将使用 brk mmap 系统调用来向内核申请更多空间。
        当程序将需要的空间大小传入 malloc 时, glibc首先将其加上 8 字节的额外开销 ( 用于保存 size 域, 因为 prev_size 域实际占用的是上一块的空间故不算 额外开销 ) 然后对齐 16 字节 , 如果不足 32 字节则分配 32 字节。接下来我们的叙述中 , 请求大小皆指已 经处理之后对齐 16 字节的大小。
2.2.1 检查 Fast bins
        如果请求大小满足 Fast bins, 则在对应的 bin 中寻找是否有相同大小的块 , 如果有则直接将其取出返回给程序 , 同时更新 fast bins 中对应 bin 存储的链表头指针。
2.2.2 检查 Small bins
        如果块大小符合 Small bins, 则在对应大小的Small bin 中寻找是否有合适的块 , 如果有则直接返回 , 同时更新 Small bins 中对应 bin 的链表中该块的上一块和下一块的指针。对于 Fast bins Small bins 来说 , 每个 bin 中的块大小都是相同的 , 所以只要对应的 bin 中有块 , 就能够直接返回恰好符合的块。
2.2.3 处理 Unsorted bin
        如果之前没能返回恰好符合的块, 则开始处理Unsorted bin 中的块 ( 这里有一个例外 , 如果 Unsorted bin 中只有一个块且这个块是 last_remainder, 而且大小足够 , 则优先使用此块 , 分裂后将前一块返回给用户 , 剩下的一块作为新的 last_remainder 再次放入Unsorted bin. )
        具体来说, 处理循环如下 :
        a) 逐个迭代 Unsorted bin 中的块 , 如果发现块的大小正好是需要的大小 , 则迭代过程中止 , 直接返回此块 ; 否则将此块放入到对应的 Small bin 或者large bin , 这也是整个 glibc 堆管理中唯一会将块放入 Small bins large bins 中的代码。
        b) 迭代过程直到 Unsorted bin 中没有块或超过最大迭代次数 (10000)为止。
        c) 随后开始在 Small bins large bins 中寻找最合适的块 ( 指大于请求大小的最小块 ), 如果能够找到, 则分裂后将前一块返回给用户 , 剩下的块放入Unsorted bin 中。
        d) 如果没能找到 , 则回到开头 , 继续迭代过程, 直到 Unsorted bin 空为止。
2.2.4 使用顶块
        如果之前的操作都没能找到合适的块, 将分裂Top chunk 返回给用户 , Top chunk 的大小仍然不足 , 则再次执行 malloc_consolidate 函数清除 Fast bins, Fast bins 已空 , 只能使用 sysmalloc 函数借助系统调用拓展空间。
2.3 释放函数
        Free 函数是 glibc 的释放接口 , 将之前分配得到的用户内存指针作为参数 , glibc 会释放这一块空间。 主要的功能由 _int_free 函数实现。
        首先, 进行一系列的检查 , 包括内存地址对齐, PREV_INUSE 位是否正确等等 , 能够发现一些破坏与 double free 的问题。
        如果块大小满足 Fast bins, 则不取消下一块的PREV_INUSE , 也不设置下一块的 prev_size 域, 直接将该块放入对应的 Fast bin , 且不进行相邻块的合并操作。
        检查被 free 内存块的前一块 (这里的前一块指连续内存中的上一块 , 通过 prev_size 域来寻找 ), 如果未使用 , 则合并这 2 , 将前一块从其 bin 中移除之后再检查其后一块 , 如果发现是 Top chunk, 则最后将合并到 Top chunk , 不放入任何 bin; 如果不是 Top chunk 且未使用 , 则再合并这 2 , 将后一块从其 bin中移除 , 并且将合并过的大块放入 Unsorted bin 中。
2.4 重分配函数
        realloc 是一项复合操作 , 既要给出之前分配的用户内存指针 , 又要传入需要的大小数值 , 旨在重
新分配这块空间以符合用户新的需求。在执行具体操作之前 , 同样会先将用户传入的大小参数进行处理以对齐 16 字节。主要功能由 _int_realloc 函数实现。
        若发现之后需要的大小比之前的大小更小, 直接对此块进行压缩操作, 分裂出的部分如果达到 块的最小大小 (32 字节 ), 则调用 _int_free 函数释放此块。
        若发现已分配的块后一块是 Top chunk(这里的后一块指的是连续内存中的下一块 , 通过 size 域来寻找 ), 则直接向 Top chunk 中扩展一部分空间 , 返回的指针与之前传入的指针相同。
        若发现下一块已经被 Free, 且下一块的大小能够满足新的需求大小 , 则向下一块中扩展 , 使用
unlink 宏将下一块从对应的 bin 中移除 , 扩展完成后再对剩下的块调用 _int_free。返回的指针与之前传入 的指针相同。
        若无法向下一块扩展, 则直接调用_int_malloc 分配新的堆块 , 然后把之前堆块中的用户数据复制到新的堆块中 , 最后对之前的块调用 _int_free 函数。
2.5 堆管理的特点
        可以看到, glibc 在堆管理方面使用了很多技巧, 最终目的都是为了能够加快分配速度、降低碎片率、 更好更合理的利用堆内存区域。而这样一来 , 安全性便成为其最大的问题 , 由于内存块的元数据与用户数据交错布置 , 导致块的元数据很容易遭到破坏 , 如果程序中有缓冲区溢出漏洞更是可以进一步的利用。
该论文中没有关于mmap()的一些讲解,比较遗憾。
如果对于ptmalloc2还有所疑惑,可以看一下这个资料: Heap Exploitation Part 1: Understanding the Glibc Heap Implementation | Azeria Labs
郑重声明,本篇博客转载自:《Glibc 堆利用的若干方法》,该论文于2018年发表于《信息安全学报》。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值