tcache_attack

tcache attack glibc 2.26

tcache 是 glibc 2.26 (ubuntu 17.10) 之后引入的一种技术,它带来了很多新的堆攻击方式。

相关结构体

tcache_entry

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

这个结构体用于储存free后的tcache堆块,单链表结构,next指针指向下一个堆块的usr data部分(fastbin指向的chunk_header部分),同时采用FILO(先进后出)的存取方式。
tcache_perthread_struct teache的管理器

typedef struct tcache_perthread_struct
{
  char counts[TCACHE_MAX_BINS]; //tcache链上的空闲堆块个数,每个链上最多7个
  tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
# define TCACHE_MAX_BINS                64
static __thread tcache_perthread_struct *tcache = NULL;

wiki上的图特别好:

工作方式

  • 第一次malloc,会先申请个堆块存放tcache_prethread_struct
  • free的堆块先放入tcache(size合适的话)
    • tcache对应链表没满之前,先放入该链表,满了之后才放入fastbin或者unsorted bin这些。
    • malloc时先从对应tcache中取
    • tcache 为空时,如果 fastbin/smallbin/unsorted bin 中有 size 符合的 chunk,会先把 fastbin/smallbin/unsorted bin 中的 chunk 放到 tcache 中,直到填满。之后再从 tcache 中取;因此 chunk 在 bin 中和 tcache 中的顺序会反过来

源码分析

__libc_malloc —> 在源码的最前面添加了tcache相关代码

void *
__libc_malloc (size_t bytes)
{
    ......
    ......
#if USE_TCACHE
  /* int_free also calls request2size, be careful to not pad twice.  */
  size_t tbytes;
  // 根据 malloc 传入的参数计算 chunk 实际大小,并计算 tcache 对应的下标
  checked_request2size (bytes, tbytes); //求出chunk实际大小
  size_t tc_idx = csize2tidx (tbytes); //计算对应idx

  // 初始化 tcache ,tcache为空(第一次malloc)的时候调用建立tcache_prethread_struct
  MAYBE_INIT_TCACHE (); 
  DIAG_PUSH_NEEDS_COMMENT;
  if (tc_idx < mp_.tcache_bins  // 根据 size 得到的 idx 在合法的范围内
      /*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
      && tcache //所需的tcache在链表中存在
      && tcache->entries[tc_idx] != NULL) // tcache->entries[tc_idx] 有 chunk
    {
      return tcache_get (tc_idx); //从链表中取出所需的chunk
    }
  DIAG_POP_NEEDS_COMMENT;
#endif
    ......
    ......
}

内存释放

tcache_put()

static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk); //找到对应chunk
  assert (tc_idx < TCACHE_MAX_BINS); //idx合法判断
  e->next = tcache->entries[tc_idx]; // 这两个操作是把free的chunk放入了链表的头部,对应了FILO的规则
  tcache->entries[tc_idx] = e; 
  ++(tcache->counts[tc_idx]); //对应tcache数量加1
}

我们可以看出没什么大的保护操作,没有指针置0。
__libc_free 变化不大 ,变化主要是在_int_free里面

void
__libc_free (void *mem)
{
  ......
  ......
  MAYBE_INIT_TCACHE (); //tcache不为空时没用
  ar_ptr = arena_for_chunk (p);
  _int_free (ar_ptr, p, 0);
}

_int_free

static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
  ......
  ......
#if USE_TCACHE
  {
    size_t tc_idx = csize2tidx (size); //求出对应的idx
    if (tcache
        && tc_idx < mp_.tcache_bins // 64 idx合法,tcache链表中数量小于等于7
        && tcache->counts[tc_idx] < mp_.tcache_count) // 7
      {
        tcache_put (p, tc_idx); //放入链表
        return;
      }
  }
#endif

内存申请

libcmalloc里面我们可以看出是如何进入tcache_get的。
tcache_get()

static __always_inline void *
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  assert (tc_idx < TCACHE_MAX_BINS);  //判断idx是否合法
  assert (tcache->entries[tc_idx] > 0); //取出来的指针不为0,
  tcache->entries[tc_idx] = e->next; //取出后链表的补齐
  --(tcache->counts[tc_idx]); // 获得一个 chunk,counts 减一
  return (void *) e;
}

我们可以看出其检查很弱,差不多算是tcache->entries[tc_idx] = e->next;这个注意点,意思差不多就是free的tcache里面next指针要存在,不过也可以伪造。

如果没有链表里没有合适的tcache,那么于之前libc的操作类似,不过这样我们看出tcache的优先级,源码中先对其进行判断。

tcache机制带来的内存分配变化

malloc时会把内存块移入tcache

  1. 首先,申请的内存块符合 fastbin 大小时并且在 fastbin 内找到可用的空闲块时,会把该 fastbin 链上的其他内存块放入 tcache 中。
  2. 其次,申请的内存块符合 smallbin 大小时并且在 smallbin 内找到可用的空闲块时,会把该 smallbin 链上的其他内存块放入 tcache 中。
  3. 当在 unsorted bin 链上循环处理时,当找到大小合适的链时,并不直接返回,而是先放到 tcache 中,继续处理。

tcache 所带来的的相关攻击方式

tcache poisoning

伪造next指针就可以任意地址读写,修改之后,两次malloc就可以把相应堆块拿出来。

tcache dup

适用于libc2.27 2.26,但是2.29出现了相关检查,2.28还不太清楚奥
我们从上面的源码可以知道tcache_put()也没有什么检查,算是判断了一下idx是否合法,那么就可以直接double free,而且不需要想fastbin里面那样担心fasttop的检查

tcache perthread corruption

tcache_perthread_struct管理tcache结构,它还是个堆块,那么我们之前说的任意地址读写也可以用来控制这个堆块。

tcache house of spirit

类似于之前glibc的house of spirit,不过更加简单,因为你只需要寻找两个相邻的内存单元,伪造出size和next指针,直接free操作,就可以实现对应大小对应地址堆块的控制。

在 smallbin 中包含有空闲块的时候,会同时将同大小的其他空闲块,放入 tcache 中,此时也会出现解链操作,但相比于 unlink 宏,缺少了链完整性校验。因此,原本 unlink 操作在该条件下也可以使用。这个的意思就是之前的unlink不需要绕过链表中对应堆块是否正确的检查了,更加简单,威力也更大了。

libc leak

和之前的unsorted bin attack大部分一样,不过要先把对应的tcache消耗完

Tcache Check 相关影响写到了tcache dup里面

这个其实就是使得tcache dup操作失效了。

index 6d7a6a8..f730d7a 100644 (file)
--- a/malloc/malloc.c
+++ b/malloc/malloc.c
@@ -2967,6 +2967,8 @@ mremap_chunk (mchunkptr p, size_t new_size)
 typedef struct tcache_entry
 {
   struct tcache_entry *next;
+  /* This field exists to detect double frees.  */
+  struct tcache_perthread_struct *key;
 } tcache_entry;

 /* There is one of these for each thread, which contains the
@@ -2990,6 +2992,11 @@ tcache_put (mchunkptr chunk, size_t tc_idx)
 {
   tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
   assert (tc_idx < TCACHE_MAX_BINS);
+
+  /* Mark this chunk as "in the tcache" so the test in _int_free will
+     detect a double free.  */
+  e->key = tcache;
+
   e->next = tcache->entries[tc_idx];
   tcache->entries[tc_idx] = e;
   ++(tcache->counts[tc_idx]);
@@ -3005,6 +3012,7 @@ tcache_get (size_t tc_idx)
   assert (tcache->entries[tc_idx] > 0);
   tcache->entries[tc_idx] = e->next;
   --(tcache->counts[tc_idx]);
+  e->key = NULL;
   return (void *) e;
 }

@@ -4218,6 +4226,26 @@ _int_free (mstate av, mchunkptr p, int have_lock)
   {
     size_t tc_idx = csize2tidx (size);

+    /* Check to see if it's already in the tcache.  */
+    tcache_entry *e = (tcache_entry *) chunk2mem (p);
+
+    /* This test succeeds on double free.  However, we don't 100%
+       trust it (it also matches random payload data at a 1 in
+       2^<size_t> chance), so verify it's not an unlikely coincidence
+       before aborting.  */
+    if (__glibc_unlikely (e->key == tcache && tcache))
+      {
+       tcache_entry *tmp;
+       LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
+       for (tmp = tcache->entries[tc_idx];
+            tmp;
+            tmp = tmp->next)
+         if (tmp == e)
+           malloc_printerr ("free(): double free detected in tcache 2");
+       /* If we get here, it was a coincidence.  We've wasted a few
+          cycles, but don't abort.  */
+      }
+
     if (tcache
        && tc_idx < mp_.tcache_bins
        && tcache->counts[tc_idx] < mp_.tcache_count)

相关例题可以看看我的2020_heap_practice博文里面的2018LCTF eaay_heap

相关参考:

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/implementation/tcache-zh/


   转载规则


《tcache_attack》 时钟 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
Glibc-2.23-malloc源码审计 Glibc-2.23-malloc源码审计
Glibc-2.23-malloc源码审计 malloc经常用,但是之前光看了关键代码,没有细读,寒假正好有时间深入学习堆利用,就想着同时深入了解malloc的堆分配机理,glibc-2.23 开头前1000行都是各种注释和定义,可以边看
2020-01-30
下一篇 
2020_heap_practice 2020_heap_practice
2019护网杯 mergeheap这个题目的逻辑比较清晰,难的地方在于利用思路。 遍观整个程序: 自己实现的read函数没有off-by-one: delete函数没有UAF之类的 因此整个程序没有什么明显的漏洞,下面就要对执行逻辑进行分析
2020-01-30
  目录