之前做的一个CTF题没有头绪,然后看了wp以后作者利用了结构体,对这东西并不了解,所以学习了一波
FILE结构体
我们在使用标准库的时候会使用fopen,这个函数会返回一个结构体,并且文件的结构体被存放在堆中.
在绝大多数情况下,某个操作有其对应的逆操作,那么它的指针可能就被定义在堆区.
我们首先看一下FILE结构体的相关代码
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;
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
};
其中flag的前两个字节被称为头,其余的字节表示了某个文件的读写情况
/* Magic number and bits for the _flags field. The magic number is
mostly vestigial, but preserved for compatibility. It occupies the
high 16 bits of _flags; the low 16 bits are actual flag bits. */
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED 0x0002
#define _IO_NO_READS 0x0004 /* Reading not allowed. */
#define _IO_NO_WRITES 0x0008 /* Writing not allowed. */
#define _IO_EOF_SEEN 0x0010
#define _IO_ERR_SEEN 0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close. */
#define _IO_LINKED 0x0080 /* In the list of all open files. */
#define _IO_IN_BACKUP 0x0100
#define _IO_LINE_BUF 0x0200
#define _IO_TIED_PUT_GET 0x0400 /* Put and get pointer move in unison. */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
/* 0x4000 No longer used, reserved for compat. */
#define _IO_USER_LOCK 0x8000
flag字段后面的字段则表示相关读写缓冲区的地址,ptr表示当前地址,base表示起始地址,end表示结束地址.
并且所有的结构体被_chain串成了一个链表,链表头为_IO_list_all,它是一个全局变量.打开一个文件的时候默认三个流将会被打开,stdin、stdout、stderr,它们是在链表的最后面,每多打开一个文件流,将会使用头插法将文件结构体放在链表的头部
后面还会根据情况对结构体进行扩展,但是我们主要讲可以最基本的,后面的大家可以自己进行了解.
我们的FILE结构体其实在它的外面还有另一层结构体
struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}
也就是说我们的FILE结构体其实本质上是_IO_FILE结构体。并且vatable指针指向了某些函数指针。我们看到vatable指针的数据类型是IO_jump_t,其实在该类型中有很多函数指针,其他的一些IO操作就使用到了这些函数指针
所以我们可以看见我之前画的图不是很准确,体现不出结构体的层次感,下面还有一个图比较规范和标准
标准库IO
想要调用IO进行操作,有标准库和系统库两种,但是我们每次都是首选使用标准库.因为标准库的可移植性比较好。但是标准库其实就是调用了系统库。所以我们每回不需要关心我们是在Linux系统还是Windows系统。
fread函数
size_t fread ( void *buffer, size_t size, size_t count, FILE *stream) ;
这是该函数的定义,我们会发现它的返回值是size_t类型的。该函数的作用就是从stream按照size为一组,一共count组的形式输入到buffer中去。这个函数返回实际被成功输入的组数.
fread函数的真实函数名为_IO_fread,但是其功能实现其实是在函数_IO_sgetn 中
_IO_size_t
_IO_fread (buf, size, count, fp)
void *buf;
_IO_size_t size;
_IO_size_t count;
_IO_FILE *fp;
{
...
bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
...
}
在_IO_sgetn 函数中会调用_IO_XSGETN,而_IO_XSGETN 是_IO_FILE_plus.vtable 中的函数指针,在调用这个函数时会首先取出 vtable 中的指针然后再进行调用。
_IO_size_t
_IO_sgetn (fp, data, n)
_IO_FILE *fp;
void *data;
_IO_size_t n;
{
return _IO_XSGETN (fp, data, n);
}
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_xsgetn 函数的
if (fp->_IO_buf_base
&& want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
{
if (__underflow (fp) == EOF)
break;
continue;
}
fwrite函数
该函数和fread函数类似,就是把输入变成了输出size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);
fwrite函数和fread函数差不多,fwrite函数的原型是_IO_fwrite,而它是调用_IO_XSPUTN函数来实现写入的功能.这个函数在调用的时候也需要取出vatable中的指针,然后再进行调
用written = _IO_sputn (fp, (const char *) buf, request);
在_IO_XSPUTN 对应的默认函数_IO_new_file_xsputn 中会调用同样位于 vtable 中的_IO_OVERFLOW
/* Next flush the (full) buffer. */ if (_IO_OVERFLOW (f, EOF) == EOF)
_IO_OVERFLOW 默认对应的函数是_IO_new_file_overflow
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return EOF;
在_IO_new_file_overflow 内部最终会调用系统接口 write 函数
fopen
fopen 在标准 IO 库中用于打开文件,函数原型如下
FILE *fopen(char *filename, *type);
在 fopen 内部会创建 FILE 结构并进行一些初始化操作,下面来看一下这个过程
首先在 fopen 对应的函数__fopen_internal 内部会调用 malloc 函数,分配 FILE 结构的空间。因此我们可以获知 FILE 结构是存储在堆上的
*new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));
之后会为创建的 FILE 初始化 vtable,并调用_IO_file_init 进一步初始化操作
_IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
_IO_file_init (&new_f->fp);
在_IO_file_init 函数的初始化操作中,会调用_IO_link_in 把新分配的 FILE 链入_IO_list_all 为起始的 FILE 链表中
void
_IO_link_in (fp)
struct _IO_FILE_plus *fp;
{
if ((fp->file._flags & _IO_LINKED) == 0)
{
fp->file._flags |= _IO_LINKED;
fp->file._chain = (_IO_FILE *) _IO_list_all;
_IO_list_all = fp;
++_IO_list_all_stamp;
}
}
之后__fopen_internal 函数会调用_IO_file_fopen 函数打开目标文件,_IO_file_fopen 会根据用户传入的打开模式进行打开操作,总之最后会调用到系统接口 open 函数,这里不再深入。
if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL)
return __fopen_maybe_mmap (&new_f->fp.file);
fclose
fclose 是标准 IO 库中用于关闭已打开文件的函数,其作用与 fopen 相反。
int fclose(FILE *stream)
功能:关闭一个文件流,使用 fclose 就可以把缓冲区内最后剩余的数据输出到磁盘文件中,并释放文件指针和有关的缓冲区
fclose 首先会调用_IO_unlink_it 将指定的 FILE 从_chain 链表中脱链
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
_IO_un_link ((struct _IO_FILE_plus *) fp);
之后会调用_IO_file_close_it 函数,_IO_file_close_it 会调用系统接口 close 关闭文件
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp);
最后调用 vtable 中的_IO_FINISH,其对应的是_IO_file_finish 函数,其中会调用 free 函数释放之前分配的 FILE 结构。
_IO_FINISH (fp);
这就是FILE相关的一些结构体,有些东西现在还没办法深入,再了解一段时间就可以继续深入下去,但是了解了这些大概对IO的利用有了一个想法和思路,后面就是实践
本位参考自CTFwiki、FILE结构体及漏洞利用方法,感谢大佬们
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)