Tcache(Thread Cache)是glibc(GNU C Library)从2.26 版本开始引入的一个特性,旨在提升内存分配性能。在tcache中,每个线程都有自己的缓存,可以减少线程间的互斥和锁的竞争。
默认情况下,大小小于等于 1032(0x408)字节的chunk会被放入tcache中。
分配释放:当程序进行malloc操作时,会优先检查tcache是否有可用的chunk,如果有,就直接返回。同样,当进行free操作时,如果chunk的大小符合要求,并且对应的tcache bin还未满(默认每个bin 可以存放 7 个chunk),就会把chunk放入tcache。否则,会按照原来的流程,放入unsorted bin或者其他的bin中。
数据结构:Tcache的数据结构主要是一个数组,每个元素都是一个单向链表的头节点。数组的下标对应了chunk的大小,即第 i 个元素对应了大小为 (i+1)*16 的chunk 的链表。链表中的每个节点都是一个空闲的chunk,节点的第一个字段存放了指向下一个节点的指针。
tcache在内存中的数据结构示意图如下:
+----+ +------+ +------+
我们先来看看缓存投毒的基本攻击思路,核心代码如下:
size_t stack_var; // 目标投毒的地址
然后我们来分过程看每一个环节的堆内存布局变化
intptr_t *a = malloc(128); // addr: 0x5555555592a0
查看此时的堆内存布局
tcache链表有点像一个栈,遵循LIFO的原则
pwndbg> heapinfo
size_t stack_var; // addr: 0x7fffffffe508
此时我们观察到 tcache_entry[7] 的指向
pwndbg> heapinfo
pwndbg> heapinfo
若要细究其原理,得从glibc中对应的源码入手:
/* We overlay this structure on the user-data portion of a chunk when the chunk is stored in the per-thread cache. */
tcache_entry 结构体本质上是一个单链表指针,tcache_perthread_struct 存储了所有的 tcache 入口,通过 counts 记录每个 tcache 链的个数
tcache poisoning 漏洞涉及到两个函数:
分配函数 tcache_get
找到对应的 tcache_entry 表项
取出链表的头节点返回
回收函数 tcache_put
将chunk强制转为 tcache_entry结构
头插法将其插入到对应的 tcache_entry 表项中
本质上是用链表实现了一个栈结构,FIFO
static void *
static void
重点是这行代码:
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
chunk2mem 的宏是这样的,即将chunk指针往后移动指向用户数据区域
/* Convert a chunk address to a user mem pointer without correcting
而关键在于,代码中直接强制转化,将其转为tcache_entry 结构,这代表着,用户数据的前8个字节(64位)存储了tcache的next指针
这就意味着我们可以直接修改next指针,从而获得任意地址写的机会,因此tcache的利用相比fastbin事实上更加简单了
题目源码和 exp 可以在这里找到
https://github.com/ret2school/ctf/tree/master/2023/greyctf/pwn/writemeabook
main函数主要功能:
输入作者签名
调用secure_library 设置 seccomp
write_book程序主要功能
int __cdecl main(int argc, const char **argv, const char **envp)
write_books 函数,功能总结为:
1337 能泄露出一次给定分配块的地址
1 新增一本书
2 编辑一本书
3 删除一本书
unsigned __int64 write_books()
向书架中插入一本书,并且在书的尾部,写上作者签名和一个 magic number
可以看到一个书chunk的大小为输入的内容 + 0x10,并且会存储在 book 结构体中的size字段
unsigned __int64 write_book()
编辑一本书,但是注意到这里能够输入的内容为 books[idx].size ,而这就意味着我们可以多输入 0x10 的内容(oob,即out-of-bounds)来实现 chunk overlap(因为上文分析道用户数据的长度事实上只有 books[idx].size - 0x10
unsigned __int64 rewrite_book()
删除一本书,调用 free 函数
unsigned __int64 throw_book()
题目存在很明显的漏洞点,即利用oob可以实现overlap
来计算下我们要怎么做到 tcache poisioning
首先必须要两个 tcache,参照前面的示例(需要有一个tcache来修改指针指向)
其次,我们不能直接改 chunk 指针(前面的示例是在源码呢所以可以直接改),所以还需要一个快来通过overlap来修改指针
最后,为了达到 overlap 的目的,前面还需要一个块,通过oob溢出来实现overlap
连续申请4个chunk,4个chunk的目的分别为:
chunk1 泄露heap base addr + oob覆盖chunk2
chunk2 修改 chunk3的next指针,实现 tcache poisoning
chunk3 通过next指针获得一段可写的内存
chunk4 用作 0x40 tcache的填充
修改chunk1,oob修改chunk2的大小
释放chunk4,填充到0x40 tcache
chunk2的大小被修改为0x40,和 chunk3 实现overlap
修改chunk2的内容,覆盖chunk3 的next指针
books结构体的地址是固定的,地址为 0x4040e0 ,每个 book 结构体前 0x8 个字节存储这本书的 size ,另外 0x8 字节存储这本书在 chunk 地址
当我们获得任意地址写的时候,就可以针对 0x4040e0 这个堆块去写入内容,再利用 rewrite_book 来实现劫持got表泄露 libc base addr
我们写入的内容为:
edit(1, pwn.flat([
观察内存布局:
pwndbg> x/40gx 0x4040e0
此时我们就可以理解为
第一本书的内存地址为 0x4040a0(实际上这个为 stdout 的got表) size 为 0xff
第二本书的内存地址为 0x404018(实际上这个为 free 的got表) size 为 0xff
第三本书的内存地址为 0x4040c0 (实际上为 secret_msg 的地址),size 为 0x8
第四本书的内存地址为 0x4040e0 (实际上我 sym.books 的地址,方便我们二次写入,size 为 0xff
于是可以劫持free的got表来实现打印 stdout@got 表项,再通过确定的偏移泄露出 libc base addr
# free@got => puts
程序有seccomp保护,只允许read、write、open和exit
于是我们需要通过向栈上写入ROP的方式来读flag,首先计算栈帧
泄露环境变量地址来计算栈帧(注意第4本书我们之前设置了指向自身,因此可以反复编辑)
# leak stack (environ)
栈帧地址:也就是调用这个函数返回的ret地址
获得栈帧地址后,使用 pwntools 自带的 rop 模块来实现
rop = pwn.ROP(libc, base=stackframe_rewrite)
由于堆内存布局的原因,地址可能不一样,这里记录某次调试过程:
Book的结构如下:
4个chunk的布局
# chunk2 => sz extended
此时的chunk2大小已经被修改了
此时 tcache3 的 next 指针已经被修改了
利用 tcache poisioning 修改 books 的结构,布局如下,至此 tcache poisoning 的利用就完成了
how2heap/tcache_poisoning.c at master · shellphish/how2heap · GitHub
(https://github.com/shellphish/how2heap/blob/master/glibc\_2.27/tcache\_poisoning.c)
往期推荐