selph
selph
发布于 2023-07-14 / 132 阅读
0
0

glibc源码解读01:Fastbin的申请与释放与Fastbin Dup简介

从libc2.23开始,到最新版,都会有的,慢慢更新ing

本文源码版本:2.23

从fastbin中申请chunk

__libc_malloc

首先是调用malloc函数:

void *
__libc_malloc (size_t bytes)
{
  mstate ar_ptr;
  void *victim;

  // 判断变量__malloc_hook是否有值,有值就跳转执行
  void *(*hook) (size_t, const void *)
    = atomic_forced_read (__malloc_hook);
  if (__builtin_expect (hook != NULL, 0))
    return (*hook)(bytes, RETURN_ADDRESS (0));

  // 取出arena地址到ar_ptr
  arena_get (ar_ptr, bytes);

  // 申请内存
  victim = _int_malloc (ar_ptr, bytes);
  /* Retry with another arena only if we were able to find a usable arena
     before.  */// 如果申请失败了,就重试一次
  if (!victim && ar_ptr != NULL)
    {
      LIBC_PROBE (memory_malloc_retry, 1, bytes);
      ar_ptr = arena_get_retry (ar_ptr, bytes);
      victim = _int_malloc (ar_ptr, bytes);
    }

  if (ar_ptr != NULL)
    (void) mutex_unlock (&ar_ptr->mutex);

  assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
          ar_ptr == arena_for_chunk (mem2chunk (victim)));
  return victim;  // 申请成功了就返回
}

在第17行这里,调用_int_malloc正式进入malloc的流程

_int_malloc

这里传入的参数有2个,参数1是arena的地址,参数2是申请内存的大小

首先是判断传入的arena是否有值,如果没有,则不从arena中分配内存

static void *
_int_malloc (mstate av, size_t bytes)
{
...

  // 没有可用arena,调用sysmalloc从mmap中获取chunk
  /* There are no usable arenas.  Fall back to sysmalloc to get a chunk from
     mmap.  */
  if (__glibc_unlikely (av == NULL))
    {
      void *p = sysmalloc (nb, av);
      if (p != NULL)
	      alloc_perturb (p, bytes);
      return p;
    }

接下来紧接着就是fastbin相关的代码了:

  /*
     If the size qualifies as a fastbin, first check corresponding bin.
     This code is safe to execute even if av is not yet initialized, so we
     can try it without checking, which saves some time on this fast path.
   */
  // 如果大小满足fastbin,首先检查相关的bin
  // 检查申请大小是否小于global_max_fast
  if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
    {
      idx = fastbin_index (nb); // 计算fastbin索引
      mfastbinptr *fb = &fastbin (av, idx); // 取出fastbin指针
      mchunkptr pp = *fb; // 取出fastbin第一个成员
      do  // 取一个合适的chunk
        {
          victim = pp;
          if (victim == NULL)
            break;
        }
      while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))
             != victim);	// 这里取出fd的值到bin里
      if (victim != 0)  // 如果取出成功
        { // 校验:判断取出的chunk的大小是否满足fastbin所属的大小
          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大小,这里的get_max_fast()是个全局变量:global_max_fast

#define get_max_fast() global_max_fast

然后紧接着调用fastbin_index()来计算该申请大小所对应的fastbin索引,对于64位程序,计算方式就是右移4位然后减去2

然后从fastbin数组中取出bin的地址到fb,取出第一个成员的地址到pp

接下来就是取出pp,校验一下,如果校验通过了,就计算其用户空间地址,然后返回,如果校验不通过,则返回NULL

这里的校验是,判断申请的大小对应的fastbin索引,是否和fastbin所对应的索引一样,说人话就是,校验申请的chunk的大小是否符合该fastbin,这也是为什么使用fastbin dup的时候需要寻找大小满足要求的fake chunk

到此,从fastbin中申请chunk的流程就走完了,后续流程则是判断smallbin,largebin,unsortedbin,从top chunk申请,这些内容后面再读,一点一点推进

释放chunk到fastbin

__libc_free

void
__libc_free (void *mem)
{
  mstate ar_ptr;
  mchunkptr p;                          /* chunk corresponding to mem */
  // 检查__free_hook是否有值,有就调用返回
  void (*hook) (void *, const void *)
    = atomic_forced_read (__free_hook);
  if (__builtin_expect (hook != NULL, 0))
    {
      (*hook)(mem, RETURN_ADDRESS (0));
      return;
    }
  // 0地址不进行操作
  if (mem == 0)                              /* free(0) has no effect */
    return;
  // 把user地址变成chunk地址,也就是-0x10
  p = mem2chunk (mem);
  // 检查chunk是否是map的,映射的内存会单独处理,由chunk标志位的第二位决定
  if (chunk_is_mmapped (p))                       /* release mmapped memory. */
    {
      /* see if the dynamic brk/mmap threshold needs adjusting */
      if (!mp_.no_dyn_threshold
          && p->size > mp_.mmap_threshold
          && p->size <= DEFAULT_MMAP_THRESHOLD_MAX)
        {
          mp_.mmap_threshold = chunksize (p);
          mp_.trim_threshold = 2 * mp_.mmap_threshold;
          LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2,
                      mp_.mmap_threshold, mp_.trim_threshold);
        }
      munmap_chunk (p);
      return;
    }
  // 获取chunk对应的arena
  ar_ptr = arena_for_chunk (p);
  _int_free (ar_ptr, p, 0); // 调用_int_free
}

先检查__free_hook,然后检查是否是映射内存,如果都没,则进入free流程

_int_free

static void
_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 */

  const char *errstr = NULL;
  int locked = 0;
  // 去除标志位,获取chunk大小
  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))
    {
      errstr = "free(): invalid pointer";
    errout:
      if (!have_lock && locked)
        (void) mutex_unlock (&av->mutex);
      malloc_printerr (check_action, errstr, chunk2mem (p), av);
      return;
    }
  /* 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)))
    {
      errstr = "free(): invalid size";
      goto errout;
    }

  check_inuse_chunk(av, p);

刚开始,先是获取chunk的大小,通过去除标志位的值来获取

然后校验一下内存对齐,这个chunk不应该位于奇怪的位置上

/* 如果是合格的,放置chunk到fastbin,以至于下次可以更快的使用
    If eligible, place chunk on a fastbin so it can be found
    and used quickly in malloc.
  */
  // 大小检查,是否符合fastbin范围
  if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())

#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))
      {
	/* 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;
	  }
      }

    free_perturb (chunk2mem(p), size - 2 * SIZE_SZ);

    set_fastchunks(av);
    unsigned int idx = fastbin_index(size); // 计算索引
    fb = &fastbin (av, idx);  // 取出对应的bin的地址(在arena中

    /* Atomically link P to its fastbin: P->FD = *FB; *FB = P;  */
    mchunkptr old = *fb, old2;  // old是第一个成员
    unsigned int old_idx = ~0u;   // 0xffffffff
    do
      {
	/* Check that the top of the bin is not the record we are going to add
	   (i.e., double free).  */// 如果释放的chunk的地址和该bin的第一个chunk的地址相同,则说明正在发生双重释放
	if (__builtin_expect (old == p, 0))
	  {
	    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) // 如果该bin里已经有chunk了
	  old_idx = fastbin_index(chunksize(old));  // 计算索引
	p->fd = old2 = old; // 给新的chunk的fd写入原本第一个bin chunk的地址
      }
    while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2);// 这里把chunk加入到了bin里
    // 这里把p写入到了fb里,fb是bin的地址
    if (have_lock && old != NULL && __builtin_expect (old_idx != idx, 0))
      {
	errstr = "invalid fastbin entry (free)";
	goto errout;
      }
  }

接下来检查是否满足fastbin大小要求,如果满足了,就根据大小计算所属fastbin的索引

接下来又是一个安全检查,检查双重释放:第一个成员和当前是否的chunk是否是相同地址,如果是,则表示发生双重释放,这里说明如果不是连续释放两个同样的chunk,中间参一个其他的chunk,就能绕过该缓解

最后的处理则是判断bin里是否有成员了,如果有,则计算原本成员的索引,把其地址写入新的chunk.fd里,把新的chunk写入arena中

最后还有一个校验:原本成员的索引和当前新加入的chunk的索引需要一样,说人话就是,校验chunk大小是否满足该bin

然后跳出if函数就返回了

Fastbin dup 介绍

fastbin dup是一种堆利用手法,功能是链接一个fake chunk到fastbin,通过Double Free

造成根本原因是:Double-Free

分析源码可知,Double-Free漏洞的缓解措施仅仅是检查当前释放的chunk和bin里第一个chunk是否是同一个chunk,如果不是,则正常执行

释放chunk的顺序是A->A,会触发缓解

如果释放chunk的顺序是A->B->A,则能绕过缓解

有了Double-Free,就可以再申请一个chunk,并修改其中的内容,同时也就修改了fastbin chunk 的fd指针,从而将fake chunk给加入到了fastbin中


评论