house of apple

简介

House of apple 是 roderick01 师傅在 2022 年 7 月提出的一种新型的 IO 利用思路,总共提出了三种调用方式,分为了 House of apple1House of apple2House of apple3,这三种调用方式都是基于 roderick01 师傅发现的新调用链:IO_FILE -> _wide_data

前置基础

libc2.34 以上的高libc版本中,glibc 逐渐将许多 hook全局变量移除了,包括 _malloc_hook / _free_hook / _realloc_hook ,这就导致我们无法在向从前那样利用 hook 作为钩子函数来 getshell 了,所以到了高版本,堆利用便离不开对 _IO_FILE 的伪造和对 IO 流的劫持

在 House of apple 之前的高版本 IO 利用方式有(详细可以见我之前写的几篇文章):

  1. House of pig :多手法联合IO利用之House of pig 学习利用
  2. House of kiwi & House of emma :IO利用之House of kiwi & House of emma
  3. House of cat(提出时间比House of apple 稍早):新型 IO 利用方法初探—House of cat 学习利用

浏览了这些 IO 利用方式我们可以发现,之前的这些 IO 调用方式都需要至少两次 largebin attack 或者 需要任意读写操作,例如House of pig 需要利用 tcache stashing unlink attacklargebin attack 结合,而House of kiwi、House of emma、House of cat 等都需要至少两次的 largebin attack 或任意写来修改对应的 _IO_xxx_jumps 甚至是需要再劫持其他结构(House of emma 需要绕过或劫持 TLS 结构体中的 _pointer_chk_guard)

利用条件

House of apple到现在仍是用途最广,攻击效果最好的高版本堆利用方式之一,很大程度上来源于他的利用之方便。

House of apple的利用条件如下:

  1. 程序从main函数返回或能调用exit函数
  2. 能泄露出heap地址和libc地址
  3. 能使用一次largebin attack

只需要一次 largebin attack 就意味着House of apple 的调用链更加简便并能够在更加苛刻的条件下利用成功

利用思路

House of apple 总共有三条调用链:

House of apple1 是通过控制 fp->_wide_data 来实现的一个任意地址写已知地址的作用,相当于一次 largebin attack 主要是用来结合 FSOPsetcontext+61 等调用链来实现getshell,暂按下不表

House of apple2 和 House of apple3 都是通过 FILE 结构体的伪造来进行攻击,这里主要介绍House of apple2的调用方法

House of apple2 延续了之前利用largebin attack 劫持IO_FILE结构体的思想,在House of apple2中,我们要利用的是 _IO_FILE的一个成员 _wide_data

附 : _IO_list_all 中的成员

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
pwndbg> p *_IO_list_all
$2 = {
file = {
_flags = -72540025,
_IO_read_ptr = 0x7ff1eaad7643 <_IO_2_1_stderr_+131> "",
_IO_read_end = 0x7ff1eaad7643 <_IO_2_1_stderr_+131> "",
_IO_read_base = 0x7ff1eaad7643 <_IO_2_1_stderr_+131> "",
_IO_write_base = 0x7ff1eaad7643 <_IO_2_1_stderr_+131> "",
_IO_write_ptr = 0x7ff1eaad7643 <_IO_2_1_stderr_+131> "",
_IO_write_end = 0x7ff1eaad7643 <_IO_2_1_stderr_+131> "",
_IO_buf_base = 0x7ff1eaad7643 <_IO_2_1_stderr_+131> "",
_IO_buf_end = 0x7ff1eaad7644 <_IO_2_1_stderr_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ff1eaad76a0 <_IO_2_1_stdout_>,
_fileno = 2,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x7ff1eaad87d0 <_IO_stdfile_2_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7ff1eaad6780 <_IO_wide_data_2>,//这个变量是我们需要劫持的
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ff1eaad34a0 <_IO_file_jumps>//vtable
}

程序从main返回或者执行exit后会遍历_IO_list_all存放的每一个IO_FILE结构体,如果满足条件的话,会调用每个结构体中vtable->_overflow函数指针指向的函数。

在 libc2.23 之前是没有 vtable 检测的,也就是说可以任意劫持执行的函数。而在glibc2.24之后增加了对 vtable 合法性的检测的 IO_validate_vtable 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Perform vtable pointer validation.  If validation fails, terminate
the process. */
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
const char *ptr = (const char *) vtable;
uintptr_t offset = ptr - __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}

所以一些高版本的 IO 攻击方法都需要利用各种手法来绕过vtable检测

但是 _wide_data 这个成员很特殊,这个成员结构体中的 _wide_vtable和调用vtable里函数指针一样,在调用 _wide_vtable 虚表里面的函数时,也同样是使用宏去调用,但其没有关于vtable的合法性检查

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
pwndbg> p _IO_wide_data_2
$4 = {
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_IO_state = {
__count = 0,
__value = {
__wch = 0,
__wchb = "\000\000\000"
}
},
_IO_last_state = {
__count = 0,
__value = {
__wch = 0,
__wchb = "\000\000\000"
}
},
_codecvt = {
__cd_in = {
step = 0x0,
step_data = {
__outbuf = 0x0,
__outbufend = 0x0,
__flags = 0,
__invocation_counter = 0,
__internal_use = 0,
__statep = 0x0,
__state = {
__count = 0,
__value = {
__wch = 0,
__wchb = "\000\000\000"
}
}
}
},
__cd_out = {
step = 0x0,
step_data = {
__outbuf = 0x0,
__outbufend = 0x0,
__flags = 0,
__invocation_counter = 0,
__internal_use = 0,
__statep = 0x0,
__state = {
__count = 0,
__value = {
__wch = 0,
__wchb = "\000\000\000"
}
}
}
}
},
_shortbuf = L"",
_wide_vtable = 0x7ff1eaad2f60 <_IO_wfile_jumps>
}
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)

#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)

#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)

#define _IO_WIDE_JUMPS(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)

那么我们的利用思路如下

  1. 劫持IO_FILEvtable_IO_wfile_jumps
  2. 控制_wide_data为可控的堆地址空间
  3. 控制_wide_data->_wide_vtable为可控的堆地址空间
  4. 控制程序执行IO流函数调用,最终调用到_IO_Wxxxxx函数即可控制程序的执行流

roderick01 师傅在文章里一共给出了三条可行的能执行到 _IO_Wxxxxx 函数的利用,包括了:

  • _IO_wfile_overflow
  • _IO_wfile_underflow_mmap
  • _IO_wdefault_xsgetn

源码分析

1. _IO_wfile_overflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
{
/* Allocate a buffer if needed. */
if (f->_wide_data->_IO_write_base == 0)
{
_IO_wdoallocbuf (f);//执行目标
// ......
}
}
}

需要执行到 _IO_wdoallocbuf (f) 函数

再看看 _IO_wdoallocbuf (f) 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
void
_IO_wdoallocbuf (FILE *fp)
{
if (fp->_wide_data->_IO_buf_base)
return;
if (!(fp->_flags & _IO_UNBUFFERED))
if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)//
return;
_IO_wsetb (fp, fp->_wide_data->_shortbuf,
fp->_wide_data->_shortbuf + 1, 0);
}
libc_hidden_def (_IO_wdoallocbuf)

可以发现有一步执行了 (wint_t)_IO_WDOALLOCATE (fp) 函数,也就是提到的 _IO_WXXXXX 函数之一

调用链如下:

1
_IO_wfile_overflow --> _IO_wdoallocbuf --> _IO_WDOALLOCATE

2. _IO_wfile_underflow_mmap

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
static wint_t
_IO_wfile_underflow_mmap (FILE *fp)
{
struct _IO_codecvt *cd;
const char *read_stop;

if (__glibc_unlikely (fp->_flags & _IO_NO_READS))
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end)
return *fp->_wide_data->_IO_read_ptr;

cd = fp->_codecvt;

/* Maybe there is something left in the external buffer. */
if (fp->_IO_read_ptr >= fp->_IO_read_end
/* No. But maybe the read buffer is not fully set up. */
&& _IO_file_underflow_mmap (fp) == EOF)
/* Nothing available. _IO_file_underflow_mmap has set the EOF or error
flags as appropriate. */
return WEOF;

/* There is more in the external. Convert it. */
read_stop = (const char *) fp->_IO_read_ptr;

if (fp->_wide_data->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_wide_data->_IO_save_base != NULL)
{
free (fp->_wide_data->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_wdoallocbuf (fp);
}
//......
}

同样是调用到了 _IO_wdoallocbuf (fp) 函数

调用链如下:

1
_IO_wfile_underflow_mmap --> _IO_wdoallocbuf --> _IO_WDOALLOCATE

3. _IO_wdefault_xsgetn

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
size_t
_IO_wdefault_xsgetn (FILE *fp, void *data, size_t n)
{
size_t more = n;
wchar_t *s = (wchar_t*) data;
for (;;)
{
/* Data available. */
ssize_t count = (fp->_wide_data->_IO_read_end
- fp->_wide_data->_IO_read_ptr);
if (count > 0)
{
if ((size_t) count > more)
count = more;
if (count > 20)
{
s = __wmempcpy (s, fp->_wide_data->_IO_read_ptr, count);
fp->_wide_data->_IO_read_ptr += count;
}
else if (count <= 0)
count = 0;
else
{
wchar_t *p = fp->_wide_data->_IO_read_ptr;
int i = (int) count;
while (--i >= 0)
*s++ = *p++;
fp->_wide_data->_IO_read_ptr = p;
}
more -= count;
}
if (more == 0 || __wunderflow (fp) == WEOF)
break;
}
return n - more;
}
libc_hidden_def (_IO_wdefault_xsgetn)

可以看到有一个 __wunderflow (fp) 函数

看看这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
wint_t
__wunderflow (FILE *fp)
{
if (fp->_mode < 0 || (fp->_mode == 0 && _IO_fwide (fp, 1) != 1))
return WEOF;

if (fp->_mode == 0)
_IO_fwide (fp, 1);
if (_IO_in_put_mode (fp))
if (_IO_switch_to_wget_mode (fp) == EOF)
return WEOF;
// ......
}

_IO_switch_to_wget_mode (fp) 函数中会调用 _IO_WOVERFLOW (fp, WEOF) 函数

1
2
3
4
5
6
7
8
int
_IO_switch_to_wget_mode (FILE *fp)
{
if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
if ((wint_t)_IO_WOVERFLOW (fp, WEOF) == WEOF)
return EOF;
// .....
}

调用链如下

1
_IO_wdefault_xsgetn --> __wunderflow --> _IO_switch_to_wget_mode --> _IO_WOVERFLOW

具体劫持情况中函数的各参数需要满足的条件可以看看roderick01师傅的原文章:[原创] House of apple 一种新的glibc中IO攻击方法 (2)