0%

IO_FILE源码分析:stdout任意读写

IO_FILE源码分析:stdout任意读写

stdin 只能输入数据到缓冲区,因此只能进行写,而 stdout 会将数据拷贝至输出缓冲区,并将输出缓冲区中的数据输出出来,所以如果可控 stdout 结构体,通过构造可实现利用其进行任意地址读以及任意地址写

stdout任意写

任意写的主要原理为:构造好输出缓冲区将其改为想要任意写的地址,当输出数据可控时,会将数据拷贝至输出缓冲区,即实现了将可控数据拷贝至我们想要写的地址

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# define _IO_new_file_xsputn _IO_file_xsputn

_IO_size_t
_IO_new_file_xsputn (f, data, n) /* n==request */
_IO_FILE *f;
const void *data;
_IO_size_t n;
{
register const char *s = (const char *) data;
_IO_size_t to_do = n;
int must_flush = 0;
_IO_size_t count;

if (n <= 0)
return 0;

/* First figure out how much space is available in the buffer. */
count = f->_IO_write_end - f->_IO_write_ptr; /* 判断输出缓冲区还有多少空间 */
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
{
count = f->_IO_buf_end - f->_IO_write_ptr; /* 判断输出缓冲区还有多少空间 */
if (count >= n) /* 输出缓冲区够用 */
{
register const char *p;
for (p = s + n; p > s; )
{
if (*--p == '\n')
{
count = p - s + 1; /* 重新调整输出缓冲区的可用size */
must_flush = 1;
break;
}
}
}
}
/* Then fill the buffer. */
if (count > 0) /* 如果输出缓冲区有空间,则先把数据拷贝至输出缓冲区 */
{
if (count > to_do)
count = to_do;
if (count > 20)
{
#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; /* 计算是否还有目标输出数据剩余 */
}
else
{
register char *p = f->_IO_write_ptr;
register int i = (int) count;
while (--i >= 0)
*p++ = *s++;
f->_IO_write_ptr = p;
}
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)
return n - to_do;

/* Try to maintain alignment: write a whole number of blocks.
dont_write is what gets left over. */
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缓冲区没有满时,会先将要输出的数据复制到缓冲区中,可通过这一点来实现任意地址写的功能,可以看到任意写好像很简单,只需将 _IO_write_ptr 指向 write_start_IO_write_end 指向 write_end 即可

将上述条件综合描述为:

  • 设置 _flag &~ _IO_NO_WRITES_flag &~ 0x8
  • 设置 _flag & _IO_CURRENTLY_PUTTING_flag | 0x8000
  • 设置 _IO_write_ptr 指向 write_start (目标地址)
  • 设置 _IO_write_end 指向 write_end (目标地址结束)

stdout任意读

想要输出的内容会先放入输出缓冲区中,然后再输出到屏幕上

  • 如果可以控制 _IO_write_base ,就可以控制屏幕打印的起始地址
  • 如果可以控制 _IO_write_ptr ,就可以控制屏幕打印的结束地址
  • 如果满足一些条件,下次调用输出函数时,就可以输出我们想要的内容

f->_IO_write_end > f->_IO_write_ptr 时(输出缓冲区内有可用的空间),会调用 memcpy 拷贝数据到输出缓冲区,然后输出到屏幕,为了不让 memcpy 覆盖我们想要打印的数据,所以需要构造 f->_IO_write_end 等于 f->_IO_write_ptr

接着进入 _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
# define _IO_new_file_overflow _IO_file_overflow

int
_IO_new_file_overflow (f, ch)
_IO_FILE *f;
int ch;
{
if (f->_flags & _IO_NO_WRITES)
/* 检测IO_FILE的_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 == 0)
{
if (f->_IO_write_base == 0) /* 表明输出缓冲区尚未建立 */
{
_IO_doallocbuf (f); /* 调用_IO_doallocbuf函数去分配输出缓冲区 */
_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 (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_new_do_write(f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
/* 执行_IO_new_do_write,利用系统调用write输出输出缓冲区 */
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_new_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(其定义为 #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_baseread_start ,构造 _IO_write_ptrread_end

跟进去 _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
# define _IO_new_do_write _IO_do_write

int
_IO_new_do_write (fp, data, to_do)
_IO_FILE *fp;
const char *data;
_IO_size_t to_do;
{
return (to_do == 0 || new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}

static
int
new_do_write (fp, data, to_do)
_IO_FILE *fp;
const char *data;
_IO_size_t to_do;
{
_IO_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
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)
{
_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;
}
  • 看到在调用 _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

将上述条件综合描述为:

  • 设置 _flag &~ _IO_NO_WRITES_flag &~ 0x8
  • 设置 _flag & _IO_CURRENTLY_PUTTING_flag | 0x800
  • 设置 _fileno 为1
  • 设置 _IO_write_base 指向想要泄露的地方
  • 设置 _IO_write_ptr 指向泄露结束的地址
  • 设置 _IO_read_end 等于 _IO_write_base 或设置 _flag & _IO_IS_APPENDING(即 _flag | 0x1000
  • 设置 _IO_write_end 等于 _IO_write_ptr (非必须)