File-Stream Oriented Programming
文件描述符的结构
Linux中文件描述符的结构,定义在glibc/libio/bits/types/struct_FILE.h中和glibc/libio/libioP.h中
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
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 _flags2;
__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];
_IO_lock_t *_lock;
};
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;
};
对于进程打开的每一个文件描述符,libc会分配一个_IO_FILE_plus结构体,这个结构体位于用户进程的堆中。一个进程的文件描述符以单链表的形式存储,头指针是libc的_IO_list_all。
在一个进程开启时,会自动打开三个文件描述符,分别为stderr,stdout和stdin,这三个描述符结构体分配在libc中。当使用fopen等打开新的文件描述符时,新的描述符结构体加入到这个链表的头部。
对于一个文件描述符的操作,实际上是通过_IO_FILE_plus中vtable指向的一个_IO_jump_t类型的结构体实现的。这个结构体定义在glibc/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);
};
对于文件描述符,其结构体中vtable字段通常指向libc中_IO_file_jumps结构体,这是一个_IO_jump_t的实例
const struct _IO_jump_t _IO_file_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
JUMP_INIT(overflow, _IO_file_overflow),
JUMP_INIT(underflow, _IO_file_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_default_pbackfail),
JUMP_INIT(xsputn, _IO_file_xsputn),
JUMP_INIT(xsgetn, _IO_file_xsgetn),
JUMP_INIT(seekoff, _IO_new_file_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, _IO_new_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
通过这个结构体一系列函数指针,可完成对文件的读写关闭等一系列操作。
文件描述符结构体的利用
使用fopen等打开的文件,其文件描述符会被分配在进程的堆上,通过堆上的漏洞可以对其中的字段进行改写等,达到控制PC的目的。
在libc-2.23以前,通用的利用方式为,改写文件描述符结构体的vtable字段,使之指向待执行的函数,通过控制偏移,使得对该文件描述符执行write,read或close等操作时实际上执行了攻击者需要的函数。
在libc-2.23以后,每次调用vtable指向的函数时,对vtable字段进行合法性检查,这个检查定义在glibc/libio/libioP.h中
/* 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;
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr - (uintptr_t) __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;
}
IO_validate_vtable()检查vtable字段是否指向libc中__libc_IO_vtables区域,如果检查不通过,则调用_IO_vtable_check()进行进一步检查。
1.改写vtable指针,指向_IO_str_jumps
适用于libc-2.27版本及以前
一种利用方式是,使vtable字段指向__libc_IO_vtables区域另一vtable,_IO_str_jumps,这也是一个_IO_jump_t类型的结构体,在内存中位于_IO_file_jumps附近
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)
};
在_IO_file_jumps中,对文件进行关闭操作的_IO_file_finish指针,在_IO_str_jumps的相同偏移处,为_IO_str_finish。若将文件描述符结构体的vtable字段指向了_IO_str_jumps,对这个文件进行关闭操作时,实际上会调用_IO_str_finish,这个函数定义如下
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);
}
可见,在这个函数中,首先会将文件描述符结构体cast为_IO_strfile类型,然后将其中_s._free_buffer字段的值当作函数,将_IO_buf_base字段的值当作参数进行一次调用。此处,_s._free_buffer字段相对于文件描述符结构体的偏移为0xe8。
同时,可以注意到,进行上述调用前会对文件描述符结构体的flag字段进行一些检查,在执行flcose()到完成上述调用的过程中还会进行几次flag检查,需要对flag进行设置通过上述检查完成最终调用,一般设置flag为0x8000
我们可以通过堆上的漏洞,首先将一个文件描述符结构体的vtable字段改写为指向_IO_str_jumps,然后在结构体的0xe8偏移处写入system()函数的地址,在结构体的_IO_buf_base写入"/bin/sh"的地址,此时对这个文件描述符进行fclose操作,将会执行system("/bin/sh")从而得到shell
具体实例参考pwn2win2020-command
上述利用方法仅适用于libc-2.27及以前,从libc-2.28版本开始,_IO_str_finish的实现改为
void _IO_str_finish (_IO_FILE *fp, int dummy) { if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) free (fp->_IO_buf_base); fp->_IO_buf_base = NULL; _IO_default_finish (fp, 0); }
无法再使用上述利用方法