4.13 利用 _IO_FILE 结构

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 结构体的指针:

  1. // libio/libioP.h
  2. struct _IO_jump_t
  3. {
  4. JUMP_FIELD(size_t, __dummy);
  5. JUMP_FIELD(size_t, __dummy2);
  6. JUMP_FIELD(_IO_finish_t, __finish);
  7. JUMP_FIELD(_IO_overflow_t, __overflow);
  8. JUMP_FIELD(_IO_underflow_t, __underflow);
  9. JUMP_FIELD(_IO_underflow_t, __uflow);
  10. JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
  11. /* showmany */
  12. JUMP_FIELD(_IO_xsputn_t, __xsputn);
  13. JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
  14. JUMP_FIELD(_IO_seekoff_t, __seekoff);
  15. JUMP_FIELD(_IO_seekpos_t, __seekpos);
  16. JUMP_FIELD(_IO_setbuf_t, __setbuf);
  17. JUMP_FIELD(_IO_sync_t, __sync);
  18. JUMP_FIELD(_IO_doallocate_t, __doallocate);
  19. JUMP_FIELD(_IO_read_t, __read);
  20. JUMP_FIELD(_IO_write_t, __write);
  21. JUMP_FIELD(_IO_seek_t, __seek);
  22. JUMP_FIELD(_IO_close_t, __close);
  23. JUMP_FIELD(_IO_stat_t, __stat);
  24. JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
  25. JUMP_FIELD(_IO_imbue_t, __imbue);
  26. #if 0
  27. get_column;
  28. set_column;
  29. #endif
  30. };
  31. /* We always allocate an extra word following an _IO_FILE.
  32. This contains a pointer to the function jump table used.
  33. This is for compatibility with C++ streambuf; the word can
  34. be used to smash to a pointer to a virtual function table. */
  35. struct _IO_FILE_plus
  36. {
  37. _IO_FILE file;
  38. const struct _IO_jump_t *vtable;
  39. };
  40. extern struct _IO_FILE_plus *_IO_list_all;

vtable 指向的函数跳转表其实是一种兼容 C++ 虚函数的实现。当程序对某个流进行操作时,会调用该流对应的跳转表中的某个函数。

  1. // libio/libio.h
  2. struct _IO_FILE {
  3. int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
  4. #define _IO_file_flags _flags
  5. /* The following pointers correspond to the C++ streambuf protocol. */
  6. /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  7. char* _IO_read_ptr; /* Current read pointer */
  8. char* _IO_read_end; /* End of get area. */
  9. char* _IO_read_base; /* Start of putback+get area. */
  10. char* _IO_write_base; /* Start of put area. */
  11. char* _IO_write_ptr; /* Current put pointer. */
  12. char* _IO_write_end; /* End of put area. */
  13. char* _IO_buf_base; /* Start of reserve area. */
  14. char* _IO_buf_end; /* End of reserve area. */
  15. /* The following fields are used to support backing up and undo. */
  16. char *_IO_save_base; /* Pointer to start of non-current get area. */
  17. char *_IO_backup_base; /* Pointer to first valid character of backup area */
  18. char *_IO_save_end; /* Pointer to end of non-current get area. */
  19. struct _IO_marker *_markers;
  20. struct _IO_FILE *_chain;
  21. int _fileno;
  22. #if 0
  23. int _blksize;
  24. #else
  25. int _flags2;
  26. #endif
  27. _IO_off_t _old_offset; /* This used to be _offset but it's too small. */
  28. #define __HAVE_COLUMN /* temporary */
  29. /* 1+column number of pbase(); 0 is unknown. */
  30. unsigned short _cur_column;
  31. signed char _vtable_offset;
  32. char _shortbuf[1];
  33. /* char* _save_gptr; char* _save_egptr; */
  34. _IO_lock_t *_lock;
  35. #ifdef _IO_USE_OLD_IO_FILE
  36. };
  37. struct _IO_FILE_complete
  38. {
  39. struct _IO_FILE _file;
  40. #endif
  41. #if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
  42. _IO_off64_t _offset;
  43. # if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
  44. /* Wide character stream stuff. */
  45. struct _IO_codecvt *_codecvt;
  46. struct _IO_wide_data *_wide_data;
  47. struct _IO_FILE *_freeres_list;
  48. void *_freeres_buf;
  49. # else
  50. void *__pad1;
  51. void *__pad2;
  52. void *__pad3;
  53. void *__pad4;
  54. # endif
  55. size_t __pad5;
  56. int _mode;
  57. /* Make sure we don't get into trouble again. */
  58. char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
  59. #endif
  60. };
  61. extern struct _IO_FILE_plus _IO_2_1_stdin_;
  62. extern struct _IO_FILE_plus _IO_2_1_stdout_;
  63. extern struct _IO_FILE_plus _IO_2_1_stderr_;

进程中的 FILE 结构会通过 _chain 域构成一个链表,链表头部用全局变量 _IO_list_all 表示。

另外 _IO_wide_data 结构也是后面需要的:

  1. /* Extra data for wide character streams. */
  2. struct _IO_wide_data
  3. {
  4. wchar_t *_IO_read_ptr; /* Current read pointer */
  5. wchar_t *_IO_read_end; /* End of get area. */
  6. wchar_t *_IO_read_base; /* Start of putback+get area. */
  7. wchar_t *_IO_write_base; /* Start of put area. */
  8. wchar_t *_IO_write_ptr; /* Current put pointer. */
  9. wchar_t *_IO_write_end; /* End of put area. */
  10. wchar_t *_IO_buf_base; /* Start of reserve area. */
  11. wchar_t *_IO_buf_end; /* End of reserve area. */
  12. /* The following fields are used to support backing up and undo. */
  13. wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
  14. wchar_t *_IO_backup_base; /* Pointer to first valid character of
  15. backup area */
  16. wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */
  17. __mbstate_t _IO_state;
  18. __mbstate_t _IO_last_state;
  19. struct _IO_codecvt _codecvt;
  20. wchar_t _shortbuf[1];
  21. const struct _IO_jump_t *_wide_vtable;
  22. };

fopen

下面我们来看几个函数的实现。

  1. // libio/iofopen.c
  2. _IO_FILE *
  3. __fopen_internal (const char *filename, const char *mode, int is32)
  4. {
  5. struct locked_FILE
  6. {
  7. struct _IO_FILE_plus fp;
  8. #ifdef _IO_MTSAFE_IO
  9. _IO_lock_t lock;
  10. #endif
  11. struct _IO_wide_data wd;
  12. } *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE)); // 为 FILE 结构分配空间
  13. if (new_f == NULL)
  14. return NULL;
  15. #ifdef _IO_MTSAFE_IO
  16. new_f->fp.file._lock = &new_f->lock;
  17. #endif
  18. #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
  19. _IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
  20. #else
  21. _IO_no_init (&new_f->fp.file, 1, 0, NULL, NULL);
  22. #endif
  23. _IO_JUMPS (&new_f->fp) = &_IO_file_jumps; // 设置 vtable = &_IO_file_jumps
  24. _IO_file_init (&new_f->fp); // 调用 _IO_file_init 函数进行初始化
  25. #if !_IO_UNIFIED_JUMPTABLES
  26. new_f->fp.vtable = NULL;
  27. #endif
  28. if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL) // 打开目标文件
  29. return __fopen_maybe_mmap (&new_f->fp.file);
  30. _IO_un_link (&new_f->fp);
  31. free (new_f);
  32. return NULL;
  33. }
  34. _IO_FILE *
  35. _IO_new_fopen (const char *filename, const char *mode)
  36. {
  37. return __fopen_internal (filename, mode, 1);
  38. }
  1. // libio/fileops.c
  2. # define _IO_new_file_init _IO_file_init
  3. void
  4. _IO_new_file_init (struct _IO_FILE_plus *fp)
  5. {
  6. /* POSIX.1 allows another file handle to be used to change the position
  7. of our file descriptor. Hence we actually don't know the actual
  8. position before we do the first fseek (and until a following fflush). */
  9. fp->file._offset = _IO_pos_BAD;
  10. fp->file._IO_file_flags |= CLOSED_FILEBUF_FLAGS;
  11. _IO_link_in (fp); // 调用 _IO_link_in 函数将 fp 放进链表
  12. fp->file._fileno = -1;
  13. }
  1. // libio/genops.c
  2. void
  3. _IO_link_in (struct _IO_FILE_plus *fp)
  4. {
  5. if ((fp->file._flags & _IO_LINKED) == 0)
  6. {
  7. fp->file._flags |= _IO_LINKED;
  8. #ifdef _IO_MTSAFE_IO
  9. _IO_cleanup_region_start_noarg (flush_cleanup);
  10. _IO_lock_lock (list_all_lock);
  11. run_fp = (_IO_FILE *) fp;
  12. _IO_flockfile ((_IO_FILE *) fp);
  13. #endif
  14. fp->file._chain = (_IO_FILE *) _IO_list_all; // fp 放到链表头部
  15. _IO_list_all = fp; // 链表头 _IO_list_all 指向 fp
  16. ++_IO_list_all_stamp;
  17. #ifdef _IO_MTSAFE_IO
  18. _IO_funlockfile ((_IO_FILE *) fp);
  19. run_fp = NULL;
  20. _IO_lock_unlock (list_all_lock);
  21. _IO_cleanup_region_end (0);
  22. #endif
  23. }
  24. }

fread

  1. // libio/iofread.c
  2. _IO_size_t
  3. _IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
  4. {
  5. _IO_size_t bytes_requested = size * count;
  6. _IO_size_t bytes_read;
  7. CHECK_FILE (fp, 0);
  8. if (bytes_requested == 0)
  9. return 0;
  10. _IO_acquire_lock (fp);
  11. bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested); // 调用 _IO_sgetn 函数
  12. _IO_release_lock (fp);
  13. return bytes_requested == bytes_read ? count : bytes_read / size;
  14. }
  1. // libio/genops.c
  2. _IO_size_t
  3. _IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n)
  4. {
  5. /* FIXME handle putback buffer here! */
  6. return _IO_XSGETN (fp, data, n); // 调用宏 _IO_XSGETN
  7. }
  1. // libio/libioP.h
  2. #define _IO_JUMPS_FILE_plus(THIS) \
  3. _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)
  4. #if _IO_JUMPS_OFFSET
  5. # define _IO_JUMPS_FUNC(THIS) \
  6. (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
  7. + (THIS)->_vtable_offset))
  8. # define _IO_vtable_offset(THIS) (THIS)->_vtable_offset
  9. #else
  10. # define _IO_JUMPS_FUNC(THIS) _IO_JUMPS_FILE_plus (THIS)
  11. # define _IO_vtable_offset(THIS) 0
  12. #endif
  13. #define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
  14. #define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)

所以 _IO_XSGETN 宏最终会调用 vtable 中的函数,即:

  1. // libio/fileops.c
  2. _IO_size_t
  3. _IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
  4. {

fwrite

  1. // libio/iofwrite.c
  2. _IO_size_t
  3. _IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
  4. {
  5. _IO_size_t request = size * count;
  6. _IO_size_t written = 0;
  7. CHECK_FILE (fp, 0);
  8. if (request == 0)
  9. return 0;
  10. _IO_acquire_lock (fp);
  11. if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
  12. written = _IO_sputn (fp, (const char *) buf, request); // 调用 _IO_sputn 函数
  13. _IO_release_lock (fp);
  14. /* We have written all of the input in case the return value indicates
  15. this or EOF is returned. The latter is a special case where we
  16. simply did not manage to flush the buffer. But the data is in the
  17. buffer and therefore written as far as fwrite is concerned. */
  18. if (written == request || written == EOF)
  19. return count;
  20. else
  21. return written / size;
  22. }
  1. // libio/libioP.h
  2. #define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
  3. #define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)

_IO_XSPUTN 最终将调用下面的函数:

  1. // libio/fileops.c
  2. _IO_size_t
  3. _IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
  4. {

fclose

  1. // libio/iofclose.c
  2. int
  3. _IO_new_fclose (_IO_FILE *fp)
  4. {
  5. int status;
  6. CHECK_FILE(fp, EOF);
  7. #if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
  8. /* We desperately try to help programs which are using streams in a
  9. strange way and mix old and new functions. Detect old streams
  10. here. */
  11. if (_IO_vtable_offset (fp) != 0)
  12. return _IO_old_fclose (fp);
  13. #endif
  14. /* First unlink the stream. */
  15. if (fp->_IO_file_flags & _IO_IS_FILEBUF)
  16. _IO_un_link ((struct _IO_FILE_plus *) fp); // 将 fp 从链表中取出
  17. _IO_acquire_lock (fp);
  18. if (fp->_IO_file_flags & _IO_IS_FILEBUF)
  19. status = _IO_file_close_it (fp); // 关闭目标文件
  20. else
  21. status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
  22. _IO_release_lock (fp);
  23. _IO_FINISH (fp);
  24. if (fp->_mode > 0)
  25. {
  26. #if _LIBC
  27. /* This stream has a wide orientation. This means we have to free
  28. the conversion functions. */
  29. struct _IO_codecvt *cc = fp->_codecvt;
  30. __libc_lock_lock (__gconv_lock);
  31. __gconv_release_step (cc->__cd_in.__cd.__steps);
  32. __gconv_release_step (cc->__cd_out.__cd.__steps);
  33. __libc_lock_unlock (__gconv_lock);
  34. #endif
  35. }
  36. else
  37. {
  38. if (_IO_have_backup (fp))
  39. _IO_free_backup_area (fp);
  40. }
  41. if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr)
  42. {
  43. fp->_IO_file_flags = 0;
  44. free(fp); // 释放 FILE 结构体
  45. }
  46. return status;
  47. }

FSOP

FSOP(File Stream Oriented Programming)是一种劫持 _IO_list_all(libc.so中的全局变量) 来伪造链表的利用技术,通过调用 _IO_flush_all_lockp() 函数来触发,该函数会在下面三种情况下被调用:

  • libc 检测到内存错误时
  • 执行 exit 函数时
  • main 函数返回时

当 glibc 检测到内存错误时,会依次调用这样的函数路径:malloc_printerr -> __libc_message -> __GI_abort -> _IO_flush_all_lockp -> _IO_OVERFLOW

  1. // libio/genops.c
  2. int
  3. _IO_flush_all_lockp (int do_lock)
  4. {
  5. int result = 0;
  6. struct _IO_FILE *fp;
  7. int last_stamp;
  8. #ifdef _IO_MTSAFE_IO
  9. __libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
  10. if (do_lock)
  11. _IO_lock_lock (list_all_lock);
  12. #endif
  13. last_stamp = _IO_list_all_stamp;
  14. fp = (_IO_FILE *) _IO_list_all; // 将其覆盖为伪造的链表
  15. while (fp != NULL)
  16. {
  17. run_fp = fp;
  18. if (do_lock)
  19. _IO_flockfile (fp);
  20. if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) // 条件
  21. #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
  22. || (_IO_vtable_offset (fp) == 0
  23. && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
  24. > fp->_wide_data->_IO_write_base))
  25. #endif
  26. )
  27. && _IO_OVERFLOW (fp, EOF) == EOF) // fp 指向伪造的 vtable
  28. result = EOF;
  29. if (do_lock)
  30. _IO_funlockfile (fp);
  31. run_fp = NULL;
  32. if (last_stamp != _IO_list_all_stamp)
  33. {
  34. /* Something was added to the list. Start all over again. */
  35. fp = (_IO_FILE *) _IO_list_all;
  36. last_stamp = _IO_list_all_stamp;
  37. }
  38. else
  39. fp = fp->_chain; // 指向下一个 IO_FILE 对象
  40. }
  41. #ifdef _IO_MTSAFE_IO
  42. if (do_lock)
  43. _IO_lock_unlock (list_all_lock);
  44. __libc_cleanup_region_end (0);
  45. #endif
  46. return result;
  47. }
  1. // libio/libioP.h
  2. #define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
  3. #define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)

于是在 _IO_OVERFLOW(fp, EOF) 的执行过程中最终会调用 system('/bin/sh')

还有一条 FSOP 的路径是在关闭 stream 的时候:

  1. // libio/iofclose.c
  2. int
  3. _IO_new_fclose (_IO_FILE *fp)
  4. {
  5. int status;
  6. CHECK_FILE(fp, EOF);
  7. #if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
  8. /* We desperately try to help programs which are using streams in a
  9. strange way and mix old and new functions. Detect old streams
  10. here. */
  11. if (_IO_vtable_offset (fp) != 0)
  12. return _IO_old_fclose (fp);
  13. #endif
  14. /* First unlink the stream. */
  15. if (fp->_IO_file_flags & _IO_IS_FILEBUF)
  16. _IO_un_link ((struct _IO_FILE_plus *) fp);
  17. _IO_acquire_lock (fp);
  18. if (fp->_IO_file_flags & _IO_IS_FILEBUF)
  19. status = _IO_file_close_it (fp);
  20. else
  21. status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
  22. _IO_release_lock (fp);
  23. _IO_FINISH (fp); // fp 指向伪造的 vtable
  24. if (fp->_mode > 0)
  25. {
  26. #if _LIBC
  27. /* This stream has a wide orientation. This means we have to free
  28. the conversion functions. */
  29. struct _IO_codecvt *cc = fp->_codecvt;
  30. __libc_lock_lock (__gconv_lock);
  31. __gconv_release_step (cc->__cd_in.__cd.__steps);
  32. __gconv_release_step (cc->__cd_out.__cd.__steps);
  33. __libc_lock_unlock (__gconv_lock);
  34. #endif
  35. }
  36. else
  37. {
  38. if (_IO_have_backup (fp))
  39. _IO_free_backup_area (fp);
  40. }
  41. if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr)
  42. {
  43. fp->_IO_file_flags = 0;
  44. free(fp);
  45. }
  46. return status;
  47. }
  1. // libio/libioP.h
  2. #define _IO_FINISH(FP) JUMP1 (__finish, FP, 0)
  3. #define _IO_WFINISH(FP) WJUMP1 (__finish, FP, 0)

于是在 _IO_FINISH (fp) 的执行过程中最终会调用 system('/bin/sh')

libc-2.24 防御机制

但是在 libc-2.24 中加入了对 vtable 指针的检查。这个 commit 新增了两个函数:IO_validate_vtable_IO_vtable_check

  1. // libio/libioP.h
  2. /* Perform vtable pointer validation. If validation fails, terminate
  3. the process. */
  4. static inline const struct _IO_jump_t *
  5. IO_validate_vtable (const struct _IO_jump_t *vtable)
  6. {
  7. /* Fast path: The vtable pointer is within the __libc_IO_vtables
  8. section. */
  9. uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
  10. const char *ptr = (const char *) vtable;
  11. uintptr_t offset = ptr - __start___libc_IO_vtables;
  12. if (__glibc_unlikely (offset >= section_length))
  13. /* The vtable pointer is not in the expected section. Use the
  14. slow path, which will terminate the process if necessary. */
  15. _IO_vtable_check ();
  16. return vtable;
  17. }
  1. // libio/vtables.c
  2. void attribute_hidden
  3. _IO_vtable_check (void)
  4. {
  5. #ifdef SHARED
  6. /* Honor the compatibility flag. */
  7. void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
  8. #ifdef PTR_DEMANGLE
  9. PTR_DEMANGLE (flag);
  10. #endif
  11. if (flag == &_IO_vtable_check)
  12. return;
  13. /* In case this libc copy is in a non-default namespace, we always
  14. need to accept foreign vtables because there is always a
  15. possibility that FILE * objects are passed across the linking
  16. boundary. */
  17. {
  18. Dl_info di;
  19. struct link_map *l;
  20. if (_dl_open_hook != NULL
  21. || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
  22. && l->l_ns != LM_ID_BASE))
  23. return;
  24. }
  25. #else /* !SHARED */
  26. /* We cannot perform vtable validation in the static dlopen case
  27. because FILE * handles might be passed back and forth across the
  28. boundary. Therefore, we disable checking in this case. */
  29. if (__dlopen != NULL)
  30. return;
  31. #endif
  32. __libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
  33. }

所有的 libio vtables 被放进了专用的只读的 __libc_IO_vtables 段,以使它们在内存中连续。在任何间接跳转之前,vtable 指针将根据段边界进行检查,如果指针不在这个段,则调用函数 _IO_vtable_check() 做进一步的检查,并且在必要时终止进程。

libc-2.24 利用技术

_IO_str_jumps

在防御机制下通过修改虚表的利用技术已经用不了了,但同时出现了新的利用技术。既然无法将 vtable 指针指向 __libc_IO_vtables 以外的地方,那么就在 __libc_IO_vtables 里面找些有用的东西。比如 _IO_str_jumps(该符号在strip后会丢失):

  1. // libio/strops.c
  2. const struct _IO_jump_t _IO_str_jumps libio_vtable =
  3. {
  4. JUMP_INIT_DUMMY,
  5. JUMP_INIT(finish, _IO_str_finish),
  6. JUMP_INIT(overflow, _IO_str_overflow),
  7. JUMP_INIT(underflow, _IO_str_underflow),
  8. JUMP_INIT(uflow, _IO_default_uflow),
  9. JUMP_INIT(pbackfail, _IO_str_pbackfail),
  10. JUMP_INIT(xsputn, _IO_default_xsputn),
  11. JUMP_INIT(xsgetn, _IO_default_xsgetn),
  12. JUMP_INIT(seekoff, _IO_str_seekoff),
  13. JUMP_INIT(seekpos, _IO_default_seekpos),
  14. JUMP_INIT(setbuf, _IO_default_setbuf),
  15. JUMP_INIT(sync, _IO_default_sync),
  16. JUMP_INIT(doallocate, _IO_default_doallocate),
  17. JUMP_INIT(read, _IO_default_read),
  18. JUMP_INIT(write, _IO_default_write),
  19. JUMP_INIT(seek, _IO_default_seek),
  20. JUMP_INIT(close, _IO_default_close),
  21. JUMP_INIT(stat, _IO_default_stat),
  22. JUMP_INIT(showmanyc, _IO_default_showmanyc),
  23. JUMP_INIT(imbue, _IO_default_imbue)
  24. };
  1. // libio/libioP.h
  2. #define JUMP_INIT_DUMMY JUMP_INIT(dummy, 0), JUMP_INIT (dummy2, 0)

这个 vtable 中包含了一个叫做 _IO_str_overflow 的函数,该函数中存在相对地址的引用(可伪造):

  1. int
  2. _IO_str_overflow (_IO_FILE *fp, int c)
  3. {
  4. int flush_only = c == EOF;
  5. _IO_size_t pos;
  6. if (fp->_flags & _IO_NO_WRITES)
  7. return flush_only ? 0 : EOF;
  8. if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
  9. {
  10. fp->_flags |= _IO_CURRENTLY_PUTTING;
  11. fp->_IO_write_ptr = fp->_IO_read_ptr;
  12. fp->_IO_read_ptr = fp->_IO_read_end;
  13. }
  14. pos = fp->_IO_write_ptr - fp->_IO_write_base;
  15. if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only)) // 条件 #define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
  16. {
  17. if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
  18. return EOF;
  19. else
  20. {
  21. char *new_buf;
  22. char *old_buf = fp->_IO_buf_base;
  23. size_t old_blen = _IO_blen (fp);
  24. _IO_size_t new_size = 2 * old_blen + 100; // 通过计算 new_size 为 "/bin/sh\x00" 的地址
  25. if (new_size < old_blen)
  26. return EOF;
  27. new_buf
  28. = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size); // 在这个相对地址放上 system 的地址,即 system("/bin/sh")
  29. [...]
  1. // libio/strfile.h
  2. struct _IO_str_fields
  3. {
  4. _IO_alloc_type _allocate_buffer;
  5. _IO_free_type _free_buffer;
  6. };
  7. struct _IO_streambuf
  8. {
  9. struct _IO_FILE _f;
  10. const struct _IO_jump_t *vtable;
  11. };
  12. typedef struct _IO_strfile_
  13. {
  14. struct _IO_streambuf _sbf;
  15. struct _IO_str_fields _s;
  16. } _IO_strfile;

所以可以像下面这样构造:

  • fp->_flags = 0
  • fp->_IO_buf_base = 0
  • fp->_IO_buf_end = (bin_sh_addr - 100) / 2
  • fp->_IO_write_ptr = 0xffffffff
  • fp->_IO_write_base = 0
  • fp->_mode = 0

有一点要注意的是,如果 bin_sh_addr 的地址以奇数结尾,为了避免除法向下取整的干扰,可以将该地址加 1。另外 system(“/bin/sh”) 是可以用 one_gadget 来代替的,这样似乎更加简单。

完整的调用过程:malloc_printerr -> __libc_message -> __GI_abort -> _IO_flush_all_lockp -> __GI__IO_str_overflow

与传统的 house-of-orange 不同的是,这种利用方法不再需要知道 heap 的地址,因为 _IO_str_jumps vtable 是在 libc 上的,所以只要能泄露出 libc 的地址就可以了。

在这个 vtable 中,还有另一个函数 _IO_str_finish,它的检查条件比较简单:

  1. void
  2. _IO_str_finish (_IO_FILE *fp, int dummy)
  3. {
  4. if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) // 条件
  5. (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); // 在这个相对地址放上 system 的地址
  6. fp->_IO_buf_base = NULL;
  7. _IO_default_finish (fp, 0);
  8. }

只要在 fp->_IO_buf_base 放上 “/bin/sh” 的地址,然后设置 fp->_flags = 0 就可以了绕过函数里的条件。

那么怎样让程序进入 _IO_str_finish 执行呢,fclose(fp) 是一条路,但似乎有局限。还是回到异常处理上来,在 _IO_flush_all_lockp 函数中是通过 _IO_OVERFLOW 执行的 __GI__IO_str_overflow,而 _IO_OVERFLOW 是根据 __overflow 相对于 _IO_str_jumps vtable 的偏移找到具体函数的。所以如果我们伪造传递给 _IO_OVERFLOW(fp) 的 fp 是 vtable 的地址减去 0x8,那么根据偏移,程序将找到 _IO_str_finish 并执行。

所以可以像下面这样构造:

  • fp->_mode = 0
  • fp->_IO_write_ptr = 0xffffffff
  • fp->_IO_write_base = 0
  • fp->_wide_data->_IO_buf_base = bin_sh_addr (也就是 fp->_IO_write_end)
  • fp->_flags2 = 0
  • fp->_mode = 0

完整的调用过程:malloc_printerr -> __libc_message -> __GI_abort -> _IO_flush_all_lockp -> __GI__IO_str_finish

_IO_wstr_jumps

_IO_wstr_jumps 也是一个符合条件的 vtable,总体上和上面讲的 _IO_str_jumps 差不多:

  1. // libio/wstrops.c
  2. const struct _IO_jump_t _IO_wstr_jumps libio_vtable =
  3. {
  4. JUMP_INIT_DUMMY,
  5. JUMP_INIT(finish, _IO_wstr_finish),
  6. JUMP_INIT(overflow, (_IO_overflow_t) _IO_wstr_overflow),
  7. JUMP_INIT(underflow, (_IO_underflow_t) _IO_wstr_underflow),
  8. JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
  9. JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wstr_pbackfail),
  10. JUMP_INIT(xsputn, _IO_wdefault_xsputn),
  11. JUMP_INIT(xsgetn, _IO_wdefault_xsgetn),
  12. JUMP_INIT(seekoff, _IO_wstr_seekoff),
  13. JUMP_INIT(seekpos, _IO_default_seekpos),
  14. JUMP_INIT(setbuf, _IO_default_setbuf),
  15. JUMP_INIT(sync, _IO_default_sync),
  16. JUMP_INIT(doallocate, _IO_wdefault_doallocate),
  17. JUMP_INIT(read, _IO_default_read),
  18. JUMP_INIT(write, _IO_default_write),
  19. JUMP_INIT(seek, _IO_default_seek),
  20. JUMP_INIT(close, _IO_default_close),
  21. JUMP_INIT(stat, _IO_default_stat),
  22. JUMP_INIT(showmanyc, _IO_default_showmanyc),
  23. JUMP_INIT(imbue, _IO_default_imbue)
  24. };

利用函数 _IO_wstr_overflow

  1. _IO_wint_t
  2. _IO_wstr_overflow (_IO_FILE *fp, _IO_wint_t c)
  3. {
  4. int flush_only = c == WEOF;
  5. _IO_size_t pos;
  6. if (fp->_flags & _IO_NO_WRITES)
  7. return flush_only ? 0 : WEOF;
  8. if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
  9. {
  10. fp->_flags |= _IO_CURRENTLY_PUTTING;
  11. fp->_wide_data->_IO_write_ptr = fp->_wide_data->_IO_read_ptr;
  12. fp->_wide_data->_IO_read_ptr = fp->_wide_data->_IO_read_end;
  13. }
  14. pos = fp->_wide_data->_IO_write_ptr - fp->_wide_data->_IO_write_base;
  15. if (pos >= (_IO_size_t) (_IO_wblen (fp) + flush_only)) // 条件 #define _IO_wblen(fp) ((fp)->_wide_data->_IO_buf_end - (fp)->_wide_data->_IO_buf_base)
  16. {
  17. if (fp->_flags2 & _IO_FLAGS2_USER_WBUF) /* not allowed to enlarge */
  18. return WEOF;
  19. else
  20. {
  21. wchar_t *new_buf;
  22. wchar_t *old_buf = fp->_wide_data->_IO_buf_base;
  23. size_t old_wblen = _IO_wblen (fp);
  24. _IO_size_t new_size = 2 * old_wblen + 100; // 使 new_size * sizeof(wchar_t) 为 "/bin/sh" 的地址
  25. if (__glibc_unlikely (new_size < old_wblen)
  26. || __glibc_unlikely (new_size > SIZE_MAX / sizeof (wchar_t)))
  27. return EOF;
  28. new_buf
  29. = (wchar_t *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size
  30. * sizeof (wchar_t)); // 在这个相对地址放上 system 的地址
  31. [...]

利用函数 _IO_wstr_finish

  1. void
  2. _IO_wstr_finish (_IO_FILE *fp, int dummy)
  3. {
  4. if (fp->_wide_data->_IO_buf_base && !(fp->_flags2 & _IO_FLAGS2_USER_WBUF)) // 条件
  5. (((_IO_strfile *) fp)->_s._free_buffer) (fp->_wide_data->_IO_buf_base); // 在这个相对地址放上 system 的地址
  6. fp->_wide_data->_IO_buf_base = NULL;
  7. _IO_wdefault_finish (fp, 0);
  8. }

最新动态

来自 glibc 的 master 分支上的一次 commit,不出意外应该会出现在 libc-2.28 中。

该方法简单粗暴,用操作堆的 malloc 和 free 替换掉原来在 _IO_str_fields 里的 _allocate_buffer_free_buffer。由于不再使用偏移,就不能再利用 __libc_IO_vtables 上的 vtable 绕过检查,于是上面的利用技术就都失效了。:(

CTF 实例

请查看章节 6.1.24、6.1.25 和 6.1.26。另外在章节 3.1.8 中也有相关内容。

附上偏移,构造时候方便一点:

  1. 0x0 _flags
  2. 0x8 _IO_read_ptr
  3. 0x10 _IO_read_end
  4. 0x18 _IO_read_base
  5. 0x20 _IO_write_base
  6. 0x28 _IO_write_ptr
  7. 0x30 _IO_write_end
  8. 0x38 _IO_buf_base
  9. 0x40 _IO_buf_end
  10. 0x48 _IO_save_base
  11. 0x50 _IO_backup_base
  12. 0x58 _IO_save_end
  13. 0x60 _markers
  14. 0x68 _chain
  15. 0x70 _fileno
  16. 0x74 _flags2
  17. 0x78 _old_offset
  18. 0x80 _cur_column
  19. 0x82 _vtable_offset
  20. 0x83 _shortbuf
  21. 0x88 _lock
  22. 0x90 _offset
  23. 0x98 _codecvt
  24. 0xa0 _wide_data
  25. 0xa8 _freeres_list
  26. 0xb0 _freeres_buf
  27. 0xb8 __pad5
  28. 0xc0 _mode
  29. 0xc4 _unused2
  30. 0xd8 vtable

参考资料