初识IO,puts源码分析,利用IO泄露数据。
初识IO
尽可能地写详细一点,一些地方借鉴了hollk师傅的博客https://blog.csdn.net/qq_41202237/article/details/113845320。也是头一次学习IO有什么地方错误的,还请各位大佬能够帮忙提点一下。
知识点补充:
在我们程序中用一些FILE的结构,用于描述我们打开的文件,我们称之为文件流。程序执行中,有已经自动打开的三个结构,stdin、stdout 和 stderr 这三个全局文件流指针都是定义在 C 标准库(libc)中,并且存储在libc的数据段中的。它们是 FILE 类型的全局变量,分别指向标准输入流、标准输出流和标准错误流。而我们使用fopen函数打开的文件结构会呗存储在堆区中。下面是我们FILE结构的定义。
FILE结构体
他定义在我们的libio.h中,具体如下:
struct _IO_FILE {
int _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; //指向当前读取位置的指针
char* _IO_read_end; //指向读取缓冲区结束位置的指针
char* _IO_read_base; //指向读取缓冲区开始位置的指针,通常三个一起使用来进行数据的读取操作,其中base和end分别标记了起始和终点的位置,ptr则进行数据的遍历。
char* _IO_write_base; //指向当前写入位置的指针。
char* _IO_write_ptr; //指向写入缓冲区结束位置的指针。
char* _IO_write_end; //指向写入缓冲区结束位置的指针。同上三个一起完成数据的写入操作。
char* _IO_buf_base; //指向整个缓冲区(包括读取和写入缓冲区)开始位置的指针。
char* _IO_buf_end; //指向整个缓冲区结束位置的指针。
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; //指向旧读取区域的开始位置的指针,用于标记/回退功能。
char *_IO_backup_base; //指向备份区域的第一个有效字符的指针。
char *_IO_save_end; //指向非当前读取区域的结束位置的指针。
struct _IO_marker *_markers; //指向文件流的标记链表的头指针,用于流中的标记和定位。
struct _IO_FILE *_chain; //指向下一个 _IO_FILE 结构体的指针,用于维护一个文件流链表。
int _fileno; //存储与此文件流相关联的文件描述符。
#if 0
int _blksize;
#else
int _flags2; //存储与此文件流相关联的文件描述符。
#endif
_IO_off_t _old_offset; //存储旧的文件偏移量,用于定位操作
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column; //存储旧的文件偏移量,用于定位操作
signed char _vtable_offset; //存储虚拟函数表(vtable)的偏移量。
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;
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
};
_ flags的所能取的值:
_IO_MAGIC:0xFBAD0000
- 这是一个魔术数字,用来标识
_IO_FILE结构体的有效性。_IO_USER_BUF:1
- 指示用户自己提供了缓冲区。
_IO_UNBUFFERED:2
- 指示文件流是无缓冲的。
_IO_NO_READS:4
- 指示文件流不支持读取操作。
_IO_NO_WRITES:8
- 指示文件流不支持写入操作。
_IO_EOF_SEEN:0X10
- 指示在文件流中已经遇到了文件末尾(EOF)。
_IO_ERR_SEEN:0X20
- 指示在文件流中已经遇到了错误。
_IO_DELETE_DONT_CLOSE:0X40
- 指示在关闭文件流时不要关闭底层的文件描述符。
_IO_LINKED:0X80
- 指示文件流是链表中的一个节点。
_IO_IN_BACKUP:0X100
- 指示当前正在处理备份区域。
_IO_LINE_BUF:0X200
- 指示文件流使用行缓冲。
_IO_CURRENTLY_PUTTING:0X800
从结构可知,有一个偏移与虚拟函数表有关,这个东西存储了很多的IO函数地址。如下所示:
pwndbg> print _IO_file_jumps
$3 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7e6fb20799d0 <_IO_new_file_finish>, //用于完成文件流的操作,通常是关闭文件流并释放资源。
__overflow = 0x7e6fb207a740 <_IO_new_file_overflow>, //指向 _IO_new_file_overflow 函数,用于处理写入操作时缓冲区溢出的情况。
__underflow = 0x7e6fb207a4b0 <_IO_new_file_underflow>, //用于处理读取操作时缓冲区不足的情况。
__uflow = 0x7e6fb207b610 <__GI__IO_default_uflow>, //是默认的下溢(underflow)处理函数,通常用于从文件中填充缓冲区。
__pbackfail = 0x7e6fb207c990 <__GI__IO_default_pbackfail>, //用于处理将字符退回(pushback)到流中失败的情况。
__xsputn = 0x7e6fb20791f0 <_IO_new_file_xsputn>, //用于将数据写入流中,同时更新流的位置。
__xsgetn = 0x7e6fb2078ed0 <__GI__IO_file_xsgetn>, //用于从流中读取数据,同时更新流的位置。
__seekoff = 0x7e6fb20784d0 <_IO_new_file_seekoff>, //用于改变流的文件偏移量。
__seekpos = 0x7e6fb207ba10 <_IO_default_seekpos>, //用于改变流的文件偏移量到特定的位置。
__setbuf = 0x7e6fb2078440 <_IO_new_file_setbuf>, //用于为流设置一个新的缓冲区。
__sync = 0x7e6fb2078380 <_IO_new_file_sync>, //用于同步流的状态,通常是将缓冲区的内容写入文件。
__doallocate = 0x7e6fb206d190 <__GI__IO_file_doallocate>, //用于分配流的内部缓冲区。
__read = 0x7e6fb20791b0 <__GI__IO_file_read>, //用于从文件中读取数据。
__write = 0x7e6fb2078b80 <_IO_new_file_write>, //用于将数据写入文件。
__seek = 0x7e6fb2078980 <__GI__IO_file_seek>, //用于移动文件流的读/写位置。
__close = 0x7e6fb2078350 <__GI__IO_file_close>, //用于关闭文件流。
__stat = 0x7e6fb2078b70 <__GI__IO_file_stat>, //用于获取文件的状态信息。
__showmanyc = 0x7e6fb207cb00 <_IO_default_showmanyc>, //用于显示流中可以无阻塞读取的字符数量。
__imbue = 0x7e6fb207cb10 <_IO_default_imbue> //用于设置流的区域设置(locale)。
}
进程中的 FILE 结构会通过_chain 域彼此连接形成一个链表,链表头部用全局变量_IO_list_all 表示,通过这个值我们可以遍历所有的 FILE 结构。如下所示:
pwndbg> x/60gx 0x7ffff7800000+0x3c5520
0x7ffff7bc5520 <_IO_list_all>: 0x00007ffff7bc5540 0x0000000000000000 //表示当前链表头部的FILE结构地址为0x00007ffff7bc5540(_IO_2_1_stderr_)
0x7ffff7bc5530: 0x0000000000000000 0x0000000000000000
0x7ffff7bc5540 <_IO_2_1_stderr_>: 0x00000000fbad2887 0x00007ffff7bc55c3 //fbad2887为 _flags的值
0x7ffff7bc5550 <_IO_2_1_stderr_+16>: 0x00007ffff7bc55c3 0x00007ffff7bc55c3
0x7ffff7bc5560 <_IO_2_1_stderr_+32>: 0x00007ffff7bc55c3 0x00007ffff7bc55c3
0x7ffff7bc5570 <_IO_2_1_stderr_+48>: 0x00007ffff7bc55c3 0x00007ffff7bc55c3
0x7ffff7bc5580 <_IO_2_1_stderr_+64>: 0x00007ffff7bc55c4 0x0000000000000000 //这一块都是缓冲区的相关地址
0x7ffff7bc5590 <_IO_2_1_stderr_+80>: 0x0000000000000000 0x0000000000000000
0x7ffff7bc55a0 <_IO_2_1_stderr_+96>: 0x0000000000000000 0x00007ffff7bc5620 //_chain的值,表示下一个FILE结构为_IO_2_1_stdout_
0x7ffff7bc55b0 <_IO_2_1_stderr_+112>: 0x0000000000000002 0xffffffffffffffff
0x7ffff7bc55c0 <_IO_2_1_stderr_+128>: 0x0000000000000000 0x00007ffff7bc6770
0x7ffff7bc55d0 <_IO_2_1_stderr_+144>: 0xffffffffffffffff 0x0000000000000000
0x7ffff7bc55e0 <_IO_2_1_stderr_+160>: 0x00007ffff7bc4660 0x0000000000000000
0x7ffff7bc55f0 <_IO_2_1_stderr_+176>: 0x0000000000000000 0x0000000000000000
0x7ffff7bc5600 <_IO_2_1_stderr_+192>: 0x00000000ffffffff 0x0000000000000000
0x7ffff7bc5610 <_IO_2_1_stderr_+208>: 0x0000000000000000 0x00007ffff7bc36e0 //虚拟函数表地址
-----------------------------------------------------------------------------
0x7ffff7bc5620 <_IO_2_1_stdout_>: 0x00000000fbad2084 0x0000000000000000
0x7ffff7bc5630 <_IO_2_1_stdout_+16>: 0x0000000000000000 0x0000000000000000
0x7ffff7bc5640 <_IO_2_1_stdout_+32>: 0x0000000000000000 0x0000000000000000
0x7ffff7bc5650 <_IO_2_1_stdout_+48>: 0x0000000000000000 0x0000000000000000
0x7ffff7bc5660 <_IO_2_1_stdout_+64>: 0x0000000000000000 0x0000000000000000
我们可以看到,为什么FILE结构体并没有虚拟函数表的地址参数,这里还有存储虚拟函数表的地址,其实_IO_FILE 结构外包裹着另一种结构_IO_FILE_plus,其中包含了一个重要的指针 vtable 指向了一系列函数指针。
在 libc2.23 版本下,32 位的 vtable 偏移为 0x94,64 位偏移为 0xd8
struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}
puts函数源码分析
源码在线查看网站:https://elixir.bootlin.com/glibc/glibc-2.31/source
puts源码位置:glibc/libio/ioputs.c
#include "libioP.h"
#include <string.h>
int
_IO_fputs (const char *str, _IO_FILE *fp)
{
_IO_size_t len = strlen (str); //计算长度
int result = EOF; //初始化result变量为EOF,EOF是一个常量,表示文件结束或错误。
CHECK_FILE (fp, EOF); //检查文件指针fp是否有效。如果fp无效,则函数返回EOF。
_IO_acquire_lock (fp); //获取文件流fp的锁,以确保线程安全。
if ((_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1) // 检查文件流fp是否具有有效的虚拟函数表偏移量,或者文件流的宽度不是宽字符流(-1表示不关心宽度)。
&& _IO_sputn (fp, str, len) == len) //如果上述条件为真,并且_IO_sputn函数成功将str中的len个字符写入文件流fp,则继续执行。
result = 1; //如果字符串成功写入,则将result设置为1,表示成功
_IO_release_lock (fp); //释放文件流fp的锁。
return result;
}
libc_hidden_def (_IO_fputs)
#ifdef weak_alias
weak_alias (_IO_fputs, fputs) //为_IO_fputs创建一个弱别名fputs,这样fputs函数调用实际上会调用_IO_fputs。
# ifndef _IO_MTSAFE_IO
strong_alias (_IO_fputs, __fputs_unlocked)
libc_hidden_def (__fputs_unlocked)
weak_alias (_IO_fputs, fputs_unlocked)
libc_hidden_ver (_IO_fputs, fputs_unlocked)
# endif
#endif
经过分析,_IO_puts在过程当中调用了一个叫做_IO_sputn函数(_IO_fwrite也会调用这个),_IO_sputn其实是一个宏,它的作用就是调用_IO_2_1_stdout_中的vtable所指向的_xsputn,也就是_IO_new_file_xsputn函数。这个函数是fwrite和fputs等函数的底层实现,它处理将数据写入文件流的缓冲区,并在必要时将缓冲区的内容刷新到文件中。
接下来,我们再去看看_IO_new_file_xsputn函数的源码,位置为glibc/libio/fileops.c
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
const char *s = (const char *) data; //将传入的data指针转换为const char *类型,并赋值给s变量。
_IO_size_t to_do = n; //初始化to_do变量为n,表示需要写入的字节数。
int must_flush = 0; //初始化must_flush变量为0,表示是否需要刷新缓冲区。
_IO_size_t count = 0; //初始化count变量为0,表示实际写入缓冲区的字节数。
if (n <= 0) //如果需要写入的字节数n小于等于0,则直接返回0。
return 0;
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING)) //检查文件流f是否是行缓冲模式,并且当前正在写入。
{
count = f->_IO_buf_end - f->_IO_write_ptr; //计算缓冲区中剩余的空间。
if (count >= n) //如果缓冲区中剩余的空间足以容纳所有数据,则不需要刷新缓冲区。
{
const char *p;
for (p = s + n; p > s; ) //从字符串的末尾开始向前搜索换行符。
{
if (*--p == '\n') //如果找到换行符,则计算需要写入缓冲区的字节数,并设置must_flush为1。
{
count = p - s + 1;
must_flush = 1;
break;
}
}
}
}
else if (f->_IO_write_end > f->_IO_write_ptr) //如果文件流不是行缓冲模式,则计算缓冲区中剩余的空间。
count = f->_IO_write_end - f->_IO_write_ptr;
/* Then fill the buffer. */
if (count > 0) //如果缓冲区中有空间,则将数据写入缓冲区。
{
if (count > to_do)
count = to_do;
#ifdef _LIBC //检查是否在glibc环境中编译,以决定使用__mempcpy还是memcpy函数。
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;
if (_IO_OVERFLOW (f, EOF) == EOF) //尝试刷新缓冲区,如果失败则返回错误。
return to_do == 0 ? EOF : n - to_do;
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;
}
if (to_do) //如果还有剩余数据需要写入,则调用_IO_default_xsputn函数处理。
to_do -= _IO_default_xsputn (f, s+do_write, to_do);
}
return n - to_do;
}
经过上述最后一步的判断,如果还有剩余则说明输出缓冲区未建立或者空间已满,那么就需要通过_IO_OVERFLOW函数来建立或清空缓冲区,这个函数主要是实现刷新缓冲区或建立缓冲区的功能。在vtable中为__overflow
_IO_new_file_overflow函数的源码如下:位置也在在glibc/libio/fileops.c
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) //检查文件流f的标志,如果设置了_IO_NO_WRITES(表示文件流不允许写入),则设置错误标志,设置errno为EBADF,并返回EOF。
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL) //检查文件流是否当前不在写入模式或者没有分配缓冲区。
{
if (f->_IO_write_base == NULL) //如果缓冲区没有分配,则调用_IO_doallocbuf分配缓冲区,并初始化缓冲区指针。
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
if (__glibc_unlikely (_IO_in_backup (f))) //如果文件流处于备份模式(即之前有未完成的读取操作),则释放备份区域,并调整读取指针。
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_base;
}
if (f->_IO_read_ptr == f->_IO_buf_end) //如果读取指针已经到达缓冲区末尾,将读取指针重置到缓冲区开始处。
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
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->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr; //如果文件流是行缓冲或无缓冲模式,调整写入结束指针。
}
if (ch == EOF) //如果传入的字符是EOF,调用_IO_do_write将缓冲区内容写入文件,并返回。
return _IO_do_write (f, f->_IO_write_base, //如果缓冲区已满,调用_IO_do_flush刷新缓冲区。
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; //将字符ch写入缓冲区,并移动写入指针。
if ((f->_flags & _IO_UNBUFFERED) //如果文件流是无缓冲模式,或者行缓冲模式且ch是换行符,调用_IO_do_write将缓冲区内容写入文件。
|| ((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;
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)
简单的说,就是处理缓冲区不够用,有数据还未写入,调用这个函数来根据模式来处理剩下的数据。
在我们在尽显IO泄露时,我们想要利用的就是最后的_IO_do_write (f, f->_IO_write_base,f->_IO_write_ptr - f->_IO_write_base)。这个函数执行后会调用系统调用write输出输出缓冲区,也就是无缓冲模式下,利用它会将当前缓冲区里的数据直接输出的操作。当我们将此时的缓冲区进行修改,就可以来进行泄露数据。比如指向含有libc相关数据的区域。
大概流程:

接下来我们使用一个简单c程序来串几遍puts的执行流程。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int winner ( char *ptr);
int main()
{
char setence[50]="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
puts(setence);
return 0;
}
第一遍,不做任何修改
- 进入puts函数,先判断了长度

-
在这里,获取了_ IO_2_1_stdout_结构体,并对其的_ flag变量进行了判断,来确定是否有效

-
此时的stdout结构如下所述,缓冲区都为0,还没进行初始化。

- 在进行了一些上锁操作,和判断虚函数表是否有效后,通过偏移从虚函数表中调用
_IO_new_file_xsputn函数



- 通过计算当前缓冲区的大小,发现空间不够,因此去调用overflow函数

- 进入overflow函数后,先判断出来当前缓冲区未被分配,因此去调用
_IO_doallocbuf函数申请。我们可以看到成功申请到一个大小为0x400的堆空间


- 继续往下走,开始将将读缓冲区,写缓冲区进行相关的设置。

- 通过判断发现是行缓冲火无缓冲,直接将结束写缓冲区指针进行调整。

- 然后跳转到
_IO_do_write函数

- 经过计算,返回空到
_IO_file_xsputn函数,然后计算出了当前缓冲区的大小。

- 调用
_IO_default_xsputn函数将我们需要输出的数据保存到缓冲区中。


- 接下来调用overflow函数

- 经过一系列判断后,发现是行缓冲模式,将设置好写的缓冲区当前指针,长度算好,调用
_IO_do_write函数将数据写到标准输出上。


利用IO进行泄露
首先我们这个操作的最终目标是执行_IO_new_file_overflow函数中的_IO_do_write (f, f->_IO_write_base,f->_IO_write_ptr - f->_IO_write_base)部分。如果我们能够修改_IO_write_ptr 和__IO_write_base的指向,那就可以达到任意地址读的效果。但是我们还需要分析代码和函数的执行流程,对其中的监测点进行绕过。
执行流程如下:
_IO_puts --> _IO_new_file_xsputn--> _IO_OVERFLOW--> _IO_do_write--> _IO_new_do_write--> _IO_SYSWRITE
相关源码:
进入,判断调用IO_sputn函数
_IO_fputs (const char *str, _IO_FILE *fp)
{
......
if ((_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)&& _IO_sputn (fp, str, len) == len)
......
}
这里继续调用overflow函数
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
......
if (_IO_OVERFLOW (f, EOF) == EOF)
......
}
下面就需要绕过一些判断。
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES)
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL) //检查文件流是否当前不在写入模式或者没有分配
{
一堆设置缓冲区指针的操作
}
.......
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;
}
先我们来看第一个判断条件,这里判断_flags的标志位是否包含_IO_NO_WRITES,将_flags和_IO_NO_WRITES进行一个按位与的操作,我们可以向前翻一下flag规则的章节,_flag与_IO_NO_WRITES各自定义的常量为:
#define _IO_MAGIC 0xFBAD0000 /* 魔数 */
#define _IO_NO_WRITES 8 /* 不可写 */
可以看到_flag魔数的常量为0xfbad0000,_IO_NO_WRITES不可写标志位的常量为8,我们返回上图的程序中,如果进行按位与操作之后的结果为真,则返回为错误。一旦返回的是错误,那么我们就无法继续利用后面的_IO_do_write函数,所以我们要将此处的与运算为假:
#define _IO_MAGIC 0xFBAD0000
#define _IO_NO_WRITES 8
_flags & _IO_NO_WRITES = 0
_flags = 0xfbad0000
第二个判断是为了检查输出缓冲区是否为空,如果为空则进行分配空间,并且会初始化指针。一旦进行初始化操作,那么就会覆盖掉我们事先在stdout的_IO_write_base的数据,要知道,我们已经提前修改好了俩个指针指向我们需要泄露的区域,如果执行这部分函数,会导致我们修改的指针再次被改。所以这个判断条件也不进入,那么我们将if判断条件的值为假即可.
#define _IO_MAGIC 0xFBAD0000
#define _IO_CURRENTLY_PUTTING 0x800
f->_flags & _IO_CURRENTLY_PUTTING = 1
_flags = 0xfbad0800
我们到了_IO_new_do_write函数,这个函数也就是个封装。继续执行
_IO_new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
return (to_do == 0
|| (_IO_size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}
经过分析,我们要到达count = _IO_SYSWRITE (fp, data, to_do);处,需要进行选择一段代码执行。我们这里选择执行第一段,我们只需要将fp->_flags & _IO_IS_APPENDING = 1即可,只对_flag修改不会影响其他部分:
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); //最终目的
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}
#define _IO_MAGIC 0xFBAD0000
#define _IO_IS_APPENDING 0x1000
fp->_flags & _IO_IS_APPENDING = 1
_flags = 0xfbad1000
汇总:
define _IO_MAGIC 0xFBAD0000
define _IO_NO_WRITES 8
define _IO_CURRENTLY_PUTTING 0x800
define _IO_IS_APPENDING 0x10001、设置
_flags & _IO_NO_WRITES = 0
2、设置_flags & _IO_CURRENTLY_PUTTING = 1
3、设置_flags & _IO_IS_APPENDING = 1最终得到_flags = 0xFBAD1800 设置_IO_write_base指向想要泄露的位置,_IO_write_ptr指向泄露结束的地址
我们继续用写的简单程序实操一下。
- 将程序先执行到这里。看一看当前打stdout结构,_ flag为 0xfbad2084 ,缓冲区还没指定


- 我们直接修改他们当前的值,将写缓冲区指向有libc相关地址数据的区域,比如这里的0x7ffff7bc5680-0x7ffff7bc5690


- 让程序继续执行,可以看到,打印出了libc相关数据


浙公网安备 33010602011771号