我们先分析一下tcache chunk这个结构体(chunk进入了tcache bin时的结构体)

下面是libc 2.35源码中对进入到tcache binchunk使用的结构体

typedef struct tcache_entry
{
  struct tcache_entry *next;
  /* This field exists to detect double frees.  */
  uintptr_t key;
} tcache_entry;

源码分析

对于tcache chunknext值,这里我们也浅分析一下,这里截取了_int_freetcache的源码部分:

#if USE_TCACHE
  {
    size_t tc_idx = csize2tidx (size);
    if (tcache != NULL && tc_idx < mp_.tcache_bins)
      {
    /* 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_key))
      {
        tcache_entry *tmp;
        size_t cnt = 0;
        LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
        for (tmp = tcache->entries[tc_idx];
         tmp;
         tmp = REVEAL_PTR (tmp->next), ++cnt)
          {
        if (cnt >= mp_.tcache_count)
          malloc_printerr ("free(): too many chunks detected in tcache");
        if (__glibc_unlikely (!aligned_OK (tmp)))
          malloc_printerr ("free(): unaligned chunk detected in tcache 2");
        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->counts[tc_idx] < mp_.tcache_count)
      {
        tcache_put (p, tc_idx);
        return;
      }
      }
  }

这里我们先关注一下,chunktcache bin的部分,我们tcache chunknext值是如何来的

tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

  /* Mark this chunk as "in the tcache" so the test in _int_free will
     detect a double free.  */
  e->key = tcache_key;

  e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

tcahce_put先把得到chunk指针转化为定义的tcache_entry,这里chunk2mem是返回我们chunkmem区域,mem

chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of previous chunk, if unallocated (P clear)  |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of chunk, in bytes                     |A|M|P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             User data starts here...                          .
        .                                                               .
        .             (malloc_usable_size() bytes)                      .
        .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             (size of chunk, but used for application data)    |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of next chunk, in bytes                |A|0|1|
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

然后就是把tcache_key传入到e->key的位置,next值是通过tcache->entries[tc_idx]的值进行计算的,tc_idx是我们上面对应对应大小tcache bin的相关下标,也就是取出对应tcache bin最后一个chunk( tcache bin 是先进后出)

#define PROTECT_PTR(pos, ptr) \
  ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))

泄露heap地址原理 && 任意申请原理

通过PROTECT_PTR宏定义,我们知道next的值,是我们freechunk的指针与上一个在对应tcache binchunk的指针进行加密操作,所以,我们在泄露heap地址时,释放一个tcache chunk大小范围的chunk时,如果它是对应tcache bin的第一个chunk,那么它的next值是这样的

(chunk_mem_addr >> 12) & tcache_chunk_last
(chunk_mem_addr >> 12) & 0 // tcache_chunk_last 为 空

所以我们泄露它并计算出来heap地址,又因为地址是按页加载的,也就是0x1000为开始,是12bit大小,所以后面12bit对于我们不是很重要,即是它被右移了,我们也可以左移回去

这也就是我们通过tcache chunk泄露地址的原理

高版本添加了这一个异或next指针的特性,只是为了检测取出chunk的合法性,我们从_libc_malloc截取源码:

#if USE_TCACHE
  /* int_free also calls request2size, be careful to not pad twice.  */
  size_t tbytes;
  if (!checked_request2size (bytes, &tbytes))
    {
      __set_errno (ENOMEM);
      return NULL;
    }
  size_t tc_idx = csize2tidx (tbytes);

  MAYBE_INIT_TCACHE ();

  DIAG_PUSH_NEEDS_COMMENT;
  if (tc_idx < mp_.tcache_bins
      && tcache
      && tcache->counts[tc_idx] > 0)
    {
      victim = tcache_get (tc_idx);
      return tag_new_usable (victim);
    }
  DIAG_POP_NEEDS_COMMENT;

抛开对大小和对应tcache bincount检测等等,我们可以定位到这个函数

tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  if (__glibc_unlikely (!aligned_OK (e)))
    malloc_printerr ("malloc(): unaligned tcache chunk detected");
  tcache->entries[tc_idx] = REVEAL_PTR (e->next);
  --(tcache->counts[tc_idx]);
  e->key = 0;
  return (void *) e;
}

这里又添加了一个取出检测的机制aligned_OK也就是检查对应的chunk指针是不是以0位结尾的,防止我们任意伪造位置,这个在fastbinchunk过程也有对应的检测,这个对于tcache bin来说就没有特别恶心,源码如下:

#define aligned_OK(m)  (((unsigned long)(m) & MALLOC_ALIGN_MASK) == 0)

这里最重要的是这个宏定义REVEAL_PTR:

#define REVEAL_PTR(ptr)  PROTECT_PTR (&ptr, ptr)

看得出来就是对于我们上面进行一个解密操作,如果我们想控制tcache bin链,至少是需要泄露出来heap地址,计算出来当前对应tcahce binchunk指针的地址,然后对我们想要申请的地址进行加密操作也就是

(tcache_bin_chunk_last >> 12 ) & (target_chunk_addr)

这样就可以控制对应tcache bin最后的chunknext指针,使得我们可以控制tcache bin的链表,实现任意申请地址

如何绕过tcache的double free检测

结束掉前面的abalabala,我们来到这次的重点

在很多条件下,我们有很多种方式去绕过它,例如:

最轻松的莫过于可以记录多个chunk指针且我们free完后可以修改,可以进行A->B,然后对Anext值进行修改就可以A->C

free完后不能进行修改,我们可以tcache bin填满进入fastbin,再通过A->b->A进行绕过,然后可以同上操作

但是如果我们只能记录一个chunk指针,那么我们便无法满足count在取出对应的chunk后,还满足不为零的条件,因为我们需要正面绕过tcachedouble free的检测

其他绕过方法都比较常见,故这里只讲最后一种

源码分析 二

这里是chunktcache bin表链的检测

if (__glibc_unlikely (e->key == tcache_key))
      {
        tcache_entry *tmp;
        size_t cnt = 0;
        LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
        for (tmp = tcache->entries[tc_idx];
         tmp;
         tmp = REVEAL_PTR (tmp->next), ++cnt)
          {
        if (cnt >= mp_.tcache_count)
          malloc_printerr ("free(): too many chunks detected in tcache");
        if (__glibc_unlikely (!aligned_OK (tmp)))
          malloc_printerr ("free(): unaligned chunk detected in tcache 2");
        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.  */
          }
      }

看到这个free(): double free detected in tcache 2是不是很眼熟

这个就是我们double free了同一个tcache chunk后的原因,但是绕过方法异常简单e->key == tcache_key,回到我们最上面的结构体

typedef struct tcache_entry
{
  struct tcache_entry *next;
  /* This field exists to detect double frees.  */
  uintptr_t key;
} tcache_entry;

在这里就可以知道key是起什么作用的,就是我们释放chunk后,如何判断我们chunk是否已经被释放过,所以我们可以通过可以修改函数把chunkkey给抹去后,再次释放,就可以进入tcache bin的表链

虽然但是,总觉得这个key值过于随便了,emmmm身为一个pwn手,还是不bb赖赖了,到时候更新版本,直接上难度

至于为什么会报这个错,是因为

for (tmp = tcache->entries[tc_idx];
         tmp;
         tmp = REVEAL_PTR (tmp->next), ++cnt)

把每一个tcache bin里的chunk给遍历出来,刚好tmp值等于我们free掉的chunk指针,满足了这个:

if (tmp == e)
          malloc_printerr ("free(): double free detected in tcache 2");

至此原因及原理,讲述完毕

点击收藏 | 1 关注 | 1 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖