前言

本文记录了glibc2.27-glibc2.35关于tcache堆利用的一些变化

参考文章:

https://www.freebuf.com/articles/system/234219.html

https://www.roderickchan.cn/zh-cn/2023-03-01-analysis-of-glibc-heap-exploitation-in-high-version/

glibc2.27

在 glibc 2.27 版本中,引入了 tcache 机制,用于优化堆分配的性能。tcache 适用于 0x20 到 0x410 字节范围内的小块内存。释放堆块时,glibc 会优先检查对应大小的 tcache 是否已满,只有在 tcache 满载时,堆块才会被存入 fastbin 或 unsorted bin 等其他 bin 结构。每个 tcache bin 最多可存储 7 个堆块。

在 glibc 2.27 及更早的版本中,tcache 机制缺乏安全性检查,允许 任意 double free。攻击者可以修改 tcache 中堆块的 fd 指针,该指针指向的是前一个堆块的 user data 段,并且 glibc 不会检查 size 字段,从而可以伪造指针,实现 任意地址分配。这一漏洞利用手法被称为 tcache poisoning(tcache 污染)。

glibc2.31

在 glibc 2.31 版本中,tcache 结构体 新增了 key 字段,用于增强安全性。

当一个堆块被释放并进入 tcache 时,glibc 会首先检查该堆块的 key 字段 是否等于当前线程 tcache_perthread_struct 结构体的地址:

  • 如果 key 字段不同,则允许将该堆块存入 tcache,并将 key 字段更新为 tcache 地址。
  • 如果 key 字段相同,glibc 则会遍历 tcache 链表,检查其中是否已存在该堆块,从而防止 double free。

因此,若要绕过此检查并 对同一 tcache 级别的堆块进行 double free,可以在释放前 手动修改 key 字段为其他值,再执行 free,从而成功绕过重复释放检测。

glibc2.35

  • tcachebin 堆指针异或加密(glibc-2.32 引入)
  • tcahebin 链的数量检查(glibc-2.33 引入)
  • fastbin 堆指针异或加密(glibc-2.32 引入)
  • 堆内存对齐检查(glibc-2.32 引入)
  • 移除__malloc_hook 和__free_hook(glibc-2.34 引入)
  • 引入 tcache_key 作为 tcache 的 key 检查(glibc-2.34 引入)

tcachebin 堆指针异或加密

在 glibc 2.31 及更高版本中,tcache 机制针对 fd 指针 引入了新的保护措施,使其不再是简单的前一个堆块地址,而是经过以下处理:

  1. 取当前堆块的地址并 左移 12 位。
  2. 将该值与原本应存入的 fd 指针值(前一个堆块的地址) 进行 XOR 运算,得到新的 fd 值。

绕过方法

当 tcache 仅存有一个堆块 时,可以通过其 fd 指针 提取用于加密的 XOR 密钥:

  • 读取 fd 指针值,然后左移 12 位,即可得到 堆地址。
  • 该泄漏的堆地址可用于后续的 解密 和 伪造 fd 指针。

在 伪造 fd 指针 时:

  • 计算目标 fd 指向的地址,即伪造的下一个堆块地址。
  • 用此地址与 泄漏的 XOR 密钥 进行 XOR 计算,得到正确的加密 fd 值。
  • 将计算出的 伪造 fd 写入目标堆块,确保其符合 tcache 机制的检查。

注意:后续申请的堆块地址应尽量接近泄漏的堆块地址,避免因地址差距过大导致 XOR 计算的密钥变化,影响利用成功率。

tcache 链数量检查

从tcache中重新申请堆块时,会检查当前tcache的数量,如果数量为0,就不会再从tcache中申请。

在此之前,可以在tcache的第一个堆块的fd位置进行tcache poisoning,现在需要多申请至少一个,然后在第二个上改。

堆内存对齐检查

通过错位来申请堆块变成历史,申请的堆块必须对其0x10。

引入 tcache_key 作为 tcache 的 key 检查

tcache_key是一个随机的值,每次放入tcache的堆块的key字段都是随机值,无法通过泄漏tcache的key字段来泄漏堆地址。

移除__malloc_hook 和__free_hook

在2.34以前,可以通过向这两个钩子函数写入system地址或者one_gadget地址来实现getshell,现在成为了历史。

一般来说glibc2.35以后的getshell,可以通过篡改IO_FILE结构体来实现,也可以通过下面这种办法:

在Linux C中,environ是一个全局变量,它储存着系统的环境变量。

它储存在libc中

因此environ是沟通libc地址与栈地址的桥梁。

通过libc找到environ地址后,泄露environ地址处的值,可以得到环境变量地址,环境变量保存在栈中,通过偏移可以得到栈上任意变量的地址。

可以通过计算得到edit函数的返回地址,然后向该地址写入rop链从而实现getshell。

以下是我写的一篇题解

https://www.nssctf.cn/note/set/11600