diff --git a/doc/3.1.8_heap_exploit_3.md b/doc/3.1.8_heap_exploit_3.md index 32d3b0c..8c8d2cd 100644 --- a/doc/3.1.8_heap_exploit_3.md +++ b/doc/3.1.8_heap_exploit_3.md @@ -763,7 +763,7 @@ struct _IO_jump_t #endif }; ``` -伪造 `_IO_jump_t` 中的 `__overflow` 为 system 函数的地址,从而达到执行 shell 的目的。另外, +伪造 `_IO_jump_t` 中的 `__overflow` 为 system 函数的地址,从而达到执行 shell 的目的。 当发生内存错误进入 `_IO_flush_all_lockp` 后,`_IO_list_all` 仍然指向 unsorted bin,这并不是一个我们能控制的地址。所以需要通过 `fp->_chain` 来将 fp 指向我们能控制的地方。所以将 size 字段设置为 0x61,因为此时 `_IO_list_all` 是 `&unsorted_bin-0x10`,偏移 0x60 位置处是 smallbins[4](或者说 bins[6])。此时,如果触发一个不适合的 small chunk 分配,malloc 就会将 old top 从 unsorted bin 放回 smallbins[4] 中。而在 `_IO_FILE` 结构中,偏移 0x60 指向 `struct _IO_marker *_markers`,偏移 0x68 指向 `struct _IO_FILE *_chain`,这两个值正好是 old top 的起始地址。这样 fp 就指向了 old top,这是一个我们能够控制的地址。 diff --git a/doc/4.13_io_file.md b/doc/4.13_io_file.md index 839b509..0ad001d 100644 --- a/doc/4.13_io_file.md +++ b/doc/4.13_io_file.md @@ -1,8 +1,598 @@ -# 4.13 利用 _IO_FILE 结构 +# 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)