前言

事情的起因还要从*ctf 2019说起,heap_master里能用unsorted bin attack实现一次任意地址写,一开始想到的是写stdout来实现地址泄露 ,但是这样要写两次才能实现,花了很大的经历找另外一次写,仍然没有找到,很绝望。

事后看wp看到是用一次写实现对变量global_max_fast的覆盖,从而实现后续的利用,对malloc以及free中涉及global_max_fast进行了一定的分析,并结合在网上找到的相关的题目进行了相应的实践。

在开始之前向大家推荐下我写的一个框架pwn_debug,写它的本意是方便大家的调试,主要的特点有:

  1. 支持带符号调试glibc,脚本中支持安装debug版的glibc(x64和x86都支持),以实现调试的时候可以看到glibc源码。
  2. 支持不同版本的glibc调试。如在ubuntu16上调试libc-2.29。
  3. 下断点方便,不管程序是否开启PIE。
  4. 使用方便,与pwntools兼容起来很简单(我觉得)。

源码分析

此次的源码是基于libc-2.23的,后续的版本加入了tcache,该机制相对来说比较简单与独立,所以还是基于2.23进行相应的分析,在64位系统上进行。
global_max_fast这个全局变量的作用是用来标志fastbin的大小的阈值,小于这个值的堆块会被认为是fastbin,使用fastbin的相应机制进行管理。看下它的定义:

#define set_max_fast(s) \
  global_max_fast = (((s) == 0)                           \
                     ? SMALLBIN_WIDTH : ((s + SIZE_SZ) & ~MALLOC_ALIGN_MASK))
#define get_max_fast() global_max_fast

set_max_fast初始化函数开始是在malloc_init_state调用的,可以看到这个宏定义的作用是设置global_max_fast默认值,默认值是0x80。

然后看malloc中对于fastbin的处理,fastbin处理很简单,就是找到对应的fastbin的单链表,并从中取出堆块,如果size检查通过就将该堆块返回:

if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
    {
      idx = fastbin_index (nb);
      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)
        {
          if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))  ## 检查size
            {
              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;  #返回
        }
    }

查看free中的fastbin相关的处理源码:

if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())

    ...
    ## 对size进行基本的检查
    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))
      {
    ...
    ## 对next chunk的size进行检查
    if (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;
      }
    ...

    ## 获取对应的fastbin index
    unsigned int idx = fastbin_index(size);
    fb = &fastbin (av, idx);

    ...

    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))
      {
        errstr = "double free or corruption (fasttop)";
        goto errout;
      }
      ...
    p->fd = old2 = old;
      }

对于fastbin的free过程主要包括如下:

  1. 对释放的堆块的size进行基本的检查。
  2. 对释放堆块的下一个堆块的size进行基本的检查。
  3. 获取释放堆块所对应的fastbin链表对应的索引。
  4. 检查是否是double free。
  5. 释放进单链表。

fastbin的单链表管理是比较简单的,与global_max_fast相关且需要注意的代码则是fastbin 所对应的index获取以及index所对应的指针获取的代码,即fastbin_index宏以及fastbin宏,对应代码如下:

#define fastbin_index(sz) \
  ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)

#define fastbin(ar_ptr, idx) ((ar_ptr)->fastbinsY[idx])

可以看到这两个宏仅仅是利用偏移来定位数组的指针,但是arena所对应的malloc_state中fastbins数组相关的定义为:

mfastbinptr fastbinsY[NFASTBINS]

#define NFASTBINS  (fastbin_index (request2size (MAX_FAST_SIZE)) + 1)

到这里问题就比较明显了,如果可以改写global_max_fast为一个较大的值,然后释放一个较大的堆块时,由于fastbins数组空间是有限的,其相对偏移将会往后覆盖,如果释放堆块的size可控,就可实现往fastbins数组(main_arena)后的任意地址写入所堆块的地址。

即利用global_max_fast进行相关的攻击

利用场景

对于global_max_fast的利用首先要解决的事情是如何覆盖global_max_fast。适用的场景应是存在任意地址写的漏洞,但是写入的地址却是不可控的(也是一个比较大的值),因为如果写入的值也是可控的话就不需要使用这个方法就能解决了,最典型的应该是unsorted bin attack,可实现往任意地址写入main_arena中的地址。

前置条件我想大概可能是需要泄露一个libc的地址,否则的话可能会像heap_master中一样需要爆破4bit的地址。

实现任意地址写的方式是:通过地址与fastbin数组的偏移计算出所需free的堆块的size,然后释放相应的堆块,即可实现往该地址写入堆块的地址以进一步利用。

计算偏移的代码可以如下:

fastbin_ptr=libc_base+libc.symbols['main_arena']+8
idx=(target_addr-fastbin_ptr)/8
size=idx*0x10+0x20

此时要解决的事情是往哪里写以达到实现利用的目的。可能有很多的地方,理论上来说只要是main_arena结构体后面的是函数指针或是结构体指针的地址都可以,目前很容易能够预想到的是:

  • _IO_list_all
  • stdout
  • stdin
  • stderr
  • __free_hook

复写前面四个就是使用IO_file攻击那一套方法,伪造结构体来实现任意读任意写或者伪造vtable来实现house of orange攻击。

复写__free_hook的话则需要一次uaf来修改释放进去的fd改成system或者one gadget,再将堆块申请出来,从而实现将__free_hook改写成system或者one gadget

实践

在网上找了一下,利用过程中涉及到global_max_fast的题目加上*ctf的heap_master,总共有4题:

  • BCTF 2018的baby_arena
  • 0CTF 2016的zerostorage
  • 胖哈勃杯pwn500-house of lemon
  • *CTF 2019的heap_master

其中胖哈勃杯pwn500-house of lemon,在网上找了半天没找到题目,问出题的0x9a82师傅求题目,师傅说年代太久远了,找不到了,所以最终题目数量是三题。

baby_arena

题目的意思比较简单,提供了三个功能分别是:Create OrderDelete Order以及login的功能。

漏洞也很明显,存在两个漏洞:一个是在create的时候遇到换行符就return了,没有加入\x00字节,导致可以泄露地址;另一个是login中存在一个栈溢出漏洞,不能覆盖返回地址,但是可以覆盖user指针,导致可以实现任意地址写的漏洞,但是该漏洞写的内容是不可控的,只能写adminclientele字符串。

如何利用?

首先使用第一个漏洞泄露处libc的地址,根据libc的地址计算得到global_max_fast以及_IO_list_all的等地址。

然后利用任意地址写的漏洞将global_max_fast从0x80覆盖为0x6E696D6461(admin),根据偏移释放一个堆块至_IO_list_all,将该堆块申请出来伪造好IO_file结构,重新free至_IO_list_all,接下来要做的就是触发FSOP,由于申请的size都会被认为是fastbin,因此想要触发错误很简单,随意申请一个大小的堆块就会触发io flush,从而getshell。

zerostorage

这题应该是比较经典的unsoted bin attack题,ctf wiki和brieflyx大佬的博客的writeup都有写。

值得一提的是writeup是基于老版本的内核,利用泄露出的libc地址通过偏移能够得到程序基址,目前主流的版本好像都不可以了,想了很久,找到一个只需要libc地址就能成功利用的方法,后续也会另外写一个详细的过程发出来。

此题最主要的一个漏洞就是uaf,在merge的时候from idto id没有进行检查,二者可以相同,导致了一个uaf漏洞的形成,从而可以实现泄露libc地址、堆地址和进行unsorted bin attack

在得到泄露出的地址后,利用unsorted bin attackglobal_max_fast覆盖成main_arena上的地址。接下来释放的堆块都会被当成fastbin往main_arena后面的地址上复写了。

但是往哪里写,前面_IO_list_all以及__free_hook这些目标的偏移所对应的size都是大于0x1000的,而题目的size是限制在0x1000以内的,使得申请出来的堆块size没法达到目标,所以没办法后续进行利用。

现有的writeup是老版本的内核中可通过libc基址得到程序基址,在程序bss段里构造伪造的fastbin从而实现泄露随机的异或key然后实现任意写。

没有程序基址如何操作呢,关键因素最后找到merge的时候没有对merge出来的size进行检查,由于堆块最大可为0x1000,因此最大可以merge出来0x2000大小的堆块,可以满足需求,复写到_IO_list_all,从而可通过伪造io file,像baby_arena一样的利用方式,最终拿到shell。由于篇幅的限制,细节就不说了,会在之后的文章给出。

heap_master

heap_master官方解是用large bin attack,经过分析,也可以使用unsorted bin attackglobal_max_fast结合起来来实现get shell。

这题首先mmap出来0x10000大小的内存,给了addeditdelete三个选项,add函数是通过malloc申请出来一个堆块,但是edit以及delete都是对于mmap出来的堆块操作,一个很奇怪的题。

分析下来,漏洞存在的地方是可以在大的内存块中伪造堆块释放进去main_arena里,同时仍然可以edit,可以说是一个变相的uaf。

没有泄露,也不能控制malloc出来的堆块,如何利用呢?答案在于unsorted bin attackglobal_max_fast

具体来说,首先伪造堆块释放到unsorted bin里面,然后再edit 堆块的bk的后俩字节为global_max_fast-0x10的地址,进行unsorted bin attack以实现将global_max_fast覆盖为main_arena中的地址,由于后两字节的低12字节是确定的,因此只需要爆破4 bit就可以了,还是很快的。

global_max_fast复写后,我们就拥有了任意地址写堆块地址的能力,往哪里写,写哪一个堆块地址进去呢?

目前首要解决的问题仍然是如何泄露地址,此时就想到了修改stdout结构体里面的内容来实现任意地址泄露,原理在文章里描述的比较清楚了,要想实现地址泄露,需要修改stdout file结构体实现以下条件:

  • _IO_write_base指向想要泄露的地方。
  • _IO_write_ptr指向泄露结束的地址。
  • _IO_read_end等于_IO_write_base以绕过限制。

此时任意地址写的目标就确定了,就是上面三个字段的地址,根据偏移将上面三个字段设置为堆地址,其中_IO_write_base以及_IO_read_end指向之前释放进unsorted bin里包含libc地址的堆块的地址,_IO_write_ptr指向它结束的地址,完成以后,再次调用printf函数的时候libc地址就会泄露出来了。

泄露完成以后就好做了,由于释放堆块的size是可以随意伪造的,因此我们可以将目标定位__free_hook(size为0x3920),如何向该hook指针填入system地址呢,原理是利用uaf,具体的操作是先释放一个堆块到__free_hook中,此时__free_hook包含的是堆的地址,然后edit那个堆块,将它的fd改写成system地址,然后再将堆块申请出来,链表操作完成以后system地址的值就会填入到__free_hook里了,再释放一个堆块即可得到shell。

house of lemon

很可惜没有找到题目,哪位师傅如果有的话可以联系我一下,我也想看看。题目的设计与解法0x9a82师傅在它的博客里写的很清楚了,有需要的可以学习学习。

小结

文章主要描述了有关堆利用中global_max_fast相关的原理以及题目的解析,感觉这种方法相关的一些场景包括:

  • 可能能够得到libc地址。
  • 能够控制free堆块的size。
  • 能往任意地址写但是却无法控制写的内容。

以此来实现往main_arena后面的任意地址写堆块地址的效果,以实现后续的利用,相关的漏洞利用方式包括unsorted bin attack以及house of orange(IO file)等。

所有题目的链接和脚本在我的github里面,exp的编写用了pwn_debug,因此再次向大家推荐下这个框架。

参考链接

  1. BCTF2018 baby_arena
  2. 0CTF 2016 - Zerostorage Writeup
  3. 胖哈勃杯Pwn400、Pwn500详解
点击收藏 | 0 关注 | 1 打赏
登录 后跟帖