mirror of
https://github.com/nganhkhoa/CTF-All-In-One.git
synced 2024-12-25 11:41:16 +07:00
877 lines
27 KiB
Markdown
877 lines
27 KiB
Markdown
# 4.13 利用 _IO\_FILE 结构
|
||
|
||
- [FILE 结构](#file-结构)
|
||
- [FSOP](#fsop)
|
||
- [libc-2.24 防御机制](#libc-224-防御机制)
|
||
- [libc-2.24 利用技术](#libc-224-利用技术)
|
||
- [最新动态](#最新动态)
|
||
- [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
|
||
};
|
||
|
||
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
|
||
/* Wide character stream stuff. */
|
||
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;
|
||
/* Make sure we don't get into trouble again. */
|
||
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
|
||
#endif
|
||
};
|
||
|
||
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` 表示。
|
||
|
||
另外 `_IO_wide_data` 结构也是后面需要的:
|
||
```c
|
||
/* Extra data for wide character streams. */
|
||
struct _IO_wide_data
|
||
{
|
||
wchar_t *_IO_read_ptr; /* Current read pointer */
|
||
wchar_t *_IO_read_end; /* End of get area. */
|
||
wchar_t *_IO_read_base; /* Start of putback+get area. */
|
||
wchar_t *_IO_write_base; /* Start of put area. */
|
||
wchar_t *_IO_write_ptr; /* Current put pointer. */
|
||
wchar_t *_IO_write_end; /* End of put area. */
|
||
wchar_t *_IO_buf_base; /* Start of reserve area. */
|
||
wchar_t *_IO_buf_end; /* End of reserve area. */
|
||
/* The following fields are used to support backing up and undo. */
|
||
wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
|
||
wchar_t *_IO_backup_base; /* Pointer to first valid character of
|
||
backup area */
|
||
wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */
|
||
|
||
__mbstate_t _IO_state;
|
||
__mbstate_t _IO_last_state;
|
||
struct _IO_codecvt _codecvt;
|
||
|
||
wchar_t _shortbuf[1];
|
||
|
||
const struct _IO_jump_t *_wide_vtable;
|
||
};
|
||
```
|
||
|
||
下面我们来看几个函数的实现。
|
||
#### 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 -> __GI_abort -> _IO_flush_all_lockp -> _IO_OVERFLOW`。
|
||
```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) // fp 指向伪造的 vtable
|
||
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; // 指向下一个 IO_FILE 对象
|
||
}
|
||
|
||
#ifdef _IO_MTSAFE_IO
|
||
if (do_lock)
|
||
_IO_lock_unlock (list_all_lock);
|
||
__libc_cleanup_region_end (0);
|
||
#endif
|
||
|
||
return result;
|
||
}
|
||
```
|
||
```c
|
||
// libio/libioP.h
|
||
|
||
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
|
||
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)
|
||
```
|
||
于是在 `_IO_OVERFLOW(fp, EOF)` 的执行过程中最终会调用 `system('/bin/sh')`。
|
||
|
||
还有一条 FSOP 的路径是在关闭 stream 的时候:
|
||
```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);
|
||
|
||
_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); // fp 指向伪造的 vtable
|
||
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);
|
||
}
|
||
|
||
return status;
|
||
}
|
||
```
|
||
```c
|
||
// libio/libioP.h
|
||
|
||
#define _IO_FINISH(FP) JUMP1 (__finish, FP, 0)
|
||
#define _IO_WFINISH(FP) WJUMP1 (__finish, FP, 0)
|
||
```
|
||
于是在 `_IO_FINISH (fp)` 的执行过程中最终会调用 `system('/bin/sh')`。
|
||
|
||
|
||
## libc-2.24 防御机制
|
||
但是在 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()` 做进一步的检查,并且在必要时终止进程。
|
||
|
||
|
||
## libc-2.24 利用技术
|
||
#### _IO_str_jumps
|
||
在防御机制下通过修改虚表的利用技术已经用不了了,但同时出现了新的利用技术。既然无法将 vtable 指针指向 `__libc_IO_vtables` 以外的地方,那么就在 `__libc_IO_vtables` 里面找些有用的东西。比如 `_IO_str_jumps`(该符号在strip后会丢失):
|
||
```c
|
||
// libio/strops.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)
|
||
};
|
||
```
|
||
```c
|
||
// libio/libioP.h
|
||
|
||
#define JUMP_INIT_DUMMY JUMP_INIT(dummy, 0), JUMP_INIT (dummy2, 0)
|
||
```
|
||
这个 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)) // 条件 #define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
|
||
{
|
||
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 为 "/bin/sh\x00" 的地址
|
||
if (new_size < old_blen)
|
||
return EOF;
|
||
new_buf
|
||
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size); // 在这个相对地址放上 system 的地址,即 system("/bin/sh")
|
||
[...]
|
||
```
|
||
```c
|
||
// libio/strfile.h
|
||
|
||
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;
|
||
```
|
||
所以可以像下面这样构造:
|
||
- 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`,它的检查条件比较简单:
|
||
```c
|
||
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); // 在这个相对地址放上 system 的地址
|
||
fp->_IO_buf_base = NULL;
|
||
|
||
_IO_default_finish (fp, 0);
|
||
}
|
||
```
|
||
只要在 `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` 差不多:
|
||
```c
|
||
// libio/wstrops.c
|
||
|
||
const struct _IO_jump_t _IO_wstr_jumps libio_vtable =
|
||
{
|
||
JUMP_INIT_DUMMY,
|
||
JUMP_INIT(finish, _IO_wstr_finish),
|
||
JUMP_INIT(overflow, (_IO_overflow_t) _IO_wstr_overflow),
|
||
JUMP_INIT(underflow, (_IO_underflow_t) _IO_wstr_underflow),
|
||
JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
|
||
JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wstr_pbackfail),
|
||
JUMP_INIT(xsputn, _IO_wdefault_xsputn),
|
||
JUMP_INIT(xsgetn, _IO_wdefault_xsgetn),
|
||
JUMP_INIT(seekoff, _IO_wstr_seekoff),
|
||
JUMP_INIT(seekpos, _IO_default_seekpos),
|
||
JUMP_INIT(setbuf, _IO_default_setbuf),
|
||
JUMP_INIT(sync, _IO_default_sync),
|
||
JUMP_INIT(doallocate, _IO_wdefault_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_wstr_overflow`:
|
||
```c
|
||
_IO_wint_t
|
||
_IO_wstr_overflow (_IO_FILE *fp, _IO_wint_t c)
|
||
{
|
||
int flush_only = c == WEOF;
|
||
_IO_size_t pos;
|
||
if (fp->_flags & _IO_NO_WRITES)
|
||
return flush_only ? 0 : WEOF;
|
||
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
|
||
{
|
||
fp->_flags |= _IO_CURRENTLY_PUTTING;
|
||
fp->_wide_data->_IO_write_ptr = fp->_wide_data->_IO_read_ptr;
|
||
fp->_wide_data->_IO_read_ptr = fp->_wide_data->_IO_read_end;
|
||
}
|
||
pos = fp->_wide_data->_IO_write_ptr - fp->_wide_data->_IO_write_base;
|
||
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)
|
||
{
|
||
if (fp->_flags2 & _IO_FLAGS2_USER_WBUF) /* not allowed to enlarge */
|
||
return WEOF;
|
||
else
|
||
{
|
||
wchar_t *new_buf;
|
||
wchar_t *old_buf = fp->_wide_data->_IO_buf_base;
|
||
size_t old_wblen = _IO_wblen (fp);
|
||
_IO_size_t new_size = 2 * old_wblen + 100; // 使 new_size * sizeof(wchar_t) 为 "/bin/sh" 的地址
|
||
|
||
if (__glibc_unlikely (new_size < old_wblen)
|
||
|| __glibc_unlikely (new_size > SIZE_MAX / sizeof (wchar_t)))
|
||
return EOF;
|
||
|
||
new_buf
|
||
= (wchar_t *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size
|
||
* sizeof (wchar_t)); // 在这个相对地址放上 system 的地址
|
||
[...]
|
||
```
|
||
|
||
利用函数 `_IO_wstr_finish`:
|
||
```c
|
||
void
|
||
_IO_wstr_finish (_IO_FILE *fp, int dummy)
|
||
{
|
||
if (fp->_wide_data->_IO_buf_base && !(fp->_flags2 & _IO_FLAGS2_USER_WBUF)) // 条件
|
||
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_wide_data->_IO_buf_base); // 在这个相对地址放上 system 的地址
|
||
fp->_wide_data->_IO_buf_base = NULL;
|
||
|
||
_IO_wdefault_finish (fp, 0);
|
||
}
|
||
```
|
||
|
||
|
||
## 最新动态
|
||
来自 glibc 的 master 分支上的一次 [commit](https://sourceware.org/git/?p=glibc.git;a=commit;h=4e8a6346cd3da2d88bbad745a1769260d36f2783),不出意外应该会出现在 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 中也有相关内容。
|
||
|
||
附上偏移,构造时候方便一点:
|
||
```
|
||
0x0 _flags
|
||
0x8 _IO_read_ptr
|
||
0x10 _IO_read_end
|
||
0x18 _IO_read_base
|
||
0x20 _IO_write_base
|
||
0x28 _IO_write_ptr
|
||
0x30 _IO_write_end
|
||
0x38 _IO_buf_base
|
||
0x40 _IO_buf_end
|
||
0x48 _IO_save_base
|
||
0x50 _IO_backup_base
|
||
0x58 _IO_save_end
|
||
0x60 _markers
|
||
0x68 _chain
|
||
0x70 _fileno
|
||
0x74 _flags2
|
||
0x78 _old_offset
|
||
0x80 _cur_column
|
||
0x82 _vtable_offset
|
||
0x83 _shortbuf
|
||
0x88 _lock
|
||
0x90 _offset
|
||
0x98 _codecvt
|
||
0xa0 _wide_data
|
||
0xa8 _freeres_list
|
||
0xb0 _freeres_buf
|
||
0xb8 __pad5
|
||
0xc0 _mode
|
||
0xc4 _unused2
|
||
0xd8 vtable
|
||
```
|
||
|
||
|
||
## 参考资料
|
||
- [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)
|