堆漏洞利用¶
PWN · 知识域。glibc 堆(ptmalloc)漏洞利用全家族。标签:Unlink、Fastbin Attack、Unsorted Bin Attack、Large Bin Attack、Tcache Attack、Tcache Stashing Unlink。
触发特征¶
- 菜单式:create/delete/edit/show + UAF/DF/越界写/溢出。
- 先问三件事:glibc 版本(决定 tcache 语义)、有无 show(泄露通道)、有无 edit(写通道)。
版本地图¶
| glibc | 关键变化 |
|---|---|
| 2.23 | 无 tcache,fastbin/unsorted 时代经典 |
| 2.26 | tcache 引入(7 个/桶,无 size 校验) |
| 2.29 | tcache double-free 检测(key 字段) |
| 2.32 | fd 指针异或加密(protection) |
| 2.34 | 移除 __free_hook/__malloc_hook,转 FSOP/exit handlers |
| 2.35+ | House of Apple 2 / cat / potion 系 |
Unlink¶
- 经典 unlink 攻击:伪造 fd/bk 满足
fd->bk = p && bk->fd = p→ 全局指针指向自身,后续 edit 改任意地址(DEF CON Qualifier 2014 自定义分配器到 GOT)。 - 现代变体:global 最大 chunk 指针数组 + top chunk 合并(SECCON 2016 unsafe unlink 到 BSS)。
- talloc 池头伪造任意读写(Boston Key Party 2016)。
Fastbin Attack¶
- fastbin double free(bypass 检测:两个不同 chunk 间插一个)→ 分配到伪造 chunk。
- 目标:GOT、
__malloc_hook附近(size 伪造 0x7f 对齐技巧)、栈。 - LSB-only 污染:文件名正则限制下仅改堆指针低位(BSIDSSF 2019);跨桶晋升 tcache→fastbin(BCTF 2018);stdout vtable 两段劫持破 PIE+Full RELRO(ASIS CTF 2017)。
- musl libc:meta 指针 + atexit(UNbreakable 2026)。
Unsorted Bin Attack¶
- 改 victim 的 bk 指向目标-0x10 → 目标地址写入 main_arena 地址(libc 泄露或写大值)。
- 现代用法:打 stdin
_IO_buf_end(HITCON 2017);经 mp_ 结构破坏 unsorted(HITCON 2017)。 - calloc chunk 的 IS_MMAPPED 位翻转造 unsorted 泄露(0CTF 2017)。
Large Bin Attack¶
- largebin 插入时 bk_nextsize 写原语:向任意地址写堆地址 → 为 FSOP 铺路(打
_IO_list_all指向伪造 FILE)。 - 残留指针泄露:picoCTF 2018 未清零 chunk。
Tcache Attack¶
- tcache poisoning:改 next 指针 → 任意地址分配;2.32+ 需先泄露堆地址算异或保护(GF(2) 高斯消元多次投毒,Midnight Flag 2026;指针解密泄露)。
- tcache double free:key 字段清零绕过(2.29+);隐藏菜单选项触发(FireShell 2019)。
- tcache stashing unlink attack:smallbin 取走后剩余 chunk 经 tcache 放置,
bk+0x10写原语打_IO_2_1_stdout_等。 - strcpy null 字节溢出 + 后向合并(HITCON 2018);单字节引用计数下溢 UAF(WhiteHat Grand Prix 2018)。
- 菜单堆题 UAF:menu strdup/free 顺序差(PlaidCTF 2016);realloc(ptr,0) 当 free(AceBear 2018)。
House of 系列(按目标版本选)¶
- House of Force(2.28 前):改 top chunk size 巨大 → malloc 到任意地址(CSAW CTF 2016)。
- House of Orange:无 free 的程序靠 top 耗尽触发
_int_free→ FSOP 起点。 - House of Einherjar:off-by-null 触发后向合并伪造 chunk(0xFun 2026)。
- House of Lore:smallbin 伪造 bk。
- House of Spirit:伪造 fastbin chunk 释放再分配。
- House of Apple 2(2.34+ 主流):伪造
_IO_FILE走_IO_wfile_overflow→setcontext+61ROP;SUID 变体 setcontext(Midnight Flag 2026)。 - House of Cat / Secretgun 等:
_IO_wfile链系变体。
应用层堆操控¶
- 应用级 heap grooming:用业务操作(而非 malloc 调试)码堆(Codegate 2013)。
- mmap/munmap size 不匹配造线程栈重叠 UAF(0CTF 2017);Go 切片容量别名结构拷贝(CONFidence Teaser 2019);GF(2^8)/应用层组合见场题。
工具速查¶
pwndbg: heap / bins / vis # 查看堆布局
libheap / heap-viewer; gdb-gef: heap-analysis-helper
patchelf 换 libc 复现; libc-database 找版本
其他堆管理器¶
- musl libc:meta 指针结构 + atexit 利用(UNbreakable 2026)——无 tcache/fastbin 体系,套路完全不同。
- 自定义分配器:题目自带 malloc/free(常见于游戏/VM 题)——unlink 校验往往缺失,unsafe unlink 直接打(DEF CON Qualifier 2014);talloc 池头伪造(Boston Key Party 2016)。
- Windows 堆(NT heap/LFH):堆喷占位 + LFH 重用,见 渗透测试-漏洞利用 实战差异节。
- 判定:先看二进制是否静态链接 malloc / 是否带自定义 arena;题目"内存池/对象池"字样即提示自定义管理器。
转向¶
- 泄露后拿 shell 困难(2.34+)→ 其他类型PWN FSOP/exit hooks;内核堆 → Linux内核漏洞利用
例题¶
House of Apple 2 完整结构(glibc 2.34+)¶
完整链:UAF → unsorted bin 泄 libc → safe-linking 泄堆 → tcache 投毒到 _IO_list_all → 伪造 FILE → exit 触发:
fake_file = flat({
0x00: b' sh\x00', # _flags:" sh" → system(fp) 的命令
0x20: p64(0), # _IO_write_base = 0
0x28: p64(1), # _IO_write_ptr = 1 (> base 才走 overflow)
0x88: p64(heap_addr), # _lock:合法可写地址即可
0xa0: p64(wide_data_addr), # _wide_data
0xd8: p64(io_wfile_jumps), # vtable = _IO_wfile_jumps(过 vtable 校验)
}, filler=b'\x00')
fake_wide_data = flat({
0x18: p64(0), 0x30: p64(0),
0xe0: p64(fake_wide_vtable_addr), # _wide_vtable(宽字符 vtable 不校验!)
})
fake_wide_vtable = flat({ 0x68: p64(libc.sym.system) }) # __doallocate 偏移
触发链:exit() → _IO_flush_all_lockp → _IO_wfile_overflow → _IO_wdoallocbuf → system(fp),而 fp 开头是 " sh\x00"。
Safe-linking 手工混淆(glibc 2.32+)¶
tcache 的 fd 指针已加密,投毒时必须自己算:
mangled_fd = target_addr ^ (current_chunk_addr >> 12)
# 反向泄露:free 后 fd = 0,读出的 mangled NULL = chunk_addr >> 12 → 直接得到堆基址页
SUID 场景变体:system 换 setcontext(Midnight Flag 2026)¶
SUID 程序里 system("/bin/sh") 会被 dash 因 uid != euid 丢权——把宽 vtable 目标换成 setcontext+61,先 setuid(0) 再 shell:
fake_wide_vtable = flat({ 0x68: p64(libc.sym.setcontext + 61) })
# setcontext 从 RDX(= fp->_wide_data)偏移处取寄存器:
fake_wide_data = flat({
0x68: p64(0), # RDI = 0 → setuid(0)
0xa0: p64(rop_chain_addr), # RSP
0xa8: p64(libc.sym.setuid), # RIP(第一跳)
0xe0: p64(fake_wide_vtable_addr),
})
rop = flat([pop_rdi_ret, 0, libc.sym.setuid, libc.sym.system,
next(libc.search(b'/bin/sh\x00'))])
关键认知:setcontext 是万能栈迁移器——RSP/RDI/RIP 全部从可控内存加载;FILE 体系 + setcontext 是 2.34+ 时代堆题的标准终局。