house of apple2

本文最后更新于:2025年2月4日 下午

前情提要

glibc高版本中逐渐移除了__malloc_hook/__free_hook/__realloc_hook等hook全局变量(实现还在,但2.34中被直接赋为NULL,且无实际应用),伪造IO_FILE结构体并利用IO流攻击便成为高版本题目主流。

house of apple的使用条件为:

程序能从main函数返回或能调用exit函数、能得到heap_baselibc_base、能使用一次largebin attack

当程序从main函数返回或执行exit函数的时候,均会调用fcloseall函数,最后会遍历_IO_list_all存放的每一个IO_FILE结构体,如果满足条件的话,会调用每个结构体中vtable->_overflow函数指针指向的函数。使用largebin attack可以劫持_IO_list_all变量,将其替换为伪造的IO_FILE结构体,并同时修改_wide_data成员的值,并且可以利用_IO_wstrn_overflow将已知地址空间上的某些值修改为一个已知值。

amd64下,_wide_dataIO_FILE中的偏移为0xa0。

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

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

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
/*
/libio/libioP.h
*/
// 2.28 ~ 2.40.9000
// 代码后方注释为某变量 2.23 ~ 2.27.9000对应一些变量类型的写法
struct _IO_FILE_plus
{
FILE file; // _IO_FILE
const struct _IO_jump_t *vtable;
};
/*
/libio/stdfiles.c
*/
DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);
struct _IO_FILE_plus *_IO_list_all = &_IO_2_1_stderr_;


pwndbg> p *_IO_list_all
$1 = {
file = {
_flags = -72540025,
_IO_read_ptr = 0x7ffff7e1b723 <_IO_2_1_stderr_+131> "",
_IO_read_end = 0x7ffff7e1b723 <_IO_2_1_stderr_+131> "",
_IO_read_base = 0x7ffff7e1b723 <_IO_2_1_stderr_+131> "",
_IO_write_base = 0x7ffff7e1b723 <_IO_2_1_stderr_+131> "",
_IO_write_ptr = 0x7ffff7e1b723 <_IO_2_1_stderr_+131> "",
_IO_write_end = 0x7ffff7e1b723 <_IO_2_1_stderr_+131> "",
_IO_buf_base = 0x7ffff7e1b723 <_IO_2_1_stderr_+131> "",
_IO_buf_end = 0x7ffff7e1b724 <_IO_2_1_stderr_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7e1b780 <_IO_2_1_stdout_>,
_fileno = 2,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x7ffff7e1ca60 <_IO_stdfile_2_lock>,
_offset = -1,
_codecvt = 0x0,
**_wide_data = 0x7ffff7e1a8a0 <_IO_wide_data_2>,**
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7e17600 <_IO_file_jumps> // vtable
}

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

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

glibc2.23及之前没有vtable的检测,可以任意劫持执行的函数。之后增加了对vtable合法性的检测的IO_validate_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
/*
/libio/libioP.h
*/
// 2.38 ~ 2.40.9000
/* 执行vtable指针验证。如果验证失败,终止进程。 */
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
uintptr_t ptr = (uintptr_t) vtable; // 将vtable指针转换为uintptr_t类型的整数
uintptr_t offset = ptr - (uintptr_t) &__io_vtables; // 计算vtable指针相对于预期vtable区域起始地址的偏移量

// 如果vtable指针的偏移量大于或等于预定义的vtable区域长度,说明vtable指针不在预期的区域内
if (__glibc_unlikely (offset >= IO_VTABLES_LEN))
/* 如果vtable指针不在预期的区域内,调用慢路径进行处理,这将会终止进程(如果必要) */
_IO_vtable_check (); // 检查vtable指针的有效性,若无效则终止程序

return vtable; // 返回验证通过的vtable指针
}
// 2.24 ~ 2.37.9000
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;
}

不同版本有一点代码编写上的区别,但是最终效果是完全一致的。

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/*
/libio/libio.h
*/
struct _IO_wide_data
{
wchar_t *_IO_read_ptr; /* Current read pointer */
wchar_t *_IO_read_end; /* End of get area. */
wchar_t *_IO_read_base; /* Start of putback+get area. */
wchar_t *_IO_write_base; /* Start of put area. */
wchar_t *_IO_write_ptr; /* Current put pointer. */
wchar_t *_IO_write_end; /* End of put area. */
wchar_t *_IO_buf_base; /* Start of reserve area. */
wchar_t *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
wchar_t *_IO_backup_base; /* Pointer to first valid character of
backup area */
wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */

__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;

wchar_t _shortbuf[1];

const struct _IO_jump_t *_wide_vtable;
};


pwndbg> p _IO_wide_data_2
$1 = {
_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 = 0x7ffff7e170c0 <_IO_wfile_jumps>
}
1
2
3
4
5
6
7
8
9
#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函数即可控制程序的执行流

目前已知三条可行的能执行到_IO_Wxxxxx函数的利用:

  1. _IO_wfile_overflow

  2. _IO_wfile_underflow_mmap

  3. _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
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
/*
/libio/wfileops.c
*/
// 2.37 ~ 2.40.9000
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
|| f->_wide_data->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_wide_data->_IO_write_base == 0)
{
_IO_wdoallocbuf (f); // 在此尝试执行
...
}
...
}
// 2.23 ~ 2.36.9000
wint_t
_IO_wfile_overflow (_IO_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); // 在此尝试执行
...
}
...
}
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
/*
/libio/wgenops.c
*/
// 2.28 ~ 2.40.9000
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);
}
// 2.23 ~ 2.27.9000
void
_IO_wdoallocbuf (_IO_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);
}

调用链为:

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
41
42
43
44
45
/*
/libio/wfileops.c
*/
// 2.38 ~ 2.40.9000
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);
}
...
}
// 此下版本不存在该函数,故不存在此调用链

调用链为:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/*
/libio/wgenops.c
*/
// 2.28 ~ 2.40.9000
// 代码后方注释为某变量 2.23 ~ 2.27.9000对应一些变量类型的写法
size_t
_IO_wdefault_xsgetn (FILE *fp, void *data, size_t n) // _IO_FILE
{
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); // _IO_ssize_t
if (count > 0)
{
if ((size_t) count > more) // _IO_size_t
count = more;
if (count > 20)
{
s = __wmempcpy (s, fp->_wide_data->_IO_read_ptr, count);
fp->_wide_data->_IO_read_ptr += count;
/*
// 2.23 ~ 2.26.9000此处写法为
#ifdef _LIBC
s = __wmempcpy (s, fp->_wide_data->_IO_read_ptr, count);
#else
memcpy (s, fp->_wide_data->_IO_read_ptr, count);
s += count;
#endif
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
/libio/wgenops.c
*/
// 2.28 ~ 2.40.9000
// 代码后方注释为某变量 2.23 ~ 2.27.9000对应一些变量类型的写法
wint_t
__wunderflow (FILE *fp) // _IO_FILE
{
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;
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
/*
/libio/wgenops.c
*/
// 2.28 ~ 2.40.9000
// 代码后方注释为某变量 2.23 ~ 2.27.9000对应一些变量类型的写法
int
_IO_switch_to_wget_mode (FILE *fp) // _IO_FILE
{
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

house of apple2
http://example.com/2024/12/06/house-of-apple2/
作者
OSLike
发布于
2024年12月6日
许可协议