wiki上的house系列利用手法后面的文章再补,最近看的题发现IO_FILE的攻击使用的特别多,所以就先学习一下IO_FILE的利用手法,本文的简化源码来自R4bb1t师傅的博客(地址:https://n0va-scy.github.io/2019/09/21/IO_FILE/)
文章参考的是hollk师傅的文章:(地址:https://blog.youkuaiyun.com/qq_41202237/article/details/113845320)
IO_FILE概述:
总所周知,Linux系统对于文件操作这一概念是利用的非常广的,所以这个IO_FILE其实顾名思义就是Linux系统下对于IO执行流的控制结构体
FILE结构体在程序执行fopen的过程中被创建,通常定义一个指向FILE类型的指针来接受这个返回值。
下面看一下这个IO_FILE结构体的源码:
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
};
可以看见它很长啊,因为它要实现的功能很多,但是其实仔细看会发现它的源码涉及到了两个非常重要功能:read 和 write 也就是读写功能,这也是这篇文章主要介绍的关于IO_FILE的第一种利用手法:泄露,并且这个泄露可以在程序中没有任何直接写出的输出功能时泄露。
在标准的I/O库中,在一个程序开始时一般有三个流被自动创建,分别是:stderr,stdin,stdout(分别对应异常检测,标准输入和标准输出)它们都会有一个对应的FILE结构体,而这些FILE结构体通过结构体中的:*struct _IO_FILE _chain;进行连接,其表头是_IO_list_all
IO_FILE的上层建筑:
前面提到的IO_FILE结构体其实外面还包了一层结构体,叫做_IO_FILE_plus,它的结构如下:
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
可以看见这个结构体里面有两个成员,一个_IO_FILE的结构体,一个_IO_jump_类型的指针,而这个指针vtable非常重要,它就是所谓的虚表。
那么什么是虚表呢,这里先简单说一下,后面对于IO_FILE的劫持利用中会再次提到它的。在学习C\C++时会接触到一个概念,就是虚函数,这些虚函数其实就是为后面对于类的继承提供一个函数的接口,,所以在每一个有虚函数参与的类中都会生成对应的一个虚表,而这个虚表中存放的就是相关操作函数的指针。
回到我们这个_IO_jump_t,在这个结构体中就存放了大量IO_FILE会在后面用到的函数指针,下面把这些指针贴出来:
#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);
/* 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
};
_flags:
前面我可以在IO_FILE结构体中可以发现这么一个变量:int _flags 这个变量在结构体中第一个被定义,所哟它的作用在整个利用流程中也非常重要。
如果对ELF文件有所了解的话应该就会知道在文件开头,也就是文件头的前面十六个字节包含了魔数,文件类型等信息,所以这里的_flags位其实高两位字节上是固定的一个魔数:0xfbad0000,标志了这是一个ELF文件。
低两位的字节上存放了文件的执行状态,在后面的执行过程中通过与宏定义进行按位与运算来判断程序该如何执行,其规则如下:
/* Magic numbers and bits for the _flags field.
The magic numbers use the high-order bits of _flags;
the remaining bits are available for variable flags.
Note: The magic numbers must all be negative if stdio
emulation is desired. */
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000
#define _IO_FLAGS2_MMAP 1
#define _IO_FLAGS2_NOTCANCEL 2
#ifdef _LIBC
# define _IO_FLAGS2_FORTIFY 4
#endif
#define _IO_FLAGS2_USER_WBUF 8
#ifdef _LIBC
# define _IO_FLAGS2_SCANF_STD 16
# define _IO_FLAGS2_NOCLOSE 32
# define _IO_FLAGS2_CLOEXEC 64
#endif
/* These are "formatting flags" matching the iostream fmtflags enum values. */
#define _IO_SKIPWS 01
#define _IO_LEFT 02
#define _IO_RIGHT 04
#define _IO_INTERNAL 010
#define _IO_DEC 020
#define _IO_OCT 040
#define _IO_HEX 0100
#define _IO_SHOWBASE 0200
#define _IO_SHOWPOINT 0400
#define _IO_UPPERCASE 01000
#define _IO_SHOWPOS 02000
#define _IO_SCIENTIFIC 04000
#define _IO_FIXED 010000
#define _IO_UNITBUF 020000
#define _IO_STDIO 040000
#define _IO_DONT_CLOSE 0100000
#define _IO_BOOLALPHA 0200000
而后面关于IO_FILE的libc泄露就用到与这里相关的内容。
puts()函数分析:
因为这篇文章是将泄露,所以我们就分析一下这个可以用来泄露的puts函数
IO_puts:
首先找到puts函数的源码:
int
_IO_puts (const char *str)
{
int result = EOF;
size_t len = strlen (str);
_IO_acquire_lock (_IO_stdout);
if ((_IO_vtable_offset (_IO_stdout) != 0
|| _IO_fwide (_IO_stdout, -1) == -1)
&& _IO_sputn (_IO_stdout, str, len) == len
&& _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
result = MIN (INT_MAX, len + 1);
_IO_release_lock (_IO_stdout);
return result;
}
IO_puts的主要功能是调用一个IO_sputn 函数,而这个IO_sputn 函数是一个宏定义,它指向 IO_new_file_xsputn(在前面的_IO_jump_t结构体中有提到)的函数调用,所以IO_puts函数最终是要调用 IO_new_file_xsputn这个函数。
IO_new_file_xsputn:
这里先将这个函数的代码放出来(经过别的师傅简化):
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
const char *s = (const char *) data;
_IO_size_t to_do = n;
int must_flush = 0;
_IO_size_t count = 0;
if (n <= 0)
return 0;
/* This is an optimized implementation.
If the amount to be written straddles a block boundary
(or the filebuf is unbuffered), use sys_write directly. */
/* First figure out how much space is available in the buffer. */
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
...
}
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
/* Then fill the buffer. */
if (count > 0)
{
if (count > to_do)
count = to_do;
#ifdef _LIBC
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
#else
memcpy (f->_IO_write_ptr, s, count);
f->_IO_write_ptr += count;
#endif
s += count;
to_do -= count;
}
if (to_do + must_flush > 0)
{
_IO_size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)
/* If nothing else has to be written we must not signal the
caller that everything has been written. */
return to_do == 0 ? EOF : n - to_do;
/* Try to maintain alignment: write a whole number of blocks. */
block_size = f->_IO_buf_end - f->_IO_buf_base;
do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
if (do_write)
{
count = new_do_write (f, s, do_write);
to_do -= count;
if (count < do_write)
return n - to_do;
}
/* Now write out the remainder. Normally, this will fit in the
buffer, but it's somewhat messier for line-buffered files,
so we let _IO_default_xsputn handle the general case. */
if (to_do)
to_do -= _IO_default_xsputn (f, s+do_write, to_do);
}
return n - to_do;
}
结合代码和注释可以大概理清这个程序的流程:首先计算缓冲区还有多少空间可以进行写入(通过一个: f->IO_write_end - f->_IO_write_ptr 的指针减法来计算),然后将需要写入的数据写入到缓冲区中,之后在检查一下需要写入的数据是不是已经完全写入,如果有剩余就说明缓冲区空间不足或者缓冲区还没有建立,这时通过if判断就需要调用 _IO_OVERFLOW 来进行缓冲区的建立和刷新,而这个 _IO_OVERFLOW 跟前面的 IO_sputn一样是个宏定义,它指向了虚表中的 _IO_overflow_t函数,而我们对于IO_FILE的利用本质上其实是溢出的进阶操作,所以是一定会启用这个函数的。
IO_overflow_:
首先还是看一下这个函数的源码(依然是简化过的):
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
...
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL){
...
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base); //进入目标
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return EOF;
*f->_IO_write_ptr++ = ch;
if ((f->_flags & _IO_UNBUFFERED)
|| ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
if (_IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base) == EOF)
return EOF;
return (unsigned char) ch;
}
在代码的后半部分可以发现这样一句代码:
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base); //进入目标
这句代码就是让程序执行IO_do_write这个函数,而这是个打印函数(起始地址是输出缓冲区起始地址,长度为当前输出指针与起始地址的差),而这正好是我们想要的功能,所以我们要让程序顺利进入这个判断并执行这段代码,而在这个代码之前有两个if判断,其内容都是如果判断为真就会让程序设置错误并退出,所以我们要绕过这两个判断。
首先看第一个判断:
if (f->_flags & _IO_NO_WRITES)
这里将flags位于 _IO_NO_WRITES按位与,这个 _IO_NO_WRITES就是判断当前IO_FILE是否有正常的输入,回到前面说的flags的规则看一下,发现这个 _IO_NO_WRITES的宏定义是:
#define _IO_NO_WRITES 8
为了让这个判断为假,所以flags在低字节的对应位置上就把其置为0就可以了,所以此时flags设置为:0xfbad0000就好。
接下来是第二个判断:
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
与第一个判断差不多,这里检查了flags的 _IO_CURRENTLY_PUTTING 是否是真和 f -> _IO_write_base是否为空,由于我们会在 _IO_write_base中写入我们伪造的数据,所以我们的输入是正常的,因此这两个判断都为假。
绕过两个判断后就可以进入这个 _IO_do_write函数了。
_IO_do_write and new_do_write:
这个函数没有什么好说的它主要调用了另一个函数:new_do_write,这个函数的参数与它是一样的,所以我们直接看一下new_do_write的代码:
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
...
_IO_size_t count;
if (fp->_flags & _IO_IS_APPENDING)
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
// 调用函数输出输出缓冲区
count = _IO_SYSWRITE (fp, data, to_do); //最终输出
...
return count;
}
这个函数还要进行两个判断,这两个判断是if else if的结构,这里的我们的目的是要让程序正常执行这一行代码:
count = _IO_SYSWRITE (fp, data, to_do); //最终输出
这段代码调用了 _IO_SYSWRITE也就是执行系统调用write函数,参数还是经典的老三样:结构体指针,地址,长度。
但是在此之前我们要在前面的if else if中选一个进入判断,这里说一下为什么一定要选一个判断进入而不是直接绕过这两个判断:
个人理解:因为这里的对于IO_FILE的代码没有给全,其实在这些代码之后还有对IO_FILE结构体中这些成员的继续检测试用。
这里所说的必须进入一个判断我觉得意思应该是不得不进入其中一个判断,如果我们两个都绕过可能会导致程序在后面对这些变量的使用和判断出问题致使程序崩溃。
如果强行绕开第一个判断进入第二个判断,由于第二个判断里面涉及到对指针和成员变量数值的操作过多,使程序崩溃的概率就更大,所以这里是两害相权取其轻选择了进入第一个判断。
所以我们选择进入第一个if判断,这个判断相对来说造成的影响就比较小了,内部仅仅将偏移设置为标准值,不会影响后续的输出流程。并且if判断的条件也很容易满足,我们只需要将fp->_flags & _IO_IS_APPENDING = 1
就可以了
综上,我们的flags就应该设计为下面这种样子:
f->_flags & _IO_NO_WRITES = 0
f->_flags & _IO_CURRENTLY_PUTTING = 1
fp->_flags & _IO_IS_APPENDING = 1
根据flags的规则:
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_NO_WRITES 8
我们把这些宏定义与固定的魔数:0xfbad0000进行按位或就可以得到我们最终的flags设计:0xfbad1800
然后只要将 _IO_write_base指向需要泄露的地址, _IO_write_ptr指向泄露结束的地址就可以了。