适用版本: 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 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