为什么需要 safe-linking
glibc 2.32 起,tcache 和 fastbin 的空闲链表指针不再明文存储,而是与 chunk 地址做异或编码。目的是让"只知道堆上内容、不知道堆地址"的攻击者无法直接伪造 fd/next 指针。
编码公式
PROTECT_PTR 与 REVEAL_PTR 的定义(glibc 源码 malloc.c):
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
也就是说,chunk 里存的 next 字段实际是:
存储值 = (本 chunk 的 user 区地址 >> 12) ^ 下一个 chunk 的地址
关键破绽:链上只剩一个节点时
tcache 是单链表。当一个 bin 里只有一个空闲 chunk 时,它的 next 指向 NULL:
存储值 = (chunk >> 12) ^ 0 = chunk >> 12
一次 UAF 读就能拿到 chunk >> 12。有了它,任何指向该 chunk 附近的伪造 next 都可以正确编码:
mask = u64(leaked_next) # = chunk_addr >> 12
poisoned = p64(target ^ mask) # 写回 next 字段
tcache 投毒的完整流程
- 申请三个同 size 的 chunk A、B、C;
- 依次 free,tcache 链变成 C -> B -> A;
- 用 UAF 写改 C 的 next 为
target ^ mask; - 连续 malloc 三次:拿到 C、B 之后,第三次返回 target。
注意 glibc 2.29+ 的 tcache key 双重释放检查,以及 tcache_get 里的对齐检查(aligned_OK,目标地址需 16 字节对齐)——所以投毒目标一般选全局函数指针表这类天然对齐的地址。
小结
safe-linking 提高了一点门槛,但没有改变本质:只要能泄露堆地址(哪怕只有 »12 的部分),编码就形同虚设。下一篇用一道完整的 UAF 题演示从泄露到 getshell 的全过程。