首页
社区
课程
招聘
[原创]_IO_FILE劫持的一点总结
2021-1-3 20:47 7034

[原创]_IO_FILE劫持的一点总结

2021-1-3 20:47
7034

本文首先介绍文件流中涉及到的重要struct,并从fopen打开一个文件开始分析了打开文件的过程,之后通过分析fread函数调用,并从汇编层面分析vtable函数跳转表执行的过程,给出可利用方案,最后分析了libc对该劫持的漏洞利用缓解措施。本文源码参考glibc-2.23版本

文件流中的重要结构体

  • IO_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;

  _IO_off64_t _offset;

  /* Wide character stream stuff.  */
  struct _IO_codecvt *_codecvt;
  struct _IO_wide_data *_wide_data;
  struct _IO_FILE *_freeres_list;
  void *_freeres_buf;

  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
};
  • IO_FILE_PLUS 的 结构体
struct _IO_FILE_plus
{
  _IO_FILE file;
  const struct _IO_jump_t *vtable;
};;
  • 通过 fopen 打开文件的时候,会在 堆上 为该文件申请一片 locked_FILE 结构体的空间,用来存储该文件的文件流信息
  struct locked_FILE
  {
    struct _IO_FILE_plus fp;
    _IO_lock_t lock;
    struct _IO_wide_data wd;
  } *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));
  • 随后会初始化 结构体中fp的值
_IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
_IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
  • 将该堆结构链入全局变量 _IO_list_all 
通过_IO_file_init将fp链入全局输入输出流链表_IO_list_all
_IO_file_init (&new_f->fp);

//该列表上的stdin,stdout,stderr是存储在libc中的,DEF_STDFILE是初始化其对应的结构体的
//_IO_file_init (&new_f->fp);
DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);

struct _IO_FILE_plus *_IO_list_all = &_IO_2_1_stderr_;
//_IO_list_all可以通过libc的基地址计算出来

fread过程分析

fread的读取

  • 读取的过程,fread会调用 iofread.c中的_IO_fread函数,继而会调用fileops.c中的_IO_file_xsgetn,在read的过程中,涉及到的_IO_FILE结构体中的指针有如下几个
  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. */   // 缓冲区的大小,读取的内容存放的地方
  • 首先要设置的是_IO_buf_base,以及 _IO_buf_end指针,如果_IO_buf_base的值为空,则通过_IO_doallocbuf 为_IO_buf_base申请一块内存,glibc2.23调试申请到的堆空间为4096byte,该空间将作为缓冲区从文件读取数据
glibc-2.23\glibc-2.23\libio\genops.c
_IO_doallocbuf 
    -> _IO_DOALLOCATE (fp) ===> _IO_file_doallocate
F:\源码阅读\glibc-2.23\glibc-2.23\libio\filedoalloc.c
int _IO_file_doallocate (_IO_FILE *fp){
    ...
  p = malloc (size);
  if (__glibc_unlikely (p == NULL))
    return EOF;
  _IO_setb (fp, p, p + size, 1); // __IO_setb会设置_IO_buf_base
  }
  • 通过fp->_IO_read_end 和 _IO_read_ptr判断已经读取出的数据大小,若已经取出需要的数据
  have = fp->_IO_read_end - fp->_IO_read_ptr;
      if (want <= have) // want为需要的字节长度,have为已读取到的字节长度,如果满足已经读取到的字节长度满足需要读取的字节长度,则将缓冲区中的数据cp到读取区域
    {
      memcpy (s, fp->_IO_read_ptr, want);// 拷贝到缓冲区
      fp->_IO_read_ptr += want;
      want = 0;
    }
  • 如果已读取字节,分为未读取和 已经读取部分字节但是不满足需要的字节数两种情况
      if (have > 0)
        {
#ifdef _LIBC
          s = __mempcpy (s, fp->_IO_read_ptr, have);
#else
          memcpy (s, fp->_IO_read_ptr, have);
          s += have;
#endif
          want -= have;
          fp->_IO_read_ptr += have;
        }
  • 通过 __underflow 从文件读取字节, 通过_IO_SYSREAD将文件内容读入缓冲区中,缓冲区的大小由_IO_buf_base和_IO_buf_end来决定,后续从缓冲区copy到堆空间上时,则通过_IO_read_base和_IO_read_end来决定。
\glibc-2.23\libio\genops.c
__underflow -> _IO_UNDERFLOW ===> _IO_new_file_underflow
\glibc-2.23\libio\fileops.c
_IO_new_file_underflow
_IO_new_file_underflow (_IO_FILE *fp){
  ...
   fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;//__IO_read_base指向 _IO_buf_base
   fp->_IO_read_end = fp->_IO_buf_base;
   fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
    = fp->_IO_buf_base;

  count = _IO_SYSREAD (fp, fp->_IO_buf_base,
               fp->_IO_buf_end - fp->_IO_buf_base);//_IO_file_read fileops.c
   fp->_IO_read_end += count;
 ...
 }
  • __underflow读取数据完后,循环进行下一次读操作
      if (fp->_IO_buf_base
          && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
        {
          if (__underflow (fp) == EOF)
        break;

          continue;
        }

劫持分析

vtable的调用

  • vtable是一个指针,该指针指向的区域,存放了IO_jump_t 结构体 ,是一个跳转中转表
  • 以fread为例,_IO_fread函数中调用了_IO_segetn 函数
_IO_size_t
_IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
  /* FIXME handle putback buffer here! */
  return _IO_XSGETN (fp, data, n); //fileops.c -> _IO_file_xsgetn
}
  • 对于 _IO_XSGETN 的调用,展开宏定义后
 ((*(__typeof__ (((struct _IO_FILE_plus){}).vtable) *)(((char *) ((fp))) + __builtin_offsetof (struct _IO_FILE_plus, vtable)))->__xsgetn) (fp, data, n)
  • 其对应的汇编代码如下,其中,
sub     rsp, 8          ; Alternative name is '_IO_sgetn'
mov     rax, [fp + 0D8h] ; fp的偏移0xd8的vtable的值,32 位的 vtable 偏移为 0x94,64 位偏移为 0xd8
call    qword ptr [rax+40h] ; 调用指定函数,[rax+40h]指向libc上
add     rsp, 8
retn
####
  • 在[rax]处存储的内容如下,通过call 跳转到指定的地址,可以通过overwrite vtable的值,或者改写[rax + offset]处的内容来达到劫持程序流的目的
$47 = {  __dummy = 0, 
  __dummy2 = 0, 
  __finish = 0x7ffff7ab81d3 <_IO_new_file_finish>, 
  __overflow = 0x7ffff7ab8981 <_IO_new_file_overflow>, 
  __underflow = 0x7ffff7ab8724 <_IO_new_file_underflow>, 
  __uflow = 0x7ffff7ab96da <__GI__IO_default_uflow>, 
  __pbackfail = 0x7ffff7aba2ad <__GI__IO_default_pbackfail>, 
  __xsputn = 0x7ffff7ab7c6b <_IO_new_file_xsputn>, 
  __xsgetn = 0x7ffff7ab7ebc <__GI__IO_file_xsgetn>, 
  __seekoff = 0x7ffff7ab73a2 <_IO_new_file_seekoff>, 
  __seekpos = 0x7ffff7ab9907 <_IO_default_seekpos>, 
  __setbuf = 0x7ffff7ab724b <_IO_new_file_setbuf>, 
  __sync = 0x7ffff7ab7191 <_IO_new_file_sync>, 
  __doallocate = 0x7ffff7aac841 <__GI__IO_file_doallocate>, 
  __read = 0x7ffff7ab7b69 <__GI__IO_file_read>, 
  __write = 0x7ffff7ab7bc6 <_IO_new_file_write>, 
  __seek = 0x7ffff7ab7930 <__GI__IO_file_seek>, 
  __close = 0x7ffff7ab7165 <__GI__IO_file_close>, 
  __stat = 0x7ffff7ab7bad <__GI__IO_file_stat>, 
  __showmanyc = 0x7ffff7aba3f5 <_IO_default_showmanyc>, 
  __imbue = 0x7ffff7aba3fb <_IO_default_imbue>
  }

劫持程序流后的参数传递

  • 以fread为例,当程序调用_IO_XSGETN 函数的时候,会通过查_IO_file_jumps表来解析出_IO_XSGETN对应的函数,当可以overwrite _IO_file_jumps表,或者劫持到vtable 指针的时候,就可以实现程序的控制流劫持
_IO_size_t
_IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
  /* FIXME handle putback buffer here! */
  return _IO_XSGETN (fp, data, n);
}
  • _IO_XSGETN 的跳转定义为JUMP2(FUNC,THIS,X1,X2)  ,this传入的是FUNC,在代码中是fp的地址,所以劫持后的第一个参数的位置在fp的位置处
#在跳转的时候函数定义为JUMP2(FUNC,THIS,X1,X2)
#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
  • 劫持分两类讨论
  • 第一种,对于stderr,stdout,stdin的劫持,只能overwrite _IO_FILE_plus结构体中的vtable指针,使其指向我们所伪造的跳转表,达到劫持目的,该方法也普适于其他的文件流。
  • 第二种,通过overwrite跳转表中的函数指针来达到覆写的目的
  • 要实现以上的任意读写,前提需要已知libc的基地址,并且程序漏洞可以实现任意地址写
libc_base_address -> _IO_list_all -> 任意结构体地址 -> 偏移计算vtable -> vtable表地址
libc_base_address -> _std_in/_std_out/_std_err地址 -> 偏移计算vtable -> vtable表地址

针对vtable的劫持缓解机制

  • 在libc-2.24版本上,增加了对vtable的劫持检测机制,在Libc-2.23版本一下的跳转是如下实现的
//libc-2.23
# define _IO_JUMPS_FUNC(THIS) _IO_JUMPS_FILE_plus (THIS)
//libc-2.24
# define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))
  • 在libc-2.24版本上,增加的检查机制IO_validate_vtable定义如下,如果offset大于 section_length,则进入_IO_vtable_check()来检查这个vtable是否合法。
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
  /* Fast path: The vtable pointer is within the __libc_IO_vtables
     section.  */
  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))
    /* The vtable pointer is not in the expected section.  Use the
       slow path, which will terminate the process if necessary.  */
    _IO_vtable_check ();
  return vtable;
}

新的利用方式

  • 在libc-2.24版本以后(大于等于)针对IO_FILE文件的vtable劫持增加了检测机制使得无法再使用vtable来劫持程序流
  • 但是IO_FILE的结构体中的_IO_buf_base,_IO_buf_end仍可以用来读取、写入数据
  • 在libc中不仅仅只有_IO_file_jumps这么一个vtable,还有一个叫做_IO_str_jmps的,这个vtable不在check的范围内(引自ctf-wiki)


















[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

最后于 2021-1-5 18:03 被seana编辑 ,原因:
收藏
点赞2
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回