长亭百川云 - 文章详情

原创 | 一文带你理解tcache缓存投毒

SecIN技术平台

110

2024-07-13

tcache结构分析

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在内存中的数据结构示意图如下:

+----+    +------+     +------+

了解tcache poisoning

我们先来看看缓存投毒的基本攻击思路,核心代码如下:

size_t stack_var; // 目标投毒的地址

然后我们来分过程看每一个环节的堆内存布局变化

  1. 连续申请两个chunk,再释放,此时释放的chunk进入到tcache管理起来
intptr_t *a = malloc(128); // addr: 0x5555555592a0

查看此时的堆内存布局

tcache链表有点像一个栈,遵循LIFO的原则

pwndbg> heapinfo
  1. 根据上文提到的内存布局,相同大小的tcache 通过链表维护起来。修改指针指向(后面会分析),使得tcache链表的指针指向栈上的地址
size_t stack_var; // addr: 0x7fffffffe508

此时我们观察到 tcache_entry[7] 的指向

pwndbg> heapinfo
  1. 申请一次tcache分配,此时获得是之前释放的b chunk,此时的tcache已经被
pwndbg> heapinfo
  1. 第二次申请tcache 分配,本来这里是获得之前的a chunk的,但是由于 tcache 的指向已经发生了变化,导致我们可以获得一次针对栈上的地址进行读写的机会

若要细究其原理,得从glibc中对应的源码入手:

从源码层面分析tcache

tache的数据结构如下:

/* 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函数

main函数主要功能:

  1. 输入作者签名

  2. 调用secure_library 设置 seccomp

  3. write_book程序主要功能

int __cdecl main(int argc, const char **argv, const char **envp)

write_books

write_books 函数,功能总结为:

  • 1337 能泄露出一次给定分配块的地址

  • 1 新增一本书

  • 2 编辑一本书

  • 3 删除一本书

unsigned __int64 write_books()

write_book

向书架中插入一本书,并且在书的尾部,写上作者签名和一个 magic number

可以看到一个书chunk的大小为输入的内容 + 0x10,并且会存储在 book 结构体中的size字段

unsigned __int64 write_book()

rewrite_book 漏洞点

编辑一本书,但是注意到这里能够输入的内容为 books[idx].size ,而这就意味着我们可以多输入 0x10 的内容(oob,即out-of-bounds)来实现 chunk overlap(因为上文分析道用户数据的长度事实上只有 books[idx].size - 0x10

unsigned __int64 rewrite_book()

throw_book

删除一本书,调用 free 函数

unsigned __int64 throw_book()

解题思路分析

题目存在很明显的漏洞点,即利用oob可以实现overlap

利用 tcache poisoning

来计算下我们要怎么做到 tcache poisioning

  1. 首先必须要两个 tcache,参照前面的示例(需要有一个tcache来修改指针指向)

  2. 其次,我们不能直接改 chunk 指针(前面的示例是在源码呢所以可以直接改),所以还需要一个快来通过overlap来修改指针

  3. 最后,为了达到 overlap 的目的,前面还需要一个块,通过oob溢出来实现overlap

malloc chunk

连续申请4个chunk,4个chunk的目的分别为:

  1. chunk1 泄露heap base addr + oob覆盖chunk2

  2. chunk2 修改 chunk3的next指针,实现 tcache poisoning

  3. chunk3 通过next指针获得一段可写的内存

  4. chunk4 用作 0x40 tcache的填充

chunk1 oob to overlap

  1. 修改chunk1,oob修改chunk2的大小

  2. 释放chunk4,填充到0x40 tcache

  3. chunk2的大小被修改为0x40,和 chunk3 实现overlap

  4. 修改chunk2的内容,覆盖chunk3 的next指针

泄漏libc base

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

ROP绕过seccomp

程序有seccomp保护,只允许read、write、open和exit

于是我们需要通过向栈上写入ROP的方式来读flag,首先计算栈帧

泄露环境变量地址来计算栈帧(注意第4本书我们之前设置了指向自身,因此可以反复编辑)

    # leak stack (environ)

栈帧地址:也就是调用这个函数返回的ret地址

获得栈帧地址后,使用 pwntools 自带的 rop 模块来实现

rop = pwn.ROP(libc, base=stackframe_rewrite)

EXP调试

由于堆内存布局的原因,地址可能不一样,这里记录某次调试过程:

分配4个chunk

Book的结构如下:

4个chunk的布局

oob

  # chunk2 => sz extended

此时的chunk2大小已经被修改了

tcache poisoning

此时 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)

往期推荐

原创 | ClassLoader动态类加载

原创 | SpringWeb常见鉴权措施与垂直越权检测

原创 | 浅谈Apache与CVE-2023-20860

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2