IO_FILE Exploit
Structrue
_IO_FILE
:
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;#ifdef _IO_USE_OLD_IO_FILE};
_IO_FILE_plus
:
struct _IO_FILE_plus{ FILE file; const struct _IO_jump_t *vtable;};
_IO_jump_t
:
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);};
下面简单说说一些c函数对_IO_jump_t虚表里面函数的调用情况
printf/puts
最终会调用_IO_file_xsputn
fclose
最终会调用_IO_file_finish
fwrite
最终会调用_IO_file_xsputn
fread
最终会调用_IO_fiel_xsgetn
scanf/gets
最终会调用_IO_file_xsgetn
_IO_strfile
:
struct _IO_str_fields{ /* These members are preserved for ABI compatibility. The glibc implementation always calls malloc/free for user buffers if _IO_USER_BUF or _IO_FLAGS2_USER_WBUF are not set. */ _IO_alloc_type _allocate_buffer_unused; _IO_free_type _free_buffer_unused;};/* This is needed for the Irix6 N32 ABI, which has a 64 bit off_t type, but a 32 bit pointer type. In this case, we get 4 bytes of padding after the vtable pointer. Putting them in a structure together solves this problem. */struct _IO_streambuf{ FILE _f; const struct _IO_jump_t *vtable;};typedef struct _IO_strfile_{ struct _IO_streambuf _sbf; struct _IO_str_fields _s;} _IO_strfile;
#define _IO_MAGIC 0xFBAD0000 /* Magic number */#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */#define _IO_MAGIC_MASK 0xFFFF0000#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */#define _IO_UNBUFFERED 2#define _IO_NO_READS 4 /* Reading not allowed */#define _IO_NO_WRITES 8 /* Writing not allowd */#define _IO_EOF_SEEN 0x10#define _IO_ERR_SEEN 0x20#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/#define _IO_IN_BACKUP 0x100#define _IO_LINE_BUF 0x200#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */#define _IO_CURRENTLY_PUTTING 0x800#define _IO_IS_APPENDING 0x1000#define _IO_IS_FILEBUF 0x2000#define _IO_BAD_SEEN 0x4000#define _IO_USER_LOCK 0x8000
FSOP
FSOP的核心思想就是劫持 _IO_list_all
的值来伪造链表和其中的 _IO_FILE
项,但是单纯的伪造只是构造了数据还需要某种方法进行触发。
FSOP选择的触发方法是调用 _IO_flush_all_lockp
,
这个函数会刷新 _IO_list_all
链表中所有项的文件流,相当于对每个 FILE
调用 fflush
,也对应着会调用 _IO_FILE_plus.vtable
中的 _IO_overflow
_IO_flush_all_lockp (int do_lock){ int result = 0; FILE *fp; // ... for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain) { run_fp = fp; if (do_lock) _IO_flockfile (fp); if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)) ) && _IO_OVERFLOW (fp, EOF) == EOF) result = EOF; //... } // ... return result;}
_IO_flush_all_lockp 的触发方式:
- 当执行 exit 函数时
- 当执行流从 main 函数返回时
- 当 libc 执行 abort 流程时 [glibc <= 2.23]
trace:
[#0] _IO_flush_all_lockp(do_lock=0x0)[#1] _IO_cleanup()[#2] __run_exit_handlers(...)[#3] __GI_exit(status=<optimized out>)[#4] __libc_start_main(...)[#5] _start()
glibc < 2.24
由于位于 libc
数据段的 vtable
是不可以进行写入的,所以我们只能伪造 vtable
伪造 vtable
劫持程序流程的中心思想就是针对 _IO_FILE_plus
的 vtable
动手脚,通过把 vtable
指向我们控制的内存,并在其中布置函数指针来实现。
2.24 <= glibc <= 2.27
_IO_OVERFLOW
中加入 IO_validate_vtable
对 vtable 做了检测,vtable
必须要满足在 __stop___IO_vtables
和 __start___libc_IO_vtables
之间,而我们伪造的vtable通常不满足这个条件。
但是我们找到一条出路,那就是 _IO_str_jumps
与 _IO_wstr_jumps
就位于二者之间,
所以我们是可以利用他们来通过 IO_validate_vtable
的检测的,只需要将 vtable
填成_IO_str_jumps
或 _IO_wstr_jumps
就行。
下面列出几种可行方法,但不限于这几种:
利用
_IO_str_jumps
中的_IO_str_finish
函数- 为绕过
_IO_flush_all_lockp
中的检查进入到_IO_OVERFLOW
,需构造_mode <= 0
_IO_write_ptr > _IO_write_base
- 构造
vtable = _IO_str_jumps - 0x8
, 使_IO_str_finish
函数替代_IO_OVERFLOW
函数,(因为_IO_str_finish
在_IO_str_jumps
中的偏移为0x10
,而_IO_OVERFLOW
在原vtable
中的偏移为0x18
) - 构造
fp -> _IO_buf_base
作为参数 - 构造
fp->flags & _IO_USER_BUF == 0
- 构造
fp->_s._free_buffer = &system
(_free_buffer = fp->vtable + 0x10
) - 调用
_IO_flush_all_lockp
函数,触发_IO_str_finish
,从而达到调用(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base)
- 为绕过
利用
_IO_str_jumps
中的_IO_str_overflow
函数- 为绕过
_IO_flush_all_lockp
中的检查进入到_IO_OVERFLOW
,需构造_mode <= 0
_IO_write_ptr > _IO_write_base
- 构造
vtable = _IO_str_jumps
,使_IO_str_overflow
函数替代_IO_OVERFLOW
函数,(因为_IO_str_finish
在_IO_str_jumps
中的偏移为0x18
,而_IO_OVERFLOW
在原vtable
中的偏移为0x18
) - 构造
fp->flags & _IO_USER_BUF == 0 && fp->flags & _IO_NO_WRITES == 0
- 构造
new_size = (fp->_IO_buf_end - fp->_IO_buf_base) * 2 + 100 = binsh_addr
- 构造
fp->_s._allocate_buffer = &system
(fp->_s._allocate_buffer = fp->vtable + 0x8
) - 调用
_IO_flush_all_lockp
函数,触发_IO_str_finish
,从而达到调用(*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size)
- 为绕过
2.28 <= glibc
由于指针函数被替换为库函数:
_IO_str_finish
中(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base)
被替换为free (fp->_IO_buf_base)
_IO_str_overflow
中(*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size)
被替换为malloc (new_size)
因此,上述两种方法不能再可行,但可以利用 _IO_str_overflow
中的 malloc
任意大小和 free
任意地址的 chunk。
Problem:
- https://www.root-me.org/en/Challenges/App-System/ELF-x64-FILE-structure-hijacking
Ref:
- https://www.anquanke.com/post/id/216290
利用 IO_write_base
实现 leak
修改 libc.sym["_IO_2_1_stdout_"] - 0x43
处的 chunk
为
payload = b''.join([ b'\0' * 0x33, p64(0xfbad1887), # _flags p64(0), # _IO_read_ptr p64(0), # _IO_read_end p64(0), # _IO_read_base p64(libc.sym['__curbrk']), # _IO_write_base p64(libc.sym['__curbrk'] + 8) # _IO_write_ptr])