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 已经在链里了,这就形成了环。
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 返回一个栈地址。这意味着什么?意味着你能在栈上写数据,控制返回地址、函数指针——直接拿到代码执行权。
具体步骤:
- 在栈上布置伪造的 chunk 头:需要伪造 size 字段,并且 size 要落在 fastbin 范围内(0x20 ~ 0x80)。
- 触发 free,把伪造 chunk 释放进 fastbin:free 的参数必须指向伪造 chunk 的 user data 起始位置。
- malloc 相同大小的 chunk:fastbin 会返回伪造 chunk 的地址,也就是栈地址。
- 往这个栈地址写数据:覆盖返回地址或关键变量。
看一个简化示例:
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); // 覆盖返回地址
}
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。整个过程环环相扣,少一步都不行。
下面这张图展示了这三种技术的核心关系:
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 变种都会容易理解得多。