上篇文章描述了vtable check以及绕过vtalbe check的方法之一,利用vtable段中的_IO_str_jumps来进行FSOP。本篇则主要描述使用缓冲区指针来进行任意内存读写。 从前面fread以及fwrite的分析中,我们知道了FILE结构体中的缓冲区指针是用来进行输入输出的,很容易的就想到了如果能过伪造这些缓冲区指针,在一定的条件下应该可以完成任意地址的读写。 本文包括两部分: ·使用stdin标准输入缓冲区进行任意地址写。·使用stdout标准输出缓冲区进行任意地址读写。 接下来描述这两部分的原理以及给出相应的题目实践,原理介绍部分是基于已经拥有可以伪造IO FILE结构体的缓冲区指针漏洞的基础上进行的。在后续过程假设我们目标写的地址是write_start,写结束地址为write_end;读的目标地址为read_start,读的结束地址为read_end。 前几篇传送门: ·IO FILE之fopen详解[1]·IO FILE之fread详解[2]·IO FILE之fwrite详解[3]·IO FILE之fclose详解[4]·IO FILE之劫持vtable及FSOP[5]·IO FILE 之vtable劫持以及绕过[6] stdin标准输入缓冲区进行任意地址写这一部分主要阐述的是使用stdin标准输入缓冲区指针进行任意地址写的功能。 原理分析先通过fread回顾下通过输入缓冲区进行输入的流程: 1.判断fp->_IO_buf_base输入缓冲区是否为空,如果为空则调用的_IO_doallocbuf去初始化输入缓冲区。2.在分配完输入缓冲区或输入缓冲区不为空的情况下,判断输入缓冲区是否存在数据。3.如果输入缓冲区有数据则直接拷贝至用户缓冲区,如果没有或不够则调用__underflow函数执行系统调用读取数据到输入缓冲区,再拷贝到用户缓冲区。
IO FILE 之任意读写
假设我们能过控制输入缓冲区指针,使得输入缓冲区指向想要写的地址,那么在第三步调用系统调用读取数据到输入缓冲区的时候,也就会调用系统调用读取数据到我们想要写的地址,从而实现任意地址写的目的。 根据fread的源码,我们再看下要想实现往write_start写长度为write_end - write_start的数据具体经历了些什么。 _IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
...
if (fp->_IO_buf_base == NULL)
{
...
//输入缓冲区为空则初始化输入缓冲区
}
while (want > 0)
{
have = fp->_IO_read_end - fp->_IO_read_ptr;
if (have > 0)
{
...
//memcpy
}
if (fp->_IO_buf_base
&& want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
{
if (__underflow (fp) == EOF) ## 调用__underflow读入数据
...
}
...
return n - want;
}
上面贴出了一些关键代码,首先是_IO_file_xsgetn函数,函数先判断输入缓冲区_IO_buf_base是否为空,如果为空的话则调用_IO_doallocbuf初始化缓冲区,因此需构造_IO_buf_base不为空。 接着函数中当输入缓冲区有剩余时即_IO_read_end -_IO_read_ptr >0,会将缓冲区中的数据拷贝至目标中,因此想要利用输入缓冲区实现读写,最好使_IO_read_end -_IO_read_ptr =0即_IO_read_end ==_IO_read_ptr。 同时还要求读入的数据size要小于缓冲区数据的大小,否则为提高效率会调用read直接读。 _IO_file_xsgetn函数中当缓冲区不能满足需求时会调用__underflow去读取数据,查看__underflow。 int
_IO_new_file_underflow (_IO_FILE *fp)
{
_IO_ssize_t count;
...
## 如果存在_IO_NO_READS标志,则直接返回
if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
## 如果输入缓冲区里存在数据,则直接返回
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
...
##调用_IO_SYSREAD函数最终执行系统调用读取数据
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
...
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)
在_IO_new_file_underflow函数中先判断fp->_IO_read_ptr < fp->_IO_read_end是否成立,成立则直接返回,因此再次要求伪造的结构体_IO_read_end ==_IO_read_ptr,绕过该条件检查。 接着函数会检查_flags是否包含_IO_NO_READS标志,包含则直接返回。标志的定义是#define _IO_NO_READS 4,因此_flags不能包含4。 最终系统调用_IO_SYSREAD (fp, fp->_IO_buf_base,fp->_IO_buf_end - fp->_IO_buf_base)读取数据,因此要想利用stdin输入缓冲区需设置FILE结构体中_IO_buf_base为write_start,_IO_buf_end为write_end。同时也需将结构体中的fp->_fileno设置为0,最终调用read (fp->_fileno, buf, size))读取数据。 将上述条件综合表述为: 1.设置_IO_read_end等于_IO_read_ptr。2.设置_flag &~ _IO_NO_READS即_flag &~ 0x4。3.设置_fileno为0。4.设置_IO_buf_base为write_start,_IO_buf_end为write_end;且使得_IO_buf_end-_IO_buf_base大于fread要读的数据。 实践实践的题目是whctf2017的stackoverflow,这一年也是这一种利用方式的兴起之年,这一题是很经典的一题。 题目首先是输入name,并把name输出出来,由于name未进行初始化设置且读取数据后未加入\x00,可以由此泄露出libc地址。 接着进入主功能函数,漏洞在先使用temp变量保存了输入的size,但是后续最后写\x00的时候使用的是temp,而不是size,因此存在一个溢出写\x00的漏洞。 在之前的文章中,我们知道了当申请堆块大小很大时(0x200000),申请出来的堆块会紧挨着libc,因此我们可以利用这个溢出写\x00的漏洞往libc的内存中写入一个\x00字节。 往哪里写一个\x00字节,后续改变整个内存结构而拿到shell?答案时stdin结构体中的\x00,我们先看下输入之前的stdin结构体中的数据:
IO FILE 之任意读写
IO FILE 之任意读写
可以看到在glibc 2.24中,stdin结构体中存储_IO_buf_end指针内存地址的末尾刚好为\x00,若利用漏洞我们将_IO_buf_base末尾写\x00,则会使得_IO_buf_base指向stdin结构体中存储_IO_buf_end指针内存地址,即可利用输入缓冲区覆盖_IO_buf_end。 我们可将_IO_buf_end覆盖为__malloc_hook+0x8,则输入时最后控制写的数据为stdin中的_IO_buf_end指针位置到__malloc_hook+0x8,以实现控制__malloc_hook。 原理就是如此,需要多提两点。 一是IO_getc函数的作用是刷新_IO_read_ptr,每次会从输入缓冲区读一个字节数据即将_IO_read_ptr加一,当_IO_read_ptr等于_IO_read_end的时候便会调用read读数据到_IO_buf_base地址中。 二是往malloc_hook写什么,由于one gadget用不了,因此在栈中找到了一个gadget,地址为0x400a23,可以读取数据形成栈溢出,从而进行ROP,拿到shell。 .text:0000000000400A23 lea rax, [rbp+name]
.text:0000000000400A27 mov esi, 50h ; count
.text:0000000000400A2C mov rdi, rax ; input
.text:0000000000400A2F call input_data
stdout标准输入缓冲区进行任意地址读写上半部分使用了stdin进行任意地址写,这部分主要阐述stdout来进行任意地址读写。stdin只能输入数据到缓冲区,因此只能进行写。而stdout会将数据拷贝至输出缓冲区,并将输出缓冲区中的数据输出出来,所以如果可控stdout结构体,通过构造可实现利用其进行任意地址读以及任意地址写。 任意写任意写的主要原理为:构造好输出缓冲区将其改为想要任意写的地址,当输出数据可控时,会将数据拷贝至输出缓冲区,即实现了将可控数据拷贝至我们想要写的地址。 想要实现上述功能,查看fwrite源码中如何才能实现该功能: _IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
...
## 判断输出缓冲区还有多少空间
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
## 如果输出缓冲区有空间,则先把数据拷贝至输出缓冲区
if (count > 0)
{
...
memcpy (f->_IO_write_ptr, s, count);
任意写功能的实现在于IO缓冲区没有满时,会先将要输出的数据复制到缓冲区中,可通过这一点来实现任意地址写的功能。可以看到任意写好像很简单,只需将_IO_write_ptr指向write_start,_IO_write_end指向write_end即可。 任意读利用stdout进行任意地址读的原理为:控制输出缓冲区指针指向我们输入的地址,构造好条件,使得输出缓冲区为已经满的状态,再次调用输出函数时,程序会刷新输出缓冲区即会输出我们想要的数据,实现任意读。 仍然是查看fwrite源码中如何才能实现该功能: _IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
_IO_size_t count = 0;
...
## 判断输出缓冲区还有多少空间
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
## 如果输出缓冲区有空间,则先把数据拷贝至输出缓冲区
if (count > 0)
{
...
//memcpy
}
if (to_do + must_flush > 0)
{
if (_IO_OVERFLOW (f, EOF) == EOF)
当f->_IO_write_end > f->_IO_write_ptr时,会调用memcpy拷贝数据,因此最好构造条件f->_IO_write_end等于f->_IO_write_ptr。 接着进入_IO_OVERFLOW函数,去刷新输出缓冲区,跟进去: int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
## 判断标志位是否包含_IO_NO_WRITES
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
## 判断输出缓冲区是否为空
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);
return (unsigned char) ch;
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)
可以看到_IO_new_file_overflow,首先判断_flags是否包含_IO_NO_WRITES,如果包含则直接返回,因此需构造_flags不包含_IO_NO_WRITES,其定义为#define _IO_NO_WRITES 8; 接着判断缓冲区是否为空以及是否包含_IO_CURRENTLY_PUTTING标志位,如果包含的话则做一些多余的操作,可能不可控,因此最好定义_flags不包含_IO_CURRENTLY_PUTTING,其定义为#define _IO_CURRENTLY_PUTTING 0x800。 接着调用_IO_do_write去输出输出缓冲区,其传入的参数是f->_IO_write_base,大小为f->_IO_write_ptr - f->_IO_write_base。因此若想实现任意地址读,应构造_IO_write_base为read_start,构造_IO_write_ptr为read_end。 跟进去_IO_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;
}
看到在调用_IO_SYSWRITE之前还判断了fp->_IO_read_end != fp->_IO_write_base,因此需要构造结构体使得_IO_read_end等于_IO_write_base。 也可以构造_flags包含_IO_IS_APPENDING,_IO_IS_APPENDING的定义为#define _IO_IS_APPENDING 0x1000,这样就不会走后面的这个判断而直接执行到_IO_SYSWRITE了,一般我都是设置_IO_read_end等于_IO_write_base。 最后_IO_SYSWRITE调用write (f->_fileno, data, to_do)输出数据,因此还需构造_fileno为标准输出描述符1。 将上述条件综合描述为: 1.设置_flag &~ _IO_NO_WRITES即_flag &~ 0x8。2.设置_flag & _IO_CURRENTLY_PUTTING即_flag | 0x8003.设置_fileno为1。4.设置_IO_write_base指向想要泄露的地方;_IO_write_ptr指向泄露结束的地址。5.设置_IO_read_end等于_IO_write_base或设置_flag & _IO_IS_APPENDING即_flag | 0x1000。6.设置_IO_write_end等于_IO_write_ptr(非必须)。满足上述五个条件,可实现任意读。 实践使用stdout进行任意读写比较经典的一题应该是hctf2018的babyprintf_ver2了,下面来进行利用描述。 题目直接给出了程序基址。 然后存在明显的溢出,可以覆盖stdout,但是无法覆盖stdout的vtable,因为它会修正。 具体该如何利用呢,首先使用stdout任意读来泄露libc地址。构造的FILE结构体如下(使用pwn_debug[7]的IO_FILE_plus模块): io_stdout_struct=IO_FILE_plus()
flag=0
flag&=~8
flag|=0x800
flag|=0x8000
io_stdout_struct._flags=flag
io_stdout_struct._IO_write_base=pro_base+elf.got['read']
io_stdout_struct._IO_read_end=io_stdout_struct._IO_write_base
io_stdout_struct._IO_write_ptr=pro_base+elf.got['read']+8
io_stdout_struct._fileno=1
以此来泄露read的地址。 接着使用stdout的任意地址写来写__malloc_hook,构造的FILE结构体如下: io_stdout_struct=IO_FILE_plus()
flag=0
flag&=~8
flag|=0x8000
io_stdout_write=IO_FILE_plus()
io_stdout_write._flags=flag
io_stdout_write._IO_write_ptr=malloc_hook
io_stdout_write._IO_write_end=malloc_hook+8
最终将one gaget 写入malloc_hook。如何触发malloc呢,可以使用输出较大的字符打印来触发malloc函数或是%n来触发,其中%n可触发malloc的原因是在于__readonly_area会通过fopen打开maps文件来读取内容来判断地址段是否可写,而fopen会调用malloc函数申请空间,因此触发。 可能会有人对于觉得flag|=0x8000这行构造代码觉得比较奇怪,需要解释下,在printf函数中会调用_IO_acquire_lock_clear_flags2 (stdout)来获取lock从而继续程序,如果没有_IO_USER_LOCK标志的话,程序会一直在循环,而_IO_USER_LOCK定义为#define _IO_USER_LOCK 0x8000,因此需要设置flag|=0x8000才能够使exp顺利进行。_IO_acquire_lock_clear_flags2 (stdout)的汇编代码如下: 0x7f0bcf15d850 <__printf_chk+96> mov rbp, qword ptr [rip + 0x2a16f9]
0x7f0bcf15d857 <__printf_chk+103> mov rbx, qword ptr [rbp]
0x7f0bcf15d85b <__printf_chk+107> mov eax, dword ptr [rbx]
0x7f0bcf15d85d <__printf_chk+109> and eax, 0x8000
0x7f0bcf15d862 <__printf_chk+114> jne __printf_chk+202 <0x7f0bcf15d8ba>
小结使用IO FILE来进行任意内存读写真的是个很强大的功能,构造起来也比较容易。但是对于FILE结构体的伪造,个人感觉可能最容易出问题的地方还是_flags字段的构造,可能某个地方不注意就导致程序走偏了,因此感觉可能还是把默认的stdout和stdin直接拷贝出来用会比较好一些,同时pwn_debug的IO_FILE_plus模块提供了apiarbitrary_write_check以及arbitrary_read_check来进行相应检测,看相应字段是否设置正确。 至此IO FILE系列描述完毕,前四篇对IO函数fopen、fread、fwrite以及fclose的源码分析;后面三篇介绍了针对IO FILE的相关利用,包括劫持vtable、vtable引入的check机制以及相应的后续利用方式。在整个过程中为方便构造IO 结构体还在pwn_debug中加入了IO_FILE_plus模块。 最后一句,阅读源码对于学习是一件很有帮助的事情。 相关文件及脚本链接[8] 参考链接1.HCTF 2018 部分 PWN writeup--babyprinf_ver2[9]2.浅析IO_FILE结构及利用[10]3.教练!那根本不是IO!——从printf源码看libc的IO[11] 文章首发于先知-IO FILE 之任意读写[12] References[1] IO FILE之fopen详解: https://ray-cp.github.io/archivers/IO_FILE_fopen_analysis[2] IO FILE之fread详解: https://ray-cp.github.io/archivers/IO_FILE_fread_analysis[3] IO FILE之fwrite详解: https://www.tttang.com/archive/1279/[4] IO FILE之fclose详解: https://ray-cp.github.io/archivers/IO_FILE_fclose_analysis[5] IO FILE之劫持vtable及FSOP: https://ray-cp.github.io/archivers/IO_FILE_vtable_hajack_and_fsop[6] IO FILE 之vtable劫持以及绕过: https://ray-cp.github.io/archivers/IO_FILE_vtable_check_and_bypass[7] pwn_debug: https://github.com/ray-cp/pwn_debug[8] 链接: https://github.com/ray-cp/pwn_category/tree/master/IO_FILE/arbitrary_read_write[9] HCTF 2018 部分 PWN writeup--babyprinf_ver2: https://ray-cp.github.io/archivers/HCTF-2018-PWN-writeup#babyprintf_ver2[10] 浅析IO_FILE结构及利用: https://xz.aliyun.com/t/3344#toc-1[11] 教练!那根本不是IO!——从printf源码看libc的IO: https://www.anquanke.com/post/id/86945[12] 先知-IO FILE 之任意读写: https://xz.aliyun.com/t/5853
|