IO_FILE利用之利用_IO_2_1_stdout泄露libc
IO_FILE利用之利用_IO_2_1_stdout泄露libc
FILE结构
FILE在linux系统的标准IO库使用来描述文件结构,称之为文件流。这里提及的”流“其实是一种抽象的概念,无论是硬件还是软件其实都没有”流“一说,只是人们为了便于描述数据的流向而创造的名称。比如说当我们要输出磁盘中记录的数据,那么在计算机中首先会将磁盘中的数据加载进内存,那么磁盘–>内存这种流向就被抽象叫做”流“
FILE结构在程序执行fopen函数时会自动进行创建,并分配在堆中。我们常定义一个指向FILE结构的指针来接收这个返回值
FILE结构定义在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;
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
};
一个进程中的FILE结构会通过_chain域彼此连接形成一个链表,链表的头部用全局变量 _IO_list_all表示,通过这个值我们能遍历所有的FILE结构。
如图:

在标准I/O库中,每个程序启动时stdin、stdout、stderr这三个文件流会自动打开。因此在初始状态下,_IO_list_all指向了一个由这些文件流构成的链表,但是这三个文件流是位于libc.so的数据段上,而我们使用fopen创建的文件流是分配在堆内存上的
_IO_FILE_plus结构
在FILE结构外还包裹了另一种结构_IO_FILE_plus,其中包含了一个重要的指针vtable(虚表)指向了一系列函数指针:
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;
};
虚函数表是一个存储在内存中的表格,其中包含了类中所有虚函数的指针,每个类都有自己的虚函数表。当调用虚函数时,编译器通过虚函数表来确定应该调用哪个函数的实现。
这里可以看见vtable是IO_jump_t 类型的指针,IO_jump_t中保存了一些函数指针,在后面我们会看到在一系列标准 IO 函数中会调用这些函数指针。也就是说,如果使用_IO_FILE_plus去定义一个结构体指针的话,我们既可以使用IO_FILE中的结构体成员变量,也能使用IO_jump_t中的函数指针
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);
};
_flags规则
_flag是IO_FILE结构体中的第一个成员变量,这个成员变量在利用 _IO_2_1_stdout泄露libc的时候起了很重要的作用。
_flag规则: _flag的高两位字节是由libc固定的,不同的libc可能存在差异,但是基本上都是0xfbad0000
高两位字节的作用是作为一个标识,标志这是一个什么文件。而低两位的位数决定了程序的执行状态,低两位规则如下:
#define _IO_MAGIC 0xFBAD0000 // 文件流结构体的魔数
#define _OLD_STDIO_MAGIC 0xFABC0000 // 旧版 stdio 的魔数(兼容用)
#define _IO_MAGIC_MASK 0xFFFF0000 // 魔数掩码(提取高 16 位)
#define _IO_USER_BUF 1 // 用户自定义缓冲区(关闭时不释放)
#define _IO_UNBUFFERED 2 // 无缓冲模式(直接操作文件描述符)
#define _IO_NO_READS 4 // 禁止读操作(只写模式)
#define _IO_NO_WRITES 8 // 禁止写操作(只读模式)
#define _IO_EOF_SEEN 0x10 // 已检测到文件结束符(EOF)
#define _IO_ERR_SEEN 0x20 // 发生 I/O 错误(如磁盘故障)
#define _IO_BAD_SEEN 0x4000 // 流处于不可恢复的错误状态
#define _IO_DELETE_DONT_CLOSE 0x40 // 关闭时不调用 close(_fileno)
#define _IO_LINKED 0x80 // 与其他流链接(共享缓冲区)
#define _IO_IN_BACKUP 0x100 // 正在备份数据(如内存映射回写)
#define _IO_LINE_BUF 0x200 // 行缓冲模式(遇到 '\n' 刷新)
#define _IO_TIED_PUT_GET 0x400 // 读写指针逻辑绑定(如 fgetpos/fsetpos)
#define _IO_CURRENTLY_PUTTING 0x800 // 当前处于写操作中
#define _IO_IS_APPENDING 0x1000 // 追加模式(文件打开时指针在末尾)
#define _IO_IS_FILEBUF 0x2000 // 文件缓冲区(非终端或管道)
#define _IO_USER_LOCK 0x8000 // 用户自定义锁(控制并发访问)
在执行流程中一般会将_flag和定义常量进行按位与运算,并根据与运算的结构进行判断如何执行。
puts()函数执行流程
_IO_puts --> _IO_new_file_xsputn
puts()函数在源码中的表现形式为_IO_puts:
int
_IO_puts (const char *str)
{
int result = EOF; // 初始化返回值为 EOF(表示失败)
size_t len = strlen (str); // 计算字符串长度(不含 '\0')
_IO_acquire_lock (stdout); // 获取 stdout 的锁(防止并发写入)
// 检查流的虚表偏移和宽字符模式是否合法
if ((_IO_vtable_offset (stdout) != 0
|| _IO_fwide (stdout, -1) == -1) // 确保流处于字节模式(非宽字符)
&& _IO_sputn (stdout, str, len) == len // 将字符串写入缓冲区
&& _IO_putc_unlocked ('\n', stdout) != EOF) // 写入换行符
result = MIN (INT_MAX, len + 1); // 成功时返回写入字符数(含 '\n')
_IO_release_lock (stdout); // 释放锁
return result; // 返回结果
}
这里可以看到_IO_puts在过程当中调用了一个叫做_IO_sputn函数(_IO_fwrite也会调用这个),_IO_sputn其实是一个宏,它的作用就是调用_IO_2_1_stdout_中的vtable所指向的_xsputn,也就是_IO_new_file_xsputn函数
_IO_new_file_xsputn --> _IO_OVERFLOW
size_t
_IO_new_file_xsputn (FILE *f, const void *data, size_t n)
{
const char *s = (const char *) data;
size_t to_do = n;
int must_flush = 0;
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))
{
count = f->_IO_buf_end - f->_IO_write_ptr;
if (count >= n)
{
const char *p;
for (p = s + n; p > s; )
{
if (*--p == '\n')
{
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; /* Space available. */
/* Then fill the buffer. */
if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
to_do -= count;
}
if (to_do + must_flush > 0)
{
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;
}
首先进入函数之后判断输出缓冲区还有多少空间,这里是由_IO_write_end - _IO_write_base得来的,这两个是FILE结构体中的两个成员变量,分别是输出结束地址和真实输出地址
关键代码:
1335 if (__IO_OVERFLOW (f, EOF) == EOF)
1336 /* If nothing else has to be written we must not signal the
1337 caller that everything has been written. */
1338 return to_do == 0 ? EOF : n - to_do;
经过上述最后一步的判断,如果还有剩余则说明输出缓冲区未建立或者空间已满,那么就需要通过_IO_OVERFLOW函数来建立或清空缓冲区,这个函数主要是实现刷新缓冲区或建立缓冲区的功能。在vtable中为__overflow
_IO_new_file_overflow --> _IO_do_write
int
_IO_new_file_overflow (FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
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)
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving that
alone, so it can continue to correspond to the external position). */
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)
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;
}
上述代码关键在于 _IO_do_write (f, f->_IO_write_base,f->_IO_write_ptr - f->_IO_write_base) ,我们需要成功执行 _ IO _ do_write()函数,这个函数作用是调用write输出输出缓冲区,传入的参数分别为:stdout结构体、_IO_write_base(输出缓冲区起始地址)和size(_IO_write_end - _IO_write_base计算得来)
这时,我们可以事先在stdout的_IO_write_base的位置部署要输出的起始地址,那么再去利用_IO_do_write函数,即可打印部分内存地址,打印出来的内容就包含我们所需要泄露的libc
为了执行_ IO _ do_write()函数,我们得绕过前面的检查
首先:
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
这里判断_flags的标志位是否包含 _IO_NO_WRITES,
#define _IO_MAGIC 0xFBAD0000 /* 魔数 */
#define _IO_NO_WRITES 8 // 禁止写操作(只读模式)
为了通过这个检查,我们得将此处的运算计算为假,所以我们只需要将_flag设置为0xfbad0000即可
_flag=
0xFBAD0000-->11111011101011010000000000000000(第三位为0即可)_IO_NO_WRITES = 8 -->
00000000000000000000000000001000
第二个大检查:
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL){...}
由于_IO_write_base我们会先覆盖地址,所以 f->_IO_write_base == NULL必定为假,接下来我们令(f->_flags & _IO_CURRENTLY_PUTTING) == 0 为假即可,设置_flags = 0xfbad0800
#define _IO_MAGIC 0xFBAD0000
#define _IO_CURRENTLY_PUTTING 0x800
f->_flags & _IO_CURRENTLY_PUTTING = 1
_flags = 0xfbad0800
第三个检查:
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,f->_IO_write_ptr - f->_IO_write_base);
由于前面传进来的参数就是EOF所以不用管
if (__IO_OVERFLOW (f, EOF) == EOF)
绕过了这些检查后
我们就成功进入了_IO_do_write() 函数
_IO_new_do_write --> new_do_write
int
_IO_new_do_write (FILE *fp, const char *data, size_t to_do)
{
return (to_do == 0
|| (size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}
我们进入_IO_do_write() 函数之后就会进入 _IO_new_do_write函数,该函数只是调用了new_do_write函数,参数分别为stdout结构体,输出缓冲区起始地址,输出长度
跟进new_do_write函数
new_do_write --> _IO_SYSWRITE
static size_t
new_do_write (FILE *fp, const char *data, size_t to_do)
{
size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
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;
}
关键代码:count = _IO_SYSWRITE (fp, data, to_do);首先明确目标是进入这个函数,该函数会执行系统调用write。
if (fp->_flags & _IO_IS_APPENDING)
else if (fp->_IO_read_end != fp->_IO_write_base)
接下来就是考虑这两个判断语句了我们进入第一个if语句的话可以执行count = _IO_SYSWRITE (fp, data, to_do);,所以我们就不需要满足else if (fp->_IO_read_end != fp->_IO_write_base)的条件了(这个条件比较难满足)
所以我们将_flags设置为0xfbad1000即可
#define _IO_MAGIC 0xFBAD0000
#define _IO_IS_APPENDING 0x1000
fp->_flags & _IO_IS_APPENDING = 1
_flags = 0xfbad1000
接下来就可以执行 _IO_SYSWRITE (fp, data, to_do)函数打印出我们一开始设置的要输出的起始地址,从而达到泄露libc的目的了
总结:
我们得满足以下条件来执行_IO_SYSWRITE (fp, data, to_do):
-
_flags & _IO_NO_WRITES = 0 _flags & _IO_CURRENTLY_PUTTING = 1 _flags & _IO_IS_APPENDING = 1 _flags = 0xFBAD1800 -
设置_IO_write_base指向想要泄露的位置,_IO_write_ptr指向泄露结束的地址(不需要一定设置指向结尾,stdout结构中自带地址也足够泄露libc)
参考blog:https://hollk.blog.csdn.net/article/details/113845320

浙公网安备 33010602011771号