4. 堆溢出基础
任务目标
编写一个C程序,在堆上分配两个相邻的缓冲区,通过向第一个缓冲区写入超出其大小的数据,覆盖第二个缓冲区的堆元数据(chunk header),最终触发崩溃或异常,直观展示堆溢出对堆管理结构的破坏。
核心原理
glibc的ptmalloc2分配器在堆块头部存储prev_size和size字段(各8字节)。当程序向堆缓冲区写入超出边界的数据时,会覆盖相邻堆块的头部元数据。在释放(free)或合并(consolidate)这些被破坏的堆块时,分配器会依据被篡改的元数据进行操作,导致段错误或双重释放等异常。
完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
// 分配两个相邻的堆块
char *buf1 = (char *)malloc(8); // 实际可用16字节(含8字节头部)
char *buf2 = (char *)malloc(8);
if (!buf1 || !buf2) {
printf("内存分配失败\n");
return 1;
}
printf("buf1 地址: %p\n", (void *)buf1);
printf("buf2 地址: %p\n", (void *)buf2);
printf("buf2 的堆块头部(前16字节)位于 buf1+16 处\n\n");
// 正常写入buf1(8字节内)
strcpy(buf1, "1234567");
printf("正常写入后 buf1: %s\n", buf1);
// 查看buf2的原始堆块头部(前16字节)
printf("\n溢出前 buf2 头部(16字节):\n");
for (int i = 0; i < 16; i++) {
printf("%02x ", (unsigned char)buf2[-16 + i]);
}
printf("\n\n");
// ===== 堆溢出触发点 =====
// 向buf1写入超出8字节的数据,覆盖buf2的堆块头部
// buf1实际可用16字节(含8字节头部),从buf1[0]到buf1[15]不越界
// 但buf1[16]开始就是buf2的prev_size字段
memset(buf1, 'A', 24); // 写入24字节:8字节数据 + 8字节padding + 8字节覆盖buf2的prev_size
// 此时buf2的prev_size被覆盖为0x4141414141414141
// 但buf2的size字段尚未被破坏(位于buf2[-8]处)
printf("溢出后 buf2 头部(16字节):\n");
for (int i = 0; i < 16; i++) {
printf("%02x ", (unsigned char)buf2[-16 + i]);
}
printf("\n\n");
// 进一步覆盖buf2的size字段(位于buf2[-8])
// 需要再写入8字节,使总覆盖达到32字节
memset(buf1, 'B', 32); // 覆盖到buf2的size字段
printf("深度溢出后 buf2 头部(16字节):\n");
for (int i = 0; i < 16; i++) {
printf("%02x ", (unsigned char)buf2[-16 + i]);
}
printf("\n\n");
// ===== 触发崩溃 =====
// 释放被破坏的堆块,glibc在合并/释放时会读取被篡改的元数据
printf("尝试释放被破坏的堆块...\n");
free(buf1); // 释放buf1时,glibc会检查相邻的buf2块并尝试合并
// 由于buf2的size字段被篡改为0x4242424242424242,合并操作会访问非法地址
// 如果程序没有崩溃,继续释放buf2
printf("释放 buf2...\n");
free(buf2);
printf("程序未崩溃(某些系统/分配器配置下可能不崩溃)\n");
return 0;
}
编译与运行
# 编译(关闭栈保护以稳定复现,但堆保护仍生效)
gcc -o heap_overflow heap_overflow.c -fno-stack-protector
# 运行
./heap_overflow
验证方法与判定标准
运行程序后,观察以下现象:
- 溢出前:buf2头部显示正常的堆块元数据(size字段低3位为标志位,值为0x21或0x11等)
- 第一次溢出(24字节):buf2的prev_size被覆盖为
0x4141414141414141(即'AAAA'的ASCII码) - 深度溢出(32字节):buf2的size字段被覆盖为
0x4242424242424242(即'BBBB') - free(buf1)时:glibc尝试向后合并buf2,读取被篡改的size字段,访问非法内存地址,触发段错误(Segmentation Fault)
通过条件:程序在free(buf1)处崩溃,输出类似Segmentation fault (core dumped),证明堆元数据被破坏后导致分配器异常。若程序未崩溃(某些系统或分配器配置下),则观察buf2头部被篡改的十六进制输出,同样证明堆溢出已破坏相邻堆块元数据。
关键说明
- 堆块的实际可用空间可能大于请求大小(glibc最小分配16字节),因此
malloc(8)实际可用16字节,写入16字节以内不会越界 - buf2的堆块头部位于
buf2 - 16处(prev_size占8字节,size占8字节) - glibc 2.27+引入了tcache机制,小堆块释放时先进入tcache,可能不会立即触发合并;若需稳定复现崩溃,可分配较大堆块(如
malloc(200))绕过tcache,或使用mallopt(M_TCACHE, 0)禁用tcache - 本案例仅演示堆溢出对元数据的破坏机制,实际漏洞利用还需考虑ASLR、堆布局等因素