0%

IO_FILE源码分析:fwrite

IO_FILE源码分析:fwrite

直接上源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
_IO_size_t
_IO_fwrite (buf, size, count, fp)
const void *buf;
_IO_size_t size;
_IO_size_t count;
_IO_FILE *fp;
{
_IO_size_t request = size * count;
_IO_size_t written = 0;
CHECK_FILE (fp, 0);
if (request == 0)
return 0;
_IO_cleanup_region_start ((void (*) __P ((void *))) _IO_funlockfile, fp);
_IO_flockfile (fp);
if (fp->_vtable_offset != 0 || _IO_fwide (fp, -1) == -1)
written = _IO_sputn (fp, (const char *) buf, request); /* vtable->__xsputn */
_IO_funlockfile (fp);
_IO_cleanup_region_end (0);
if (written == request)
return count;
else
return written / size;
}

这里重点关注一下 _IO_sputn 函数(vtable->__xsputn),其他的东西都不用管

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;
}

fwrite 的主要实现在 _IO_new_file_xsputn 中,整体流程包含四个部分:

  • 首先判断输出缓冲区还有多少剩余,如果有剩余则将目标输出数据拷贝至输出缓冲区(能拷贝多少就拷贝多少)
  • 如果输出缓冲区没有剩余(输出缓冲区未建立也是没有剩余)或输出缓冲区不够则调用 _IO_OVERFLOW 建立输出缓冲区或刷新输出缓冲区
  • 输出缓冲区刷新后判断剩余的目标输出数据是否超过块的size,如果超过块的size,则不通过输出缓冲区直接以块为单位,使用 new_do_write 输出目标数据
  • 如果按块输出数据后还剩下一点数据则调用 _IO_default_xsputn 将数据拷贝至输出缓冲

接下来就分析这些过程:(第一部分在注释中已经解释了,略过)

建立输出缓冲区或刷新输出缓冲区

1
2
3
4
5
6
7
 if (to_do + must_flush > 0) 
/* 如果还有目标数据剩余,此时则表明输出缓冲区未建立或输出缓冲区已经满了 */
{
_IO_size_t block_size, do_write;
if (_IO_OVERFLOW (f, EOF) == EOF)
return n - to_do;
......

追踪进入 _IO_OVERFLOW :

1
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)

可以发现 _IO_OVERFLOW 的调用是虚表调用(可以进行劫持或伪造,FSOP惯用手段),其指向内容为 _IO_new_file_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;
}

该函数功能主要是实现刷新输出缓冲区或建立缓冲区,其核心部分就是“_IO_new_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;
}

以块为单位直接输出数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
      /* 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);
/* 如果超过输入缓冲区"f->_IO_buf_end - f->_IO_buf_base"的大小,则为了提高效率,不再使用输出缓冲区 */

if (do_write)
{
count = new_do_write (f, s, do_write);
/* 以块为基本单位直接将缓冲区调用new_do_write输出 */
to_do -= count;
if (count < do_write)
return n - to_do;
}

已经经历过了 _IO_OVERFLOW ,此时的IO FILE缓冲区指针的状态是处于刷新的初始化状态,输出缓冲区中也没有数据

根据输出size的大小,判断是否需要继续使用输出缓存区

剩余目标输出数据放入输出缓冲区中

1
2
3
4
5
6
     /* 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);
}

跟进函数 _IO_default_xsputn :

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
_IO_size_t
_IO_default_xsputn (f, data, n)
_IO_FILE *f;
const void *data;
_IO_size_t n;
{
const char *s = (char *) data;
_IO_size_t more = n;
if (more <= 0)
return 0;
for (;;)
{
/* Space available. */
_IO_ssize_t count = f->_IO_write_end - f->_IO_write_ptr;
if (count > 0)
{
if ((_IO_size_t) count > more)
count = more;
if (count > 20) /* 当长度大于20时,使用memcpy拷贝 */
{
#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 if (count <= 0)
count = 0;
else /* 当长度小于20时,使用for循环赋值拷贝 */
{
char *p = f->_IO_write_ptr;
_IO_ssize_t i;
for (i = count; --i >= 0; )
*p++ = *s++;
f->_IO_write_ptr = p;
}
more -= count;
}
if (more == 0 || _IO_OVERFLOW (f, (unsigned char) *s++) == EOF)
/* 如果输出缓冲区为空,则调用_IO_OVERFLOW进行输出 */
break;
more--;
}
return n - more;
}

函数最主要的作用就是将剩余的目标输出数据拷贝到输出缓冲区里


在 fwrite 的源码分析中,我们发现了一个老朋友: _IO_OVERFLOW

_IO_OVERFLOW 可是FSOP的常客,因为发生内存错误时会调用 _IO_flush_all_lockp ,进而调用 _IO_OVERFLOW ,就是因为它容易触发并且容易被劫持(或伪造),FSOP才拥有如此广阔的应用范围

后续的漏洞利用分析中,我会重点分析FSOP的原理,主要原因还是我做了一个FSOP和堆上ORW相结合的题目(做的我想吐),趁着印象还比较深刻,想总结分析一下(还有个原因就是:之前总结的FSOP有点草率)