news 2026/10/28 1:45:04

01|Tcache Poisoning 理论:2.27 为什么完全不校验 fd

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
01|Tcache Poisoning 理论:2.27 为什么完全不校验 fd

01|Tcache Poisoning 理论:2.27 为什么完全不校验 fd

一句话:tcache 是单链表,malloc 取块只读entries[tc_idx]的 next,不做任何地址校验。
核心:*B = target→ 两次 malloc → 第二次返回 target
适用:glibc 2.26 ~ 2.28(2.29 加 key,2.32 加 Safe-Linking)


0. 前提条件

想用 tcache poisoning,必须同时满足这四条:

条件

说明

不满足会怎样

glibc ≥ 2.26

有 tcache 机制

只能走 fastbin / unsorted bin

目标尺寸 ≤ 0x410

tcache 的最大档位

块会进 unsorted,没有 tcache 链

能改已释放块的 next

UAF / 堆溢出 / 块重叠

没有入口,无法投毒

目标地址可写

否则第二次 malloc 用的时候崩

只能改「可写且不在乎原值」的地方


1. 什么是 Tcache

在 C 语言里,我们用malloc()申请内存、用free()释放内存,这些内存来自堆(Heap)。

把堆想象成一个巨大的公共图书馆:malloc()是借书,free()是还书。

以前每次还书,图书管理员都要严格按分类把书放回指定书架,还要把相邻的空位合并起来——严谨,但慢。

为了提速,Linux 的内存管理器(glibc 2.26 起)引入了一个新机制:Tcache(Thread Local Caching,线程本地缓存)。

它就像管理员桌边的一辆「临时还书车」:

  • 速度极快:还书时不再去书架找位置,直接扔到还书车最上面。
  • 后进先出(LIFO):有人借同样大小的书,直接从最上面拿刚放上去的那本。
  • 容量有限:2.26~2.29 默认每档最多7个(mp_.tcache_count),2.30 起改为64个。放满后剩下的才进 fastbin / unsorted bin。

2. Tcache 在内存里长什么样

内存块正在使用时,里面装的是你的数据。

但它被free()放进 tcache 之后,前 16 个字节会被系统征用(64 位):

+0x00 next ← 指向链表中下一个空闲块 +0x08 key ← 2.29 起才有,存的是 tcache 结构自身地址,用于检测 double free

于是这些释放的块被串成了一条单向链表。

假设依次申请 A、B,然后依次释放:

free(A) → tcache: A -> NULL free(B) → tcache: B -> A -> NULL (后进先出,B 在最前) malloc() → 返回 B,链表变回 A -> NULL

账本结构(tcache_perthread_struct)

tcache 不是一个独立对象,它的「账本」就放在堆的第一块里,共0x250字节:

heap + 0x000 tcache_perthread_struct ├─ counts[64] 每档已缓存的数量(1 字节 × 64) └─ entries[64] 每档的链表头(8 字节 × 64) heap + 0x250 ← 第一个用户 chunk 从这里开始

布局图:

+----------------------+ 0x000 | counts[0..63] | | entries[0..63] | +----------------------+ 0x250 | 用户 malloc 的第一块 | +----------------------+

这就是为什么算堆偏移时,要先扣掉 0x250。


3. 为什么 Tcache 容易被攻击

Tcache 设计的唯一目的就是快。为了极致速度,早期版本(2.26 / 2.27)几乎去掉了所有安全检查:

  • 不检查重复释放:同一本书连续还两次,管理员不会发现。
  • 不检查指针合法性:还书车里夹了张写着奇怪地址的纸条,管理员照单全收。

这就给了攻击者巨大的操作空间。

3.1 源码依据:为什么改 next 就能拿到任意地址

取块路径(_int_malloc,glibc 2.27):

size_t tc_idx = csize2tidx (tbytes); if (tc_idx < mp_.tcache_bins && tcache && tcache->entries[tc_idx] != NULL) /* ← 只判断非空 */ return tcache_get (tc_idx);

tcache_get:

static __always_inline void * tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e->next; /* ← 把我们的 next 当成新链头 */ --(tcache->counts[tc_idx]); return (void *) e; /* ← 直接返回,不校验 */ }

两行决定一切:

entries = e->next → 我们改的 next 变成了新链头 return (void *)e → e 被当成合法内存交出去

全程没有任何一处检查:e在不在堆上、是否对齐、size 对不对。

注意:判断能否取块看的是entries[tc_idx]是否为空,不是counts。
所以即使 counts 被减成负数,只要 entries 非空就还能取。


4. 核心攻击手法:Tcache Poisoning

终极目标:欺骗malloc(),让它把一块我们本不该拥有的内存(比如存放关键函数指针的区域)分配给我们;拿到之后就能随意篡改。

前提:程序存在 UAF(Use-After-Free)——内存被释放后,指针没有被清空,你依然能往里写。

第一步:申请内存

申请两个内存块:Chunk A、Chunk B。

第二步:释放内存

依次free(A)、free(B),链表变成:

B -> A -> NULL

系统认为:下一次 malloc 给 B,再下一次给 A。

第三步:投毒

利用 UAF 改掉已经释放的 B 里那个指向 A 的 next,写成目标地址:

B -> Target_Address

系统被骗了——它以为 B 的下一个空闲块就是那个地址。

第四步:收网

第一次malloc():取走 B,同时把entries更新成B->next,也就是Target_Address。

tcache: Target_Address

第五步:达成

第二次malloc():系统把Target_Address当成合法空闲块返回给你。

到这里,你就拿到了那块内存的写权限。

关键:必须两次 malloc。第一次只是把原块取回来,第二次才返回目标地址。

4.1 触发条件

项

要求

释放次数

≥ 1 次(让块进入 tcache)

投毒时机

free 之后、malloc 之前

投毒长度

8 字节就够(只需改 next)

取块次数

第2次 malloc 才返回目标地址

目标地址

必须可写


5. 版本边界与防御机制

因为早期 Tcache 太容易被攻击,后续版本不断打补丁。

glibc 2.29:加入 key 检查

系统在块的+0x08处多写一个key字段,值是tcache 结构自身的地址。

free时如果发现e->key等于该地址,会再遍历链表确认是否重复,命中就报错:

free(): double free detected in tcache 2

绕过思路:用 UAF 把 key 覆盖掉,或者走更复杂的堆重叠。

glibc 2.32:Safe-Linking(指针加密)

next 不再明文保存,而是异或加密:

#define PROTECT_PTR(pos, ptr) \ ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))

注意:右移的是「存 next 这个字段自身的地址」,不是块地址。
存的时候:stored = (pos >> 12) ^ real_ptr
取的时候:real_ptr = (pos >> 12) ^ stored

绕过思路:先用信息泄露拿到堆地址,本地算好异或结果再写进去。

版本边界表

glibc

tcache

校验

投毒可行性

≤ 2.25

无

—

走 fastbin

2.26 ~ 2.28

有

无

✅ 本文

2.29

有

e->key == tcache(防 double free)

⚠️ 单次 UAF 仍可

2.32+

有

Safe-Linking(next 加密)

❌ 需先泄堆地址

2.34+

有

同上

❌ 且__free_hook已被删除


6. 完整例子

假设有个全局变量admin_status,地址0x601000,正常为 0,我们的目标是改成 1。

程序存在 UAF:内存还给系统后,指针没断,你依然能往里写。

6.1 攻击代码

#include <stdlib.h> /* 目标:想篡改的全局变量,No PIE 下地址固定 */ long admin_status = 0; int main() { /* 【第一步】申请内存:两个 0x20 请求 → 各得 0x30 的 chunk */ long *A = malloc(0x20); long *B = malloc(0x20); /* 【第二步】释放:先 A 后 B,LIFO 所以 B 在链表头 */ free(A); free(B); /* 【第三步】投毒:UAF,改 B 的 next 指向目标地址 */ *B = 0x601000; /* 【第四步】收网:两次 malloc */ long *C = malloc(0x20); /* 返回 B,链头变成 0x601000 */ long *D = malloc(0x20); /* 返回 0x601000 */ /* 【第五步】致命一击 */ *D = 1; /* 等价于 admin_status = 1 */ return 0; }

编译运行:

gcc -o demo demo.c -no-pie ./ld-2.27.so --library-path . ./demo

预期:结束时admin_status从0变成1。

注意:真实程序里*B = ...是「用已释放的指针」,这就是 UAF。
真实利用里通常通过edit之类的功能写,而不是直接解引用。

6.2 内存里的暗箱操作

第一步 · 申请

系统从堆里切两块给 A、B,此时 tcache 空:

tcache: 空

第二步 · 释放

tcache: [ Chunk B ] -> [ Chunk A ] -> NULL

第三步 · 投毒

正常逻辑里,B 的前 8 字节存的是指向 A 的指针。
我们执行*B = 0x601000,把这张「纸条」改掉:

tcache: [ Chunk B ] -> [ 0x601000 ]

系统并不会检查0x601000是不是合法空闲内存,它只认指针。

第四步 · 收网

malloc() 拿 C → 返回 B,链表变: [ 0x601000 ] malloc() 拿 D → 返回 0x601000

第五步 · 致命一击

*D = 1; /* 实际写的是 admin_status */

本来只能在堆里活动,靠欺骗 tcache,拿到了一个不在堆上的地址的控制权。


7. 与其他技巧对比

技巧

写入值

值可控

前置条件

tcache poisoning

你填的任意值

✅

能改已释放块的 next

fastbin attack

你填的任意值

✅

目标 +8 处的 size 要匹配 fastbin index

unsorted bin attack

bin 头地址(libc 地址)

❌

块要在 unsorted bin 里

large bin attack

victim 的堆地址

❌

需要两次插入排序

一句话:tcache 是唯一「无校验 + 值可控」的组合,所以最省事。


8. 常见误判

误判

实际情况

改一次 next 就能拿到目标

必须两次 malloc,第二次才返回目标

counts 归零就取不出来了

只看 entries 是否为空

2.29 之后完全不能用

单次 UAF 仍可,受限的是 double free

可以把 system 地址写进 next

next 是「下次 malloc 返回的地址」,不是函数指针的值

目标地址随便填

必须可写,否则拿到后使用时崩溃


9. 一句话总结

tcache 的 next 是整条链上唯一不被校验的指针;改它,就等于改了下一次 malloc 的返回值。


📌 相关阅读

  • 下一篇:《02|Fastbin Attack 理论:size 校验与错位 chunk 构造》
  • 实战配套:《16|Tcache Poisoning 实战:UAF 改 fd 打 __free_hook》
  • 进阶:《15|House of Pig 理论:两次 large bin attack + FSOP》

全部 exp 与 gdb 脚本:<GitHub 链接>

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 1:42:16

axure9.0 热区常见问题与避坑技巧

一、 点击热区没有反应原因&#xff1a;热区被其他元件、动态面板遮挡&#xff0c;层级过低&#xff1b;解决&#xff1a;右键热区 → 置顶&#xff0c;确保处于页面最上层。二、 编辑时看不到热区原因&#xff1a;关闭了视图遮罩&#xff1b;解决&#xff1a;顶部【视图】→勾…

作者头像 李华
网站建设 2026/10/28 1:42:15

2026年前端已死?看完论坛吵架贴,才懂活路在哪

这几天&#xff0c;某知名技术论坛里有个帖子火了&#xff0c;看得人心里五味杂陈。28岁入行三年的该前端楼主, 最近心态发生了崩裂, 对此问其缘由, 是因公司大力推行AI, 致使他目睹隔壁后端同事借助AI轻易就完成了本属前端的工作, 往昔前端那曾如“护城河”般的存在, 于当下AI…

作者头像 李华
网站建设 2026/10/28 1:42:08

从潜空间去噪到显存排错:Stable Diffusion 本地部署与 txt2img 调参指南

简介&#xff1a;这份PDF文档围绕Stable Diffusion的安装与使用展开&#xff0c;面向希望入门人工智能绘画的开发者、设计爱好者以及具备一定Python基础的学习者&#xff0c;帮助其理清从环境准备到图像生成的整体脉络。文档共1个PDF文件&#xff0c;约814KB&#xff0c;以单文…

作者头像 李华
网站建设 2026/10/28 1:41:32

Folia 全屏歌词播放器安装指南:10 分钟跑通第一首歌

Folia 全屏歌词播放器安装指南&#xff1a;10 分钟跑通第一首歌 【免费下载链接】folia-major 专注于绚丽的歌词动画效果的本地音乐/navidrome/第三方多平台在线音乐播放器 项目地址: https://gitcode.com/GitHub_Trending/fo/folia-major Folia 是一款把歌词渲染成全屏…

作者头像 李华