House Of Botcake
参考资料:https://www.cnblogs.com/brain-Z/p/16853137.html
House of Botcake:
感觉不是很复杂的利用手法,这里就简单对攻击方式进行分析,之后通过一道题目去深入了解House of Botcake
https://www.yuque.com/idcm/wnemg9/gvr9dhnayk94sxfw,如果了解利用手法可以直接看题目。
house of botcake 的利用背景:
在 Glibc 2.28 及以前,Tcache 几乎是在裸奔。当你 free(chunk) 时,系统根本不管这个 chunk 是不是已经在 Tcache 里了。所以你只要连续 free(A); free(A);,就能造成 Double Free,让 chunk 的 fd 指向自己,轻松实现 Tcache Poisoning。
glibc 2.29 开始,Tcache 的结构中引入了 key 字段(指向对应 tcache 结构体自身的指针)。当释放 chunk 到 tcache 时,系统会遍历该单向链表检查 key,从而直接缓解了传统的 Tcache Dup(Double Free)。House of Botcake 通过将 chunk 隐藏在 Unsorted Bin 的合并块中,完美绕过了这个 key 的检查,是这个大版本区间内的核心利用手法。
key 字段检查:
Glibc 2.29 引入了 key 字段检查。
当你释放一个 chunk 进 Tcache 时,它的用户数据区前 16 字节会被强制写入两个值:
next** (原fd)**:指向下一个 Tcache chunk。key:指向当前线程的 Tcache 管理结构体 (tcache_perthread_struct) 的地址。
防守逻辑是这样的: 当你试图 free(A) 时,Glibc 会先看一眼 A->key。
- 如果
A->key == tcache的地址,说明这个 chunk 可能已经在 Tcache 里了。 - 接着,系统会遍历对应的 Tcache 链表。如果真的在链表里找到了
A,就会立刻抛出double free or corruption (fasttop)异常并中止程序。
house of botcake 的利用手法:
House of Botcake 的巧妙之处在于:key 字段的写入和检查,仅仅针对 Tcache。如果 chunk 被释放到了别的 Bin,比如 Unsorted Bin,系统压根不会写入 key,也不会检查 key。
Botcake 就是利用了这个盲区,通过堆块合并把目标 chunk 伪装起来。
完整的绕过推演过程如下:
假设你有两个相邻的堆块:Chunk A (低地址) 和 Chunk B (高地址,目标块)。你的目标是对 Chunk B 进行 Double Free。
- 填满 Tcache: 首先连续释放 7 个同等大小的 chunk,把 Chunk B 对应的 Tcache 链表填满。
- 第一次释放 Chunk B(无 Key): 执行
free(B)。因为 Tcache 已经满了,系统只能把 Chunk B 扔进 Unsorted Bin。 __进入 Unsorted Bin 的 chunk,只会写入fd和bk指针,不会写入 Tcache 的key。此时 Chunk B 的key位置通常是垃圾数据或者bk的一部分。 - 释放 Chunk A 触发合并: 执行
free(A)。Chunk A 也进入 Unsorted Bin。Glibc 会检查相邻的块,发现上面的 Chunk B 也是空闲的,于是将 A 和 B 合并成一个巨大的 Unsorted Bin chunk。 - 此时,Chunk B 在物理内存上已经成为了这个巨大 chunk 的内部一部分。
- 腾出 Tcache 空间: 随便
malloc一个对应大小的 chunk,让满载的 Tcache 腾出一个空位(变成 6/7)。 - 第二次释放 Chunk B: 利用漏洞如 UAF,再次执行
free(B)。 这时候 Glibc 开始走释放逻辑了:- Glibc 检查
B->key是否等于tcache。 - 因为 Chunk B 之前是进入 Unsorted Bin 并被合并的,它的
key根本就没被设置过,所以B->key != tcache。 - 检查直接通过!Glibc 以为这是一个新鲜的 chunk,开开心心地把它放进了 Tcache 中。
- Glibc 检查
最终结果导致堆情况如下:
- Chunk B 存在于 Tcache 中,我们可以对他随意分配。
- Chunk B 同时也包裹在那个巨大的 Unsorted Bin chunk 里面。
接下来只需要把那个巨大的 Unsorted Bin chunk 申请出来,就可以直接覆写包含在其中的 Chunk B 的内存。由于 Chunk B 现在在 Tcache 链表里,覆写它的前 8 个字节,就等于篡改了 Tcache 的 fd 指针,从而实现了任意地址分配。
为什么要制造堆重叠?
其实我在这里有一点疑惑:既然 Chunk B 掉进 Unsorted Bin 之后就没有 key 了,那我不合并,直接再 free(B) 一次不行吗?为什么非要搭上一个 Chunk A 去触发合并呢?
制造合并堆块的根本目的,不仅仅是为了绕过检查,更是为了达成堆利用创造出 Overlapping Chunks
绕过 key 检查只是手段,而内存重叠才是我们为了劫持控制流真正想要的结果。
1. 假设不合并,会发生什么?
如果只释放 Chunk B 到 Unsorted Bin,然后直接利用 UAF 再次 free(B),虽然绕过了 Tcache 的 key 检查,B 会进入 Tcache。此时 B 既在 Unsorted Bin,又在 Tcache 中。 这确实是一个 Double Free,但是你很难利用它。 因为当你再次 malloc 把 B 申请出来时,你想要修改它的 fd 指针来实现 Tcache Poisoning。但在你申请它的瞬间,它就从 free 状态变成了 allocated 状态,你需要下一次再去编辑它,这往往受限于程序本身的逻辑,比如有的题目只能写一次,或者不允许直接编辑堆块。(换句话来说,如果题目没有限制 edit 的次数,我们就可以不考虑制造堆重叠)
2. 合并
加入 Chunk A 并触发合并后,情况发生了质变。
当 Chunk A 和 Chunk B 合并成一个巨大的堆块(我们称之为 Chunk A+B)时,Glibc 的视角发生了变化:
- Glibc 认为:现在 Unsorted Bin 里只有一个巨大的空闲块 Chunk A+B。原本属于 Chunk B 的那片内存,现在变成了这个巨大堆块的 user_data 的一部分。
此时,我们利用漏洞再次 free(B),把 B 塞进 Tcache。
它既是挂在 Unsorted Bin 里的 Chunk A+B,也是挂在 Tcache 里的 Chunk B,而且物理位置完全被包裹在 Chunk A+B 的内部。
3. 获得合法修改 Tcache fd 指针的权限。
- 申请大块:你发送请求,
malloc一个和 Chunk A+B 尺寸匹配的内存。 - 篡改 chunkB_fd:现在你拥有了这整块巨大内存的合法写入权,由于 Chunk B 被包裹在里面,你可以像往常输入普通字符串一样,直接向这块内存写入数据。我们可以把 chunkB 的 fd 指针修改成
__free_hook、system的 GOT 表,或者是某个保存返回地址的栈空间。 - 任意地址分配:接下来只需要再
malloc两次Chunk B 的大小,利用 Tcache Poisoning 第二次malloc返回的指针就会直接指向你刚才伪造的目标地址。show 的时候就可以泄露出 heap_base
更新: 2026-04-28 21:03:53
原文: https://www.yuque.com/idcm/wnemg9/qqi8qrvhoqvqv4uy