最近公司被要求参加某网络安全比赛,所以借此机会又重新阅读了 glibc malloc 的最新代码,发现了许多之前未曾深究的细节。故整理成此文,也算是对从前文章的补充了。

前言

几年前已经写过了一篇 ptmalloc 与 glibc 堆漏洞利用,但是一来当时学习仓促,很多内容自己也只是一知半解;二来已经时过境迁,当时的 glibc 距今也更新了不少,而且当时理解的内容太久没有复习又全部还给老师了。因此,本文又重新将其整理一遍,当然不再介绍基础概念,只记录重点以备考试时快速查阅。

数据结构

首先在 arena 中实际上只分成两个 BIN,分别是 fastbinsYbins。而 bins 中包含了常用的 unsorted bin、small bin、fast bin,统称为 regular bins

在 regular bins 数组中,bin_at(0) 不存在,bin_at(1) 为 unsorted bin,小于 MIN_LARGE_SIZE 的为 small bin,大于等于则为 large bin。对于 32/64 位系统来说,该值分别为 0x200/0x400:

#define NBINS             128
#define NSMALLBINS         64
#define SMALLBIN_WIDTH MALLOC_ALIGNMENT // -> 0x08/0x10
#define SMALLBIN_CORRECTION (MALLOC_ALIGNMENT > 2 * SIZE_SZ) // -> 0
#define MIN_LARGE_SIZE ((NSMALLBINS - SMALLBIN_CORRECTION) * SMALLBIN_WIDTH) // -> 0x200/0x400

双链表 Trick

上面说到 unsorted bin 对应 bin_at(1),但注意不要与 bins[1] 弄混,其实现如下:

#define unsorted_chunks(M)          (bin_at (M, 1))
#define bin_at(m, i) \(mbinptr) (((char *) &((m)->bins[((i) - 1) * 2]))                \- offsetof (struct malloc_chunk, fd))

可以看到 bin_at 对应的 bins 索引是 (i-1) * 2,而且取到地址之后还回退一个偏移。乍看起来很奇怪,但实际上这是写代码的一个小优化,返回的地址会转换为 chunk 指针,但实际上并不是一个正常的块,而只是将其当做一个空闲的头部,只使用其中的 fd/bk 字段,分别指向不同的 bin。假设返回的指针是 P,则有:

P->fd = m->bins[(i-1) * 2];
P->bk = m->bins[((i-1) * 2) + 1];

也就是说,bin_at(i) 返回的 chunk, 似 chunk 又非 chunk,本身在 bins 中占两个坑,虽然不能当做真的 chunk 返回给用户使用,但是却正好当做双链表的固定表头。

实际测试一下可能更清楚一些,在 unsorted bin 为空时,有以下属性:

pwndbg> p *main_arena.bins[0]
$1 = {mchunk_prev_size = 94692393318320,mchunk_size = 0,fd = 0x7f5bfe942cc0 <main_arena+96>,bk = 0x7f5bfe942cc0 <main_arena+96>,fd_nextsize = 0x7f5bfe942cd0 <main_arena+112>,bk_nextsize = 0x7f5bfe942cd0 <main_arena+112>
}pwndbg> distance main_arena.bins[0] &main_arena.bins[0]
0x7f5bfe942cc0->0x7f5bfe942cd0 is 0x10 bytes (0x2 words)

bin 中的唯一一个 chunk,其 fd 和 bk 都指向自身,而这个 自身,却是 bin 所在位置往前 0x10 字节的地址,即在全局 main_arena 结构体中,.data 段的地址。

下面分别介绍一下各个 bin 中值得一提的特性。

FastBin

fastbinsY 数组中保存着不同长度的单链表头指针,但实际上数组的大小会比链表总数要略大一些,空出来的可能是留着过年。

#define MAX_FAST_SIZE (80 * SIZE_SZ / 4) // 0x50/0xa0

以 32 位程序为例,只用到了 0x10, 0x18, … 0x40 共 7 个 bin,但数组总大小为 11,fastbin 的上限是 0x50 字节;

对于 64 位程序,也是只用到了 0x20, 0x30, …, 0x80 共 7 个 bin,数组大小为 10,理论上最大的 fastbin 可以达到 0xa0 字节;

fastbin 的特性如下:

  • 进入 fastbin 的 chunk,其 in-use 位为 1,因此不参与相邻块的合并;
  • 同一个 bin 中所有的 chunk 大小都相等,因此不需要排序;
  • 由于 fastbin 是按照最小 chunk 对齐大小增长的,因此从 fastbin 中分配的块必然是 exact fit 的,即申请 chunk 大小与 bin 大小完全一致;
  • LIFO,这是为了提升 cache 命中率;

待释放的块 P 进入 fastbin 时,首先获取对应大小 bin 链表头的指针,称为 FP,然后执行以下操作将 P 插入单链表的头部:

P->fd = *FB;
*FB = P;

同理,要从 fastbin 取出块时,则直接取出链表头的元素:

P = *FP;
*FP = P->fd;

这些指针操作是触发堆漏洞时候构造内存读写原语的重要来源,因此值得重点关注。

Unsorted Bin

  • 只有一个,即 bin_at(1) = bins[0]
  • 内部的 chunk 可以是任何大小,且如其名字而言,不进行排序;
  • 双链表,FIFO;
  • 只有一次重新被使用的机会;
  • 对于小请求,大部分情况下要求 exact fit,即尺寸一样才会返回;但如果此时bin 中只剩下一个 chunk 且为 last_remainder 且足够大,会对其进行分割,此时是 best fit
  • 分割后多出的部分或者遍历时不满足要求的 chunk 会全部放回到各自的 small/large bin 中;
  • 进入 unsorted bin 之前,会先进行前后合并;

待释放的块 P 进入 unsorted bin 时,会插入到 bin 的第一个元素中,如下:

bck = unsorted_chunks(av);
fwd = bck->fd;P->fd = fwd;
P->bk = bck;
if (!in_smallbin_range(size)) {p->fd_nextsize = NULL;p->bk_nextsize = NULL;
}
bck->fd = p;
fwd->bk = p;

注意这里用到了本节开头提到的双链表 trick,bck 是双链表的头,这里可以看做是一个 chunk (虽然实际上不是)。 因此,上述双链表插入的时候,并没有像 fastbin 一样直接替换表头,而是插在了表头的后面。

既然是 FIFO,而且在插入时候从表头的 fd 插入,那么取出时自然就优先从链表末尾即 bk 拿出了,取出的相关操作如下:

while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{bck = victim->bk;/* remove from unsorted list */unsorted_chunks (av)->bk = bck;bck->fd = unsorted_chunks (av);
}

Small Bin

要说特点,Small Bin 可以说是在这些数据结构中最平平无奇的了,包含了一些似乎大家都有的性质,比如:

  • 双链表,FIFO;
  • 每个 bin 中 chunk 的大小都相同;
  • 因此从中分配的策略属于 exact fit

但也有一些特别的,由于指定 bin 中的 chunk 大小都相同,所以也可以将其中 chunk 的大小称为 bin 的大小。

  • Small Bin 本应有 NSMALLBINS = 64 个,但 bin_at(0) 不存在,bin_at(1) 是 unsorted bin,因此实际上只有 62 个;
  • 在 64 位程序中;
    • 最小的 small bin 大小为 0x20,最大为 0x3f0
    • 每个 bin 之间的大小相差 0x10
  • 在 32 位程序中:
    • 最小的 small bin 大小为 0x10,最大为 0x1f8
    • 每个 bin 之间的大小相差 0x8

如果没有失忆的话,应该会发现,small bin 和 fast bin 的大小范围有所重叠。但它们的业务范围不同,前者是 unsorted bin 投胎失败后的去处;而后者则是赵家人优待窗口,释放后可以不通过 unsorted bin 直接进入。

更上层的还有天龙人 tcache,后文会详细介绍。

这里先说出链操作,其实和 unsorted bin 类似,都是双链表的删除操作,取的是链表末尾的 chunk:

#define last(b)      ((b)->bk)idx = smallbin_index (nb);
bin = bin_at (av, idx);
if ((victim = last (bin)) != bin) {bck = victim->bk;bin->bk = bck;bck->fd = bin;
}

入链相对复杂,在于 regular bin 都只能从 unsorted bin 的残羹冷炙中获取生源。但复杂的是算法,数据操作还是很简单的:

victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

bck 是链表头,一个伪 chunk,所以上面的代码本质还是将 victim chunk 插入到链表的头(的下一个位置)。

Large Bin

前文说过,NBINS 一共就 128 个, Small Bin(等等) 占了 64 个,那么剩下来的就是 Large Bin 了。它和其他 bin 相比尽管有一些类似,但更多的是不同。其特性如下:

  • 同一个 bin 中块的大小可以不同,不同大小的块需要经过排序,相同大小的采用 FIFO 顺序进出;
  • bin 中包含两个双链表;
  • 取出时候按照 best fit 的原则获取;
  • 不同的 bin 可以分为 6 组,每组的 bin 之间大小间隔公差不同,见下表;
bin_at bins 间隔 HEX 备注
0-63 64 16 0x10 62 个 Small Bin, [0x20, 0x400)
64-95 32 64 0x40 Large Bin, [0x400, 0xc00)
96-111 16 512 0x200 Large Bin, [0xc00, 0x2c00)
112-119 8 4096 0x1000 Large Bin, [0x2c00, 0xac00)
120-123 4 32768 0x8000 Large Bin, [0xac00, 0x2ac00)
124-125 2 262144 0x40000 Large Bin, [0x2ac00, 0xaac00)
126 1 其他 - Large Bin, [0xaac00, …)

上面备注的 large bin 每组大小区间可能有点不准确,我是按照间隔大小以及文档中所声明的 bin 个数计算的。但分组本身并不很重要,实际计算以 largebin_index_64 宏的结果为准(比如第一组实际上是 64~96)。

largebin 中不同 size 的 chunk 使用 fd_nextsize/bk_nextsize 相连,相同 size 的使用 fd/bk 指针相连,例如下图 bin 中有 7 个 chunk:

关键点:

  • 链表头的 fd 指向最大的块;bk 指向最小的块;
  • 相同大小的块中,只有第一个块的 fd_nextsizebk_nextsize 指向下一个大小的块,后续块的 fd_nextsize、bk_nextsize 会置零;
  • fd_nextsize/bk_nextsize 属性构成一个循环双链表,从 fd 第一个元素开始 ,fd_nextsize 指向下一个更小的块,最小的块会指向最大的块;注意这个双链表并不包含所有的块;
  • fd/bk 属性也构成一个循环双链表,从表头第一个元素开始 fd 指向下一个 size 小于等于自身 size 的块,串起了所有的块;
  • nextsize 双链表不包含表头,因为表头是个伪 chunk,只有 fd 和 bk 属性;

插入 large bin 的流程伪代码如下:

victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;if (fwd != bck) {// 与 bin 中最小的块比较if (size < chunksize_nomask(bck->bk)) {fwd = bck;bck = bck->bk;victim->fd_nextsize = fwd->fd;victim->bk_nextsize = fwd->fd->bk_nextsize;fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;} else {// 从 fwd = bin->bk 即最大的块开始搜索while (size < chunksize_nomask (fwd)) {fwd = fwd->fd_nextsize;}/* 此时 fwd 指向能满足申请 size 的最小块 */if (size == chunksize_nomask (fwd)) {// 如果已经存在对应大小的块,插入对应次级链表的第二个位置fwd = fwd->fd;} else {victim->fd_nextsize = fwd;victim->bk_nextsize = fwd->bk_nextsize;fwd->bk_nextsize = victim;victim->bk_nextsize->fd_nextsize = victim;}bck = fwd->bk;}
} else { // bin 为空victim->fd_nextsize = victim->bk_nextsize = victim;
}victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

代码虽然看起来很复杂,但思路其实很直观,bckfwd 分别表示待插入块所在位置的前后节点,前面一系列操作只是确定其位置,最后执行双链表的插入操作。而对于 fd/bk_nextsize 的操作也和上图中介绍的流程一致,目的还是为了让 chunk 按照大小顺序排好。

删除 large bin 的逻辑也类似:

bin = bin_at (av, idx);
victim = first (bin); // victim = 最大的块
victim = victim->bk_nextsize; // victim = 最小的块
while (((size = chunksize (victim)) < nb)) {// 从小到大搜索,直至尺寸满足victim = victim->bk_nextsize;
}if (victim != last (bin)&& chunksize_nomask (victim) == chunksize_nomask (victim->fd)) {// 如果相同尺寸的块不止一个,从第二个开始取,防止修改 nextsize 链表victim = victim->fd;
}unlink_chunk (av, victim);

unlink_chunk 就是老版本中的 unlink 宏,现在已经升级为了函数,但操作还是一样的,标准双链表删除,其中针对 largebin 的还执行了 nextsize 双链表的删除操作:

unlink_chunk (mstate av, mchunkptr p) {mchunkptr fd = p->fd;mchunkptr bk = p->bk;fd->bk = bk;bk->fd = fd;/* 确认为 largebin 且 p 在 nextsize 链表中 */if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL) {if (fd->fd_nextsize == NULL) {/* 下一个块不在链表中,将其加入 */if (p->fd_nextsize == p)fd->fd_nextsize = fd->bk_nextsize = fd;else {fd->fd_nextsize = p->fd_nextsize;fd->bk_nextsize = p->bk_nextsize;p->fd_nextsize->bk_nextsize = fd;p->bk_nextsize->fd_nextsize = fd;}} else {/* 下一个块也在链表中,功成身退 */p->fd_nextsize->bk_nextsize = p->bk_nextsize;p->bk_nextsize->fd_nextsize = p->fd_nextsize;}}
}

由前面的逻辑可以推测,如果同样大小的块有不止一个,那么肯定不会删除在 nextsize 双链表中的那个(即第一个)。因此,如果需要从 nextsize 链表中删除,就说明了对应大小的块只有一个。其实这个预设可以简化删除的逻辑,但 unlink 作为通用的函数,不能对传入的 p 做额外预设,因此还是检查了所有情况。

TCache Bin

tcache 也是个缓存,本身就是个数组,比 fastbin 快那么一丢丢,但胜在支持的尺寸多。其主要特性如下:

  • 一共有 TCACHE_MAX_BINS = 64 个 tcache bin;
  • 每个 bin 最多只有 TCACHE_FILL_COUNT = 7 个 chunk;
  • bin 之间大小相差 0x10,实际尺寸到索引通过 csize2tidx 计算;
  • 最大的 bin 支持的大小为 0x410,这是用户申请的大小,实际块大小要再加上头部;

其实 tcache 从 2.26 加入 glibc 之后,其结构和代码逻辑都发生了很大的变化。早期什么检查都没有,但每一代都会新增一些校验,导致 tcache 已经不是那么好利用了,所以现在基本上都是先将其填满,然后再去利用常规方法进行利用。

算法

这里简单把堆分配和释放的过程记录下来。在遇到一些细节的时候可以去查看相关代码。

malloc

内存的关键分配过程如下:

  1. 如果 tcache 中有合适的块,直接取出返回;
  2. 如果 fastbin 中有合适的块,直接返回;
  3. 如果是小块,则尝试从对应的 smallbin 中取出返回;
  4. 如果是大块,先 malloc_consolidate
  5. 循环遍历 unsorted bin:
    1. 如果请求的是小块,且 bin 中只有一块 last_remainder,且大小满足,则对其进行切割,然后返回;
    2. 不管要不要,先将当前块从 unsorted bin 中删除;
    3. 如果当前块大小和请求大小完全相等,可直接返回;
    4. 否则将当前块放到对应的 small、large bin 中;
  6. 如果是大块,在 large bin 中搜索最合适的;
  7. 兜底方案,从 top_chunk 中分裂出新的块并返回;

关于 tcache 的横刀夺爱:

  • 在第 2 和 第 3 步,相同大小的其他块也会取出放到对应的 tcache bin 中;
  • 在 5.3 步,并不直接返回,而是先放到 tcache bin 中然后继续遍历;
  • 在 unsorted bin 每次循环的末尾会检查 tcache 有没有拿够,拿够了就放人;

free

接着看内存释放的过程:

  1. 如果对应的 tcache bin 有空位,直接放进去;
  2. 如果是 fastbin 的大小,放到对应的 fastbin 中;
  3. 尝试合并相邻低地址的块,也称为后向合并(backward);
  4. 如果相邻高地址的块不是 top chunk,则尝试前向合并(forward);
    • 否则直接合并然后更新 top chunk,直接返回;
  5. 将合并后的块放到 unsorted bin 中;
  6. 合并后的块大小如果大于某个阈值,则触发 malloc_consolidate
  7. 如果 top chunk 大于 mp.trim_threshold,则执行 systrim

其中 malloc_consolidate 在内存分配的时候也遇到过,主要作用是将 fastbin 中的块都清空并归还系统,能与 top chunk 合并的就合并,不能合并的就插入 unsorted bin 的头部。

不管是 malloc_consolidate 还是 systrim,其目的都是为了减少内存碎片,后者的作用是作为 sysmalloc 的逆操作,通过 brk 系统调用将多余的内存还给系统。

后记

本文算是把 ptmalloc 复习了一遍,重新梳理了其中的亿点细节,并编撰成文以备日后参考。其实很多 CTF 比赛还是能够对现实安全研究带来思路的,去享受,去学习,去分享,也许这才是 CTF 的原本价值吧。


版权声明: 自由转载-非商用-非衍生-保持署名 (CC 4.0 BY-SA)
原文地址: https://evilpan.com/2022/07/17/ptmalloc-notes/
微信订阅: 『有价值炮灰』
TO BE CONTINUED.


ptmalloc cheatsheet相关推荐

  1. 机器学习面试必备 CheatSheet

    点击上方"视学算法",选择加"星标"或"置顶" 重磅干货,第一时间送达 转自 | PyVision 机器学习/数据科学面试常见知识点 Che ...

  2. QGIS打印布局cheatsheet

    作为一篇技术类po文,就不花大量笔墨谈那些奇妙的思想了,以简洁为主. 一.目的 比如你想画一张类似于这样的图(不考虑地图的具体内容),你想知道: 如何添加经纬度? 如何将多个不同的地图放在一起? 如何 ...

  3. Web Bundler CheatSheet, 选择合适的构建打包工具

    题注:Web Bundler CheatSheet 属于 Awesome-CheatSheet 系列,盘点数个常用的开发打包工具清单.欢迎加入阿里南京前端团队,欢迎关注阿里南京技术专刊了解更多讯息. ...

  4. glibc(ptmalloc)内存暴增问题解决

    from:http://blog.chinaunix.net/uid-18770639-id-3385860.html 点击(此处)折叠或打开 #include <stdio.h> #in ...

  5. 【Linux 内核 内存管理】内存管理架构 ② ( 用户空间内存管理 | malloc | ptmalloc | 内核空间内存管理 | sys_brk | sys_mmap | sys_munmap)

    文章目录 一.用户空间内存管理 ( malloc / free / ptmalloc / jemalloc / tcmalloc ) 二.内核空间内存管理 1.内核内存管理系统调用 ( sys_brk ...

  6. c++内存管理优化之ptmalloc,tcmalloc,jemalloc使用实例

    ptmalloc 是glibc的内存分配管理 tcmalloc 是google的内存分配管理模块 jemalloc 是BSD的提供的内存分配管理 写一段代码测试一下 #include <stdl ...

  7. unix cheatsheet

    unix cheatsheet 转载于:https://www.cnblogs.com/iljyya/p/4801630.html

  8. ptmalloc内存分配释放

    出处 分配: 1)获取分配区的锁,为了防止多个线程同时访问同一个分配区,在进行分配之前需要取得分配区域的锁.线程先查看线程私有实例中是否已经存在一个分配区,如果存在尝试对该分配区加锁,如果加锁成功,使 ...

  9. Linux glibc内存管理:用户态内存分配器——ptmalloc实现原理

    文章目录 ptmalloc 设计假设 Arena Chunk Bins 内存分配.释放流程 总结 C++ STL : SGI-STL空间配置器源码剖析 Linux 内存管理 | 物理内存管理:物理内存 ...

最新文章

  1. 20个命令行工具监控 Linux 系统性能(转载)
  2. KubeCon + CloudNativeCon北美2018年会议透明度报告:一项破纪录的CNCF活动
  3. 在updatepanel中使用fileupload控件
  4. A - Junk-Mail Filter HDU - 2473
  5. Spring JDBC的学习
  6. Excel中快速调整行高或列宽为适合内容显示的长度
  7. 【论文】Awesome Relation Extraction Paper(关系抽取)(PART III)
  8. 使用 IntraWeb (2) - Hello IntraWeb
  9. FaceBook ATC 弱网测试工具环境搭建
  10. python_三目运算
  11. 《Algorithms 4th Edition》读书笔记——2.4 优先队列(priority queue)-Ⅵ
  12. GRE阅读高频机经原文及题目之能源消费(逻辑)
  13. Git之git pull【warning】Pulling without specifying how to reconcile divergent branches is
  14. html自动弹出公告代码,网页html弹窗公告代码
  15. 面试官:什么是静态代理?什么是动态代理?注解、反射你会吗?
  16. RFID技术是怎么构成的,主要分为哪几部分?
  17. 服务器购买之后要做什么(一)
  18. linux颜色吸取工具,深度商店应用QElectroTech、Krita、深度取色器、深度截图
  19. todesk显示已登录在_Todesk无法运行
  20. Java内省用法_java内省机制及PropertyUtils使用方法

热门文章

  1. hfc网络文件服务器,网络、LAN、HFC、PLC四种接入方式的总结_网络_LAN_HFC_课课家...
  2. 两年数据对比柱形图_如何选择PPT数据图表类型?
  3. 大连英语培训百家外语支招考研英语冲刺如何备考
  4. android 下载apk并自动安装,Android下载apk并自动安装(兼容Android7.0)
  5. ActionBar取消底部分隔线效果
  6. 李白的四首诗鸿蒙胜安卓,人工智能与诗词高手,模仿《静夜思》各写首诗,竟是人工智能胜...
  7. 【Django在线教育平台】04.模板配置,完成用户登录相关页面和逻辑
  8. Win10系统怎样关闭Windows Defende杀毒软件
  9. 磁卡的工作原理【转】
  10. 数领科技|Solidworks运行所需要的配置需求建议