FILE结构体及漏洞利用方法

2019-12-30  本文已影响0人  Fish_o0O

0x00 写在前面

第一次接触FILE结构体,是在BCTF2018baby_arena中,利用了FSOP,当时感觉结构体中字段太多了,没有理清楚到底是什么关系。也是时隔非常非常非常久,咸鱼的我终于开始系统的学习一下FILE结构体。本篇基于libc-2.27源码对该版本文件流的数据结构、相关操作以及一些常见的漏洞利用方法进行分析。

0x01 数据结构

struct _IO_FILE_plus
{
    FILE file;
    const struct _IO_jump_t *vtable;
}

中间包含了一个我们常说的FILE结构体,以及_IO_jump_t的一个虚表结构体。

typedef struct _IO_FILE FILE;

_IO_FILE结构体在glibc/libio/bits/libio.h中定义如下:

struct _IO_FILE
{
    int _flags;  /* High-order word is _IO_MAGIC; rest is flags. */

    /* The following pointers correspond to the C++ streambuf protocol. */
    char *_IO_read_ptr;        /* Current read pointer */
    char *_IO_read_end;      /* End of get area. */
    char *_IO_read_base;    /* Start of puback+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_FILE *_chain;
    int _fileno;
    ......
    int _flags2;
    ......
#ifdef _IO_USE_OLD_IO_FILE
};

struct _IO_FILE_complete
{
    struct _IO_FILE _file;
#endif
    ......
    size_t __pad5;
    int _mode;
    char _unused2[15 * sizeof(int) - 4 * sizeof(void*) - sizeof(size_t)];
};

其中_flags字段标志了该FILE结构体的读写等属性,该字段的前2个字节固定为0xFBAD的魔术头,其具体数值在glibc/libio/libio.h中进行宏定义如下:

/* 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

_IO_read_ptr字段为输入缓冲区的当前地址
_IO_read_end字段为输入缓冲区的结束地址
_IO_read_base字段为输入缓冲区的起始地址
_IO_write_base字段为输出缓冲区的起始地址
_IO_write_ptr字段为输出缓冲区的当前地址
_IO_write_end字段为输出缓冲区的结束地址
_IO_buf_base字段为输入输出缓冲区的起始地址
_IO_buf_end字段为输入输出缓冲区的结束地址
_chain字段为指向下一个_IO_FILE结构体的指针,在gilbc/libio/libioP.h中有如下声明:

extern struct _IO_FILE_plus *_IO_list_all;

该变量为一个单链表的头结点,该单链表用于管理程序中所有的FILE结构体,并通过_chain字段索引下一个FILE结构体,每个程序中该链表的最后3个节点从后往前固定为_IO_2_1_stdin_IO_2_1_stdout_IO_2_1_stderr,之前是用户新申请的FILE结构体,每次新申请的FILE结构体会插在该链表的表头。大概长成下面这样:

_IO_list_all
值得注意的是,在_IO_FILE结构体定义的内部有一个宏#ifdef _IO_USE_OLD_IO_FILE,如果不存在_IO_USE_OLD_IO_FILE的宏定义,则会将后面的}以及下一个结构体_IO_FILE_complete的定义头给跳过,即扩充了_IO_FILE结构体,使其拥有了更多的字段。_IO_2_1_stdin_IO_2_1_stdout_IO_2_1_stderrFILE结构体均为扩展后的。比如某次调试中的_IO_2_1_stdout结构如下(从_lock之后到vtable之前的字段均为扩展后的):
_IO_2_1_stdin_
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
};

如上图所示,所有的FILE结构体的虚表指针均指向虚表_IO_file_jumps,在进行IO操作时,都会调用到该结构体中的函数。

0x02 相关操作

fopen

fopenstdio库中的函数,其在glibc/include/stdio.h中宏定义如下:

#define fopen(fname, mode) _IO_new_fopen(fname, mode)

stdio.h宏定义可知,平时我们常用的fopen函数其实为定义在glibc/libio/iofopen.c中的_IO_new_fopen函数,该函数直接调用了__fopen_internal函数。

__fopen_internal
#define _IO_JUMPS(THIS) (THIS)->vtable

即将_IO_FILE_plus结构体中的虚表指针赋值为虚表_IO_file_jumps的地址。

versioned_symbol(libc, _IO_new_file_fopen, _IO_file_fopen, GLIBC_2_1);

_IO_file_fopen函数等价于_IO_new_file_fopen函数,该函数定义于同一文件的第211行(太长了就不一次性全部贴了)。

_IO_new_file_fopen(1)
_IO_new_file_fopen4个参数,分别是文件指针、文件名、属性、是否为32位,其中第一个参数为前面步骤初始化的_IO_FILE结构体指针,第2、3两个参数为用户在调用stdio.hfopen函数传入的参数,第四个参数为glibc/libio、iofopen.c_IO_new_fopen函数调用__fopen_internal函数时传入的常亮1。该段代码除了声明变量外主要进行了2个操作:检查该文件流是否打开、根据调用参数的主属性为该文件流添加flag
第一个操作通过调用_IO_file_is_open函数来实现,该函数在glibc/libio/libioP.h中宏定义如下:
#define _IO_file_is_open(__fp) ((__fp)->_fileno != -1)

即通过检查FILE结构体的_fileno是否为合法序号来判断检该文件流是否为已打开状态。
第二个操作则是通过mode,即fopen函数第二个参数的第一个字符来确定该文件流的属性,并添加对应的flag。在写入flag字段前,代码中有3个比那里那个来分别存储不同的属性,这三个变量分别是omodeoflagsread_write,其中omode标志文件的读写属性,oflags标志文件的修改方式,read_write标志文件内容的读写方式。有如下对应关系:

mode omode oflags read_write
r O_RDONLY(只读) NULL(无) _IO_NO_WRITES(不给写)
w O_WRONLY(只写) O_CREAT|O_TRUNC(新建/覆盖) _IO_NO_READS(不给读)
a O_WRONLY(只写) O_CREAT|O_APPEND(新建/追加) _IO_NO_READS|_IO_IS_APPENDING(不给读/给追加)

_IO_new_file_fopen继续往后走,代码如下:

_IO_new_file_fopen(2)
该段代码主要进行了2个操作:通过文件流副属性获取对应的flag、调用_IO_file_open函数打开文件。
第一个操作与主属性的表示相似,副属性有如下的对应关系:
mode omode oflags read_write _flags2
+ O_RDWR(可读可写) 不变 &_IO_IS_APPENDING 不变
x 不变 O_EXCL 不变 不变
b 不变 不变 不变 不变
m 不变 不变 不变 _IO_FLAGS32_MMAP
c 不变 不变 不变 _IO_FLAGS2_NOTCANCEL
e 不变 O_CLOEXEC 不变 _IO_FLAGS2_CLOEXEC

在将所有附属性遍历完后,会调用_IO_file_open函数用于打开文件并返回句柄,该函数有6个参数,该函数在glibc/libio/fileops.c中定义如下:

_IO_file_open
该函数中,首先会判断FILE结构体的_flags2是否有_IO_FLAGS2_NOTCANCEL位,即是否含有c的副属性,若有则会调用__open_nocancel函数,若无则会调用__open函数,从这两个函数传入了相同的参数可以看出,这两个函数实现了相似的功能,两个函数在glibc/sysdeps/unix/sysv/linux/open64.c中有宏定义如下:
strong_alias(__libc_open64 ,  __open);
...
strong_alias(__open64_nocancel, __open_nocancel);

以及还有在某些情况__open64_nocancel函数可以等价为__libc_open64函数的定义。在同一文件中,两个函数定义如下:

__libc_open64
__open64_nocancel
可以看到这两个函数在return时均调用了INLINE_SYSCALL_CALL函数,即到最后将带有文件修改方式和读写属性的flag作为参数,调用SYSCALL进行打开文件操作,并将句柄返回。(再往底层就是直接宏定义汇编代码,就不继续深究INLINE_SYSCALL_CALL函数内部了)
返回后,回到_IO_file_open函数中,接下来将打开文件后的文件流序号赋值给_fileno字段,之后调用了_IO_mask_flags将具有读写方式的属性加入FILE结构体的flags字段中,若读写方式为a(追加),则会将文件末尾作为文件的偏移。最后会调用_IO_link_in函数确保该结构体已链入_IO_list_all链表(因为在_IO_link_in函数中会有对_IO_LINKEDcheck,所以并不是重复链入),至此_IO_file_open函数执行完毕。
_IO_file_open返回后回到_IO_new_file_fopen函数,之后有的一个大段的if语句中,大概是给之前初始化的_wide_data中的各元素进行赋值,在if函数的最后将FILE结构体中的_mode字段赋值为1。该段代码大概如下(语句太长就不贴了):
if(result != NULL)
{
    cs = strstr(last_recognized +1 , ",ccs=");
    if(cs != NULL)
    {
        ......                  //大概是给_wide_data中的各元素赋值
        result->_mode = 1;
    }
}

return result;

以上,完成了对fopen函数源码的分析,该函数主要进行了3个操作:为文件流申请空间;初始化FILE结构体及虚表,包括将文件流链入_IO_list_all链表中;打开文件流,包括读取文件属性以及利用系统调用打开文件。
通过阅读源码,对文件属性有了船新的认识:

  • 除了日常用到的r/w/a/b/+之外还有x/m/c/e4个属性,而且作为主属性的r/w/a必须在fopen第二个参数的开头,即只能wb而不能bw
  • 在正常编写代码时宏观能够感到打开文件方式不一样的属性有r/w/a/+/x,而m/c/e这三个属性的采用,仅仅会在系统进行打开文件操作过程中进行一些不太影响大局的判断操作,在宏观上感觉不到。这可能也是FILE结构体中_flags_flags2两个字段的区别。
  • 在上一点中所没提到的b属性,虽然一直知道是以二进制方式打开文件,但是在_IO_new_file_fopen函数中的关于副属性的switch...case语句中,b属性并没有什么卵用。没有加入任何flag标志位,只是将last_recognized赋值为b,即最后一个识别的属性是b。就算不考虑后续代码,只看switch...case语句的结果,当b属性后面跟有其他属性,那么b属性的case中没有留下任何东西(其他属性多多少少修改了_flags/_flags2字段)。

fread

fread函数的一般用法为

fread( void *buffer, size_t size, size_t count, FILE *stream );

该函数共有4个参数,buffer代表接收从文件读取数据的变量首地址,size代表每个对象的大小,count代表对象的个数,stream是代表文件流。即该函数实现了从stream中读size * count字节数据并赋给buffer所指向的地址。该函数在glibc/libio/iofread.c中有如下宏定义:

weak_alias(_IO_fread , fread)

fread函数原形为_IO_fread函数,该函数在同一文件中定义如下:

_IO_fread
该函数的最外层代码比较短,逻辑也很清晰,首先在第34行调用了CHECK_FILE函数对将要输入的文件流进行检查,该函数在glibc/libio/libioP.h中有宏定义如下:
CHECK_FILE
即当IO_DEBUG被定义时,会对FILE结构体的_flags字段进行_IO_MAGIC_MASK位的验证,若不存在,则说明传进来的不是FILE结构体,就return 0
接下来在函数的第37、39行分别调用了_IO_acquire_lock_IO_release_lock函数,用来加锁以及去锁。
在中间的第38行,调用了_IO_sgetn函数进行读入数据操作。经过辗转后发现该函数为虚表中的__xsgetn,即_IO_file_xsgetn函数,该函数定义于glibc/libio/fileop.c中,是fread函数的关键。
_IO_file_xsgetn(1)
该函数中定义了4个变量,分别是want表示还要读入的数据字节数、have表示输入缓冲区中剩余的空间大小、count表示要读出数据的个数、s表示接收读出数据的变量地址。由于我们刚从fopen初始化过来,因此FILE结构体中的各字段仍是空值,因此会进入在第1302-1311行的if-else语句,该语句首先判断该文件流中的_IO_save_base字段是否已经赋值,即文件流是否有备份的缓冲区,若有则会将该缓冲区free掉,并去掉_IO_IN_BACKUP位,最后调用_IO_doallocbuf函数。该函数在glibc/libio/genops.c中定义如下:
_IO_doallocbuf
该函数经过一些检查后会调用_IO_DOALLOCATE函数,该函数在glibc/libio/libioP.h中有宏定义为#define _IO_DOALLOCATE(FP) JUMP0(__doallocate , FP),即为虚表中的__doallocate,对应_IO_file_doallocate函数,该函数在glibc/libio/filedoalloc.c中定义如下:
_IO_file_doallocate
可以看到在第94行给文件流字段加上了_IO_LINE_BUF字段,函数主要是在最后调用了malloc函数分配了size大小的空间给指针psize在第83行被赋值为_IO_BUFSIZ,该字段有宏定义为8192,但在第84行调用了_IO_SYSSTAT函数,该函数为虚表中的__stat,对应着_IO_file_stat函数,该函数最终将调用syscall来获取该文件状态,并初始化结构体st,初始化后的st如下图:
struct stat st
因为在第97行存在判断,因此size最后赋值为st.blksize0x1000字节,即4K大小。紧接着调用了_IO_setb函数,该函数在glibc/libio/genops.c中定义如下:
_IO_setb
该函数主要实现了对_IO_buf_base_IO_buf_end两个字段进行赋值。到这里可以知道_IO_doallocbuf函数实现了给文件流分配4K空间用作缓存缓冲区的操作。紧接着回到_IO_file_xsgetn函数是一个100多行的while循环:
_IO_file_xsgetn(2)

至此,分析完了fread函数主要流程,尤其是_IO_file_xsgetn函数的执行流程。fread函数主要进行了1个操作,调用_IO_file_xsgetn函数,当然加锁也是比较重要的。_IO_file_xsgetn函数,主要进行了3个操作:调用_IO_doallocbufFILE结构体分配缓冲区;当n <= block_size时,调用_IO_file_underflow将文件流中的数据读入缓冲区再调用memcpy从缓冲区拷贝至目标变量中;当n > block_size时,大部分先对齐到block_size,调用_IO_SYSREAD函数直接从文件流读入到目标变量,若还有剩余的数据再用老方从走缓冲区拷贝到目标变量中。
通过阅读源码,对FILE结构体以及其中各字段所代表的含义有了船新的认识:

  • 首先是从_IO_read_ptr_IO_buf_end8个字段,原本认为是共申请了3个缓冲区,通过阅读源码后知道了只申请了1个缓冲区,其中_IO_buf_base_IO_buf_end指向这个缓冲区的两端,其余6readwrite字段没事的时候都与_IO_buf_base的值相同,在进行读操作时,相当于3read指针起作用控制缓冲区中的内容,可以推出在进行写操作时,3read指针与_IO_buf_base的值相同,而3write指针独起作用控制缓冲区中的内容。
  • 其次_IO_save_base_IO_save_end3个字段,因为在该段代码中只存在几处判断,并没有实际用处,所以判断大概是为了保存某个时刻缓冲区而设置的指针。
  • 最后是在调试时发现的FILE结构体有多种形态,比如我在调试时看到的FILE结构体实际上是glibc/libio/bits/libio.h中定义的_IO_FILE_complete结构体。最终究其原因,是因为有个宏判断把定义结构体的大括号给吃掉了。如下图:
    骚骚的宏判断

fwrite

fwrite函数的一般用法为

fwrite(const void* buffer, size_t size, size_t count, FILE* stream);

fread函数相似,该函数共有4个参数,buffer代表存储要写入文件数据的首地址,size代表每个对象的大小,count代表对象的个数,stream代表文件流。即该函数实现了将buffersize * count字节数据写入stream文件流的操作。该函数在glibc/libio/iofwrite.c中有如下宏定义:

weak_alias(_IO_fwrite , fwrite)

fwrite函数原形为_IO_fwrite函数,该函数在同一文件中定义如下:

_IO_fwrite
也是一样的调用了CHECK_FILE函数进行检查,以及在调用关键函数前后加锁与去锁。在第39行调用了函数_IO_sputn,该函数在经过一系列定义和宏定义后为虚表中的__xsputn,即_IO_new_file_xsputn函数,该函数定义在glibc/libio/fileops.c中,是fwrite函数的关键。
_IO_new_file_xsputn(1)
该函数中有3个比较重要的变量,分别是s表示放有待写入数据变量的首地址,to_do表示还需要写入的字节数,must_flush表示是否需要刷新缓冲区。接下来也是按照刚从fopen初始化完的状态开始分析。此时各字段均为空,也没有_IO_LINE_BUF、_IO_CURRENTLY_PUTTING属性,所以不会进入第1233、1250行的判断句,所以count变量没有被赋值仍然为0,也不会进入第1254行的判断语句。而to_do代表的需要写入的字节数没有变,因此会直接进入第1262行的判断语句,如下图:
_IO_new_file_xsputn(2)
进入语句后,声明了两个变量block_sizedo_write,之后直接调用了_IO_OVERFLOW函数,该函数为虚表中的__overflow,即_IO_new_file_overflow函数,该函数在glibc/libio/fileops.c中定义如下:
_IO_new_file_overflow(1)
该函数首先判断文件是否含有_IO_NO_WRITES属性,即在fopen操作时是否为r只读选项,若是,则不会执行该函数直接返回。接着判断是否不含_IO_CURRENTLY_PUTTING属性或者_IO_write_base字段为空,其中_IO_CURRENTLY_PUTTING属性在该函数的第785行会进行赋值,因此该语句为判断没有正常执行过_IO_new_file_overflow函数或执行过但没有分配缓冲区的情况,会调用_IO_doallocbuf函数分配缓冲区,之后调用_IO_setg将与read相关的3个字段都赋值为_IO_buf_base,之后也会进行_IO_in_backup的检测,这几步操作在上一节_IO_new_file_underflow函数中有过详细描述,因此不再赘述。
_IO_new_file_overflow(2)
紧接着将readwrite6个字段都赋值为_IO_buf_base,并给文件流加上_IO_CURRENTLY_PUTTING属性。由于在_IO_new_file_xsputn调用该函数时的第二个参数,即函数中的ch变量为EOF,因此,会在第790行调用_IO_do_write函数并返回,传入的3个参数分别为FILE结构体指针、_IO_write_base以及_IO_write_ptr - _IO_write_base = 0。该函数在glibc/libio/fileops.c中有宏定义为_IO_new_do_write函数,其定义如下:
_IO_new_do_write
可以看到_IO_new_do_write函数中,因为本次传入的第3个参数to_do0,因此不会进行任何操作直接返回0,而不会去执行new_do_write函数。返回后回到_IO_new_file_xsputn的第1266行,不等于EOF,于是会继续执行。给block_size赋值为申请的空间大小,即4Kdo_write代表通过调用new_do_write函数进行写入的数据大小,该数值是与block_size进行对齐的。接下来也是根据to_doblock_size的大小,函数将分成不同的流程。

至此,分析完了fwrite函数主要流程,尤其是_IO_new_file_xsputn函数的执行流程。fwrite函数与fread相互对仗,fread是将文件中数据直接读入变量,或先从文件读入FILE结构体缓冲区,再利用read相关指针进行间接读入变量;fwrite是将数据直接从文件中写入变量或写入FILE结构体中的缓冲区。_IO_new_file_xsputn作为实现fwrite的重要函数,主要进行了****个操作:调用_IO_new_file_overflow函数为FILE结构体申请缓冲区;若n < block_size时,直接调用__mempcpy函数拷贝到缓冲区中;若n >= block_size时,会将对齐到block_size的部分用系统调用直接读入文件,剩余部分按与n < block_size相同的方法拷贝到缓冲区中。
通过阅读源码,对fwrite函数有了船新认识:

  • 本以为fwrite只是把fread的读操作变成写操作,其他都是相同的。然而fread执行完后不论多少数据都读入了变量中,而fwrite执行完后未对齐block_size大小的数据仍在缓冲区中,推测在执行fclose函数或在程序退出时才会真正的写入文件中。
  • _IO_new_file_overflow函数并不仅仅有上文中描述的调用_IO_doallocbuf申请缓冲区的作用,其主要担负着刷新缓冲区的作用:第一种调用情况为上文中所提到的,当_IO_buf_base字段为空,即还未初始化缓冲区时,则会调用_IO_doallocbuf函数进行申请缓冲区;若缓冲区已经初始化,且_IO_write_end == _IO_write_ptr,即缓冲区已满时,则会把这些待写入内容写入文件,之后会将_IO_write_ptr赋值为_IO_buf_base,相当于清空缓冲区的操作。

fclose

fopen函数相同,在glibc/include/stdio.h中有如下宏定义:

#define fclose(fp) _IO_new_fclose(fp)

fclose函数的原形为_IO_new_fclose函数,该函数在glibc/libio/iofclose.c中定义如下:

_IO_new_fclose
该函数主要是fopen函数的逆过程,首先在判断文件流是否含有_IO_file_flags_IO_SI_FILEBUF后,函数会执行_IO_un_link函数,该函数在glibc/libio/genops.c定义如下:
_IO_un_link
该函数是_IO_link_in函数的逆过程,主要实现了将文件流从_IO_list_all链表中卸下,以及一些对结构体中字段的善后操作。接着调用了_IO_file_close_it函数,该函数在glibc/libio/fileops.c定义如下:
_IO_file_close_it
该函数在第134行判断是否为写属性的文件流,以及是否进行过写操作,若有,则会调用_IO_do_flush函数,该函数在glibc/libio/libioP.h中有宏定义如下:
_IO_do_flush
可以看到该函数直接是针对文件流的整个缓冲区去调用了_IO_do_write函数,即实现了将仍存在于缓冲区中的数据写入文件的操作。之后调用_IO_SYSCLOSE函数,该函数对应虚表中的__close,即_IO_file_close函数,该函数在glibc/libio/fileops.c中定义如下:
_IO_file_close
该函数直接调用了__close_nocancel函数去执行系统调用对文件进行关闭。返回到_IO_new_file_close_it之后紧接着调用_IO_setb、_IO_setg、_IO_setp等函数将文件流中所有readwrite字段置0,并在第159-161行将_flags、_fileno_offset修改为一个关闭状态的属性。返回到_IO_new_fclose函数后,主要去执行了_IO_FINISH函数,该函数为虚表中的__finish,即对应_IO_new_file_finish函数,该函数在glibc/libio/fileops.c中定义如下:
_IO_new_file_finish
该函数一次调用了_IO_do_flush、_IO_SYSCLOSE以及_IO_default_finish函数,其中_IO_default_finish函数在glibc/libio/genops.c中定义如下:
_IO_default_finish
可以看到_IO_new_file_finish中调用的几个函数,均在前面正常关闭的流程中有过调用,所以基本上都不会去执行。

至此,完成了对fclose函数流程的分析,总的来说代码比较短暂,也都是对前面已经执行代码的一个逆过程,因此并没有太多需要注意的地方,主要还是印证了前面在fwrite结尾的一个预测,会在关闭文件流时去调用了_IO_do_flush函数将缓冲区内的数据写入文件。

0x03 利用方法

综上,我们只要伪造满足这4个条件的stdout结构体就能够实现任意地址读,其中第1、2个条件为文件流不能具有_IO_NO_WRITES(0x8)属性,且具有_IO_CURRENTLY_PUTTING(0x800)属性,而且_flags位自带一个_IO_MAGIC(0xfbad0000),因此构造的_flags0xfbad0800。第3、4个条件就和read、write指针息息相关了,根据条件只要构造_IO_read_end = _IO_write_base = (想要leak的起始地址)_IO_write_ptr = (想要leak的结束地址),其他3个没有提到的指针置0就可以了。
因此伪造的fake_FILE结构体大概长这样(一般用got表进行libcleak):

fake_FILE
在我们最终伪造好这个FILE结构体后再去调用针对stdout进行输出的函数就可以实现任意地址读了,常见的函数一般有printf、fwrite、puts等。

综上,我们只需要构造如下的fake_FILE就能够实现将字符串写到指定内存的操作。

fake_FILE
虽然看起来不能实现任意地址写任意数据,但若是输出我们输入的值时,则可实现任意地址写任意数据;就算输出的都是固定字符串,能够将一些判断标志改为其他值,而改变程序正常执行流程有时还是很有用的。

综上,我们只要伪造满足这4个条件的stdin结构体就能够实现任意地址写,其中第1个条件与调用时本来要写入的数据长度有关,我们要伪造的写入大小应该比其本要写入的大小更大。比如,在调用read(0 , buf , 0x10)函数时,我们构造的_IO_buf_end - _IO_buf_base就应该大于0x10;第2、3个条件综合起来可构造_IO_read_ptr == fp->_IO_read_end来同时满足,也不知道给个什么值,就默认为0吧;第4个就是_flags属性的问题了,不包含_IO_NO_READS
因此伪造的fake_FILE结构体大概长这样:

fake_FILE
在我们最终伪造好这个FILE结构体后再去调用针对stdin进行输入的函数就可以实现任意地址写了,常见的函数一般有scanf、fread、gets、fgets等。
#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函数,该函数在glibc/libio/libioP.h中定义如下:

IO_validate_vtable
该函数首先判断了调用的vtable函数地址是否在__start___libc_IO_vtable__stop___libc_IO_vtable之间,若在此之间,则说明是libc中的合法vtable地址。若不在这个区间,则会调用第876行的_IO_vtable_check函数进行进一步的检查。该函数在glibc/libio/vtable.c中定义如下:
_IO_vtable_check
由于存在预编译头,且SHARE、PTR_DEMANGLE是有定义的,因此会执行上面两个部分的检查。第一部分为判断引用的虚表指针是否为默认命名空间外重构的虚表指针,其中atomic_load_relaxed函数为获得加载指针的当前值,PTR_DEMANGLE函数则是类似canary之类的一个保护虚表不被修改的函数;第二部分则是检查引用的虚表指针是否为动态链接库中加载的函数。但我们在2.23版本中通常将堆或栈上的一块区域用来伪造为FILE结构体,同样vtable也就接在这个fake_FILE结构体的后面,所以上面的3个条件都不会满足。因此,在新版本中用曾经的利用方法最终会执行到_IO_vtable_check函数的第72行,报错并结束进程。
上面介绍完了新版本中加入的3个对vtablecheck机制,下面讲讲大神们是如何绕过check并再次实现利用的。
由于_IO_vtable_check函数中的第一个检查,有PTR_DEMANGKE函数的存在,几乎时不能够伪造对应的条件;而第二个检查,若能够伪造,则可以选择其他更方便的利用方法,而不用继续在vtable的利用上死磕了,因此这两个检查在正常情况下难以绕过。于是在调用_IO_vtable_check函数前的检查则成了关键,即调用的虚表指针必须在__start___libc_IO_vtable__stop___libc_IO_vtable之间。然后大佬们就找到了这样一组内部虚表_IO_str_jumps/_IO_wstr_jumps_IO_file_jumps/_IO_wfile_jumps相对应,但其中的函数都换成了另外一组,如下图:
_IO_str_jumps
并且发现其中的__finish对应的函数大有可为,_IO_str_finish函数在glibc/libio/strops.c中定义如下:
_IO_str_finish
可以看到,当满足条件时,会将结构体中的_s._free_buffer作为函数指针,将_IO_buf_base作为参数进行执行,因此我们可以试着利用原来FSOP的利用方法,来构造一个满足条件的fake_FILE

FSOP方法在之前的文章中有过介绍,在此只做简单描述。该方法的精髓为当程序从main函数返回或调用exit函数或libc进行abort操作时,会调用_IO_flush_all_lockp函数去遍历_IO_list_all链表中的每一个FILE结构体,而当FILE结构体满足(_mode <= 0) && (_IO_write_ptr > _IO_write_base)时,则会调用_IO_OVERFLOW函数,即vtable指针+ 0x18位置的函数。

因此,对应到此处,想要成功调用_IO_str_finish函数,则需要在FSOP的基础上将的vtable改为_IO_str_jumps - 8,此为调用函数前的利用条件。在进入函数后,首先要满足判断条件_IO_buf_base != NULL以及_flags & _IO_USER_BUF == 0,最后是利用条件_s._free_buffer == system_addr || _IO_buf_base == "/bin/sh\x00"_s._free_buffer == one_gadget。其中_IO_strfile结构体在glibc/libio/strfile.h中定义如下:

_IO_strfile结构体

综上,我们想要需要构造如下的fake_FILE结构体来利用FSOP方法来绕过新版本中对vtablecheck,从而达到利用的目的。

fake_FILE
上一篇下一篇

猜你喜欢

热点阅读