glibc 2.39 tcache safe-linking 详解:编码、泄露与伪造

tcache 单链表指针编码机制分析,如何用一次 UAF 读拿到堆地址,以及投毒时 next 字段的正确构造

为什么需要 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 投毒的完整流程

  1. 申请三个同 size 的 chunk A、B、C;
  2. 依次 free,tcache 链变成 C -> B -> A;
  3. 用 UAF 写改 C 的 next 为 target ^ mask;
  4. 连续 malloc 三次:拿到 C、B 之后,第三次返回 target。

注意 glibc 2.29+ 的 tcache key 双重释放检查,以及 tcache_get 里的对齐检查(aligned_OK,目标地址需 16 字节对齐)——所以投毒目标一般选全局函数指针表这类天然对齐的地址。

小结

safe-linking 提高了一点门槛,但没有改变本质:只要能泄露堆地址(哪怕只有 »12 的部分),编码就形同虚设。下一篇用一道完整的 UAF 题演示从泄露到 getshell 的全过程。