堆利用 TCache attack(libc2.26)

目录

Tcache机制的简述

安全性分析

利用手法

Tcache机制的简述

在TCache机制中,它为每个线程创建一个缓存,里面包含一些小堆块,无需对arena上锁即可使用,这种无锁分配算法能有不错的效率提升。

在Glibc的2.26中 新增了Tcache机制 这是ptmalloc2的缓存机制
Tcache在glibc中是默认开启的,在Tcache被开启的时候会定义如下东西

#if USE_TCACHE
/* We want 64 entries.  This is an arbitrary limit, which tunables can reduce.  */
# define TCACHE_MAX_BINS        64
# define MAX_TCACHE_SIZE    tidx2usize (TCACHE_MAX_BINS-1)

/* Only used to pre-fill the tunables.  */
# define tidx2usize(idx)    (((size_t) idx) * MALLOC_ALIGNMENT + MINSIZE - SIZE_SZ)

/* When "x" is from chunksize().  */
# define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)
/* When "x" is a user-provided size.  */
# define usize2tidx(x) csize2tidx (request2size (x))

/* With rounding and alignment, the bins are...
   idx 0   bytes 0..24 (64-bit) or 0..12 (32-bit)
   idx 1   bytes 25..40 or 13..20
   idx 2   bytes 41..56 or 21..28
   etc.  */

/* This is another arbitrary limit, which tunables can change.  Each
   tcache bin will hold at most this number of chunks.  */
# define TCACHE_FILL_COUNT 7
#endif

Tcache为每个线程都预留了这样一个特殊的bins, bin的数量是64个 每个bin中最多缓存7个chunk。在64位系统上以0x10的字节递增,从24递增到1032字节。32位系统上则从12到512字节,所以Tcache缓存的是非Large Chunk的chunk

然后引入两个新数据结构来管理Tcache中的bin

/* 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_perthread_struct位于堆的开头,也是一个堆块,大小为0x250,为每个线程分配一个的总的bin的管理结构,有两个字段 一个字段counts记录对应Tcache的bin中现有的bin数量, 第二个字段entries用来具体指向相应bin中的chunk块 这个字段类似于fastbin的单链表结构来串连 因为只有一个next指针进行指向

选取一张来自CTF-WIKI的图来描述这种串连情况就是如下所示

tcache中chunk块的组织形式

 可以看到对应chunk的原本的fd域 在Tcache中就是tcache_entry的next域被填充为了指向下一个chunk的索引指针

tcache的初始化操作如下:

static void
tcache_init(void)
{
  mstate ar_ptr;
  void *victim = 0;
  const size_t bytes = sizeof (tcache_perthread_struct);	//获得malloc需要的字节数

  if (tcache_shutting_down)
    return;

  arena_get (ar_ptr, bytes);
  victim = _int_malloc (ar_ptr, bytes);//使用malloc为该结构分配内存
  if (!victim && ar_ptr != NULL)
    {
      ar_ptr = arena_get_retry (ar_ptr, bytes);
      victim = _int_malloc (ar_ptr, bytes);
    }


  if (ar_ptr != NULL)
    __libc_lock_unlock (ar_ptr->mutex);

  /* In a low memory situation, we may not be able to allocate memory
     - in which case, we just keep trying later.  However, we
     typically do this very early, so either there is sufficient
     memory, or there isn't enough memory to do non-trivial
     allocations anyway.  */
  if (victim)
    {
      tcache = (tcache_perthread_struct *) victim;	//存放
      memset (tcache, 0, sizeof (tcache_perthread_struct));	//清零
    }

}

使用方法:

首先看能够触发tcache中放入chunk的操作:

释放堆块时,在fastbins的操作前,如果chunk符合大小要求,并且对应bins(即entries对应大小的bins)还未装满,就将其放进去

static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
  ......
  ......
#if USE_TCACHE
  {
    size_t tc_idx = csize2tidx (size);
    if (tcache
        && tc_idx < mp_.tcache_bins // 64
        && tcache->counts[tc_idx] < mp_.tcache_count) // 7
      {
        tcache_put (p, tc_idx);
        return;
      }
  }
#endif
  ......
  ......

分配堆块时,出发点有三个:

1.malloc的chunk_size是fast chunk的情形
此时会将victim之后的fast chunk挂入对应的tcache bin链表中 只有tcache->counts[tc_idx]的数量达到7或者fastbin中没有chunk才结束放入需要注意的是 chunks 在 tcache bin 的顺序和在 fastbin 中的顺序是反过来的。

#if USE_TCACHE
	  /* While we're here, if we see other chunks of the same size,
	     stash them in the tcache.  */
	  size_t tc_idx = csize2tidx (nb);
	  if (tcache && tc_idx < mp_.tcache_bins)
	    {
	      mchunkptr tc_victim;

	      /* While bin not empty and tcache not full, copy chunks over.  */
	      while (tcache->counts[tc_idx] < mp_.tcache_count
		     && (pp = *fb) != NULL)
		{
		  REMOVE_FB (fb, tc_victim, pp);
		  if (tc_victim != 0)
		    {
		      tcache_put (tc_victim, tc_idx);
	            }
		}
	    }
#endif

2.small bins与fastbins情况类似,双链表中剩余的chunk会填充到tcache中直达上限

#if USE_TCACHE
	  /* While we're here, if we see other chunks of the same size,
	     stash them in the tcache.  */
	  size_t tc_idx = csize2tidx (nb);
	  if (tcache && tc_idx < mp_.tcache_bins)
	    {
	      mchunkptr tc_victim;

	      /* While bin not empty and tcache not full, copy chunks over.  */
	      while (tcache->counts[tc_idx] < mp_.tcache_count
		     && (tc_victim = last (bin)) != bin)
		{
		  if (tc_victim != 0)
		    {
		      bck = tc_victim->bk;
		      set_inuse_bit_at_offset (tc_victim, nb);
		      if (av != &main_arena)
			set_non_main_arena (tc_victim);
		      bin->bk = bck;
		      bck->fd = bin;

		      tcache_put (tc_victim, tc_idx);
	            }
		}
	    }
#endif

3.binning code(合并chunk等其他情况中),每一个符合的chunk都会优先放入tcache中,而不是直接返回(除非已满)。然后,程序会从tcache中返回一个chunk

#if USE_TCACHE
	      /* Fill cache first, return to user only if cache fills.
		 We may return one of these chunks later.  */
	      if (tcache_nb
		  && tcache->counts[tc_idx] < mp_.tcache_count)
		{
		  tcache_put (victim, tc_idx);
		  return_cached = 1;
		  continue;
		}
	      else
		{
#endif

接下来是从tcache取出chunk的操作

1.在libc_malloc()调用int-malloc之前,如果tcache bin中有符合的chunk,则直接将它返回。(源码就不打了0.0)。

2.bining code中如果放入的chunk达到了上限,则会直接返回一个chunk,bining code结束后,如果没有直接返回,那么如果有一个符合要求的chunk被找到,则返回最后一个。

最后,需要注意的是:tcache中的chunk不会被合并,无论是相邻的chunk,还是chunk和top chunk都不会。这是因为这些chunk的PREV—INUSE位会被标记。

安全性分析

函数tcache_put()的实现

/* Caller must ensure that we know tc_idx is valid and there's room
   for more chunks.  */
static __always_inline 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_get()的实现

/* Caller must ensure that we know tc_idx is valid and there's
   available chunks to remove.  */
static __always_inline 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]); // 获得一个 chunk,counts 减一
  return (void *) e;
}

这两个函数都假设调用者已经对参数进行了检查,但是由于tcache的操作在free和malloc中往往都处于很靠前的位置,导致很多检查失效,这样做虽然有利于提高效率,但是对安全性造成了负面影响。

另外在tcache-get()函数中:assert (tcache->entries[tc_idx] >0);本意是检查tcache的chunk数量大于零,否则counts可能会发生整数溢出成负数,已经在libc-2.28进行了修复。

libc2.26中tcache机制被发现了漏洞,由于-libc-malloc()使用request2size()来将请求大小转化为实际块大小,该函数不会进行整数溢出检查,所以请求一个非常大的块时,就会导致整数溢出,从而导致错误的返回tcache bin中的块。(CVE-2017-17426),这个问题在libc2.27中已经被修复(使用check-request2size()代替使用request2size()),实现了对整数溢出的检查。

同时在libc2.28中,对tcache加入了二次释放的检查,即在tcache-entry中加入一个标志key,用于表示chunk是否已经在tcache bin中。

利用手法

1.tcache poisoning

修改tcache中的chunk的next指针

例题:HIBT CTF 2018:gundam

提取链接:CTF/2018/Hitbxctf/gundam at master · Ch4nc3n/CTF · GitHub

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值