# 4.13 利用 _IO\_FILE 结构 - [FILE 结构](#file-结构) - [FSOP](#fsop) - [防御机制](#防御机制) - [新的利用技术](#新的利用技术) - [CTF 实例](#ctf-实例) - [参考资料](#参考资料) ## FILE 结构 FILE 结构体的利用是一种通用的控制流劫持技术。攻击者可以覆盖堆上的 FILE 指针使其指向一个伪造的结构,利用结构中一个叫做 `vtable` 的指针,来执行任意代码。 我们知道 FILE 结构被一系列流操作函数(`fopen()`、`fread()`、`fclose()`等)所使用,大多数的 FILE 结构体保存在堆上(stdin、stdout、stderr除外,位于libc数据段),其指针动态创建并由 `fopen()` 返回。在 glibc(2.23) 中,这个结构体是 `_IO_FILE_plout`,包含了一个 `_IO_FILE` 结构体和一个指向 `_IO_jump_t` 结构体的指针: ```c // 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 }; /* 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; }; extern struct _IO_FILE_plus *_IO_list_all; ``` `vtable` 指向的函数跳转表其实是一种兼容 C++ 虚函数的实现。当程序对某个流进行操作时,会调用该流对应的跳转表中的某个函数。 ```c // 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 }; extern struct _IO_FILE_plus _IO_2_1_stdin_; extern struct _IO_FILE_plus _IO_2_1_stdout_; extern struct _IO_FILE_plus _IO_2_1_stderr_; ``` 进程中的 FILE 结构会通过 `_chain` 域构成一个链表,链表头部用全局变量 `_IO_list_all` 表示。 下面我们来看几个函数的实现。 #### fopen ```c // libio/iofopen.c _IO_FILE * __fopen_internal (const char *filename, const char *mode, int is32) { struct locked_FILE { struct _IO_FILE_plus fp; #ifdef _IO_MTSAFE_IO _IO_lock_t lock; #endif struct _IO_wide_data wd; } *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE)); // 为 FILE 结构分配空间 if (new_f == NULL) return NULL; #ifdef _IO_MTSAFE_IO new_f->fp.file._lock = &new_f->lock; #endif #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T _IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps); #else _IO_no_init (&new_f->fp.file, 1, 0, NULL, NULL); #endif _IO_JUMPS (&new_f->fp) = &_IO_file_jumps; // 设置 vtable = &_IO_file_jumps _IO_file_init (&new_f->fp); // 调用 _IO_file_init 函数进行初始化 #if !_IO_UNIFIED_JUMPTABLES new_f->fp.vtable = NULL; #endif if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL) // 打开目标文件 return __fopen_maybe_mmap (&new_f->fp.file); _IO_un_link (&new_f->fp); free (new_f); return NULL; } _IO_FILE * _IO_new_fopen (const char *filename, const char *mode) { return __fopen_internal (filename, mode, 1); } ``` ```c // libio/fileops.c # define _IO_new_file_init _IO_file_init void _IO_new_file_init (struct _IO_FILE_plus *fp) { /* POSIX.1 allows another file handle to be used to change the position of our file descriptor. Hence we actually don't know the actual position before we do the first fseek (and until a following fflush). */ fp->file._offset = _IO_pos_BAD; fp->file._IO_file_flags |= CLOSED_FILEBUF_FLAGS; _IO_link_in (fp); // 调用 _IO_link_in 函数将 fp 放进链表 fp->file._fileno = -1; } ``` ```c // libio/genops.c void _IO_link_in (struct _IO_FILE_plus *fp) { if ((fp->file._flags & _IO_LINKED) == 0) { fp->file._flags |= _IO_LINKED; #ifdef _IO_MTSAFE_IO _IO_cleanup_region_start_noarg (flush_cleanup); _IO_lock_lock (list_all_lock); run_fp = (_IO_FILE *) fp; _IO_flockfile ((_IO_FILE *) fp); #endif fp->file._chain = (_IO_FILE *) _IO_list_all; // fp 放到链表头部 _IO_list_all = fp; // 链表头 _IO_list_all 指向 fp ++_IO_list_all_stamp; #ifdef _IO_MTSAFE_IO _IO_funlockfile ((_IO_FILE *) fp); run_fp = NULL; _IO_lock_unlock (list_all_lock); _IO_cleanup_region_end (0); #endif } } ``` #### fread ```c // libio/iofread.c _IO_size_t _IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp) { _IO_size_t bytes_requested = size * count; _IO_size_t bytes_read; CHECK_FILE (fp, 0); if (bytes_requested == 0) return 0; _IO_acquire_lock (fp); bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested); // 调用 _IO_sgetn 函数 _IO_release_lock (fp); return bytes_requested == bytes_read ? count : bytes_read / size; } ``` ```c // libio/genops.c _IO_size_t _IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n) { /* FIXME handle putback buffer here! */ return _IO_XSGETN (fp, data, n); // 调用宏 _IO_XSGETN } ``` ```c // libio/libioP.h #define _IO_JUMPS_FILE_plus(THIS) \ _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable) #if _IO_JUMPS_OFFSET # define _IO_JUMPS_FUNC(THIS) \ (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \ + (THIS)->_vtable_offset)) # define _IO_vtable_offset(THIS) (THIS)->_vtable_offset #else # define _IO_JUMPS_FUNC(THIS) _IO_JUMPS_FILE_plus (THIS) # define _IO_vtable_offset(THIS) 0 #endif #define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2) #define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N) ``` 所以 `_IO_XSGETN` 宏最终会调用 `vtable` 中的函数,即: ```c // libio/fileops.c _IO_size_t _IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n) { ``` #### fwrite ```c // libio/iofwrite.c _IO_size_t _IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp) { _IO_size_t request = size * count; _IO_size_t written = 0; CHECK_FILE (fp, 0); if (request == 0) return 0; _IO_acquire_lock (fp); if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1) written = _IO_sputn (fp, (const char *) buf, request); // 调用 _IO_sputn 函数 _IO_release_lock (fp); /* We have written all of the input in case the return value indicates this or EOF is returned. The latter is a special case where we simply did not manage to flush the buffer. But the data is in the buffer and therefore written as far as fwrite is concerned. */ if (written == request || written == EOF) return count; else return written / size; } ``` ```c // libio/libioP.h #define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N) #define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n) ``` `_IO_XSPUTN` 最终将调用下面的函数: ```c // libio/fileops.c _IO_size_t _IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n) { ``` #### fclose ```c // libio/iofclose.c int _IO_new_fclose (_IO_FILE *fp) { int status; CHECK_FILE(fp, EOF); #if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1) /* We desperately try to help programs which are using streams in a strange way and mix old and new functions. Detect old streams here. */ if (_IO_vtable_offset (fp) != 0) return _IO_old_fclose (fp); #endif /* First unlink the stream. */ if (fp->_IO_file_flags & _IO_IS_FILEBUF) _IO_un_link ((struct _IO_FILE_plus *) fp); // 将 fp 从链表中取出 _IO_acquire_lock (fp); if (fp->_IO_file_flags & _IO_IS_FILEBUF) status = _IO_file_close_it (fp); // 关闭目标文件 else status = fp->_flags & _IO_ERR_SEEN ? -1 : 0; _IO_release_lock (fp); _IO_FINISH (fp); if (fp->_mode > 0) { #if _LIBC /* This stream has a wide orientation. This means we have to free the conversion functions. */ struct _IO_codecvt *cc = fp->_codecvt; __libc_lock_lock (__gconv_lock); __gconv_release_step (cc->__cd_in.__cd.__steps); __gconv_release_step (cc->__cd_out.__cd.__steps); __libc_lock_unlock (__gconv_lock); #endif } else { if (_IO_have_backup (fp)) _IO_free_backup_area (fp); } if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr) { fp->_IO_file_flags = 0; free(fp); // 释放 FILE 结构体 } return status; } ``` ## FSOP FSOP(File Stream Oriented Programming)是一种劫持 `_IO_list_all`(libc.so中的全局变量) 来伪造链表的利用技术,通过调用 `_IO_flush_all_lockp()` 函数来触发,该函数会在下面三种情况下被调用: - libc 检测到内存错误时 - 执行 exit 函数时 - main 函数返回时 当 glibc 检测到内存错误时,会依次调用这样的函数路径:`malloc_printerr -> _libc_message -> abort -> _IO_flush_all_lockp`。 ```c // 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) // 将其修改为 system 函数 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; // 指向我们指定的区域 } #ifdef _IO_MTSAFE_IO if (do_lock) _IO_lock_unlock (list_all_lock); __libc_cleanup_region_end (0); #endif return result; } ``` 于是对 `_IO_OVERFLOW(fp, EOF)` 的调用会变成对 `system('/bin/sh')` 的调用。 ## 防御机制 但是在 libc-2.24 中加入了对 vtable 指针的检查。这个 [commit](https://sourceware.org/git/gitweb.cgi?p=glibc.git;a=commitdiff;h=db3476aff19b75c4fdefbe65fcd5f0a90588ba51) 新增了两个函数:`IO_validate_vtable` 和 `_IO_vtable_check`。 ```c // 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; 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; } ``` ```c // 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"); } ``` 所有的 libio vtables 被放进了专用的只读的 `__libc_IO_vtables` 段,以使它们在内存中连续。在任何间接跳转之前,vtable 指针将根据段边界进行检查,如果指针不在这个段,则调用函数 `_IO_vtable_check()` 做进一步的检查,并且在必要时终止进程。 ## 新的利用技术 在防御机制下通过修改虚表的利用技术已经用不了了,但同时出现了新的利用技术。既然无法将 vtable 指针指向 `__libc_IO_vtables` 以外的地方,那么就在 `__libc_IO_vtables` 里面找些有用的东西。比如 `_IO_str_jumps`: ```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) }; ``` 这个 vtable 中包含了一个叫做 `_IO_str_overflow` 的函数,该函数中存在相对地址的引用(可伪造): ```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) 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 */ 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; // new_size 的计算方法 if (new_size < old_blen) return EOF; new_buf = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size); // fp 上的相对地址 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; } ``` 构造的条件如下: - _flags = 0 - _IO_buf_base = 0 - _IO_buf_end = (bin_sh - 100) / 2 - _IO_write_ptr = 0x7fffffffffffffff - _IO_write_base = 0 ## CTF 实例 请查看章节 6.1.24、6.1.25 和 6.1.26。另外在章节 3.1.8 中也有相关内容。 ## 参考资料 - [abusing the FILE structure](https://outflux.net/blog/archives/2011/12/22/abusing-the-file-structure/) - [Play with FILE Structure - Yet Another Binary Exploit Technique](https://www.slideshare.net/AngelBoy1/play-with-file-structure-yet-another-binary-exploit-technique)