4、x86-64 UAF漏洞:Use-After-Free原理、fastbin double free、house of spirit

UAF,全称 Use-After-Free,翻译过来就是「释放后使用」。这名字很直白——你用完一块堆内存,把它 free 掉了,但程序里还留着指向这块内存的指针。之后你又通过这个指针去读写它,那问题就来了。

我刚开始接触堆漏洞时,觉得 UAF 不就是个悬空指针吗?后来才发现,事情远没那么简单。在 glibc 的堆管理机制下,一个被释放的 chunk 会被链入各种 bin 里,它的 fd、bk 指针会被改写。如果你能控制这些指针,就能实现任意地址写。这才是 UAF 真正的威力所在。

4.1 UAF 的底层原理

先看一个最简化的例子:

// 分配两个 chunk
char *p = malloc(0x18);
char *q = malloc(0x18);

// 释放 p
free(p);

// 此时 p 是悬空指针,但程序还在用
strcpy(p, "hello");  // UAF 写
printf("%s\n", p);   // UAF 读

为什么 free 之后还能写?因为 free 只是把 chunk 放进了 fastbin,并没有清空指针 p。glibc 也不会去修改用户数据区(除非开启了某些安全检测)。所以 p 指向的内存仍然可读可写——只不过它现在属于堆管理器了。

这里有个关键点:UAF 的利用价值取决于被释放 chunk 所在的 bin 类型。不同 bin 对 chunk 的修改方式不同,攻击面也不同。

Bin 类型 释放后 chunk 的变化 UAF 利用方式
fastbin fd 指向下一个空闲 chunk 伪造 fd,实现任意地址分配
tcache fd 指向下一个空闲 chunk(单链表) 类似 fastbin,但更简单
unsorted bin fd/bk 指向 main_arena 泄露 libc 地址,改写 bk 实现 unsorted bin attack
small/large bin fd/bk 指向相邻空闲 chunk 相对复杂,但同样可构造任意写

我个人习惯把 UAF 分为两类:读 UAF 和 写 UAF。读 UAF 用来泄露地址(堆地址、libc 地址),写 UAF 用来篡改内存。大多数漏洞利用都是先读后写,两步走。

4.2 fastbin double free

double free 就是重复释放同一块内存。在 fastbin 里做 double free,核心思路是让堆管理器以为有两个不同的空闲 chunk,实际上它们指向同一块内存。

看一个经典的三明治模式:

// 分配三个 chunk
void *a = malloc(0x18);
void *b = malloc(0x18);
void *c = malloc(0x18);

// 释放顺序:a -> b -> a
free(a);
free(b);
free(c);  // 注意:这里释放 c 是为了绕过检测
free(a);  // double free!

为什么中间要插一个 b 和 c?因为 glibc 2.3.5 之后加入了简单的 double free 检测:检查当前释放的 chunk 是否等于 fastbin 顶部的 chunk。如果直接 free(a) 两次,第二次释放时 fastbin 顶部还是 a,就会被检测到。

插入 b 之后,fastbin 链表变成:a → b → a。第二次 free(a) 时,顶部是 b,不是 a,检测通过。但 a 已经在链里了,这就形成了环。

注意:glibc 2.27 之后 tcache 成为默认分配器,tcache 的 double free 检测更弱——它只检查 tcache 里是否已经有这个 chunk。绕过方式也简单:把 tcache 填满,让 chunk 进入 fastbin,再用 fastbin 的 double free 手法。

double free 之后,连续两次 malloc 会拿到同一块内存:

void *p1 = malloc(0x18);  // 拿到 a
void *p2 = malloc(0x18);  // 拿到 b
void *p3 = malloc(0x18);  // 又拿到 a!

// p1 和 p3 指向同一块内存
strcpy(p1, "evil");
printf("%s\n", p3);  // 输出 "evil"

这有什么用?如果 p1 和 p3 被不同的功能模块使用,就能实现交叉写。比如 p1 是数据缓冲区,p3 是对象指针,通过 p1 写数据就能篡改 p3 指向的对象。

4.3 House of Spirit

House of Spirit 这个名字听起来很玄乎,其实核心就一句话:在栈上伪造一个 chunk,然后 free 掉它,再 malloc 回来。

你想想看,正常情况下 malloc 返回的是堆地址。但通过 House of Spirit,你能让 malloc 返回一个栈地址。这意味着什么?意味着你能在栈上写数据,控制返回地址、函数指针——直接拿到代码执行权。

具体步骤:

  1. 在栈上布置伪造的 chunk 头:需要伪造 size 字段,并且 size 要落在 fastbin 范围内(0x20 ~ 0x80)。
  2. 触发 free,把伪造 chunk 释放进 fastbin:free 的参数必须指向伪造 chunk 的 user data 起始位置。
  3. malloc 相同大小的 chunk:fastbin 会返回伪造 chunk 的地址,也就是栈地址。
  4. 往这个栈地址写数据:覆盖返回地址或关键变量。

看一个简化示例:

void vuln() {
    char fake_chunk[0x30];  // 栈上的缓冲区
    char *p;
    
    // 伪造 chunk 头
    // fake_chunk[-8] 处需要是 size = 0x41(包含 prev_inuse 位)
    *(long long *)(fake_chunk - 8) = 0x41;
    
    // 释放这个伪造 chunk
    free(fake_chunk);  // 注意:free 的是 fake_chunk,不是 &fake_chunk
    
    // 现在 fastbin 里有一个指向栈的 chunk
    p = malloc(0x38);  // 返回 fake_chunk 的地址
    
    // 通过 p 写栈
    strcpy(p, shellcode);  // 覆盖返回地址
}
避坑指南:我曾经在调试一个 CTF 题目时,House of Spirit 死活不成功。后来发现是伪造的 size 字段没对齐——glibc 要求 chunk size 必须是 16 字节对齐(64 位下)。另外,伪造 chunk 的 next chunk 的 prev_inuse 位也要正确设置,否则 free 时会触发断言失败。

4.4 三种技术的联动关系

这三种技术不是孤立的。在实际漏洞利用中,它们经常组合使用:

  • UAF 泄露地址:通过读 UAF 拿到堆地址或 libc 地址。
  • fastbin double free 构造任意地址分配:利用 double free 让 malloc 返回一个可控地址。
  • House of Spirit 把攻击目标指向栈:当堆布局受限时,把攻击面转移到栈上。

我见过一个经典的组合攻击链:先用 UAF 泄露堆地址,然后用 double free 在堆上伪造一个 fake chunk,最后用 House of Spirit 把 fake chunk 释放进 fastbin,再 malloc 回来写 shellcode。整个过程环环相扣,少一步都不行。

下面这张图展示了这三种技术的核心关系:

UAF 漏洞利用技术体系 Use-After-Free 读 UAF(信息泄露) 写 UAF(内存篡改) 组合利用 泄露堆地址 / libc 地址 fastbin double free House of Spirit 最终目标:任意地址读写 → 代码执行 三种技术可独立使用,也可组合攻击。实际漏洞利用中,往往需要先泄露地址,再构造写原语。

4.5 实战中的注意事项

说了这么多理论,最后聊点实战经验。我在做漏洞利用时,有几个原则一直遵守:

  • 先确认 UAF 的读写能力:不是所有 UAF 都能同时读和写。有些只能读,有些只能写。先搞清楚你能做什么。
  • double free 之前先填 tcache:glibc 2.27+ 环境下,tcache 的优先级高于 fastbin。如果 tcache 没满,double free 会进入 tcache,而 tcache 的 double free 检测更严格(虽然也有绕过方式)。
  • House of Spirit 的 size 校验:glibc 2.29 之后加入了更严格的 size 校验,伪造的 size 必须与 next chunk 的 prev_size 一致。这意味着你需要同时伪造两个 chunk 头。
  • 调试器是你的眼睛:我习惯用 pwndbg 的 heap、fastbins、tcache 命令实时查看堆状态。每次 malloc/free 之后都看一眼,确认你的操作是否生效。

核心要点回顾:

  • UAF 的本质是悬空指针,利用点在于被释放 chunk 在 bin 中的指针布局。
  • fastbin double free 通过构造环形链表,让 malloc 返回同一块内存。
  • House of Spirit 把栈地址伪装成堆 chunk,实现栈上任意写。
  • 三种技术可以串联使用,形成完整的攻击链。

嗯,这一章的内容就到这里。UAF 是堆利用的入门砖,也是很多高级攻击的基础。把这一章吃透,后面的 tcache poisoning、fastbin attack 变种都会容易理解得多。