Fastbin Dup 利用原理与双重释放(Double Free)缓解机制演进
在 Linux glibc 堆内存管理机制中,Fastbin 是为了加速小尺寸内存分配而设立的单向无头链表结构。早期二进制利用中,Fastbin Dup 作为最基础且威力巨大的堆利用手法之一,核心在于通过非连续的双重释放(Double Free)破坏堆管理器的内部链表结构,使得两个指向同一块物理内存的指针同时处于可用状态,进而篡改链表指针实现任意地址写。
Fastbin 机制与 Double Free 形成机理
在 glibc 堆分配器中,Fastbin 处理通常小于 80 字节(x86-64 下默认最大支持 0x80 字节的 chunk 大小,视全局变量global_max_fast而定)的内存块。Fastbin 采用单向链表结构管理空闲 chunk,遵循后进先出(LIFO)原则。分配器使用 chunk 头部的fd(forward pointer)指针指向链表中前一个被释放的 chunk,并且在释放时不进行相邻 chunk 的合并(不清除 inuse 标志位),以此保证极致的分配与回收性能。
在 glibc 2.26 之前,glibc 对 fastbin chunk 的free操作仅进行了一处极为简陋的安全检查:校验当前被释放的 chunk 地址是否与对应 fastbin 链表头部的 chunk 地址相同。
/* glibc 早期 _int_free 中对 fastbin 的安全检查 */ if (__builtin_expect (old == p, 0)) { errstr = "double free or corruption (fasttop)"; goto errout; }这段检查只比对了old(当前 fastbin 头节点)与p(待释放 chunk),这意味着如果我们在两次释放同一个 chunk 之间插入对另一个 chunk 的释放操作,就可以轻松绕过此检查。
经典 Fastbin Dup 攻击路径推演
假设程序中按顺序申请了两个大小相等的 fastbin 块a和b:
void *a = malloc(0x20); // 实际 chunk 尺寸 0x30 void *b = malloc(0x20); // 实际 chunk 尺寸 0x30 void *c = malloc(0x20); // 执行交替释放 free(a); // fastbin[0x30]: a -> NULL free(b); // fastbin[0x30]: b -> a -> NULL free(a); // 绕过 old == p 检查,fastbin[0x30]: a -> b -> a -> ... (形成环)此时fastbin链表中已经形成了自引用环。随后攻击者按如下步骤申请并覆写:
- 第一次调用
malloc(0x20)返回指针a。此时链表头变为b,b->fd仍指向a。 - 写入数据到
a,覆盖其fd字段为一个伪造的目标地址(例如栈上伪造的 chunk 或__malloc_hook前方满足 size 检查的位置fake_chunk)。 - 第二次调用
malloc(0x20)返回指针b。此时链表头重新回到a,但a->fd已经被攻击者修改为fake_chunk。 - 第三次调用
malloc(0x20)再次返回指针a。此时分配器将a->fd(即fake_chunk)设置为新的链表头。 - 第四次调用
malloc(0x20)将直接返回fake_chunk的用户数据区,攻击者成功在受控目标内存地址获取可写句柄。
// 伪造利用代码段 void *p1 = malloc(0x20); void *p2 = malloc(0x20); void *p3 = malloc(0x20); free(p1); free(p2); free(p1); char *r1 = (char *)malloc(0x20); // 篡改 r1 (即 p1) 的 fd 指针,指向目标 fake chunk *(unsigned long long *)r1 = target_fake_chunk_addr; char *r2 = (char *)malloc(0x20); // 获得 p2 char *r3 = (char *)malloc(0x20); // 再次获得 p1 char *r4 = (char *)malloc(0x20); // 成功拿到 target_fake_chunk_addrglibc 缓解机制的技术演进
随着 glibc 版本的不断更新,针对 fastbin 双重释放和堆指针篡改的防护措施逐步收紧。
1. glibc 2.26:引入 tcache 对 fastbin 的截流
在 glibc 2.26 中引入了线程本地缓存(Thread Local Cache, tcache)。默认情况下,小于等于 0x408 字节的 chunk 会优先进入对应大小的 tcache bin(容量上限为 7 个 chunk)。
此时若要利用 fastbin dup,必须先耗尽(fill up)对应 size 的 tcache 链表。例如连续申请 7 个同尺寸 chunk 并释放填满 tcache,后续的 chunk 释放才会真正进入 fastbin,从而激活 fastbin dup 的利用链路。
2. glibc 2.29:遍历 fastbin 链表杜绝重入
从 glibc 2.29 开始,分配器在释放 fastbin chunk 时不再仅仅检查头节点,而是遍历该 fastbin 桶中的全部节点:
/* glibc 2.29+ _int_free 中的 fastbin 链表遍历检测 */ if (__builtin_expect (old == p, 0)) malloc_printerr ("double free or corruption (fasttop)"); { mchunkptr tmp = old; while (tmp) { if (__builtin_expect (tmp == p, 0)) malloc_printerr ("double free or corruption (fasttop)"); tmp = tmp->fd; } }这一检查直接终结了free(a) -> free(b) -> free(a)这种经典环状构造手法。一旦a已经存在于当前 fastbin 链表中的任何位置,再次释放a都会触发 abort 并打印错误信息。
3. glibc 2.32:Safe-Linking 指针混淆
glibc 2.32 引入了Safe-Linking(安全指针链接)机制,对 fastbin 和 tcache 的fd单链表指针进行位运算加密。
其核心混淆公式为:
#define PROTECT_PTR(pos, ptr) \ ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr))) #define REVEAL_PTR(ptr) \ PROTECT_PTR (&ptr, ptr)在 safe-linking 下,写入fd的指针必须是(当前 chunk 存放 fd 的地址 >> 12) ^ 真实目标地址。若攻击者无法泄露堆基址(Heap Base Address),就无法算出有效的 ASLR 随机化掩码,写入非法构造的指针会在解引用时触发页错误异常崩溃。
现代防御与编码修复规范
在实际工业代码与红蓝对抗场景中,针对此类堆安全隐患需要从多层进行彻底阻断:
- 指针释放后立即置空(Dangling Pointer Elimination):
在所有free(ptr)操作后强制设置ptr = NULL,从根源上杜绝 UAF(Use-After-Free)与 Double Free。
#define SAFE_FREE(p) do { \ if ((p) != NULL) { \ free(p); \ (p) = NULL; \ } \ } while(0)- 启用现代内存分配器替代方案:
在对安全性要求极高的场景中,使用Hardened Malloc(如 Android 默认的 Scudo 分配器或 GrapheneOS hardened_malloc),引入基于 chunk 元数据的隔离、校验和随机化防护,使传统的堆结构篡改手法彻底失效。