Tcache Attack

#本文根据wiki和hollk师傅的文章进行学习#

#hollk师傅的文章:https://hollk.blog.youkuaiyun.com/article/details/113400567?spm=1001.2014.3001.5502

概览:

tcache是glibc 2.26(Ubuntu 16之后)之后引入的一种新技术,目的是为了提升堆管理的效率和性能,在tcache中,虽然提升了整体的运行性能,但是改变了很多安全检查,所以就有更多可以利用的漏洞

两个结构体:

tcache中引入了两个新的结构体:tcache_entry tcache_perthread_struct

tcache_entry:

它的数据结构如下:

typedef struct tcache_entry
{
  struct tcache_entry *next;
} tcache_entry;

可以看见这个结构体里面只有一个成员:也就是指向下一个tcache_entry的指针(这个chunk要大小相同),那么从宏观上看,tcache_entry的数据结构就是一个单向链表(与fastbins类似)

值得注意的是,在tcache_entry中,指针指向的是结构体数据开始的位置,而不是他的头指针

tcache_perthread_struct:

这个结构体的数据结构如下:

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;

可以看到里面有两个成员:一个counts数组,一个装入 tcache_entry指针的数组,其中的TCACHE_MAX_BINS是宏定义的tcache的最大容纳数

那么我们可以合理的推测,这个tcache_perthread_struct结构体是用来管理tcache_entry的。事实上确实也是,他们之间的关系如下:

(图用的hollk师傅的图,我的画图技术太差了画出来不大美观)

在这里插入图片描述

两个重要函数:

tcache_put():

static void
tcache_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];
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

这里可以看见在tcache_put函数中进行的是将chunk放入tcache_entry结构体的操作,并且是直接将chunk插入到链表头部,没有进行任何检查

tcache_get():

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]);
  return (void *) e;
}

这个函数的功能就是上面那个函数的镜像了,这个函数是负责将chunk从tcache中取出来的。这里可以发现在该函数中对tcache中取chunk的操作只进行了一个:只检查了tc_idx 是否大于0(也就是对应这个链表里面还有没有成员)

Tcache Usage:

这里先简述一下tcache的工作流程(根据hollk师傅的总结):

  1. 在程序第一malloc之时会自动创建并初始化一块内存来存放 tcache_perthread_struct结构体
  2. 释放chunk时,如果chunk的size小于small bin size,在进入tcache之前会先放进fastbin或者unsorted bin中
  3. 放入tcache后:
    1. 放到对应大小的tcache之中,知道对应结构下的成员数量达到最大(7个)
    2. 对应位置的tcache被填满之后,再释放的对应大小的chunk在被释放后就会按照规则放入对应的bin当中
    3. tcache中的chunk不会互相合并,并且保留了inuse位
  4. 在程序需要分配chunk时,且需要的chunk的size在tcache的范围内,程序就会在tcache中选取对应chunk,直到tcache为空
  5. 当tcache为空时,会从bins中找对应大小的chunk
  6. 当tcache为空时如果fastbin、small bin、unsorted bin中有size符合的chunk,会先把fastbin、small bin、unsorted bin中的chunk放到tcache中,直到填满,之后再从tcache中取

取出chunk:

  // 从 tcache list 中获取内存
  if (tc_idx < mp_.tcache_bins && tcache && tcache->entries[tc_idx] != NULL)
    {
      return tcache_get (tc_idx);
    }
  DIAG_POP_NEEDS_COMMENT;
#endif
    }

验证tc_idx是否合法和tcache中是否还有成员后使用tcache_get函数将其取出

内存释放:

_int_free (mstate av, mchunkptr p, int have_lock)
{
  INTERNAL_SIZE_T size;        /* its size */
  mfastbinptr *fb;             /* associated fastbin */
  mchunkptr nextchunk;         /* next contiguous chunk */
  INTERNAL_SIZE_T nextsize;    /* its size */
  int nextinuse;               /* true if nextchunk is used */
  INTERNAL_SIZE_T prevsize;    /* size of previous contiguous chunk */
  mchunkptr bck;               /* misc temp for linking */
  mchunkptr fwd;               /* misc temp for linking */

  size = chunksize (p);

  if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
      || __builtin_expect (misaligned_chunk (p), 0))
    malloc_printerr ("free(): invalid pointer");
  if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
    malloc_printerr ("free(): invalid size");

  check_inuse_chunk(av, p);

#if USE_TCACHE
  {
    size_t tc_idx = csize2tidx (size);

    if (tcache
      && tc_idx < mp_.tcache_bins
      && tcache->counts[tc_idx] < mp_.tcache_count)
      {
        tcache_put (p, tc_idx);
        return;
      }
  }
#endif

......
}

先检查了chunk的对齐和前后堆块的释放情况后再检查tc_idx和tcache中的成员数量是否充满后将被释放的chunk放入tcache中

Tcache的利用方式:

tcache poisoning:

通过覆盖 tcache 中的 next,不需要伪造任何 chunk 结构即可实现 malloc 到任何地址(因为tcache并没有对next进行任何检查)。

以wiki上的程序为例子看一下tcache poisoning的流程(做了一些简化):

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

int main()
{
     size_t stack_var;
     fprintf(stderr, "The address of the malloc() to return is %p.\n", (char*)&stack_var);

     intptr_t *a = malloc(128);
     intptr_t *b = malloc(128);
     free(a);
     free(b);
     b[0] = (intptr_t)&stack_var;

     malloc(128);
     intptr_t *c = malloc(128);
     fprintf(stderr, "3st malloc(128): %p\n", c);
     fprintf(stderr, "We got the control\n");

     return 0;
 }

上面这个程序就模拟了一次tcache_poisoning的流程:首先定义一个size_t类型的变量stack_var,我们创建两个128字节大小的堆块,再将其释放,修改b[0] 的值为stack_var的地址,之后我们再连续申请两个也是128字节的堆块,并将后面这个申请的堆块赋给指针c

进入gdb看一下这个流程:

首先将断点下在第14行,然后看一下tcache中的情况:

在这里插入图片描述

可以看见前两个申请并被释放的128字节的堆块已经进入了tcache中0x90大小的链表中了

然后我们运行到下一行,也就是改变这个free chunk的next指向,再在gdb中看一下:

在这里插入图片描述

可以看见这里有一个0x7fffffffdfa8的地址被挂进了tcache中,之后我们将程序运行到第18行,再看一下heap的状态:

在这里插入图片描述

这里可以发现我们已经将0x7fffffffdfa8这个位置上的内存作为一个chunk分配出去并可以完全控制它了

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值