2018-05-08 11:54:50 +07:00
|
|
|
|
# 4.13 利用 _IO\_FILE 结构
|
2018-04-14 19:52:17 +07:00
|
|
|
|
|
2018-05-08 11:54:50 +07:00
|
|
|
|
- [FILE 结构](#file-结构)
|
|
|
|
|
- [FSOP](#fsop)
|
|
|
|
|
- [防御机制](#防御机制)
|
|
|
|
|
- [新的利用技术](#新的利用技术)
|
|
|
|
|
- [CTF 实例](#ctf-实例)
|
2018-04-14 19:52:17 +07:00
|
|
|
|
- [参考资料](#参考资料)
|
|
|
|
|
|
|
|
|
|
|
2018-05-08 11:54:50 +07:00
|
|
|
|
## 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 中也有相关内容。
|
|
|
|
|
|
|
|
|
|
|
2018-04-14 19:52:17 +07:00
|
|
|
|
## 参考资料
|
|
|
|
|
- [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)
|