IO_FILE利用之利用_IO_2_1_stdout泄露libc

First Post:

Last Update:

Page View: loading...

IO_FILE利用之利用_IO_2_1_stdout泄露libc

FILE结构
FILE在linux系统的标准IO库使用来描述文件结构,称之为文件流。这里提及的”流“其实是一种抽象的概念,无论是硬件还是软件其实都没有”流“一说,只是人们为了便于描述数据的流向而创造的名称。比如说当我们要输出磁盘中记录的数据,那么在计算机中首先会将磁盘中的数据加载进内存,那么磁盘–>内存这种流向就被抽象叫做”流“

FILE结构在程序执行fopen函数时会自动进行创建,并分配在堆中。我们常定义一个指向FILE结构的指针来接收这个返回值

FILE结构定义在libio.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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(虚表)指向了一系列函数指针:

1
2
3
4
5
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中的函数指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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

高两位字节的作用是作为一个标识,标志这是一个什么文件。而低两位的位数决定了程序的执行状态,低两位规则如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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)
4{
4 const char *p;
4 for (p = s + n; p > s; )
4 {
4 if (*--p == '\n')
44{
44 count = p - s + 1;
44 must_flush = 1;
44 break;
44}
4 }
4}
}
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)
4count = 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)
4/* If nothing else has to be written we must not signal the
4 caller that everything has been written. */
4return 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)
4{
4 count = new_do_write (f, s, do_write);
4 to_do -= count;
4 if (count < do_write)
4 return n - to_do;
4}

/* Now write out the remainder. Normally, this will fit in the
4 buffer, but it's somewhat messier for line-buffered files,
4 so we let _IO_default_xsputn handle the general case. */
if (to_do)
4to_do -= _IO_default_xsputn (f, s+do_write, to_do);
}
return n - to_do;
}

首先进入函数之后判断输出缓冲区还有多少空间,这里是由_IO_write_end - _IO_write_base得来的,这两个是FILE结构体中的两个成员变量,分别是输出结束地址和真实输出地址

关键代码:

1
2
3
4
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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)
4{
4 _IO_doallocbuf (f);
4 _IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
4}
/* Otherwise must be currently reading.
4 If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
4 logically slide the buffer forwards one block (by setting the
4 read pointers to all point at the beginning of the block). This
4 makes room for subsequent output.
4 Otherwise, set the read pointers to _IO_read_end (leaving that
4 alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely (_IO_in_backup (f)))
4{
4 size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
4 _IO_free_backup_area (f);
4 f->_IO_read_base -= MIN (nbackup,
4444 f->_IO_read_base - f->_IO_buf_base);
4 f->_IO_read_ptr = f->_IO_read_base;
4}

if (f->_IO_read_ptr == f->_IO_buf_end)
4f->_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))
4f->_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,
44 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()函数,我们得绕过前面的检查

首先:

1
2
3
4
5
6
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}

这里判断_flags的标志位是否包含 _IO_NO_WRITES,

1
2
#define _IO_MAGIC 0xFBAD0000 /* 魔数 */
#define _IO_NO_WRITES 8 // 禁止写操作(只读模式)

为了通过这个检查,我们得将此处的运算计算为假,所以我们只需要将_flag设置为0xfbad0000即可

_flag=0xFBAD0000 –> 11111011101011010000000000000000 (第三位为0即可)

_IO_NO_WRITES = 8 –> 00000000000000000000000000001000

第二个大检查:

1
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

1
2
3
4
5

#define _IO_MAGIC 0xFBAD0000
#define _IO_CURRENTLY_PUTTING 0x800
f->_flags & _IO_CURRENTLY_PUTTING = 1
_flags = 0xfbad0800

第三个检查:

1
2
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,f->_IO_write_ptr - f->_IO_write_base);

由于前面传进来的参数就是EOF所以不用管

1
if (__IO_OVERFLOW (f, EOF) == EOF)

绕过了这些检查后

我们就成功进入了_IO_do_write() 函数

_IO_new_do_write –> new_do_write

1
2
3
4
5
6
int
_IO_new_do_write (FILE *fp, const char *data, size_t to_do)
{
return (to_do == 0
4 || (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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
4= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
4return 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
44 && (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
44 ? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}

关键代码:count = _IO_SYSWRITE (fp, data, to_do);首先明确目标是进入这个函数,该函数会执行系统调用write。

1
2
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即可

1
2
3
4
#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)

  • 1
    2
    3
    4
    _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