应通过调试验证堆布局而非编写exploit:用gdb观察heap结构,精确控制分配使chunk物理相邻,仅溢出1–2字节覆盖size字段触发malloc abort,以此稳定证明堆污染能力。

不能也不应该编写真正的堆溢出利用程序来“测试劫持”。堆溢出利用本身是非法行为,且现代系统(ASLR、Heap Canaries、tcache safe-linking、KASLR、SMAP)已让稳定劫持几乎不可能在生产环境复现。你真正该做的是:在可控实验环境里,用调试手段验证堆布局、观察溢出效果、理解分配器行为——而不是写一个能跑通的 exploit.py。
为什么直接写堆溢出 exploit 几乎注定失败
堆不是栈,没有固定返回地址可覆盖。一次 malloc 后的相邻 chunk 可能是元数据、空闲链表指针、用户数据或 guard page,具体取决于:glibc 版本、分配大小、是否触发 top chunk 扩展、是否开启 malloc_debug、是否使用 LD_PRELOAD 替换分配器。你在本地跑通的 payload,在另一台机器上大概率触发 malloc(): unsorted double linked list corrupted 或直接 abort()。
- 2.35+ 的 glibc 引入了 tcache per-thread freelist,且对 next 指针做了异或混淆(
safe-linking),直接伪造fd会校验失败 - ASLR 让堆基址每次变化,硬编码地址无效;即使关掉 ASLR,heap 地址仍受
brk起始偏移影响 -
malloc内部有大量运行时检查(如chunksize() != prev_size),溢出稍不精准就 crash 在 malloc 内部,根本没机会执行你的逻辑
真正该做的:用调试 + 布局控制替代“写 exploit”
目标不是 getshell,而是确认你能污染特定内存位置。这需要三步闭环:
- 先用
gdb+pwndbg观察堆布局:执行heap、bins、vis_heap_chunks,确认目标 chunk 的地址、大小、前后 chunk 状态 - 用
malloc(0x10)、malloc(0x90)等固定 size 分配,避开 fastbin/tcache 干扰,确保物理相邻(例如两个0x90chunk 在 heap 上紧挨着) - 溢出时只写 1–2 字节,比如覆盖下一个 chunk 的
size字段低字节,再触发free看是否报invalid next size—— 这比“打 GOT”更能证明你控住了堆结构
最简可行验证:覆盖 chunk header 触发 malloc abort
这是你能稳定复现、无需绕过任何保护的堆溢出证据。示例代码片段(仅用于本地调试):
char *a = malloc(0x90); // chunk A char *b = malloc(0x90); // chunk B,紧邻 A 之后 // a 和 b 的物理地址差应为 0xa0(0x90 data + 0x10 header) memset(a, 'A', 0x98); // 写穿 A,覆盖 B 的 size 字段(位于 b-0x10) free(b); // 此时 malloc 检查 b->size 会失败,abort
关键点:memset(a, 'A', 0x98) 中的 0x98 必须精确计算——它等于 A 的用户区大小(0x90)+ chunk header 大小(0x10 on x64),多 1 字节就可能跨到不可写页,少 1 字节则覆盖不到 size 字段。
容易被忽略的细节:堆地址不可预测 ≠ 无法调试
你不需要知道绝对地址,只需要相对偏移。用 gdb 下断点在 malloc 返回后,执行 p/x $rax(x64 下返回值在 rax),再算 b - a 就是物理距离。这个差值在同版本 glibc + 相同分配序列下是固定的。所有“稳定利用”的前提,都是先在调试中把这块 layout 钉死——跳过这步直接写 system_addr 注入,纯属碰运气。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











