IO_FILE

io file这一部分的知识,因为发现之前接触的太浅了

这一次源码级调试一下,看一下各种各样的io结构的原理,笔者这里照着这个师傅调试的

本文章基本算是转载这位师傅的,转载!

IO FILE

怎么源码级调试?这里笔者直接用的pwndbg里面的dir将glibc的源码带到了调试里。dir /glibc-2.23/libio/,需要注意的是启动gdb的时候需要这样:gdb 文件名,这样就可以愉快的调试啦。

需要先理解一下什么是IO FILE。进程中的FILE结构会通过_chain域链接形成一个链表。链表头部用全局变量__IO_list_all表示。一个程序启动时有三个文件流是打开的:stderr,stdout,stdin。这三个文件流位于libc.so数据段,__IO_FILE结构外包裹着另一种结构__IO_FILE_plus。如下

fopen

一个简单的fopen程序2.23下的libc。

1
2
3
4
5
6
7
8
#include <stdio.h>
#include <stdlib.h>

int main(){
FILE*fp = fopen("test","wb");
char *ptr = malloc(0x20);
return 0;
}

gdb启动完成之后s进去就可以看到fopen实际上是_IO_new_fopen函数。

1
2
3
4
5
   94 _IO_FILE *
95 _IO_new_fopen (const char *filename, const char *mode)
96 {
97 return __fopen_internal (filename, mode, 1);
98 }

从上面的源码中可以很清楚的看到又调用了__fopen_internal函数。看一下源码

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
_IO_FILE *
__fopen_internal (const char *filename, const char *mode, int is32)
{
struct locked_FILE
{
struct _IO_FILE_plus fp;
#ifdef _IO_MTSAFE_IO
_IO_lock_t lock;
#endif
struct _IO_wide_data wd;
} *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));

if (new_f == NULL)
return NULL;
#ifdef _IO_MTSAFE_IO
new_f->fp.file._lock = &new_f->lock;
#endif
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
_IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
#else
_IO_no_init (&new_f->fp.file, 1, 0, NULL, NULL);
#endif
_IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
_IO_file_init (&new_f->fp);
#if !_IO_UNIFIED_JUMPTABLES
new_f->fp.vtable = NULL;
#endif
if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL)
return __fopen_maybe_mmap (&new_f->fp.file);
locked_FILE
_IO_un_link (&new_f->fp);
free (new_f);
return NULL;
}
  1. malloc分配内存空间。
  2. _IO_no_init 对file结构体进行null初始化。
  3. _IO_file_init将结构体链接进_IO_list_all链表。
  4. _IO_file_fopen执行系统调用打开文件

malloc分配内存空间

首先malloc了一个struct locked_FILE大小的结构体,这个结构体内有_IO_FILE_plus_IO_lock_t_IO_wide_data这三个结构,其中_IO_FILE_plus为使用的IO_FILE结构体。malloc之后会发现都为0,很明显的可以看到这个结构体的大小为0x230。

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> p new_f
$2 = (struct locked_FILE *) 0x55555555b010
pwndbg> x/20gx 0x55555555b010 - 0x10
0x55555555b000: 0x0000000000000000 0x0000000000000231
0x55555555b010: 0x0000000000000000 0x0000000000000000
0x55555555b020: 0x0000000000000000 0x0000000000000000
0x55555555b030: 0x0000000000000000 0x0000000000000000
0x55555555b040: 0x0000000000000000 0x0000000000000000
0x55555555b050: 0x0000000000000000 0x0000000000000000
0x55555555b060: 0x0000000000000000 0x0000000000000000
0x55555555b070: 0x0000000000000000 0x0000000000000000
0x55555555b080: 0x0000000000000000 0x0000000000000000
0x55555555b090: 0x0000000000000000 0x0000000000000000

_IO_no_init对file结构体进行初始化操作

继续住下走会调用_IO_no_init这个函数对上面的结构体进行初始化操作,这个文件在libio/genops.c。跟着源码还可以看到的是还利用了_IO_old_init这个函数对flags这些进行初始化。

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
void
_IO_no_init (_IO_FILE *fp, int flags, int orientation,
struct _IO_wide_data *wd, const struct _IO_jump_t *jmp)
{
_IO_old_init (fp, flags);
fp->_mode = orientation;
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
if (orientation >= 0)
{
fp->_wide_data = wd;
fp->_wide_data->_IO_buf_base = NULL;
fp->_wide_data->_IO_buf_end = NULL;
fp->_wide_data->_IO_read_base = NULL;
fp->_wide_data->_IO_read_ptr = NULL;
fp->_wide_data->_IO_read_end = NULL;
fp->_wide_data->_IO_write_base = NULL;
fp->_wide_data->_IO_write_ptr = NULL;
fp->_wide_data->_IO_write_end = NULL;
fp->_wide_data->_IO_save_base = NULL;
fp->_wide_data->_IO_backup_base = NULL;
fp->_wide_data->_IO_save_end = NULL;

fp->_wide_data->_wide_vtable = jmp;
}
else
/* Cause predictable crash when a wide function is called on a byte
stream. */
fp->_wide_data = (struct _IO_wide_data *) -1L;
#endif
fp->_freeres_list = NULL;
}

void
_IO_old_init (_IO_FILE *fp, int flags)
{
fp->_flags = _IO_MAGIC|flags;
fp->_flags2 = 0;
fp->_IO_buf_base = NULL;
fp->_IO_buf_end = NULL;
fp->_IO_read_base = NULL;
fp->_IO_read_ptr = NULL;
fp->_IO_read_end = NULL;
fp->_IO_write_base = NULL;
fp->_IO_write_ptr = NULL;
fp->_IO_write_end = NULL;
fp->_chain = NULL; /* Not necessary. */

fp->_IO_save_base = NULL;
fp->_IO_backup_base = NULL;
fp->_IO_save_end = NULL;
fp->_markers = NULL;
fp->_cur_column = 0;
#if _IO_JUMPS_OFFSET
fp->_vtable_offset = 0;
#endif
#ifdef _IO_MTSAFE_IO
if (fp->_lock != NULL)
_IO_lock_init (*fp->_lock);
#endif

看一下最后的file结构被初始化成什么样子了。

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
pwndbg> p new_f->fp
$4 = {
file = {
_flags = -72548352,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0,
_flags2 = 0,
_old_offset = 0,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x55555555b0f0,
_offset = 0,
_codecvt = 0x0,
_wide_data = 0x55555555b100,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x0
}

_IO_file_init将结构体链接到_IO_list_all

结束_IO_no_init之后我们可以看到回到了__fopen_internal并继续执行_IO_file_init这个函数,跟进看一下是干什么的,跟进之后是/libio/fileops.c这个文件里的_IO_new_file_init

1
2
3
4
5
6
7
8
9
10
11
void
_IO_new_file_init (struct _IO_FILE_plus *fp)
{
/* POSIX.1 allows another file handle to be used to change the position
of our file descriptor. Hence we actually don't know the actual
position before we do the first fseek (and until a following fflush). */
fp->file._offset = _IO_pos_BAD;
fp->file._IO_file_flags |= CLOSED_FILEBUF_FLAGS;

_IO_link_in (fp);
fp->file._fileno = -1;

这个函数主要调用了_IO_link_in这个函数,继续跟进这个函数,libio/genops.c里面的_IO_link_in

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void
_IO_link_in (struct _IO_FILE_plus *fp)
{
if ((fp->file._flags & _IO_LINKED) == 0)
{
fp->file._flags |= _IO_LINKED;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
run_fp = (_IO_FILE *) fp;
_IO_flockfile ((_IO_FILE *) fp);
#endif
fp->file._chain = (_IO_FILE *) _IO_list_all;
_IO_list_all = fp;
++_IO_list_all_stamp;
#ifdef _IO_MTSAFE_IO
_IO_funlockfile ((_IO_FILE *) fp);
run_fp = NULL;
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
}
}

首先这个if是判断flag的标志位是否是_IO_LINKED,这个有什么用呢?FILE结构体是通过_IO_list_all的单链表进行管理的,如果这个结构体没有_IO_LINKED就说明这个结构体没有链接进入_IO_list_all。后面把它链接进入_IO_list_all链表,同时设置FILE结构体的_chain字段为之前的链表的值,否则直接返回。所以_IO_file_init主要功能是将FILE结构体链接进入_IO_list_all链表。

1
2
pwndbg> p _IO_list_all
$6 = (struct _IO_FILE_plus *) 0x7ffff7dd2540 <_IO_2_1_stderr_>

在没有执行下面的操作之前可以看到_IO_list_all链接的是_IO_2_1_stderr_,执行完之后_IO_list_all就指向的是申请出来的结构体。

1
2
pwndbg> p _IO_list_all
$7 = (struct _IO_FILE_plus *) 0x55555555b010

同时此时的_chain字段也指向了_IO_2_1_stderr_这里。

_IO_file_fopen打开文件句柄

设置好了_IO_LINKED这里东西之后又会回到__fopen_internal这里,接下来会执行_IO_file_fopen这个函数,跟进后发现位于libio/fileops.c这里面的_IO_new_file_fopen。这个_IO_new_file_fopen函数有点长,这里就放一部分比较重要的。

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
_IO_FILE *
_IO_new_file_fopen (_IO_FILE *fp, const char *filename, const char *mode,
int is32not64)
{

...
## 检查文件是否已打开,打开则返回
if (_IO_file_is_open (fp))
return 0;
## 设置文件打开模式
switch (*mode)
{
case 'r':
omode = O_RDONLY;
read_write = _IO_NO_WRITES;
break;
...
}
...
## 调用_IO_file_open函数
result = _IO_file_open (fp, filename, omode|oflags, oprot, read_write,
is32not64);
...
}
libc_hidden_ver (_IO_new_file_fopen, _IO_file_fopen)

会先检查文件是否打开,然后设置打开模式,最后调用了_IO_file_open这个函数跟进它。位于libio/fileops.c这个文件中的_IO_file_open

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
_IO_FILE *
_IO_file_open (_IO_FILE *fp, const char *filename, int posix_mode, int prot,
int read_write, int is32not64)
{
int fdesc;
#ifdef _LIBC
if (__glibc_unlikely (fp->_flags2 & _IO_FLAGS2_NOTCANCEL))
fdesc = open_not_cancel (filename,
posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
else
fdesc = open (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
#else
fdesc = open (filename, posix_mode, prot);
#endif
if (fdesc < 0)
return NULL;
fp->_fileno = fdesc;
_IO_mask_flags (fp, read_write,_IO_NO_READS+_IO_NO_WRITES+_IO_IS_APPENDING);
/* For append mode, send the file offset to the end of the file. Don't
update the offset cache though, since the file handle is not active. */
if ((read_write & (_IO_IS_APPENDING | _IO_NO_READS))
== (_IO_IS_APPENDING | _IO_NO_READS))
{
_IO_off64_t new_pos = _IO_SYSSEEK (fp, 0, _IO_seek_end);
if (new_pos == _IO_pos_BAD && errno != ESPIPE)
{
close_not_cancel (fdesc);
return NULL;
}
}
_IO_link_in ((struct _IO_FILE_plus *) fp);
return fp;
}

这个函数就是调用open系统调用打开文件,将文件描述符赋值给FILE结构体的_fileno 字段,最后再次调用_IO_link_in函数,确保该结构体被链接进入_IO_list_all链表。查看new_f->fp就可以看到_fileno被设置为0x3。

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
pwndbg> p new_f->fp
$9 = {
file = {
_flags = -72539004,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd2540 <_IO_2_1_stderr_>,
_fileno = 3,
_flags2 = 0,
_old_offset = 0,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x55555555b0f0,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x55555555b100,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}

执行完成之后返回FILE结构体指针。至此对fopen的调试结束。

fread

和fopen一样用个程序来,这里还是用那位师傅的程序,笔者只跟着动态调试。

1
2
3
4
5
6
7
8
#include <stdio.h>

int main(){
char data[20];
FILE *fp = fopen("test", "rb");
fread(data, 1, 20, fp);
return 0;
}

需要创建一个test文件并写入一些东西进去即可。gdb启动调试,断点下在fread这里就会看到调用了_IO_fread,看一下FILE结构体fp的内容。可以看到此时的_IO_read_ptr_IO_buf_base等指针都还是空的

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
pwndbg> p *_IO_list_all
$1 = {
file = {
_flags = -72539000,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7f995e0 <_IO_2_1_stderr_>,
_fileno = 3,
_flags2 = 0,
_old_offset = 0,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x555555559380,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x555555559390,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7f9a4a0 <_IO_file_jumps>
}

vtable中的指针内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> p *_IO_list_all->vtable
$2 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7e4a440 <_IO_new_file_finish>,
__overflow = 0x7ffff7e4aea0 <_IO_new_file_overflow>,
__underflow = 0x7ffff7e4ab50 <_IO_new_file_underflow>,
__uflow = 0x7ffff7e4bf10 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7e4d2d0 <__GI__IO_default_pbackfail>,
__xsputn = 0x7ffff7e4a030 <_IO_new_file_xsputn>,
__xsgetn = 0x7ffff7e49c10 <__GI__IO_file_xsgetn>,
__seekoff = 0x7ffff7e49470 <_IO_new_file_seekoff>,
__seekpos = 0x7ffff7e4c2a0 <_IO_default_seekpos>,
__setbuf = 0x7ffff7e48d30 <_IO_new_file_setbuf>,
__sync = 0x7ffff7e48bc0 <_IO_new_file_sync>,
__doallocate = 0x7ffff7e3d8d0 <__GI__IO_file_doallocate>,
__read = 0x7ffff7e4a210 <__GI__IO_file_read>,
__write = 0x7ffff7e49a70 <_IO_new_file_write>,
__seek = 0x7ffff7e491a0 <__GI__IO_file_seek>,
__close = 0x7ffff7e48d20 <__GI__IO_file_close>,
__stat = 0x7ffff7e49a60 <__GI__IO_file_stat>,
__showmanyc = 0x7ffff7e4d460 <_IO_default_showmanyc>,
__imbue = 0x7ffff7e4d470 <_IO_default_imbue>
}

fread实际上是_IO_fread函数,文件目录为/libio/iofread.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_IO_size_t
_IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
_IO_size_t bytes_requested = size * count;
_IO_size_t bytes_read;
CHECK_FILE (fp, 0);
if (bytes_requested == 0)
return 0;
_IO_acquire_lock (fp);
# 调用_IO_sgetn函数
bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
_IO_release_lock (fp);
return bytes_requested == bytes_read ? count : bytes_read / size;
}

源码中可以看到又调用了_IO_sgetn函数,跟进它。

1
2
3
4
5
6
  463 _IO_size_t
464 _IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n)
465 {
466 /* FIXME handle putback buffer here! */
467 return _IO_XSGETN (fp, data, n);
468 }

又调用了_IO_XSGETN#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N),继续跟进就可以发现最终调用了_IO_file_xsgetn实际上就是FILE结构体中vtable的__xsgetn函数,位于libio/fileops.c。下面贴一下比较重要的部分

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
_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
_IO_size_t want, have;
_IO_ssize_t count;
char *s = data;

want = n;

if (fp->_IO_buf_base == NULL)
{
...
# 第一部分,如果fp->_IO_buf_base为空的话则调用`_IO_doallocbuf`
_IO_doallocbuf (fp);
}

while (want > 0)
{

have = fp->_IO_read_end - fp->_IO_read_ptr;
## 第二部分,输入缓冲区里已经有足够的字符,则直接把缓冲区里的字符给目标buff
if (want <= have)
{
memcpy (s, fp->_IO_read_ptr, want);
fp->_IO_read_ptr += want;
want = 0;
}
else
{
# 第二部分,输入缓冲区里有部分字符,但是没有达到fread的size需求,先把已有的拷贝至目标buff
if (have > 0)
{
...
memcpy (s, fp->_IO_read_ptr, have);
s += have;

want -= have;
fp->_IO_read_ptr += have;
}


if (fp->_IO_buf_base
&& want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
{
## 第三部分,输入缓冲区里不能满足需求,调用__underflow读入数据
if (__underflow (fp) == EOF)
break;

continue;
}
...
return n - want;
}
libc_hidden_def (_IO_file_xsgetn)

_IO_file_xsgetn是处理fread读入数据的核心函数,分为三个部分:

  • 第一部分是fp->_IO_buf_base为空的情况,表明此时的FILE结构体中的指针未被初始化,输入缓冲区未建立,则调用_IO_doallocbuf去初始化指针,建立输入缓冲区。
  • 第二部分是输入缓冲区里有输入,即fp->_IO_read_ptr小于fp->_IO_read_end,此时将缓冲区里的数据直接拷贝至目标buff。
  • 第三部分是输入缓冲区里的数据为空或者是不能满足全部的需求,则调用__underflow调用系统调用读入数据。

接下来对_IO_file_xsgetn这三部分进行跟进并分析。

初始化输入缓冲区

首先是第一部分,在fp->_IO_buf_base为空时,也就是输入缓冲区未建立时,代码调用_IO_doallocbuf函数去建立输入缓冲区。跟进_IO_doallocbuf函数,看下它是如何初始化输入缓冲区,为输入缓冲区分配空间的,文件在/libio/genops.c中:

1
2
3
4
5
6
7
8
9
10
11
void
_IO_doallocbuf (_IO_FILE *fp)
{
if (fp->_IO_buf_base) # 如何输入缓冲区不为空,直接返回
return;
if (!(fp->_flags & _IO_UNBUFFERED) || fp->_mode > 0) #检查标志位
if (_IO_DOALLOCATE (fp) != EOF) ## 调用vtable函数
return;
_IO_setb (fp, fp->_shortbuf, fp->_shortbuf+1, 0);
}
libc_hidden_def (_IO_doallocbuf)

函数先检查fp->_IO_buf_base是否为空,如果不为空的话表明该输入缓冲区已被初始化,直接返回。如果为空,则检查fp->_flags看它是不是_IO_UNBUFFERED或者fp->_mode大于0,如果满足条件调用FILE的vtable中的_IO_file_doallocate,跟进去该函数,在/libio/filedoalloc.c中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
_IO_file_doallocate (_IO_FILE *fp)
{
_IO_size_t size;
char *p;
struct stat64 st;

...
size = _IO_BUFSIZ;
...
if (fp->_fileno >= 0 && __builtin_expect (_IO_SYSSTAT (fp, &st), 0) >= 0) # 调用`_IO_SYSSTAT`获取FILE信息
{
...
if (st.st_blksize > 0)
size = st.st_blksize;
...
}
p = malloc (size);
...
_IO_setb (fp, p, p + size, 1); # 调用`_IO_setb`设置FILE缓冲区
return 1;
}
libc_hidden_def (_IO_file_doallocate)

可以看到_IO_file_doallocate函数是分配输入缓冲区的实现函数,首先调用_IO_SYSSTAT去获取文件信息,_IO_SYSSTAT函数是vtable中的 __stat函数,获取文件信息,修改相应需要申请的size。可以看到在执行完_IO_SYSSTAT函数后,st结构体的值为:

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
pwndbg> p st
$4 = {
st_dev = 64514,
st_ino = 2248406,
st_nlink = 1,
st_mode = 33204,
st_uid = 1000,
st_gid = 1000,
__pad0 = 0,
st_rdev = 0,
st_size = 7,
st_blksize = 4096,
st_blocks = 8,
st_atim = {
tv_sec = 1661346060,
tv_nsec = 403757781
},
st_mtim = {
tv_sec = 1661346059,
tv_nsec = 331754338
},
st_ctim = {
tv_sec = 1661346059,
tv_nsec = 331754338
},
__glibc_reserved = {0, 0, 0}
}

因此size被修改为st.st_blksize所对应的大小0x1000,接着调用malloc去申请内存,申请出来的堆块如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x55555555b000
Size: 0x231

Allocated chunk | PREV_INUSE
Addr: 0x55555555b230
Size: 0x1011

Top chunk | PREV_INUSE
Addr: 0x55555555c240
Size: 0x1fdc1

pwndbg> x/20gx 0x55555555b230
0x55555555b230: 0x00007ffff7dd0260 0x0000000000001011
0x55555555b240: 0x0000000000000000 0x0000000000000000
0x55555555b250: 0x0000000000000000 0x0000000000000000

空间申请出来后,调用_IO_setb,跟进去看它干了些啥,文件在/libio/genops.c中:

1
2
3
4
5
6
7
8
9
void
_IO_setb (_IO_FILE *f, char *b, char *eb, int a)
{
...
f->_IO_buf_base = b; # 设置_IO_buf_base
f->_IO_buf_end = eb; # 设置_IO_buf_end
...
}
libc_hidden_def (_IO_setb)

函数相对比较简单的就是设置了_IO_buf_base_IO_buf_end,可以预料到_IO_setb函数执行完后,fp的这两个指针被赋上值了:

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
pwndbg> p *_IO_list_all
$5 = {
file = {
_flags = -72539000,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x55555555b240 "",
_IO_buf_end = 0x55555555c240 "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd2540 <_IO_2_1_stderr_>,
_fileno = 3,
_flags2 = 0,
_old_offset = 0,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x55555555b0f0,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x55555555b100,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}

到此,初始化缓冲区就完成了,函数返回_IO_file_doallocate后,接着_IO_file_doallocate也返回,回到_IO_file_xsgetn函数中。

拷贝输入缓冲区数据

初始化缓冲区完成之后,代码返回到_IO_file_xsgetn函数中,程序就进入到第二部分:拷贝输入缓冲区数据,如果输入缓冲区里存在已输入的数据,则把它直接拷贝到目标缓冲区里。

这部分比较简单,需要说明下的是从这里可以看出来fp->_IO_read_ptr指向的是输入缓冲区的起始地址,fp->_IO_read_end指向的是输入缓冲区的结束地址。

fp->_IO_read_end-fp->_IO_read_ptr之间的数据通过memcpy拷贝到目标缓冲区里。

执行系统调用读取数据

在输入缓冲区为0或者是不能满足需求的时候则会执行最后一步__underflow去执行系统调用read读取数据,并放入到输入缓冲区里。

因为demo里第一次读取数据,此时的fp->_IO_read_end以及fp->_IO_read_ptr都是0,因此会进入到__underflow,跟进去细看,文件在/libio/genops.c中:

1
2
3
4
5
6
7
8
9
10
11
12
13
int
__underflow (_IO_FILE *fp)
{

# 额外的检查
...
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
...
# 调用_IO_UNDERFLOW
return _IO_UNDERFLOW (fp);
}
libc_hidden_def (__underflow)

函数稍微做一些检查就会调用_IO_UNDERFLOW函数,其中一个检查是如果fp->_IO_read_ptr小于fp->_IO_read_end则表明输入缓冲区里存在数据,可直接返回,否则则表示需要继续读入数据。

检查都通过的话就会调用_IO_UNDERFLOW函数,该函数是FILE结构体vtable里的_IO_new_file_underflow,跟进去看,文件在/libio/fileops.c里:

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
int
_IO_new_file_underflow (_IO_FILE *fp)
{
_IO_ssize_t count;
...
## 如果存在_IO_NO_READS标志,则直接返回
if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
## 如果输入缓冲区里存在数据,则直接返回
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
...
## 如果没有输入缓冲区,则调用_IO_doallocbuf分配输入缓冲区
if (fp->_IO_buf_base == NULL)
{
...
_IO_doallocbuf (fp);
}
...
## 设置FILE结构体指针
fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
fp->_IO_read_end = fp->_IO_buf_base;
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;
##调用_IO_SYSREAD函数最终执行系统调用读取数据
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
...
## 设置结构体指针
fp->_IO_read_end += count;
...
return *(unsigned char *) fp->_IO_read_ptr;
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

这个_IO_new_file_underflow函数,是最终调用系统调用的地方,在最终执行系统调用之前,仍然有一些检查,整个流程为:

  1. 检查FILE结构体的_flag标志位是否包含_IO_NO_READS,如果存在这个标志位则直接返回EOF,其中_IO_NO_READS标志位的定义是#define _IO_NO_READS 4 /* Reading not allowed */
  2. 如果fp->_IO_buf_base位null,则调用_IO_doallocbuf分配输入缓冲区。
  3. 接着初始化设置FILE结构体指针,将他们都设置成fp->_IO_buf_base
  4. 调用_IO_SYSREAD(vtable中的_IO_file_read函数),该函数最终执行系统调用read,读取文件数据,数据读入到fp->_IO_buf_base中,读入大小为输入缓冲区的大小fp->_IO_buf_end - fp->_IO_buf_base
  5. 设置输入缓冲区已有数据的size,即设置fp->_IO_read_endfp->_IO_read_end += count

其中第二步里面的如果fp->_IO_buf_base位null,则调用_IO_doallocbuf分配输入缓冲区,似乎有点累赘,因为之前已经分配了,这个原因我在最后会说明。

其中第四步的_IO_SYSREAD(vtable中的_IO_file_read函数)的源码比较简单,就是执行系统调用函数read去读取文件数据,文件在libio/fileops.c,源码如下:

1
2
3
4
5
6
7
_IO_ssize_t
_IO_file_read (_IO_FILE *fp, void *buf, _IO_ssize_t size)
{
return (__builtin_expect (fp->_flags2 & _IO_FLAGS2_NOTCANCEL, 0)
? read_not_cancel (fp->_fileno, buf, size)
: read (fp->_fileno, buf, size));
}

_IO_file_underflow函数执行完毕以后,FILE结构体中各个指针已被赋值,且文件数据已读入,输入缓冲区里已经有数据,结构体值如下,其中fp->_IO_read_ptr指向输入缓冲区数据的开始位置,fp->_IO_read_end指向输入缓冲区数据结束的位置:

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
pwndbg> p *_IO_list_all
$4 = {
file = {
_flags = -72539000,
_IO_read_ptr = 0x55555555b240 "111111\n",
_IO_read_end = 0x55555555b247 "",
_IO_read_base = 0x55555555b240 "111111\n",
_IO_write_base = 0x55555555b240 "111111\n",
_IO_write_ptr = 0x55555555b240 "111111\n",
_IO_write_end = 0x55555555b240 "111111\n",
_IO_buf_base = 0x55555555b240 "111111\n",
_IO_buf_end = 0x55555555c240 "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd2540 <_IO_2_1_stderr_>,
_fileno = 3,
_flags2 = 0,
_old_offset = 0,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x55555555b0f0,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x55555555b100,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}

函数执行完后,返回到_IO_file_xsgetn函数中,由于while循环的存在,重新执行第二部分,此时将输入缓冲区拷贝至目标缓冲区,最终返回。

至此,对于fread的源码分析结束。

其他输入函数

完整分析了fread函数之后,还想知道其他一些函数(scanf、gets)等函数时如何通过stdin实现输入的,我编写了源码,并将断点下在了read函数之前,看他们时如何调用去的。

首先是scanf,其最终调用read函数时栈回溯如下:

1
2
3
4
5
6
7
read
_IO_new_file_underflow at fileops.c
__GI__IO_default_uflow at genops.c
_IO_vfscanf_internal at vfscanf.c
__isoc99_scanf at at isoc99_scanf.c
main ()
__libc_start_main

可以看到scanf最终也是调用stdin的vtable中的_IO_new_file_underflow去调用read的。不过它并不是使用_IO_file_xsgetn,而是使用vtable中的__uflow,源码如下:

1
2
3
4
5
6
7
8
9
int
_IO_default_uflow (_IO_FILE *fp)
{
int ch = _IO_UNDERFLOW (fp);
if (ch == EOF)
return EOF;
return *(unsigned char *) fp->_IO_read_ptr++;
}
libc_hidden_def (_IO_default_uflow)

__uflow函数基本上啥都没干直接就调用了_IO_new_file_underflow因此最终也是_IO_new_file_underflow实现的输入。

再看看gets函数,函数调用栈如下,与scanf基本一致:

1
2
3
4
5
6
read
__GI__IO_file_underflow
__GI__IO_default_uflow
gets
main
__libc_start_main+240

再试了试fscanf等,仍然是一样的,仍然是最终通过_IO_new_file_underflow实现的输入。虽然不能说全部的io输入都是通过_IO_new_file_underflow函数最终实现的输入,但是应该也可以说大部分是使用_IO_new_file_underflow函数实现的。

但是仍然有一个问题,由于__uflow直接就调用了_IO_new_file_underflow函数,那么输入缓冲区是在哪里建立的呢,为了找到这个问题的答案,我在程序进入到fscanf函数后又在malloc函数下了个断点,然后栈回溯:

1
2
3
4
5
6
7
8
9
malloc
__GI__IO_file_doallocate
__GI__IO_doallocbuf
__GI__IO_file_underflow
__GI__IO_default_uflow
__GI__IO_vfscanf
__isoc99_fscanf
main
__libc_start_main

原来是在__GI__IO_file_underflow分配的空间,回到上面看该函数的源码,确实有一段判断输入缓冲区如果为空则调用__GI__IO_doallocbuf函数建立输入缓冲区的代码,这就解释了__GI__IO_file_underflow第二步中为啥还会有个输入缓冲区判断的原因了,不得不感慨,代码写的真巧妙。

在结束之前我想总结下fread在执行系统调用read前对vtable里的哪些函数进行了调用,具体如下:

  • _IO_sgetn函数调用了vtable的_IO_file_xsgetn
  • _IO_doallocbuf函数调用了vtable的_IO_file_doallocate以初始化输入缓冲区。
  • vtable中的_IO_file_doallocate调用了vtable中的__GI__IO_file_stat以获取文件信息。
  • __underflow函数调用了vtable中的_IO_new_file_underflow实现文件数据读取。
  • vtable中的_IO_new_file_underflow调用了vtable__GI__IO_file_read最终去执行系统调用read。

先提一下,后续如果想通过IO FILE实现任意读的话,最关键的函数应是_IO_new_file_underflow,它里面有个标志位的判断,是后面构造利用需要注意的一个比较重要条件:

1
2
3
4
5
6
7
## 如果存在_IO_NO_READS标志,则直接返回
if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}

fwrite

调试程序如下

1
2
3
4
5
6
7
8
9
#include<stdio.h>

int main(){
char *data=malloc(0x1000);
FILE*fp=fopen("test","wb");

fwrite(data,1,0x30,fp);
return 0;
}

首先使用进行初步的跟踪,在fwrite下断点。看到程序首先断在_IO_fwrite函数中,在开始调试之前,仍然是先把传入的IO FILE fp值看一看:

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
pwndbg> p *_IO_list_all
$1 = {
file = {
_flags = -72540026,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7fb76a0 <_IO_2_1_stdout_>,
_fileno = 2,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x7ffff7fb87d0 <_IO_stdfile_2_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7ffff7fb6780 <_IO_wide_data_2>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7fb34a0 <_IO_file_jumps>
}

以及此时的vtable中的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> p *_IO_list_all->vtable
$2 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7e59f50 <_IO_new_file_finish>,
__overflow = 0x7ffff7e5ad80 <_IO_new_file_overflow>,
__underflow = 0x7ffff7e5aa20 <_IO_new_file_underflow>,
__uflow = 0x7ffff7e5bf50 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7e5d680 <__GI__IO_default_pbackfail>,
__xsputn = 0x7ffff7e595d0 <_IO_new_file_xsputn>,
__xsgetn = 0x7ffff7e59240 <__GI__IO_file_xsgetn>,
__seekoff = 0x7ffff7e58860 <_IO_new_file_seekoff>,
__seekpos = 0x7ffff7e5c600 <_IO_default_seekpos>,
__setbuf = 0x7ffff7e58530 <_IO_new_file_setbuf>,
__sync = 0x7ffff7e583c0 <_IO_new_file_sync>,
__doallocate = 0x7ffff7e4bc70 <__GI__IO_file_doallocate>,
__read = 0x7ffff7e595a0 <__GI__IO_file_read>,
__write = 0x7ffff7e58e60 <_IO_new_file_write>,
__seek = 0x7ffff7e58600 <__GI__IO_file_seek>,
__close = 0x7ffff7e58520 <__GI__IO_file_close>,
__stat = 0x7ffff7e58e40 <__GI__IO_file_stat>,
__showmanyc = 0x7ffff7e5d810 <_IO_default_showmanyc>,
__imbue = 0x7ffff7e5d820 <_IO_default_imbue>
}

从图里也看到由于刚经过fopen初始化,输入输出缓冲区没有建立,此时的所有指针都为空。

_IO_fwrite函数在文件/libio/iofwrite.c中:

1
2
3
4
5
6
7
8
9
10
_IO_size_t
_IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
_IO_size_t request = size * count;
...
if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
written = _IO_sputn (fp, (const char *) buf, request);
...
}
libc_hidden_def (_IO_fwrite)

没有做过多的操作就调用了_IO_sputn函数,该函数是vtable中的__xsputn_IO_new_file_xsputn)在文件/libio/fileops.c中,这里就不一次性把函数的所有源码都贴在这里,而是按部分贴在下面每个部分的开始的地方,不然感觉有些冗余。

如流程所示,源码分析分四个部分进行,与流程相对应,其中下面每部分刚开始的代码都是_IO_new_file_xsputn函数中的源码。

将目标输出数据拷贝至输出缓冲区

第一部分所包含的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{

_IO_size_t count = 0;
...
## 判断输出缓冲区还有多少空间
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */

## 如果输出缓冲区有空间,则先把数据拷贝至输出缓冲区
if (count > 0)
{
if (count > to_do)
count = to_do;
...
memcpy (f->_IO_write_ptr, s, count);
f->_IO_write_ptr += count;
## 计算是否还有目标输出数据剩余
s += count;
to_do -= count;

主要功能就是判断输出缓冲区还有多少空间,其中像demo中的程序所示的f->_IO_write_end以及f->_IO_write_ptr均为0,此时的输出缓冲区为0。

另一部分则是如果输出缓冲区如果仍有剩余空间的话,则将目标输出数据拷贝至输出缓冲区,并计算在输出缓冲区填满后,是否仍然剩余目标输出数据。

建立输出缓冲区或flush输出缓冲区

第二部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
## 如果还有目标数据剩余,此时则表明输出缓冲区未建立或输出缓冲区已经满了
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);

经过了上一步骤后,如果还有目标输出数据,表明输出缓冲区未建立或输出缓冲区已经满了,此时调用_IO_OVERFLOW函数,该函数功能主要是实现刷新输出缓冲区或建立缓冲区的功能,该函数是vtable函数中的__overflow_IO_new_file_overflow),文件在/libio/fileops.c中:

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
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
## 判断标志位是否包含_IO_NO_WRITES
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}

## 判断输出缓冲区是否为空
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);
}

## 初始化指针
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;
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)

__overflow函数首先检测IO FILE的_flags是否包含_IO_NO_WRITES标志位,如果包含的话则直接返回。

接着判断f->_IO_write_base是否为空,如果为空的话表明输出缓冲区尚未建立,就调用_IO_doallocbuf函数去分配输出缓冲区,_IO_doallocbuf函数源码在上一篇fread中已经分析过了就不跟过去了,它的功能是分配输入输出缓冲区并将指针_IO_buf_base_IO_buf_end赋值。在执行完_IO_doallocbuf分配空间后调用_IO_setg宏,该宏的定义为如下,它将输入相关的缓冲区指针赋值为_IO_buf_base指针:

1
2
#define _IO_setg(fp, eb, g, eg)  ((fp)->_IO_read_base = (eb),\
(fp)->_IO_read_ptr = (g), (fp)->_IO_read_end = (eg))

经过上面这些步骤,此时IO FILE的指针如下图所示,可以看到,_IO_buf_base_IO_buf_end被赋值,且输入缓冲区相关指针被赋值为_IO_buf_base

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
pwndbg> p *_IO_list_all
$3 = {
file = {
_flags = -72539004,
_IO_read_ptr = 0x55555555c250 "",
_IO_read_end = 0x55555555c250 "",
_IO_read_base = 0x55555555c250 "",
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x55555555c250 "",
_IO_buf_end = 0x55555555d250 "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd2540 <_IO_2_1_stderr_>,
_fileno = 3,
_flags2 = 0,
_old_offset = 0,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x55555555c100,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x55555555c110,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}

然后代码初始化其他相关指针,最主要的就是将f->_IO_write_base以及将f->_IO_write_ptr设置成f->_IO_read_ptr指针;将f->_IO_write_end赋值为f->_IO_buf_end指针。

接着就执行_IO_do_write来调用系统调用write输出输出缓冲区,输出的内容为f->_IO_write_ptrf->_IO_write_base之间的内容。跟进去该函数,函数在/libio/fileops.c中:

1
2
3
4
5
6
7
int
_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;
}
libc_hidden_ver (_IO_new_do_write, _IO_do_write)

该函数调用了new_do_write,跟进去,函数在/libio/fileops.c中:

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
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
_IO_size_t count;
...
## 额外判断
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);
...
## 刷新设置缓冲区指针
_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_SYSSEEK去调整文件偏移,这个函数就不跟进去了,正常执行流程不会过去这里。

接着就调用_IO_SYSWRITE函数,该函数是vtable中的__write_IO_new_file_write)函数,也是最终执行系统调用的地方,跟进去看,文件在/libio/fileops.c中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_IO_ssize_t
_IO_new_file_write (_IO_FILE *f, const void *data, _IO_ssize_t n)
{
_IO_ssize_t to_do = n;
while (to_do > 0)
{
## 系统调用write输出
_IO_ssize_t count = (__builtin_expect (f->_flags2
& _IO_FLAGS2_NOTCANCEL, 0)
? write_not_cancel (f->_fileno, data, to_do)
: write (f->_fileno, data, to_do));
...
return n;
}

执行完_IO_SYSWRITE函数后,回到new_do_write函数,刷新设置缓冲区指针并返回。

以块为单位直接输出数据

经历了缓冲区建立以及刷新缓冲区,程序返回到_IO_new_file_xsputn函数中,进入到如下代码功能块:

1
2
3
4
5
6
7
8
9
10
11
12
13
## 检查输出数据是否是大块
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;
}

运行到此处,此时已经经过了_IO_OVERFLOW函数(对输出缓冲区进行了初始化或者刷新),也就是说此时的IO FILE缓冲区指针的状态是处于刷新的初始化状态,输出缓冲区中也没有数据。

上面这部分代码检查剩余目标输出数据大小,如果超过输入缓冲区f->_IO_buf_end - f->_IO_buf_base的大小,则为了提高效率,不再使用输出缓冲区,而是以块为基本单位直接将缓冲区调用new_do_write输出。new_do_write函数在上面已经跟过了就是输出,并刷新指针设置。

由于demo程序只输出0x60大小的数据,而它的输出缓冲区大小为0x1000,因此不会进入该部分代码。

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

在以大块为基本单位把数据直接输出后可能还剩余小块数据,IO采用的策略则是将剩余目标输出数据放入到输出缓冲区里面,相关源码如下:

1
2
3
## 剩余的数据拷贝至输出缓冲区
if (to_do)
to_do -= _IO_default_xsputn (f, s+do_write, to_do);

程序调用_IO_default_xsputn函数对剩下的s+do_write数据进行操作,跟进去该函数,在/libio/genops.c中:

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
_IO_size_t
_IO_default_xsputn (_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. */
if (f->_IO_write_ptr < f->_IO_write_end)
{
_IO_size_t count = f->_IO_write_end - f->_IO_write_ptr;
if (count > more)
count = more;
if (count > 20)
{
## 输出长度大于20,则调用memcpy拷贝
memcpy (f->_IO_write_ptr, s, count);
f->_IO_write_ptr += count;
#endif
s += count;
}
else if (count)
{
## 小于20则直接赋值
char *p = f->_IO_write_ptr;
_IO_ssize_t i;
for (i = count; --i >= 0; )
*p++ = *s++;
f->_IO_write_ptr = p;
}
more -= count;
}
## 如果输出缓冲区为空,则调用`_IO_OVERFLOW`直接输出。
if (more == 0 || _IO_OVERFLOW (f, (unsigned char) *s++) == EOF)
break;
more--;
}
return n - more;
}
libc_hidden_def (_IO_default_xsputn)

可以看到函数最主要的作用就是将剩余的目标输出数据拷贝到输出缓冲区里。为了性能优化,当长度大于20时,使用memcpy拷贝,当长度小于20时,使用for循环赋值拷贝。如果输出缓冲区为空,则调用_IO_OVERFLOW进行输出。

根据源码我们也知道,demo程序中,最终会进入到_IO_default_xsputn中,并且把数据拷贝至输出缓冲区里,执行完成后,看到IO 结构体的数据如下:

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
pwndbg> p *_IO_list_all
$1 = {
file = {
_flags = -72536956,
_IO_read_ptr = 0x55555555c250 "",
_IO_read_end = 0x55555555c250 "",
_IO_read_base = 0x55555555c250 "",
_IO_write_base = 0x55555555c250 "",
_IO_write_ptr = 0x55555555c280 "",
_IO_write_end = 0x55555555d250 "",
_IO_buf_base = 0x55555555c250 "",
_IO_buf_end = 0x55555555d250 "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd2540 <_IO_2_1_stderr_>,
_fileno = 3,
_flags2 = 0,
_old_offset = 0,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x55555555c100,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x55555555c110,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}

可以看到此时的_IO_write_base0x55555555c250,而_IO_write_ptr 0x55555555c280,大小正好是0x30 。

至此源码分析结束。

其他输出函数

fwrite分析完了,知道它最主要的就是通过vtable函数里面的_IO_new_file_xsputn实现功能,且最终的建立以及刷新输出缓冲区是在_IO_new_file_overflow函数里面,最终执行系统调用write对数据进行输出是在new_do_write函数中。

下面来看一下其他输出函数的栈回溯的情况,应该也都差不多,对于下面的函数,断点下在write函数,然后查看栈回溯。

首先是printf函数,它的栈回溯为:

1
2
3
4
5
6
7
8
9
write
_IO_new_file_write
new_do_write+51
__GI__IO_do_write
__GI__IO_file_xsputn
vfprintf
printf
main
__libc_start_main

也是调用_IO_new_file_overflow函数进行的实现。但是printf函数里面情况其实也还挺复杂的,篇幅的限制,就不细说了,其他的输出函数应该也差不多。

结束之前仍然总结下fwrite在执行系统调用write前对vtable里的哪些函数进行了调用,具体如下:

  • _IO_fwrite函数调用了vtable的_IO_new_file_xsputn
  • _IO_new_file_xsputn函数调用了vtable中的_IO_new_file_overflow实现缓冲区的建立以及刷新缓冲区。
  • vtable中的_IO_new_file_overflow函数调用了vtable的_IO_file_doallocate以初始化输入缓冲区。
  • vtable中的_IO_file_doallocate调用了vtable中的__GI__IO_file_stat以获取文件信息。
  • new_do_write中的_IO_SYSWRITE调用了vtable_IO_new_file_write最终去执行系统调用write。

同时,后续如果想通过IO FILE输出缓冲区实现任意读写的话,最关键的函数应是_IO_new_file_overflow,它里面有个标志位的判断,是后面构造利用需要注意的一个比较重要条件:

1
2
3
4
5
6
7
## 判断标志位是否包含_IO_NO_WRITES
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}

Reference

https://ray-cp.github.io/page6/