目录

不同版本glibc的堆管理和新增保护机制

最近阅读了《glibc内存管理ptmalloc源码分析》一书,对ptmalloc内部机制了解更深入了一层。但是书中所分析的libc版本是2.23,如今libc以及更新到2.31,且pwn中libc的版本也普遍是2.27及以上,所以就想写一篇博客纪录一下各个版本libc堆管理的差别和新增的保护机制。

Glibc2.23

malloc和free的逻辑

这个版本的libc应该是大家最为熟悉的,在入门pwn堆题的时候,做到的题大多都是这个版本。这个版本的堆管理有挺多的漏洞,利用方法也是最多的。

首先让我们先看一下这个版本下_int_malloc的逻辑,由于这个函数的源代码太多了,在这里我仅仅只是从中提取主要部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
void * _int_malloc(size)
{
    size_t nb=req2size(size)
    if(nb <= get_max_fast()){
		//如果请求的大小小于fastbin支持的最大大小
        //那么从fast_bins中搜索,搜索到就返回
    }
    if(in_small_bin_range(nb))
    {
        //在fastbin中没有找到空闲的chunk或则请求大小大于fastbin支持的最大大小,就到这一步
        //判断申请的大小是否在small_bin范围内
        //如果在就从small_bin中搜索是否有复合的块,有就返回
    }
    else
    {
        //请求的块在largin_bin的范围内
        if(have_fastchunks(av))
            malloc_consolidate();
        //先判断fast_bins是否为空
        //不为空,则触发malloc_consolidate,将fast_bins中的堆块合并
    }
    foreeach(chunk:unsorted_bin)
    {
        //到这一步,如果请求大小在small_bin范围内,那么在fast_bins和small_bins中并未找到合适的,可能unsortedbins里面会有
        //如果大小在large_bins范围内,那么可能发生了malloc_consolidate(),并将合并后的chunk放入unsortedbins,所以unsortedbins里面可能也会有合适的chunk
        //这一步就是遍历unsortedbins里面的chunk,看是否有合适的用于分配。
        if(is_small&&only_chunk&&chunck_size>nb+MINISIZE)
        {
            //如果请求的大小在small_bins范围内,并且unsorted_bins中仅有一个堆块,这个堆块的大小大于请求大小加MINISIZE,则分配。
            //MINISIZE在不同位数系统中不一样,MINISIZE就是malloc分配的最小堆块大小,这里之所以要保证大于nb+MINISIZE是因为当将这个chunk切割后,剩下的chunk也可以用于分配。
        }
        if(size==nb)
        {
            //如果在unsortedbin中找到一个和请求大小一样的堆块,直接分配
        }
        if(in_small_range(size))
        {
            //在搜索unsortedbin的过程中,如果其大小在smallbin中,将其放入smallbin中进行管理
        }
        else
        {
            //不在smallbin中,就必定在largebin中
            //放入largebin中
        }
    }
            //到这里,我们以及把unsortedbin中的堆块,重新分配到了smallbin和largebin中,这个时候smallbin或则largebin中可能会有符合我们请求的chunk
    if(!in_small_range(nb))
    {
        //搜索largebin中,此时会按照small-first,best-fit的原则找一个合适的堆块,直接分配或则分割后分配
    }
    for(bin in bins)
    {
        //到这一步,表面在属于自己大小的idx中找不到合适的堆块,将会尝试去从比自己大的idx中找到合适的堆块
    }
    //到这里如果还没有分配成功那么就是是同Top chunk进行分配
use_top:
    if(size>=nb+MINISIZE)
    {
        //Top chunk的大小大于请求大小加MINISIZE,则从TOP chunk中分配
    }
    else if(heave_fastchunks(av))
    {
         //如果Top chunk的大小不足够用于分配,则判断fastbin中是否有chunk有则执行malloc_consolidate
        malloc_consolidate();
        //由于到了这里,fastchunks中还有堆块,则请求的堆必定是在smallbin范围内,否则在之前就以及情况fastbin了
        //之后会返回外层循环,尝试重新分配
    }
    else
    {
        //到这里如果还没有找到合适的堆块的话,那么久只有尝试申请新的堆块了。
        brk()
        //注意这里要区分主/负分配区,只有主分配去是用brk分配内存。
    }
}

在上面的分析中我们对于2.23版本libc堆管理有了一个较为清晰的流程,更高版本的libc中,堆管理的流程相差不大,只是在里面加入了一些新的机制,后面我只写出其新增的流程和其位于流程中的位置。

下面我再简单的描述一下_int_free的逻辑,当然这里也只是抽取主要部分,细节部分需要大家自己去阅读源码了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
staci void 
_int_free(mstate av,mchunkptr p){
    size_t size=chunksize(p);
    check_inuse_chunk(av,p);
    //检测所需要释放的堆块是否在使用
    if(size<=get_max_fast())
    {
        if(next_chunk.size<=2*SIZE_SZ||next_chunk.size>=system_mem)
        {
            error();
            //检查下一个堆块的大小是否正常
		} 
        if(old==p)
        {
            error();
            //检查fastbin表头是否和当前块相同,相同则是double_free
        }
        //将堆块放入fastbin头部
    }
    else if(!chunk_is_mapped(p))
    {
        if(!prev_inuse(p))
        {
            //如果上一块没有使用,则与其合并
            p=prev_chunk(p);
            unlink(av,p,bck,fwd);
        }
        if(nextchunk != av.top)
        {
            if(!next_insue(p))
            {
                //如果下一个堆块不是topchunk且未使用与其合并。
            }
        }
        else{
            //如果下一个堆块是topchunk,则与topchunk合并
            av.top=p;
        }
        if(size>FASTBIN_CONSOLIDATION_THRESHOLD)
        {
            //检查所需要释放的空间是否大于consolidate的阈值,如果大于则执行malloc_consolidate,并回收空间
            malloc_consolidate(av);
            systrim(heap);
        }
    }
    else{
        munmap_chunk(p)
    }
}

高版本的释放逻辑基本2.23类似,只不过会添加更多的保护机制,在之后的分析中我仅会列出与2.23有变化的部分,相同的部分我就不进行阐述了。

我们的堆漏洞利用,通常都是由于程序堆中存在Dangling pointer或则overflow漏洞,再加上ptmalloc在一些地方保护不严谨,其保护机制可以被我们绕过甚至利用来达到任意地址写,进而getshell。比如利用fastbin分配时检测不充分,达到任意地址分配的目的。利用unlink解链达到写地址的操作。这里我也简单罗列一下在2.23版本下,可以使用的堆利用技巧。

堆利用

  • house of spirit:构造一个fake_chunk并进行free,加入到fastbin中,下一次就可以分配我们的fake_chunk。
  • house_of_force:溢出到top_chunk,修改其size,令超大的分配可以从top_chunk中返回而不通过mmap痛过malloc(&top-x)大小的分配返回任意地址。
  • house_of_lore:利用smallbin和largebin在头部插入尾部取出的特性,伪造bin中某个chunk的bk指针为fake_chunk并且fake_chunk的fd字段需要为该bin的地址。然后下一次分配就是我们的chunk。
  • house_of_orange:利用堆溢出和IO_list_all来获取shell。利用unsorted bin攻击修改io_list_all,然后将small bin中的0x61大小堆块看成FILE结构,修改相应的数据。不过unsorted bin和把0x61大小的chunk放入small bin是同时进行的。
  • house_of_einherijar:修改相邻chunk的size和pre_inuse位,使堆块合并进而出现堆块重叠。
  • house_of_roman:利用fast_bin和IO_FILE来达到泄漏libc的目的。
  • house_of_rabbit:利用malloc_consolidate的时候没有检查该bin是否符合该idx。如果修改size为一个更大的值将可以发生堆块重叠。
  • Unlink:伪造堆块,使堆块释放时,执行unlink,从而修改一些地址,但是为了绕过unlink的保护,修改的地址有限制。
  • 堆重叠:修改size达到可以修改已经释放的chunk的目的进而可以进行更多的攻击
  • unsorted bin attack:利用unsortedbin的双向链表结构,篡改bk的值使任意地址储存一个较大的值。
  • large bin attack:利用malloc中将unsorted bin中的largin chunk放入large bin时没有做充足的检测进而可以修改任意地址为堆块的值。

2.23堆利用就介绍到这里,下面让我们来看一看2.27版本新增的机制和保护策略以及堆利用。

Glibc2.27

tcache bin机制

2.27版本比2.23多了一个tchache bin。tchache bin是在2.26版本中引入的,用来进一步提升堆管理性能。

首先我们看一下tcache bin的几个宏定义(以下代码来至libc2.27,之后将不做说明)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* 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

这里挑几个重点宏定义讲一下。TCACHE_MAX_BIN定义了用于管理tcache的最大bin数量,其被定义为64,及有64个bin用于管理tcache bin。其编号idx依次从0到63,对应的chunk size从MALLOC_ALIGNMENT*2到MALLOC_ALLIGNMENT*64,如果是64位系统,tcache bin中最大能储存0x800的大小。HACHE_FILL_COUNT定义了给个bin最多能粗存的个数。

再来看两个重要的tcache结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/* 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;

tchache_entry是一个单向列表,用来将bin中的chunk连成一个单链,其机制与fast bin类似。tcache_perthread_struct是用来管理bin的一个结构,其有一个char数组成员,用来记录给个bin已经储存了几个chunk,entries是每个不同大小bins的队头,用来遍历和管理tcache bin。tcache_perthread_struct在tcache_init()中创建,让我们一起来看一下这个函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static void
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));
    }

}

其根据tchache_perthread_struct的大小,用malloc创建一个实例并将其赋值给全局遍历,tchache,而tchache就是tchache_perthread_struct的指针。并且tchache_init是在初始话时被调用,所以此时创建的chunk是堆块中第一个创建的chunk。(如果实现了任意地址修改,就可以修改此处的chunk,进而可以控制tcache bin的数量和指针)

接下来再介绍一下加入tcache和取出tcache的两个函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* Caller must ensure that we know tc_idx is valid and there's room
   for more chunks.  */
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]);
}

/* Caller must ensure that we know tc_idx is valid and there's
   available chunks to remove.  */
static __always_inline 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_put将tcache放入bin中,仅仅只是检查了其大小是否符合tcache。tcache_get从tcache中获得一个bin,也仅仅检测了大小是否符合tcache以及bin不为空。可以看出tcache安全机制特别简单,所以引入了tcache反而使漏洞利用更简单了。

下面我们看一看引入tcache后,malloc和free做了哪些改变。

malloc和free的逻辑

首先看一下__libc_malloc,这里我们只写出该函数中我们关心的部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void *
__libc_malloc (size_t bytes)
{
    if (tc_idx < mp_.tcache_bins
      /*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
      && tcache
      && tcache->entries[tc_idx] != NULL)
    {
      return tcache_get (tc_idx);
    }
    victim = _int_malloc (ar_ptr, bytes);
}

从函数代码中,我们可以看出,在执行int_malloc之前,程序首先去搜索tcache,查看tcache中是否有空闲的bin,如果有直接从tcache中分配,如果没有才执行int_malloc。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static void *
_int_malloc (mstate av, size_t bytes)
{
    size_t nb=req2size(bytes);
    if(nb<=get_max_fast())
    {
        //与2.23的区别在于,如果大小在fast_bin的范围内,那么如果尝试从fast_bin中获取堆块
        //获取成功后,将fast_bin中剩余的堆块放入tcache中
    }
	if(in_small_bin())
    {
        //与2.23的区别在于,获取small_bin成功后,将剩下的堆块放入tcache中
    }
    else{
        malloc_consolidate();
    }
    for(chunk::unsorted_bin)
    {
        ......
        if(chunk_size==nb)
        {
            //将chunk放入tcache中,并将return_cached置为1
        }
        ......
        if(return_cached)
        {
            //如果return_cached为1,则从tcache中分配
        }
    }
    .....
    
}

2.27的_int_malloc和2.23的主要区别在于2.27会将fastbin和smallbin还有unsortedbin中多余的块放入tcache中。

接下来让我们看一下free逻辑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
    size_t size=chunksize(p);
    if(tcache)
    {
        //将该chunk放入tcache中
    }
    ......
}

2.27的free与之前主要的区别在于先检查是否处于tcache范围,且tcache未存满,如果没有存满则直接put进去,之后的操作于2.23如出一辙。

堆利用

  • tcache_dup:tcache的释放没有进行double free检测,如果存在UAF漏洞可直接对tcache进行double free 进而实现任意地址分配。
  • tcache_poisoning:类似于修改fast bin的fd字段。当程序存在溢出漏洞时,可以修改tcache的next字段,达到任意地址分配的目的。
  • tcache_house_of_spirit:类似于fast bin的house_of_spirit,但是tcache的检测比fastbin更为简单,只需要释放的chunk满足size要求即可,并不会对下一个chunk进行检测。
  • house_of_bot_cake:该方法的利用需要有UAF漏洞。释放7个chunk进而填满tcache,再释放两个相同的chunkA和B使其consolidate,再分配一个相同大小的chunk,让tcache有空余,再利用UAF再次释放chunkB,进而可以实现overlapping。
  • tcache_stashing_unlink_attack:利用分配small bin时会将多余的块放入tcache的机制,恶意修改bk的值,以达到修改某个地址为一个很大的值的目的。类似于unsorted bin attack。
  • fastbin_reverse_into_tcache:该利用方法和上一个利用方法原理类似,利用了分配fastbin时,会将多余的chunk放入tcache中。做法是先将tcache填满,再释放7个chunk,并修改fast表头的fd指针为我们想修改的地址。之后malloc8个chunk,在分配第8个chunk时,将会把fast bin中的chunk放入tcache中,从而修改目标地址。

Glibc2.29

libc2.29没有修改malloc和free的逻辑也没有新增像tcache一样的机制,只是修改了一些在malloc和free chunk时的检测机制,下面让我们一起来看一看2.29相较于之前的版本,检测机制做出了什么样的修改。

tcache

我们知道2.27版本的tcache安全保护机制十分的脆弱,很容易就能被利用,所以在2.29版本中,对tcache结构体加入了新的字段 key。

1
2
3
4
5
6
typedef struct tcache_entry
{
  struct tcache_entry *next;
  /* This field exists to detect double frees.  */
  struct tcache_perthread_struct *key;
} tcache_entry;

加这个字段的目的主要是用来检测double free。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
  assert (tc_idx < TCACHE_MAX_BINS);

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

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

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]);
  e->key = NULL;	//new
  return (void *) e;
}

在加入tcache_bin时将key字段赋值为tcahce的地址,而在取出时,会将该字段清零。为什么这么做呢我们看一下int_free中新增的检测机制就清楚了。

int_free新增检测机制

Int_free中新增了对tcache释放时,key字段的检验,下面是伪代码。

1
2
3
4
5
6
7
void * _int_free()
{
	if(e->key==tcache)
	{
		error("double free!");
	}
}

如果当前释放的chunk,其key字段为tcache的话,那么判断其为double free

在free中除了新增了对tcache的检测,还增强了unlink的检测机制。

1
2
3
4
5
6
7
8
if (!prev_inuse(p)) {
      prevsize = prev_size (p);
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      if (__glibc_unlikely (chunksize(p) != prevsize))	//new
        malloc_printerr ("corrupted size vs. prev_size while consolidating");
      unlink_chunk (av, p);
    }

在进行unlinlk解链前,会检查该chunk的大小是否和presize相同。增加了null of byte的利用难度。

int_malloc新增检测机制

在2.29int_malloc中新增了关于unsorted bin的检测

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 if (__glibc_unlikely (size <= 2 * SIZE_SZ)
              || __glibc_unlikely (size > av->system_mem))
            malloc_printerr ("malloc(): invalid size (unsorted)");
          if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ)
              || __glibc_unlikely (chunksize_nomask (next) > av->system_mem))
            malloc_printerr ("malloc(): invalid next size (unsorted)");
          if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size))
            malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)");
          if (__glibc_unlikely (bck->fd != victim)
              || __glibc_unlikely (victim->fd != unsorted_chunks (av)))
            malloc_printerr ("malloc(): unsorted double linked list corrupted");
          if (__glibc_unlikely (prev_inuse (next)))
            malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)");

检测了当前chunksize的合法性,nextchunksize的合法性以及双相链表的完整性。加入了这几个检测机制后,unsorted bin attack攻击将很难继续使用。

use_top

在int_malloc中对use_top增加了检测机制。

1
2
if (__glibc_unlikely (size > av->system_mem))//0x21000
        malloc_printerr ("malloc(): corrupted top size");

在使用top chunk时对top chunk的大小进行了检验,使得house of force再也不能使用,从此载入史册。

堆利用

2.29与2.27相比,unsorted_bin_attack,house of force,tcache_dump这三个漏洞利用技术,无法再使用。

Glibc2.30

2.30版本的libc与2.29没有太大的变化,但是对int_malloc中将unsorted bin中的内容放入large bin中时做了更多的检测,使得large bin attack再也无法使用,成为历史。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    victim->fwufad_nextsize = fwd;
    victim->bk_nextsize = fwd->bk_nextsize;
    if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
        malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
    fwd->bk_nextsize = victim;
    victim->bk_nextsize->fd_nextsize = victim;
    }
bck = fwd->bk;
if (bck->fd != fwd)
    malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");

在将unsortd bin放入large bin时会检验fwd链表的完整性,使得largin attack通不过检测,攻击不成功。large bin从此退出利用舞台,载入历史。

Glibc2.31

libc2.31版本在堆块管理和安全机制上和2.30没有什么太大的变化。

参考

ptmalloc与glibc堆漏洞利用

how2heap

glibc-2.29新增的保护机制学习总结

Heap Exploit v2.31 | 最新堆利用技巧,速速查收