fastbin attack学习小结

fastbin attack学习小结

    之前留在本地的一篇笔记,复习一下。

  下面以glibc2.23为例,说明fastbin管理动态内存的细节。先看一下释放内存的管理: 

if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())
// 检查chunk大小是否大于max_fast的大小,如果是,则chunk进入Fastbins进行处理

#if
TRIM_FASTBINS /* If TRIM_FASTBINS set, don't place chunks bordering top into fastbins */ && (chunk_at_offset(p, size) != av->top)
#endif ) { if (__builtin_expect (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0) || __builtin_expect (chunksize (chunk_at_offset (p, size)) >= av->system_mem, 0))
  /* chunk_at_offset将p+size这段内存强制看成一个chunk结构体,这里对fastbin中chunk的大小做出了限制,分配的最小的chunk不能小于2*SIZE,分配的最大的chunk不能大于av->system_mem */ {
/* We might not have a lock at this point and concurrent modifications of system_mem might have let to a false positive. Redo the test after getting the lock. */ if (have_lock || ({ assert (locked == 0); mutex_lock(&av->mutex); locked = 1; chunk_at_offset (p, size)->size <= 2 * SIZE_SZ || chunksize (chunk_at_offset (p, size)) >= av->system_mem; })) { errstr = "free(): invalid next size (fast)"; goto errout; } if (! have_lock) { (void)mutex_unlock(&av->mutex); locked = 0; } }     // 这一段代码表示对下一个chunk的size进行检查 free_perturb (chunk2mem(p), size - 2 * SIZE_SZ); set_fastchunks(av); unsigned int idx = fastbin_index(size);      // 根据chunk的大小,选择对应的Fastbin的idx fb = &fastbin (av, idx);    // /* Atomically link P to its fastbin: P->FD = *FB; *FB = P; */ mchunkptr old = *fb, old2; unsigned int old_idx = ~0u; do { /* Check that the top of the bin is not the record we are going to add (i.e., double free). */ if (__builtin_expect (old == p, 0)) {
  // 如果连续两次释放的是同一块地址的内存,会报double free的错误 errstr
= "double free or corruption (fasttop)"; goto errout; } /* Check that size of fastbin chunk at the top is the same as size of the chunk that we are adding. We can dereference OLD only if we have the lock, otherwise it might have already been deallocated. See use of OLD_IDX below for the actual check. */ if (have_lock && old != NULL) old_idx = fastbin_index(chunksize(old)); p->fd = old2 = old; } while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2); if (have_lock && old != NULL && __builtin_expect (old_idx != idx, 0)) { errstr = "invalid fastbin entry (free)"; goto errout; } }

   可以看出,glibc2.23对于double free的管理非常地松散,如果连续释放相同chunk的时候,会报错,但是如果隔块释放的话,就没有问题。在glvibc2.27及以后的glibc版本中,加入了tcache机制,加强了对use after free的检测,所以glibc2.23中针对fastbin的uaf在glibc2.27以后,就失效了。

  glibc2.23中对fastbin申请chunk的操作如下:

  if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
    {
      idx = fastbin_index (nb);
  // 获取fastbin的index mfastbinptr
*fb = &fastbin (av, idx); mchunkptr pp = *fb; do { victim = pp; if (victim == NULL) break; } while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim)) != victim); if (victim != 0) {
    // 检查链表的size是否合法
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0)) {         errstr = "malloc(): memory corruption (fast)"; errout: malloc_printerr (check_action, errstr, chunk2mem (victim), av); return NULL; } check_remalloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } }

   fastbin中检查机制比较少,而且fastbin作为单链表结构,同一链表中的元素由fd指针来进行维护。同时fastbin不会对size域的后三位进行检查,这导致glibc pwn中对于fastbin的利用和考察要比其它的bins中更加频繁。

  其中,最为常见的利用思路,就是把chunk块劫持到其他的数据段,比如栈中,bss段或者_hook地址处等等。通过how2heap上的几个例子来进行理解。

House of spirit

  house_of_spirit这种技术,通常是在别的数据段伪造一个chunk,这个chunk满足fastbin的大小,这时候,如果把这个chunk指针free掉,然后重新申请相同大小的堆块,就会把这个fake chunk申请出来。

  fake chunk的时候,需要布置fake chunk的内容来绕过一下检测:

  1.要避免double free的情况:fake chunk所指向的链表头部不能是fake chunk,上面的源码中有过展示;

  2.fake chunk的ISMMAP这个位不能为1,free时,如果是mmap分配出的chunk,会被单独处理;

  3.fake chunk的地址需要对齐(2size_t对齐);

  4.fake chunk的next chunk的大小不能等于2*SIZE_SE,同时也不能大于av->system_mem,这个从上面的源码中也可以看出。

  一般我们在看到,如果有向bss段,或者栈中有这种奇奇怪怪的输入,且输入空间比较大的时候,我们都可以考虑去伪造一个chunk。

  这里记录一下how2heap上面house_of_spirit.c的调试过程:

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

int main()
{
    fprintf(stderr, "This file demonstrates the house of spirit attack.\n");

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

    fprintf(stderr, "We will now overwrite a pointer to point to a fake 'fastbin' region.\n");
    unsigned long long *a;
    // This has nothing to do with fastbinsY (do not be fooled by the 10) - fake_chunks is just a piece of memory to fulfil allocations (pointed to from fastbinsY)
    unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));

    fprintf(stderr, "This region (memory of length: %lu) contains two chunks. The first starts at %p and the second at %p.\n", sizeof(fake_chunks), &fake_chunks[1], &fake_chunks[9]);

    fprintf(stderr, "This chunk.size of this region has to be 16 more than the region (to accommodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV_INUSE (lsb) bit is ignored by free for fastbin-sized 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, "The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.\n");
        // fake_chunks[9] because 0x40 / sizeof(unsigned long long) = 8
    fake_chunks[9] = 0x1234; // nextsize

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

  第一个断点设在malloc(1)处,在64位下的最小申请的chunk最小大小是32个字节。

 

unsigned long long *a;
 // This has nothing to do with fastbinsY (do not be fooled by the 10) - fake_chunks is just a piece of memory to fulfil allocations (pointed to from  fastbinsY)
unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));

  这里定义了一个无符号长长整型的fake_chunks的数组,并且做了一个16字节的地址对齐。以及一个无符号长长整型的指针a。

  断点下到fake_chunks[1]=0x40这里,这里就是确定了fake chunk的大小。fake chunk的大小必须落在fastbin的区间,但是next chunk的大小只需满足: > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena)即可。

  fake_chunk[9]这里伪造了next size的大小,满足条件大于2 SIZE_SE,小于system_mem。

  然后把fake_chunk的地址赋值给a,伪造完chunk块之后,就可以尝试free一下,看看会发生什么:

  可以看到,有一个不是堆上的地址进入了fastbin中(我调试的时候,系统默认的libc版本是2.27,所以这里fake chunk的地址进入了tcachebins中,但是不影响调试和调试结果,tacache主要会对double free做一个更加严格的检查)。

  这个地址,就是我们之前在栈中伪造的地址。

  然后,重新malloc,我们发现最后申请出的chunk,就是我们fake chunk的地址。

alloc_to_stack

  house_of_spirit提前布置了fake_chunk,并且主动free掉了fake_chunk,来把栈上伪造的数据加入fastbin中。alloc_to_stack没有主动free栈上的伪造的chunk,而是通过修改已经free掉的chunk的fd指针,来把栈上伪造的chunk加入到fastbin中。

#include<stdio.h>

typedef struct _chunk
{
    long long pre_size;
    long long size;
    long long fd;
    long long bk;
}CHUNK,*PCHUNK;

int main(void)
{
    CHUNK stack_chunk;
    
    void *chunk1;
    void *chunk_a;
    void *stack_ptr;
    stack_ptr=&stack_chunk;
    printf("stack_chunk address:%p\n",stack_ptr);

    stack_chunk.size=0x21;
    chunk1=malloc(0x10);
    printf("chunk1 address:%p\n",chunk1);

    free(chunk1);
    printf("chunk1 address:%p\n",chunk1);

    *(long long*)chunk1=&stack_chunk;
    malloc(0x10);
    chunk_a=malloc(0x10);
    printf("chunk_a address:%p\n",chunk_a);
    return 0;
}

  这个例子,我们调试以上代码,为了方便直接观察内存分配的布局,将关键的地址值全部打印出来,同时通过给结构体中变量赋值,省略了在栈上布置数据的过程。

  断点下在malloc(0x10)这里。可以看到,我们首先申请了一个chunk,然后释放掉,然后我们再修改chunk的fd指针为栈上伪造的chunk的地址。

   根据stack LIFO的特性,先申请一次chunk把堆上的地址分配出去,再申请一次chunk,我们就可以把我们的chunk布置到栈中。

  向我们上面直接给fd指针赋值是一种非常理想化的操作,在实际漏洞和题目中基本不会存在这种类似的利用场景。更多得需要通过uaf或者堆溢出来修改chunk的fd指针。

fastbin_dup_into_stack

  how2heap上的fastbin_dup_into_stack.c也是运用到alloc_to_stack这种技术,但是这里就需要用到double free的方式修改fastbin中chunk的fd指针了。调试过程记录如下:

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

int main()
{
    fprintf(stderr, "This file extends on fastbin_dup.c by tricking malloc into\n"
           "returning a pointer to a controlled location (in this case, the stack).\n");

    unsigned long long stack_var;

    fprintf(stderr, "The address we want malloc() to return is %p.\n", 8+(char *)&stack_var);

    fprintf(stderr, "Allocating 3 buffers.\n");
    int *a = malloc(8);
    int *b = malloc(8);
    int *c = malloc(8);

    fprintf(stderr, "1st malloc(8): %p\n", a);
    fprintf(stderr, "2nd malloc(8): %p\n", b);
    fprintf(stderr, "3rd malloc(8): %p\n", c);

    fprintf(stderr, "Freeing the first one...\n");
    free(a);

    fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);
    // free(a);

    fprintf(stderr, "So, instead, we'll free %p.\n", b);
    free(b);

    fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\n", a);
    free(a);

    fprintf(stderr, "Now the free list has [ %p, %p, %p ]. "
        "We'll now carry out our attack by modifying data at %p.\n", a, b, a, a);
    unsigned long long *d = malloc(8);

    fprintf(stderr, "1st malloc(8): %p\n", d);
    fprintf(stderr, "2nd malloc(8): %p\n", malloc(8));
    fprintf(stderr, "Now the free list has [ %p ].\n", a);
    fprintf(stderr, "Now, we have access to %p while it remains at the head of the free list.\n"
        "so now we are writing a fake free size (in this case, 0x20) to the stack,\n"
        "so that malloc will think there is a free chunk there and agree to\n"
        "return a pointer to it.\n", a);
    stack_var = 0x20;

    fprintf(stderr, "Now, we overwrite the first 8 bytes of the data at %p to point right before the 0x20.\n", a);
    *d = (unsigned long long) (((char*)&stack_var) - sizeof(d));

    fprintf(stderr, "3rd malloc(8): %p, putting the stack address on the free list\n", malloc(8));
    fprintf(stderr, "4th malloc(8): %p\n", malloc(8));
}

  fastbin_dup_into_stack中如果用到ubuntu 18.04的libc-2.27.so编译的话,由于tcache机制的检测,double free的问题会被检测出来,所以这里用patchelf修改了链接器和动态链接库。

  程序首先申请了三块堆块:

   fprintf(stderr, "Freeing the first one...\n");
    free(a);

    fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);
    // free(a);

    fprintf(stderr, "So, instead, we'll free %p.\n", b);
    free(b);

    fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\n", a);
    free(a);

  这样释放chunk是为了绕过double free的检测。这时候可以看到,0x603000这个地址,在fastbin的链表中被添加了两次。

unsigned long long *d=malloc(8);

  这时候,申请出来的堆块实际上就是0x603000地址所在的chunk。但是,由于它被free了两次,所以它现在仍然在fastbin中,这时候,我们可以控制它的fd指针。

*d = (unsigned long long) (((char*)&stack_var) - sizeof(d));

  这时候,实际上是把栈上的地址加入fastbin链表,在之后的malloc中进行分配。为了能够顺利地把fake chunk添加到fastbin链表中,stack_var之前就被赋值为20。

   fastbin如图所示,最后第四次分配的时候,就成功将栈上的目标地址分配出来了。

 fastbin_dup_consolidate

   malloc_consolidate函数为了避免堆块过于碎片化,会在一定条件下,将fastbin中的chunk合并放入unsortedbin中。触发条件如下:

 

if ((victim = last (bin)) != bin)
      // last宏定义如下  #define last(b)      ((b)->bk)
          {
          if (victim == 0) /* initialization check */
              /* victim为0,smallbin尚未进行初始化为双向链表 */
            malloc_consolidate (av);

 

  如果当前bin上有不止一个chunk(此时已经进入smallbin range),首先检查smallbin有没有进行初始化,如果smallbin没有进行初始化的话,说明smallbin上没有节点,这时候调用malloc_consolidate将对应fastbin上的chunk进行合并,合并后放入unsortedbin中等待分配。

  if (get_max_fast () != 0) {
    clear_fastchunks(av);
    // 清除av->flags有关fastbin的标志位,表示所有fastbin都为空了
    unsorted_bin = unsorted_chunks(av);
    // 首先获取unsortedbin的头节点

    /*
      Remove each chunk from fast bin and consolidate it, placing it
      then in unsorted bin. Among other reasons for doing this,
      placing in unsorted bin avoids needing to calculate actual bins
      until malloc is sure that chunks aren't immediately going to be
      reused anyway.
    */

    maxfb = &fastbin (av, NFASTBINS - 1);
    fb = &fastbin (av, 0);
    do {
      p = atomic_exchange_acq (fb, 0);
      // 替换p的值为fb的值,外层循环遍历每一个fastbin,内层循环遍历fastbin的每一个chunk
      if (p != 0) {
          // 如果p==0表示该fastbin为空
    do {
        // 内层循环遍历该fastbin的每一个chunk
      check_inuse_chunk(av, p);
      /* check_inuse_chunk宏是由do_chunk_size函数实现的
       * 检查chunk是否是mmap分配出来的
       * 检查chunk是否inuse

      /* Slightly streamlined version of consolidation code in free()*/
      nextp = p->fd;
      size = p->size & ~(PREV_INUSE|NON_MAIN_ARENA);
      // 置空PREV_INUSE和NON_MAIN_ARENA的两个标志位
      nextchunk = chunk_at_offset(p, size);
      // nextchunk = p + size
      nextsize = chunksize(nextchunk);
      // nextsize = nextchunk -> size

      if (!prev_inuse(p)) {
        prevsize = p->prev_size;
        size += prevsize;
        p = chunk_at_offset(p, -((long) prevsize));
        // chunk_at_offset
        // #define chunk_at_offset(p, s)  ((mchunkptr) (((char *) (p)) + (s)))
        // p这时候指向原p紧邻的低地址处chunk
        unlink(av, p, bck, fwd);
        // 解链向,p指向的低地址处chunk向后合并
      }

      if (nextchunk != av->top) {
        // 如果nextchunk不是top chunk的话,nextchunk向前合并
        nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
        if (!nextinuse) {
          size += nextsize;
          unlink(av, nextchunk, bck, fwd);
        // 解链时,nextchunk指向的chunk向前合并
        } else
          clear_inuse_bit_at_offset(nextchunk, 0);

        first_unsorted = unsorted_bin->fd;
        unsorted_bin->fd = p;
        first_unsorted->bk = p;
        // 将合并后的chunk加入unsortedbin中

        if (!in_smallbin_range (size)) {
            // 如果是largebin的话,将fd_nextsize和bk_nextsize指针置空
          p->fd_nextsize = NULL;
          p->bk_nextsize = NULL;
        }
        set_head(p, size | PREV_INUSE);
        p->bk = unsorted_bin;
        p->fd = first_unsorted;
        set_foot(p, size);
      }

      else {
        size += nextsize;
        set_head(p, size | PREV_INUSE);
        av->top = p;
      }

    } while ( (p = nextp) != 0);

      }
    } while (fb++ != maxfb);
  }
  else {
    malloc_init_state(av);
    check_malloc_state(av);
  }
}

  malloc_consolidate完成fastbin chunk的合并,注释中有所说明。下面来看一下how2heap中的例子。

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

int main() {
  void* p1 = malloc(0x40);
  void* p2 = malloc(0x40);
  fprintf(stderr, "Allocated two fastbins: p1=%p p2=%p\n", p1, p2);
  fprintf(stderr, "Now free p1!\n");
  free(p1);

  void* p3 = malloc(0x400);
  fprintf(stderr, "Allocated large bin to trigger malloc_consolidate(): p3=%p\n", p3);
  fprintf(stderr, "In malloc_consolidate(), p1 is moved to the unsorted bin.\n");
  free(p1);
  fprintf(stderr, "Trigger the double free vulnerability!\n");
  fprintf(stderr, "We can pass the check in malloc() since p1 is not fast top.\n");
  fprintf(stderr, "Now p1 is in unsorted bin and fast bin. So we'will get it twice: %p %p\n", malloc(0x40), malloc(0x40));
}

  malloc_consolidate函数起作用是在malloc(0x400)的过程中,在_int_malloc函数和malloc_consolidate函数处下断点进行调试。

first_unsorted = unsorted_bin->fd;
unsorted_bin->fd = p;
// 修改unsorted_bin中的fd指针 first_unsorted
->bk = p; // 将合并后的chunk加入unsortedbin中

  经历了上面这几行代码之后,原本在fastbin中的chunk就会移动到unsorted_bin中去。

   但是p1指向的chunk不会一直在unsortedhbin中,因为申请的chunk大小时0x400字节,unsortedbin中的chunk不满足这个条件,所以根据下面代码,插入到smallbin中去:

if (in_smallbin_range (size))
          // unsortedbin 中chunk插入smallbin中
            {
              victim_index = smallbin_index (size);
              bck = bin_at (av, victim_index);
              // 如果当前chunk属于smallbin,则将链表头赋值给bk
              fwd = bck->fd;
            }

  之后再次free p1指向的chunk,由于这时候,前面释放的p1在smallbin中,后一个释放的p1在fastbin中,这样一来虽然存在double free漏洞,但是绕过了fastbin的double free检测。

 

posted @ 2020-11-25 00:29  Riv4ille  阅读(511)  评论(0编辑  收藏  举报