美文网首页CTF Re&&PwnCTF-PWN
glibc fclose源代码阅读及伪造_IO_FILE利用fc

glibc fclose源代码阅读及伪造_IO_FILE利用fc

作者: L1nkM3 | 来源:发表于2018-03-22 13:25 被阅读169次

    简介

    最近学习了一下_IO_FILE的利用,刚好在pwnable.tw上碰到一道相关的题目。拿来做了一下,遇到了一些困难,不过顺利解决了,顺便读了一波相关源码,对_IO_FILE有了更深的理解。
    文章分为三部分,分别是利用原理、实例和源码阅读。源码部分比较无聊所以我把它放在了最后。

    原理

    原理

    我们使用fopen打开一个文件会在堆上分配一块内存区域用来存储FILE结构体,存储的结构体包含两个部分,前一部分为_IO_FILE结构体file,后一部分是一个指向struct IO_jump_t的指针vtable, 这个结构体种存储着一系列与文件IO相关的函数指针。
    在我们调用fclose关闭一个文件时,我们最终会调用到vtable中存储的函数指针。如果我们能够将vtable中的指针替换为我们自己想要跳转到的地址就可以劫持程序流程。

    利用前提

    1. 本文仅考虑libc版本 <= 2.23的情况。因为大于等于2.24的libc会对vtable的位置做判断,无法令其指向自己构造的区域
    2. 可以控制vtable指针或者fp指针指向的位置
    3. 有一块已知地址的可控内存区域,大小需要视情况而定

    利用方式1:直接覆盖vtable指针

    这个没什么好说的,将vtable指针指向可控内存,将__finish(off=2*SIZE_T)构造为要执行的地址即可

    利用方式2:覆盖fp指针

    有的时候我们无法直接控制FILE结构体的vtable指针,但是我们可以控制文件指针。因此我们需要伪造整个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
    

    利用实例

    题目简介

    测试用的题目来源于pwnable.tw,题目名为seethefile。为FILE结构体利用的一道比较经典的题目。在这里只谈一下解题思路,不给出exp。

    逆向分析概要

    openfile打开一个文件,文件名由用户输入,但是当文件名含flag时会退出程序
    readfile将文件中的内容读入一个全局字符数组中
    writefile将全局字符数组中的内容输出到屏幕上
    closefile关闭文件
    当输入为5时程序要求输入一个名字,然后关闭存储文件指针fp,退出程序


    main.png
    以上步骤的文件指针都存放在一个全局变量中,bss对应的结构如下 bss.png

    漏洞分析

    由逆向结果可知。在读取数字时存在栈溢出,但是程序开启了栈保护,所以不可利用;
    当输入命令5,读取用户指令时存在一个bss段的溢出,利用这个溢出我们可以覆盖fp的指针指向我们想要的位置,同时可以伪造FILE结构体。利用fclose来实现攻击。

    漏洞利用

    leak libc

    题目给出了libc的文件,为了执行libc中的system命令,还需要获取libc加载的基址。我们可以通过打开/proc/self/mmap这个虚拟文件来获取当前进程的地址空间情况。获得到libc的加载基址后就可以计算出libc中system的偏移。接下来我们就可以利用_IO_FILE结构体进行攻击。

    构造FILE

    构造FILE结构体只需要关注两个变量,第一个为FILE结构体的_flags字段,只需要_flags & 0x2000为0就会直接调用_IO_FINSH(fp),_IO_FINISH(fp)相当于调用fp->vtabl->__finish(fp)。
    将fp指向一块内存P,P偏移0的前4字节设置为0xffffdfff,P偏移4位置放上要执行的字符串指令(字符串以';'开头即可),P偏移sizeof(_IO_FILE)大小位置(vtable)覆盖为内存区域Q,Q偏移2*4字节处(vtable->__finish)覆盖为system函数地址即可。


    glibc fclose源码学习

    glibc的版本为2.23.90,复制粘贴比较多,主要是为了方便查阅。以下内容对大部分的fclose函数进行了层层解剖,很多部分与漏洞利用无太大关系,按需取用。

    _IO_FILE与_IO_FILE_plus结构体

    在阅读fclose前先来了解一些有关于FILE结构体的知识。
    在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;
    }
    

    该函数的流程可以粗略地进行如下表示:


    fclose flowchart.png

    流程图中有几个关键函数,至于加解锁什么的我忽略了:

    • _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是否置位,若置位进行下一步操作,最后将其清零

    #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;
    

    _IO_have_backup

    上面已经提到了,略略略

    free(fp)

    用户打开的FILE结构体是分配在堆上的,在fclose中最终会被free释放。
    至此glibc的fclose源代码分析完毕。

    相关文章

      网友评论

      本文标题:glibc fclose源代码阅读及伪造_IO_FILE利用fc

      本文链接:https://www.haomeiwen.com/subject/dahnqftx.html