进程中的 FILE 结构会通过_chain 域彼此连接形成一个链表,链表头部用全局变量_IO_list_all 表示,通过这个值我们可以遍历所有的 FILE 结构。

啥时chain域? 如何在gdb中调试打印呢?

我们可以在 libc.so 中找到 stdin\stdout\stderr 等符号,这些符号是指向 FILE 结构的指针,真正结构的符号是

readelf怎么找符号来?

readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep write@

1
2
3
_IO_2_1_stderr_
_IO_2_1_stdout_
_IO_2_1_stdin_
1
2
3
4
5
6
7
pwn# readelf -s /lib/x86_64-linux-gnu/libc.so.6   |grep stdin
378: 00000000001ec980 224 OBJECT GLOBAL DEFAULT 34 _IO_2_1_stdin_@@GLIBC_2.2.5
547: 00000000001ed790 8 OBJECT GLOBAL DEFAULT 34 stdin@@GLIBC_2.2.5
pwn# readelf -s /lib/x86_64-linux-gnu/libc.so.6 |grep _IO_2_1_st
378: 00000000001ec980 224 OBJECT GLOBAL DEFAULT 34 _IO_2_1_stdin_@@GLIBC_2.2.5
852: 00000000001ed6a0 224 OBJECT GLOBAL DEFAULT 34 _IO_2_1_stdout_@@GLIBC_2.2.5
1427: 00000000001ed5c0 224 OBJECT GLOBAL DEFAULT 34 _IO_2_1_stderr_@@GLIBC_2.2.5

FILE 结构

https://blog.csdn.net/xy_369/article/details/130874848

​ 关于代码, 代码存在于glibc/libio/中 主要是libio.h

​ FILE结构被一系列流操作函数(fopen() fread() fclose())等所使用,大多数的FILE结构保存在堆上(stdin、stdout、stderr除外,位于libc数据段),其指针动态创建并由fopen函数返回

_IO_FILE_plus

​ 在libc2.23版本中,这个结构体是_IO_FILE_plus, 包含了一个 _IO_FILE结构体和一个指向 _IO_jump_t结构体的指针vtable

libioP.h

1
2
3
4
5
6
7
8
9
10
/* We always allocate an extra word following an _IO_FILE.
This contains a pointer to the function jump table used.
This is for compatibility with C++ streambuf; the word can
be used to smash to a pointer to a virtual function table. */

struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};

​ 各种文件结构( _IO_FILE )采用单链表的形式连接起来( _chain域),通过 _IO_list_all访问

​ vtable为函数指针结构体,存放着各种IO相关函数的指针

image-20230807102222274

像_IO_FILE_plus这种这么打印呢

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
pwndbg> p &_IO_list_all
$1 = (struct _IO_FILE_plus **) 0x7ffff7fb05a0 <_IO_list_all>


pwndbg> p /x *(struct _IO_FILE_plus*) _IO_list_all
$3 = {
file = {
_flags = 0xfbad2086,
_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 = 0x7ffff7fb06a0,
_fileno = 0x2,
_flags2 = 0x0,
_old_offset = 0xffffffffffffffff,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = {0x0},
_lock = 0x7ffff7fb17d0,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7ffff7faf780,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0x0,
_unused2 = {0x0 <repeats 20 times>}
},
vtable = 0x7ffff7fac4a0
}

vtable 是 IO_jump_t 类型的指针,IO_jump_t 中保存了一些函数指针,在后面我们会看到在一系列标准 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
pwndbg> p /x *(struct _IO_jump_t*) _IO_list_all.vtable
$2 = {
__dummy = 0x0,
__dummy2 = 0x0,
__finish = 0x7ffff7e52f50,
__overflow = 0x7ffff7e53d80,
__underflow = 0x7ffff7e53a20,
__uflow = 0x7ffff7e54f50,
__pbackfail = 0x7ffff7e56680,
__xsputn = 0x7ffff7e525d0,
__xsgetn = 0x7ffff7e52240,
__seekoff = 0x7ffff7e51860,
__seekpos = 0x7ffff7e55600,
__setbuf = 0x7ffff7e51530,
__sync = 0x7ffff7e513c0,
__doallocate = 0x7ffff7e44c70,
__read = 0x7ffff7e525a0,
__write = 0x7ffff7e51e60,
__seek = 0x7ffff7e51600,
__close = 0x7ffff7e51520,
__stat = 0x7ffff7e51e40,
__showmanyc = 0x7ffff7e56810,
__imbue = 0x7ffff7e56820
}

初始情况

​ 初始情况下_IO_FILE 结构有_IO_2_1_stderr__IO_2_1_stdout__IO_2_1_stdin_ 三个,通过 _IO_list_all连接起来

stdfiles.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \
static struct _IO_wide_data _IO_wide_data_##FD \
= { ._wide_vtable = &_IO_wfile_jumps }; \
struct _IO_FILE_plus NAME \
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, &_IO_wide_data_##FD), \
&_IO_file_jumps};
# else
# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \
struct _IO_FILE_plus NAME \
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, NULL), \
&_IO_file_jumps};
# endif
#endif

DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);

struct _IO_FILE_plus *_IO_list_all = &_IO_2_1_stderr_;
libc_hidden_data_def (_IO_list_all)

​ 并且存在 3 个全局指针 stdinstdoutstderr 分别指向 _IO_2_1_stdin__IO_2_1_stdout__IO_2_1_stderr_ 三个结构体。

stdio.c

1
2
3
4
5
6
7
8
9
10
#undef stdin
#undef stdout
#undef stderr
_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;
_IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;
_IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;

#undef _IO_stdin
#undef _IO_stdout
#undef _IO_stderr

​ 于是初始化后的结构如下,可以看到是头插法插入新来的iofile

b71716ff58f043fbec0605f33b934102

fopen

​ fopen在标准IO库中用于打开文件,函数原型如下

1
FILE *fopen(char *filename, *type);

源码分析如下

include/stdio.h

1
2
3
#  if IS_IN (libc)
extern _IO_FILE *_IO_new_fopen (const char*, const char*);
# define fopen(fname, mode) _IO_new_fopen (fname, mode)

libio/iofopen.c

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

​ 具体代码分析

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 *
__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)); //调用malloc分配FILE结构的空间,从这里也可以知道FILE结构存储在堆中

if (new_f == NULL)
return NULL;
....
_IO_JUMPS (&new_f->fp) = &_IO_file_jumps;//初始化vtable
_IO_file_init (&new_f->fp);//调用函数进行进一步初始化操作

if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL)//打开目标文件,最后会调用到系统调用open
return __fopen_maybe_mmap (&new_f->fp.file);

_IO_un_link (&new_f->fp);
free (new_f);
return NULL;
}

fileops.c: _IO_file_init

1
2
3
4
5
6
7
8
9
10
11
12
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);//把新分配的FILE链入_IO_list_all为起始的FILE链表中
fp->file._fileno = -1;
}

genops.c 但是这个操作不是会让fp成为头部嘛????????

1
2
3
4
5
6
7
8
9
10
11
12
13
void
_IO_link_in (struct _IO_FILE_plus *fp)
{
if ((fp->file._flags & _IO_LINKED) == 0)
{
fp->file._flags |= _IO_LINKED;

fp->file._chain = (_IO_FILE *) _IO_list_all;
_IO_list_all = fp;
++_IO_list_all_stamp;

}
}

fread

​ 可以看下参考链接1给的流程图,结合代码来看

libio/iofread.c

fwrite

​ 标准IO库函数,用于向文件流中写入数据, 函数原型如下

1
size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);
  • buffer: 是一个指针,对 fwrite 来说,是需要写入文件的数据的地址;
  • size: 要写入内容的单字节数;
  • count: 要进行写入 size 字节的数据项的个数;
  • stream: 目标文件指针;(要写入的文件)
  • 返回值:实际写入的数据项个数 count。

libio/iofwrite.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
_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;
_IO_size_t written = 0;
CHECK_FILE (fp, 0);
if (request == 0)
return 0;
_IO_acquire_lock (fp);
if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
written = _IO_sputn (fp, (const char *) buf, request);
_IO_release_lock (fp);
/* We have written all of the input in case the return value indicates
this or EOF is returned. The latter is a special case where we
simply did not manage to flush the buffer. But the data is in the
buffer and therefore written as far as fwrite is concerned. */
if (written == request || written == EOF)
return count;
else
return written / size;
}
libc_hidden_def (_IO_fwrite)

​ 主要调用了 _IO_sputn 来实现写入的功能, _IO_sputn 位于 _IO_FILE_plus 的 vtable 中,调用这个函数需要首先取出 vtable 中的指针,再跳过去进行调用。 它对应了函数 _IO_new_file_xsputn,咋对应的…

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
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
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
const char *s = (const char *) data;
_IO_size_t to_do = n;
int must_flush = 0;
_IO_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)
{
const char *p;
for (p = s + n; p > s; )
{
if (*--p == '\n')
{
count = p - s + 1;
must_flush = 1;
break;
}
}
}
}
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)
count = to_do;
#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;
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)
/* If nothing else has to be written we must not signal the
caller that everything has been written. */
return 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)
{
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;
}
libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)

​ 这里主要会调用同样位于vtable中的_IO_OVERFLOW,对应函数是 _IO_new_file_overflow

fileops.c

1
2
3
4
5
6
7
8
9
10
11
12
13
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
...........
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;
..........
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)

​ 在这里会最终调用write系统调用

fclose

伪造 劫持

​ vtable 劫持分为两种,一种是直接改写 vtable 中的函数指针,通过任意地址写就可以实现。另一种是覆盖 vtable 的指针指向我们控制的内存,然后在其中布置函数指针。

​ 伪造 vtable 劫持程序流程的中心思想就是针对_IO_FILE_plus 的 vtable 动手脚,通过把 vtable 指向我们控制的内存,并在其中布置函数指针来实现。

奇怪,必须要加\n , 和缓冲区刷新有关

gcc -g 可以在调试的时候….

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int main(void)
{
FILE *fp;
long long *vtable_ptr;
fp=fopen("123.txt","rw");
vtable_ptr=*(long long*)((long long)fp+0xd8); //get vtable
printf("vtable_ptr: %p\n", (void*)vtable_ptr); // 打印vtable_ptr的值
printf("addr1:%p\n", (void*)vtable_ptr); // 打印vtable_ptr的值
printf("addr2:%p\n ",(void*)vtable_ptr);
vtable_ptr[7]=0x41414141; //xsputn

printf("call 0x41414141");
}

image-20230805203530314 image-20230805203540676

​ 但是在目前 libc2.23 版本下,位于 libc 数据段的 vtable 是不可以进行写入的。不过,通过在可控的内存中伪造 vtable 的方法依然可以实现利用。

​ vtable_addr[0]位于堆中,可以写,这样就相当于要伪造整个vtable表

示例代码

​ system_ptr根据实际情况修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

#define system_ptr 0x7ffff7a523a0;

int main(void)
{
FILE *fp;
long long *vtable_addr,*fake_vtable;

fp=fopen("123.txt","rw");
fake_vtable=malloc(0x40);

vtable_addr=(long long *)((long long)fp+0xd8); //vtable offset

vtable_addr[0]=(long long)fake_vtable;

memcpy(fp,"sh",3);

fake_vtable[7]=system_ptr; //xsputn

fwrite("hi",2,1,fp);
}

​ 可以看到伪造效果,vtable被覆盖为堆上的地址了

image-20230806114845070

​ fake_vtable[7]=system_ptr; 再把堆上相应内容改成system的地址就可以了

image-20230806115013625

​ 本地尝试不成功,但在gdb中调试的时候是成功的,怀疑是随机化的问题,关闭随机化就可以了

1
echo 0 > /proc/sys/kernel/randomize_va_space 

image-20230806114653707

参考链接

https://blog.csdn.net/qq_45323960/article/details/123810198 大部分图都是参考的这个师傅的,写得非常好! 推荐看原文

ctf-wiki

https://blog.csdn.net/xy_369/article/details/130874848