技术讨论 | Glibc中堆管理的变化 – 作者:白里个白sofr

前言

在学pwn的道路上,我们大多从linux入手,从栈到堆,各种漏洞利用,都和Glibc或多或少打过交道。我的堆入门应该和很多人一样是从libc2.23开始的,之后又经历了各种libc版本的变化,随着现在的pwn题越来越与时俱进,我们会逐渐接触更新的libc版本,因此,我们必须知道,Glibc中堆管理变化了什么,从安全角度,我们的得失又是什么呢?从libc2.27开始,我们聊一聊Glibc中堆管理的漏洞利用的得失。

关键字: CTF pwn 新版本libc libc 2.27 libc 2.29 libc 2.30 堆溢出

GLibc2.27

Glibc2.23我就不想多说了,感兴趣的朋友可以学一学pwn相关的堆漏洞利用知识,网上现在总结的也算是比较多。我就不赘述了。从Glibc2.27开始,发生了很多有趣的地方,我们一起聊一聊。

Tcache

Tcache可是说是Glibc2.27中一个大的改变,其实Tcache的引入是从Glibc2.26开始的。但是(以下个人见解)Linux中比较受欢迎的发行版,ubuntu 18.04中的libc版本是2.27,再加上很多发行版都是2.27版本,所以,我们常见的pwn题也就在这种环境下编译开发了,因此,我们直接说说2.27版本,跳过2.26版本。

我认为Tcache使得漏洞利用变简单了,其得失我总结了一下:

漏洞利用最后一哆嗦,特别简单暴力

Tcache的管理结构在堆上,比main_arena好搞一点,毕竟Libc地址一般比堆地址难搞到

Tcache有时候使得泄露Libc地址变得困难

这里我说下Tcache的机制,tcache就是一个为了内存分配速度而存在的机制,当size不大(这个程度后面讲)堆块free后,不会直接进入各种bin,而是进入tcache,如果下次需要该大小内存,直接讲tcache分配出去,是不是感觉和fastbin蛮像的,但是其size的范围比fastbin大多了,他有64个bin链数组,也就是(64+1)*size_sz*2,在64位系统中就是0x410大小,有图有真相:

也就是说,在64位情况下,tcache可以接受0x20~0x410大小的堆块。

Tcache poisoning

那么Tcache对漏洞利用来说,不像fastbin attack一样,需要寻找合适的size了,在2.27的环境下是可以直接做到任意地址写的,这一点非常nice, 这种利用方法,在也被叫做tcache poisoning。同时,在double free领域,Tcache可以直接double free,而不需要像fastbin那样,需要和链上上一个堆块不一样,也就是下面这个样子。

 /*
 heap0 ----> heap1 ----> heap0 (fastbin YES)
 heap0 ----> heap0 (fastbin NO)
 heap0 ----> heap0 (Tcahce YES)
 */

还有一点不同,就是在Tcache中,fd指向的并不是堆头,而是堆内容,这一点也是需要我们注意的。

leak libc地址

单纯在堆中leak libc地址,一般是使用size大于fastbin范围的堆块,而在有tcache的情况下,这个变得相较之前困难,我将我目前用的比较多的方法总结如下:

1、申请8个大堆块,释放8个,这里堆块大小,大于fastbin范围,就是填满tcache。

2、有double free的情况下,连续free 8次同一个堆块,这里堆块大小,大于fastbin范围。

3、申请大堆块,大于0x410。

4、修改堆上的Tcache管理结构

大致就是以上几种方法,如果还有其他的想法,欢迎交流。

Tcache Stashing Unlink Attack

网上有很多人在分析这一漏洞的时候,都是基于libc2.29分析的,其实在libc2.27中,这一漏洞就已经存在了。

这里简单讲下,这是small bin 中的检查,即:__glibc_unlikely(bck->fd != victim)

    // 获取 small bin 中倒数第二个 chunk 。
                 bck = victim->bk;
                 // 检查 bck->fd 是不是 victim,防止伪造
                 if (__glibc_unlikely(bck->fd != victim)) {
                     errstr = "malloc(): smallbin double linked list corrupted";
                     goto errout;
                 }
                 // 设置 victim 对应的 inuse 位
                 set_inuse_bit_at_offset(victim, nb);
                 // 修改 small bin 链表,将 small bin 的最后一个 chunk 取出来
                 bin->bk = bck;
                 bck->fd = bin;

我们来看看Tcache中的情况:

 #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 over.  */
       while (tcache->counts[tc_idx] < mp_.tcache_count
      && (tc_victim = last (bin)) != bin)
 {
   if (tc_victim != 0)
     {
       bck = tc_victim->bk;
       set_inuse_bit_at_offset (tc_victim, nb);
       if (av != &main_arena)
 set_non_main_arena (tc_victim);
       bin->bk = bck;
       bck->fd = bin;//0
        tcache_put (tc_victim, tc_idx);//1
             }
 }
    }
 #endif

那么,我们需要注意2个地方,就是我在源码中标注的0和1。那么这两个地方由于没有任何检查,导致了两个问题,1、任意地址写libc地址,2、将任意地址放入tcache。

那么这段的逻辑是什么呢,简单来说,当我们从smallbin中申请了一个chunk后,会将此大小的tcache用smallbin里的堆块填满。

我们来看看什么时候,终止填入呢,两个条件:tcache->counts[tc_idx] >= mp_.tcache_count || (tc_victim = last (bin)) == bin就是上述while循环中的相反的条件。也就是说,如果smallbin里没heap了或者tcache填满了,就不需要继续填充了,但是由于我们期望漏洞利用,所以需要改掉bck,这就导致(tc_victim = last (bin)) == bin这个条件是很难达到的。所以,我们需要控制tcache中的数量,但是,这里又出现了一个矛盾,那就是如果Tcache不为空,就不会从smallbin中取出堆块。

所以,综上所述,只有绕过tcache的calloc能够符合这样的要求,那么,如果,我们想要任意地址写libc,就在tcache中留一个空间,如果期望任意地址放入tcache,就在tcache中留两个空间,同时,我们需要清楚,动手脚的small bin 应该是倒数第二个smallbin。

画个图示意一下:

将Chunk1的bk指向目标地址,再calloc一个0xa0大小的chunk,参照上述的目的,确定自己需要在Tcache中留几个heap。

Tcache结构破坏

这个其实没什么好说的,只是一个tips吧,tcache的管理结构在堆上,再加上tcache宽松的检查条件,其实有时候搞一搞这里还是蛮有意思的。

libc2.27中的东西基本就讲这些了,接下来就是libc2.29了

Glibc2.29

在2.27的基础上,我们看看2.29做了哪些改变:

Tcache的double free防护

首先是一个对漏洞利用者较为遗憾的改动,就是在tcache的结构体上,加了一个key。

在官方注释上,这一增加是为了检测tcache的double free,在2.27的libc中,tcache为了速度,几乎没有什么安全保护,这一机制会缓解部分漏洞利用。那么,这一增加如何作用呢,我们可以看到,在tcache_put中,对这一结构体进行了赋值,赋值的内容就是定义的tcache_perthread_struct结构体tcache的地址,tcache就是通过这一函数来判断当前的heap是否在tcache中,当然,在tcache_get中,也会将其清理。同时在free中加了这么一段。

 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.  */
   }

也就是说在free时,如果当前的chunk的bk位置是tcache这一地址,那么就会循环检测当前大小的tcache的链表,查看链表中是否存在当前的chunk。所以,想要double free前,记得先改一下bk。

unlink前操作

在free的时候,unlink前新加了一个检查,这个不太致命,注意绕过即可。

 /* consolidate backward */
     if (!prev_inuse(p)) {
       prevsize = prev_size (p);
       size += prevsize;
       p = chunk_at_offset(p, -((long) prevsize));
       if (__glibc_unlikely (chunksize(p) != prevsize))//add
         malloc_printerr ("corrupted size vs. prev_size while consolidating");//add
       unlink_chunk (av, p);
     }

unsortbin保护

不说了,unsortbin attack我先不用了,总可以了吧(含泪)。

   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)");
 ...... ......
     /* remove from unsorted list */
           if (__glibc_unlikely (bck->fd != victim))
             malloc_printerr ("malloc(): corrupted unsorted chunks 3");
           unsorted_chunks (av)->bk = bck;
           bck->fd = unsorted_chunks (av);

libc 2.30

那么到了libc2.30其实增加的东西也是不多了。

largebin attack

在largebin 中,加了这个,刚好对largbin的bk和bk_nextsize做出了限制。

那么在插入large bin时,就不能使用large bin Attack了(关于Largebin Attack的方法,可参照我之前文章)

写在最后

如有错误欢迎指正:[email protected]

*本文作者:白里个白sofr,转载请注明来自FreeBuf.COM

来源:freebuf.com 2020-05-13 10:00:34 by: 白里个白sofr

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论