house of apple 简介 House of apple 是 roderick01 师傅在 2022 年 7 月提出的一种新型的 IO 利用思路,总共提出了三种调用方式,分为了 House of apple1
、House of apple2
、House 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 利用方式有(详细可以见我之前写的几篇文章):
House of pig :多手法联合IO利用之House of pig 学习利用
House of kiwi & House of emma :IO利用之House of kiwi & House of emma
House of cat(提出时间比House of apple 稍早):新型 IO 利用方法初探—House of cat 学习利用
浏览了这些 IO 利用方式我们可以发现,之前的这些 IO 调用方式都需要至少两次 largebin attack 或者 需要任意读写操作,例如House of pig
需要利用 tcache stashing unlink attack
和 largebin 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的利用条件如下:
程序从main
函数返回或能调用exit
函数
能泄露出heap
地址和libc
地址
能使用一次 largebin attack
只需要一次 largebin attack 就意味着House of apple 的调用链更加简便并能够在更加苛刻的条件下利用成功
利用思路 House of apple 总共有三条调用链:
House of apple1 是通过控制 fp->_wide_data
来实现的一个任意地址写已知地址的作用,相当于一次 largebin attack
主要是用来结合 FSOP
及 setcontext+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> }
程序从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 static inline const struct _IO_jump_t *IO_validate_vtable (const struct _IO_jump_t *vtable) { 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)) _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)
那么我们的利用思路如下
劫持IO_FILE
的vtable
为_IO_wfile_jumps
控制_wide_data
为可控的堆地址空间
控制_wide_data->_wide_vtable
为可控的堆地址空间
控制程序执行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) { f->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return WEOF; } if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 ) { 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; if (fp->_IO_read_ptr >= fp->_IO_read_end && _IO_file_underflow_mmap (fp) == EOF) return WEOF; read_stop = (const char *) fp->_IO_read_ptr; if (fp->_wide_data->_IO_buf_base == NULL ) { 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 (;;) { 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)