内存型漏洞常见模式(三):Use-After-Free (UAF) 的触发机理与生命周期
内存型漏洞常见模式(三):Use-After-Free (UAF) 的触发机理与生命周期

在现代二进制漏洞攻防中,随着操作系统与编译器对静态栈溢出防护(Stack Canary、NX、PIE)的日趋完善,攻击者逐渐将火力集中到更为复杂多变的堆(Heap)内存空间。而在堆利用技术领域,释放后使用(Use-After-Free, UAF) 长期占据着统治地位。
UAF 漏洞不仅广泛存在于浏览器内核(如 Chrome V8、WebKit)、操作系统内核(Windows/Linux Kernel)中,也是复杂 C/C++ 多线程应用中最难以通过静态代码分析彻底根除的逻辑缺陷之一。
一、UAF 漏洞的根本成因与生命周期模型
UAF 漏洞的物理本质极其纯粹:一块堆内存被 free() 或 delete 释放后,指向该内存的原始指针(悬垂指针, Dangling Pointer)未被及时置为 NULL,并在随后的生命周期中被程序错误地再次引用(Dereference)。
阶段 1: 内存分配 (Alloc)
Pointer A ---> [ 堆块 Chunk (大小 N, 包含虚表/函数指针) ]
阶段 2: 内存释放未清零 (Free -> Dangling)
Pointer A ---> [ 已释放的 Chunk (进入 Bin 链表, 悬垂状态) ]
阶段 3: 堆占位 (Heap Spray / Reallocation)
Pointer B ---> [ 重新申请相同大小 N 的 Chunk (攻击者控制数据) ]
(此时 Pointer A 与 Pointer B 指向同一块物理内存)
阶段 4: 触发执行 (Use)
执行 A->func_ptr() ---> 实际执行了攻击者伪造的指针,控制流被劫持!
二、经典 UAF 漏洞代码复现与虚表劫持
为了直观呈现控制流被劫持的全过程,我们通过一段包含函数指针的 C 结构体代码进行分析:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct UserSession {
char username[24];
void (*print_role)(void); // 8 字节函数指针 (64位)
} UserSession;
typedef struct AttackerControlledChunk {
char fake_payload[32]; // 同样分配 32 字节大小
} AttackerControlledChunk;
void normal_user_role(void) {
printf("[*] 普通用户权限\n");
}
void privileged_admin_shell(void) {
printf("[+] [CRITICAL] 成功劫持控制流!获取管理员特权 Shell\n");
// 实际利用中此函数指向 system("/bin/sh") 或 ROP 链
}
int main(int argc, char *argv[]) {
printf("=== Use-After-Free 原理演示 ===\n");
// 1. 分配原始对象
UserSession *session = (UserSession *)malloc(sizeof(UserSession));
strncpy(session->username, "alice", sizeof(session->username) - 1);
session->print_role = normal_user_role;
printf("[1] 原始会话创建完成,调用函数指针: ");
session->print_role();
// 2. 漏洞发生点:释放内存但未置空指针 (产生悬垂指针)
free(session);
printf("[2] 会话内存已释放,但 session 指针依然保留地址: %p\n", (void*)session);
// 3. 攻击者申请相同大小的堆块,ptmalloc/glibc 会优先复用刚释放的 tcache/fastbin 块
AttackerControlledChunk *fake_chunk = (AttackerControlledChunk *)malloc(sizeof(AttackerControlledChunk));
// 填充 24 字节垃圾数据,精准覆盖 print_role 函数指针位置
memset(fake_chunk->fake_payload, 'A', 24);
*(unsigned long long *)(fake_chunk->fake_payload + 24) = (unsigned long long)privileged_admin_shell;
printf("[3] 恶意堆块已占位原内存地址: %p\n", (void*)fake_chunk);
// 4. 再次调用原悬垂指针 (Use)
printf("[4] 再次触发原指针调用: ");
session->print_role(); // 此时执行了 privileged_admin_shell
return 0;
}
在上述代码中,当 session 释放后,堆分配器(如 glibc 的 tcache bin)会将该内存标记为空闲。随后 fake_chunk 请求相同尺寸的内存,分配器为了提升性能直接将刚释放的内存返回给它。此时攻击者注入的恶意函数指针恰好覆盖了原本 session->print_role 的偏移位置,悬垂指针一旦被解引用,程序控制流即刻失陷。
三、现实对抗中的堆风水(Heap Feng Shui)与利用壁垒
在复杂的真实应用程序(如浏览器内核)中,利用 UAF 远比简单的玩具代码艰难,攻击者通常需要精巧运用以下技术:
1. 堆风水与内存占位(Heap Spraying)
由于应用中存在大量并发分配与释放,为了确保目标堆块被攻击者的数据 100% 占位,攻击者通常先进行“堆排布整理(Heap Grooming)”:
- 批量申请大量固定大小的对象,填满零散的内存碎片。
- 制造出交替排列的对象序列,再主动释放特定对象留下“空洞”。
- 触发目标 UAF 的释放逻辑,并立即批量喷射带有恶意虚表指针的对象填补空洞。
2. 绕过 ASLR 的两阶段利用
在开启全保护的环境下,攻击者无法直接预知目标函数的内存绝对地址:
- 第一阶段(信息泄露):利用 UAF 覆盖对象的长度字段或读取残留指针,造成越界读(Out-of-Bounds Read),将动态库或堆基址泄露给攻击脚本。
- 第二阶段(控制流劫持):根据计算出的基址动态组装伪造的虚表(Fake Vtable),再次触发 UAF 执行特权代码。
四、防御与系统级免疫方案
治理 UAF 漏洞必须从语言层面、分配器加固与运行时检测三维发力:
- 指针安全规范(Safe Pointer Idiom):
- 严格落实
FREE_AND_NULL宏,在执行free(p)后强制紧跟p = NULL;。 - 在 C++ 代码中全面淘汰原始裸指针(Raw Pointers),强制使用
std::unique_ptr与std::shared_ptr智能指针管理对象生命周期。
- 严格落实
- 现代安全分配器(Hardened Allocators):
- 使用具备隔离堆(Isolated Heap)与延迟释放机制的安全分配器(如 Google PartitionAlloc、DieHarder、Scudo)。
- 安全清零(Zero-on-Free):在内存释放时立即覆写随机噪声或置零,破坏利用残留数据构造的信息泄露。
- 运行时动态检测门禁:
- 研发与集成测试阶段全面开启 LLVM AddressSanitizer(ASan)。ASan 会在堆块周围插入隔离区(Redzones),并将释放的内存放入隔离池(Quarantine),一旦检测到访问已释放内存即刻打印完整的调用溯源并中止进程。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)