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

 

前言

最近发现高质量比赛越来越多使用glibc-2.29的环境了,为了赶上出题人逐渐高(变)明(态)的出题思路,趁机学习一波glibc-2.29源码,看看对比2.27多了哪些保护措施,又有哪些利用手段失效了,并提出本人能想到的相应的应对方法。 本文讨论的都是基于64位环境,32位环境的结构体、偏移等需要相应变化。

 

tcache结构和成员函数变化

//glibc-2.29
typedef struct tcache_entry
{
  struct tcache_entry *next;
  /* This field exists to detect double frees.  */
  struct tcache_perthread_struct *key;
} tcache_entry;

//glibc-2.27
typedef struct tcache_entry
{
  struct tcache_entry *next;
} tcache_entry;

glibc-2.29在tcache_entry结构体中加上一个8字节指针key。

//glibc-2.29
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;
}

//glibc-2.27
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_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;
}

在将chunk放入tcache之后,会将chunk->key设置为tcachestruct,即是heap的开头,来表示该chunk已经放入了tcache。而将chunk从tcache取出来后则将chunk->key设置为NULL清空。 总体上对tcache的改动是在tcacheentry结构指针中增加了一个变量key,来表明该chunk是否处于tcache的状态。

 

intfree函数

//glibc-2.29
  {
    size_t tc_idx = csize2tidx (size);
    if (tcache != NULL && tc_idx < mp_.tcache_bins)
      {
	/* Check to see if it's already in the tcache.  */
	tcache_entry *e = (tcache_entry *) chunk2mem (p);

	/* This test succeeds on double free.  However, we don't 100%
	   trust it (it also matches random payload data at a 1 in
	   2^<size_t> chance), so verify it's not an unlikely
	   coincidence before aborting.  */
	if (__glibc_unlikely (e->key == tcache))
	  {
	    tcache_entry *tmp;
	    LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
	    for (tmp = tcache->entries[tc_idx];
		 tmp;
		 tmp = tmp->next)
	      if (tmp == e)
		malloc_printerr ("free(): double free detected in tcache 2");
	    /* If we get here, it was a coincidence.  We've wasted a
	       few cycles, but don't abort.  */
	  }

	if (tcache->counts[tc_idx] < mp_.tcache_count)
	  {
	    tcache_put (p, tc_idx);
	    return;
	  }
      }
  }

//glibc-2.27
{
    size_t tc_idx = csize2tidx (size);
    if (tcache//free+172
        && tc_idx < mp_.tcache_bins
        && tcache->counts[tc_idx] < mp_.tcache_count)
      {
        tcache_put (p, tc_idx);
        return;
      }
  }

glibc-2.29中增加了一个check:chunk在放入tcache之前会检查chunk->key是否为tcache,表示是否已经存在于tcache中,如果已经存在于tcache,则会检查tcache链中是否有跟他相同的堆块。 这对double free造成了很大的障碍。我认为绕过的一种方法是:如果有存在UFA漏洞或者形成堆重叠等情况,可以篡改chunk->key,使其e->key != tcache,就能不进行下面的check。

例题

int main()
{
    long* p1 = malloc(0x10);
    long* p2 = malloc(0x10);
    free(p1);
    free(p2);
    //*(p1+1)=0xdeadbeef;
    free(p1);
	return 0;
}

正常情况下,glibc-2.29会检测到tcache上的double free。

int main()
{
    long* p1 = malloc(0x10);
    long* p2 = malloc(0x10);
    free(p1);
    free(p2);
    *(p1+1)=0xdeadbeef;
    free(p1);
	return 0;
}

如果将其e->key修改后,通过调试可以发现能绕过e->key==tcahce的检查,从而实现double free。

但是这种绕过方法本身利用的堆溢出漏洞往往是可以修改fd的,这个时候其实也就没有必要修改e->key,可以直接篡改e->next(即fd)来实现任意地址写。因此要绕过这种保护机制本身又需要其他漏洞,利用起来比较麻烦。

unlink

//glibc-2.29
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);
    }

//glibc-2.27
if (!prev_inuse(p)) {
      prevsize = prev_size (p);
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      unlink(av, p, bck, fwd);
    }

glibc-2.29在unlink操作前增加了一项检查:

要合并的size和本来要释放的chunk的prevsize是否相等

这种利用方式常见于off by one,修改prev_inuse表示位为0,然后通过修改prevsize使得合并指定偏移的chunk,而size基本上都是不等于伪造的presize的,这对off by one的利用提出更高的要求。

一种方法是如果off by one溢出的那个字节可以控制,需要将合并的chunk的size改大,使其越过在其下面若干个chunk,满足size==prevsize的条件,还是可以形成chunk overlapping的。但因为off by null只可能把size改小,所以如果不能控制溢出的字节,就无法构造chunk overlapping了。

 

intmalloc函数

unsortedbin

//glibc-2.29
			mchunkptr next = chunk_at_offset (victim, size);
			
          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)");

这段代码是glibc-2.29新增的检查,有4项检查内容:

1、下一个chunk的size是否在合理区间

2、下一个chunk的prevsize是否等于victim的size

3、检查unsortedbin双向链表的完整性

4、下一个chunk的previnuse标志位是否为0

其中第三项检查内容对unsortedbin attack来说阻碍很大,因为unsortedbin attack目的是往目标地址中写入main_arena地址,需要修改victim->bk也即bck。如果还想这么利用,就需要在目标地址上写上victim的地址(通常是heap地址,因此需要提前知道heap地址),而且还有一点是不能修改victim->fd的值,除非在篡改victim->bk的时候不覆盖掉victim->fd或者已知libc地址。也就是说,在大多数情况下,除非你已知heap和libc的地址,否则很难在利用unsortedbin attack了。

 

use_top

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

glibc-2.29在使用top chunk的时候增加了检查:

size要小于等于system_mems

因为House of Force需要控制top chunk的size为-1,不能通过这项检查,所以House of Force在glibc-2.29以后就载入史册了。

示例

将top chunk的size写为-1后  显然无符号比较-1>0x21000,不能通过检查。

 

结语

glibc-2.29增加了不少的保护措施,不学习就跟不上时代潮流了。笔者通过大致阅读常用的几个函数,发现以上与安全相关的变化,主要涉及到tcache、unlink、top chunk、unsortedbin这四类结构或功能的变化。glibc-2.29可能还有其他新的机制还没有被发现,可能也有其他的绕过方式,希望各路大佬多多指导指导!

 

Reference

http://blog.eonew.cn/archives/1167

(完)