程序执行fopen等函数时会创建FILE结构,并分配在堆中,我们常定义一个指向FILE结构的指针来接收这个返回值。进程中的FILE结构会通过_chain域彼此连接形成一个单向链表,链表头部用全局变量_IO_list_all表示,通过这个值我们可以遍历所有的FILE结构,新加一个FILE节点会从链表头插入,同时更新链表头为这个新节点。
在标准I/O库中,每个程序启动时有三个文件流是自动打开的:stdin、stdout、stderr。因此在初始状态下,_IO_list_all指向了一个有这些文件流构成的链表(_IO_list_all=stderr->stdout->stdin)。我们可以在libc.so中找到stdin、stdout、stderr等符号,这些符号是指向FILE结构的指针,真正结构的符号是_IO_2_1_stderr_, _IO_2_1_stdout_, _IO_2_1_stdin_
。
pwndbg> x/100gx &_IO_list_all
0x7ffff7dd2520 <_IO_list_all>: 0x00007ffff7dd2540 0x0000000000000000 #point to stderr
0x7ffff7dd2530: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2540 <_IO_2_1_stderr_>: 0x00000000fbad2087 0x00007ffff7dd25c3
...
0x7ffff7dd25a0 <_IO_2_1_stderr_+96>: 0x0000000000000000 0x00007ffff7dd2620 #point to stdout
...
0x7ffff7dd2610 <_IO_2_1_stderr_+208>: 0x0000000000000000 0x00007ffff7dd06e0 #vtable
0x7ffff7dd2620 <_IO_2_1_stdout_>: 0x00000000fbad2887 0x00007ffff7dd26a3
...
0x7ffff7dd2680 <_IO_2_1_stdout_+96>: 0x0000000000000000 0x00007ffff7dd18e0 #point to stdin
...
0x7ffff7dd26f0 <_IO_2_1_stdout_+208>: 0x0000000000000000 0x00007ffff7dd06e0 #vtable
pwndbg> x/50gx &_IO_2_1_stdin_
0x7ffff7dd18e0 <_IO_2_1_stdin_>: 0x00000000fbad208b 0x00007ffff7dd1964
...
0x7ffff7dd1940 <_IO_2_1_stdin_+96>: 0x0000000000000000 0x0000000000000000 #point to null
...
0x7ffff7dd19b0 <_IO_2_1_stdin_+208>: 0x0000000000000000 0x00007ffff7dd06e0 #vtable
注意:这三个文件流位于libc.so的数据段,而我们使用fopen创建的文件流是分配在堆内存上的。
FILE *fopen(char *filename, *type)
1 |
|
- 在fopen对应的函数__fopen_internal内部会调用malloc函数,分配locked_FILE(包含_IO_FILE_plus包含FILE)结构的空间。因此我们可以获知FILE结构是存储在堆上的
- 为创建的FILE初始化vtable
- 调用_IO_file_init把新分配的FILE链入_IO_list_all为起始的FILE链表中
- 调用_IO_file_fopen函数打开目标文件,_IO_file_fopen会根据用户传入的打开模式进行打开操作,最后调用系统接口open函数
1 |
|
由于sizeof(FILE)=216=0xd8,因此通过fopen返回的FILE指针+0xd8即可以访问_IO_FILE_plus中的vtable:
1 |
|
vtable保存了一系列函数指针,stdio函数会调用这些函数指针进行相应的操作。
size_t fread ( void *buffer, size_t size, size_t count, FILE *stream)
1 |
|
_IO_XSGETN是_IO_FILE_plus.vtable中的xsgetn函数指针(默认是_IO_file_xsgetn),index=8,并且,FILE指针fp会作为第一个参数传递给该函数指针。
1 |
|
_IO_file_xsgetn会调用vtable中的_IO_read(默认是_IO_file_read),index=14,并且,FILE指针fp会作为第一个参数传递给该函数指针。最终会调用系统接口read函数:
1 |
|
因此fread对于调用vtable的顺序为:
8 JUMP_INIT(xsgetn, _IO_file_xsgetn)
14 JUMP_INIT(read, _IO_file_read)
size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream)
1 |
|
_IO_XSPUTN是_IO_FILE_plus.vtable中的xsputn函数指针(默认是_IO_new_file_xsputn),index=7,并且,FILE指针fp会作为第一个参数传递给该函数指针。
1 |
|
由于_IO_new_file_xsputn会调用vtable中的_IO_overflow(默认是_IO_new_file_overflow),index=3,并且,FILE指针fp会作为第一个参数传递给该函数指针。
1 |
|
_IO_new_file_overflow会调用vtable中的_IO_write(默认是_IO_new_file_write),index=15,并且,FILE指针fp会作为第一个参数传递给该函数指针。最终会调用系统接口write函数:
1 |
|
int printf (const char * format, …)/int puts (const char * str)
1 |
|
printf和puts是常用的输出函数,在printf的参数是以’\n’结束的纯字符串时,printf会被优化为puts函数并去除换行符。
puts在源码中实现的函数是_IO_puts,这个函数的操作与fwrite的流程大致相同,可见,同样是调用_IO_sputn即调用vtable中的xsputn函数指针(默认是_IO_new_file_xsputn),index=7,并且,FILE指针fp会作为第一个参数传递给该函数指针。最终会调用系统接口write函数。
如printf的调用栈为:
vfprintf+11 -> _IO_file_xsputn ->_IO_file_overflow -> funlockfile -> _IO_file_write -> write
因此fwrite/printf/puts对于调用vtable的顺序为:
7 JUMP_INIT(xsputn, _IO_file_xsputn)
3 JUMP_INIT(overflow, _IO_file_overflow)
15 JUMP_INIT(write, _IO_new_file_write)
int fclose(FILE *stream)
1 |
|
fclose依次调用vtable中的_IO_close指针,index=17和_IO_finish指针,index=2。最终会调用系统调用close函数。
1 |
|
因此fclose对于调用vtable的顺序为:
17 JUMP_INIT(close, _IO_file_close)
2 JUMP_INIT(finish, _IO_file_finish)