0x00 House of Orange

由于只是个人经验记录为主的一篇博文,所以并不打算将前人已经讲得很细致的部分再重复,因此建议初学者先阅读

0x0 free without free

本题最大特色就是功能中不提供free函数,而没有free意味着传统leak和attack方式全部失效。利用top chunk的特性来得到一个freed chunk即是orange带来的全新攻击思路。

这种操作的原理简单来说是当前堆的 top chunk 尺寸不足以满足申请分配的大小的时候,原来的 top chunk 会被释放并被置入 unsorted bin 中

  由于题目功能中存在的堆溢出,修改top chunk size非常轻松,但是也有一些特别的约束

  • 大于MINSIZE(一般为0x10)
  • 下一个chunk申请过大(0x20000)会导致调用mmap从而不会free top chunk
  • top chunk的地址+size后要满足页(0x1000)对齐

0x1 leak with unsorted&large bin

修改完top chunk的size位后malloc一个大于它的chunk,会发现原先的top chunk已经被free并被放入unsorted bin中,例如这样:

- 00005625E60F3080  00 00 00 00 00 00 00 00  61 0F 00 00 00 00 00 00
- 00005625E60F3090  78 9B 88 3A FD 7E 00 00  78 9B 88 3A FD 7E 00 00
- 00005625E60F30A0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00

借此可以leak出libc base,不过我们还需要heap base,获取起来也非常轻松,再次malloc一个在large bin范围并小于修改后的top chunk size的chunk。在malloc的分配流程中,遍历unsorted bin时由于此时被free的top chunk大小不等于malloc申请的大小,因此会先将其放入large bin中。根据large bin特性,当其中仅有一个chunk时,其fd/bk_nextsize会指向自己。第二遍遍历后会再将它分割成两个chunk,低地址的chunk就用于满足malloc的申请,例如这样:

- 00005634B6E880C0  00 00 00 00 00 00 00 00  11 04 00 00 00 00 00 00
- 00005634B6E880D0  88 71 80 7E A7 7F 00 00  88 71 80 7E A7 7F 00 00
- 00005634B6E880E0  C0 80 E8 B6 34 56 00 00  C0 80 E8 B6 34 56 00 00

分别填充0x8和0x10个字符后就可以leak得到libcbase和heapbase

0x2 IO_FILE

文件流的具体概念在此简单描述一下,最为常见的标准输出(stdout)实际上也是一个文件流,在pwndbg里查看一下:

pwndbg> p _IO_2_1_stdout_
$5 = {
  file = {
    _flags = -72537977, 
    _IO_read_ptr = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_read_end = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_read_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_write_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_write_ptr = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_write_end = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_buf_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_buf_end = 0x7ffff7dd26a4 <_IO_2_1_stdout_+132> "", 
    _IO_save_base = 0x0, 
    _IO_backup_base = 0x0, 
    _IO_save_end = 0x0, 
    _markers = 0x0, 
    _chain = 0x7ffff7dd18e0 <_IO_2_1_stdin_>, 
    _fileno = 1, 
    _flags2 = 0, 
    _old_offset = -1, 
    _cur_column = 0, 
    _vtable_offset = 0 '\000', 
    _shortbuf = "\n", 
    _lock = 0x7ffff7dd3780 <_IO_stdfile_1_lock>, 
    _offset = -1, 
    _codecvt = 0x0, 
    _wide_data = 0x7ffff7dd17a0 <_IO_wide_data_1>, 
    _freeres_list = 0x0, 
    _freeres_buf = 0x0, 
    __pad5 = 0, 
    _mode = -1, 
    _unused2 = '\000' <repeats 19 times>
  }, 
  vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}

  • 文件流间通过_chain形成单向链表,而_IO_list_all是指向这个链表中的第一个结构体,一般是_IO_2_1stderr
  • flag标志位在利用后续会遇到检测,所以一般需要对其进行构造。
  • _IO_read_xxx与_IO_write_xxx这六个指针涉及文件流读取与写入相关操作,对其进行修改可以用来进行任意地址的读取或者写入,是IO_FILE相关利用的重要部分,不过在此题中并未用到这个技巧
  • vtable指向一个虚表,其中是一些文件流操作时会用到的函数指针

    pwndbg> p *((struct _IO_jump_t *)0x7ffff7dd06e0)
    $1 = {
    __dummy = 0, 
    __dummy2 = 0, 
    __finish = 0x7ffff7a869c0 <_IO_new_file_finish>, 
    __overflow = 0x7ffff7a87730 <_IO_new_file_overflow>, 
    __underflow = 0x7ffff7a874a0 <_IO_new_file_underflow>, 
    __uflow = 0x7ffff7a88600 <__GI__IO_default_uflow>, 
    __pbackfail = 0x7ffff7a89980 <__GI__IO_default_pbackfail>, 
    __xsputn = 0x7ffff7a861e0 <_IO_new_file_xsputn>, 
    __xsgetn = 0x7ffff7a85ec0 <__GI__IO_file_xsgetn>, 
    __seekoff = 0x7ffff7a854c0 <_IO_new_file_seekoff>, 
    __seekpos = 0x7ffff7a88a00 <_IO_default_seekpos>, 
    __setbuf = 0x7ffff7a85430 <_IO_new_file_setbuf>, 
    __sync = 0x7ffff7a85370 <_IO_new_file_sync>, 
    __doallocate = 0x7ffff7a7a180 <__GI__IO_file_doallocate>, 
    __read = 0x7ffff7a861a0 <__GI__IO_file_read>, 
    __write = 0x7ffff7a85b70 <_IO_new_file_write>, 
    __seek = 0x7ffff7a85970 <__GI__IO_file_seek>, 
    __close = 0x7ffff7a85340 <__GI__IO_file_close>, 
    __stat = 0x7ffff7a85b60 <__GI__IO_file_stat>, 
    __showmanyc = 0x7ffff7a89af0 <_IO_default_showmanyc>, 
    __imbue = 0x7ffff7a89b00 <_IO_default_imbue>
    }
    
    

本题劫持控制流的根方法就是劫持vtable,劫持流程如下:

malloc_printerr->__libc_message->abort->fflush(_IO_flush_all_lockp)->vtable->_IO_OVERFLOW

malloc_printerr是malloc源码中专门用于输出报错信息的函数,只要malloc或是free的过程中某项检查出现了问题,例如double free,就会调用malloc_printerr

而经历一番调用链后,关键点在_IO_flush_all_lockp中发生,截取一些关键部分代码:

{
......
  fp = (_IO_FILE *) _IO_list_all; //_IO_list_all赋给fp
  while (fp != NULL)
    {
......
	  && _IO_OVERFLOW (fp, EOF) == EOF)//调用vtable中的_IO_OVERFLOW,改为system或onegadget即可劫持控制流
......
      if (last_stamp != _IO_list_all_stamp) //若无法bypass,则通过结构体中_chain寻找下一个文件流
	{
......
	  fp = (_IO_FILE *) _IO_list_all;
	  last_stamp = _IO_list_all_stamp;
	}
      else
	fp = fp->_chain;
    }
......
}
  • 函数开始时fp赋值为_IO_list_all,同时在调用可被劫持的_IO_OVERFLOW之前需要bypass一定的条件,若bypass失败则会让fp=_IO_list_all->_chain,直到fp==0
  • 因此若_IO_list_all指向的结构体内部无法构造从而bypass失败,可以考虑让它的_chain指向一块可控区域,在下一次循环中再尝试调用_IO_OVERFLOW
  • 自然,若_IO_list_all直接指向可控内存,就可以一次性构造完bypass条件调用_IO_OVERFLOW

以上介绍了是如何通过IO_FILE利用来进行控制流劫持,关键点在于修改_IO_list_all,或者是直接对文件流结构链表中任意一个元素中的vtable进行劫持,就可以在malloc_printerr调用时劫持控制流。接下来的问题就在于如何修改文件流链表。

0x3 Unsorted Bin Attack on _IO_list_all

Unsorted Bin Attack是一项较为简单的攻击方式,简单来说就是若我们能控制正处于unsorded bin
中某个chunk的bk,就可以将main_arena+0x58这个固定的数值写入bk+0x10的位置。

一般而言,main_arena+0x58这个无法控制的值对于任意地址写入来说并没有啥大用,比较常见的用法有改大global_max_fast等等,仅仅将其视为一个较大的数值来覆盖一个想让它变大的变量,而个人认为house of orange最精妙的地方就在于,让它作为指针成为了利用链的核心。

可能看到这里你已经想到了用Unsorted Bin Attack修改_IO_list_all为main_arena+0x58,此时将其视为文件流结构体的话,一般来说画风会是这个样子的:

pwndbg> p *(struct _IO_FILE_plus *)0x7ffff7dd1b78
$1 = {
  file = {
    _flags = 1433903120, 
    _IO_read_ptr = 0x555555758510 "", 
    _IO_read_end = 0x555555758510 "", 
    _IO_read_base = 0x555555758510 "", 
    _IO_write_base = 0x7ffff7dd1b88 <main_arena+104> "\020\205uUUU", 
    _IO_write_ptr = 0x7ffff7dd1b88 <main_arena+104> "\020\205uUUU", 
    _IO_write_end = 0x7ffff7dd1b98 <main_arena+120> "\210\033\335\367\377\177", 
    _IO_buf_base = 0x7ffff7dd1b98 <main_arena+120> "\210\033\335\367\377\177", 
    _IO_buf_end = 0x7ffff7dd1ba8 <main_arena+136> "\230\033\335\367\377\177", 
    _IO_save_base = 0x7ffff7dd1ba8 <main_arena+136> "\230\033\335\367\377\177", 
    _IO_backup_base = 0x7ffff7dd1bb8 <main_arena+152> "\250\033\335\367\377\177", 
    _IO_save_end = 0x7ffff7dd1bb8 <main_arena+152> "\250\033\335\367\377\177", 
    _markers = 0x7ffff7dd1bc8 <main_arena+168>, 
    _chain = 0x7ffff7dd1bc8 <main_arena+168>, 
    _fileno = -136504360, 
    _flags2 = 32767, 
    _old_offset = 140737351850968, 
    _cur_column = 7144, 
    _vtable_offset = -35 '\335', 
    _shortbuf = <incomplete sequence \367>, 
    _lock = 0x7ffff7dd1be8 <main_arena+200>, 
    _offset = 140737351851000, 
    _codecvt = 0x7ffff7dd1bf8 <main_arena+216>, 
    _wide_data = 0x7ffff7dd1c08 <main_arena+232>, 
    _freeres_list = 0x7ffff7dd1c08 <main_arena+232>, 
    _freeres_buf = 0x7ffff7dd1c18 <main_arena+248>, 
    __pad5 = 140737351851032, 
    _mode = -136504280, 
    _unused2 = "\377\177\000\000(\034\335\367\377\177\000\000\070\034\335\367\377\177\000"
  }, 
  vtable = 0x7ffff7dd1c38 <main_arena+280>
}

这一块区域指向的是smallbins数组所在的位置,要想将其作为fake文件流结构体进行构造是非常困难的。

而且先前关于_IO_flush_all_lockp的源码分析中,其实在执行到_IO_OVERFLOW (fp, EOF) == EOF之前需要bypass一些约束条件,简单来说有两种方法,如下:

  • fp->_mode <= 0
  • fp->_IO_write_ptr > fp->_IO_write_base

或者

  • _IO_vtable_offset (fp) == 0
  • fp->_mode > 0
  • *(fp->_wide_data+0x20) > *(fp->_wide_data+0x18)

因此此时 _IO_list_all指向的位置无法构造为满足攻击条件的结构体,但我们注意到_chain所在的位置正好是0x60大小的 smalbins 链表首节点,因此下一轮 fp->_chain 就会把它指向的 chunk 当做一个 _IO_FILE 结构体,在其中构造 vtable 指向存放着 system 地址的地址(伪造的虚表)。此处我们打算在 malloc_printerr 中劫持控制流的话,让 fake_vtable 指向的fake struct _IO_jump_t 中的 __overflow 为 system 即可。由于 heap_base 已知,这一步很轻松。

关于参数传递,回顾一下利用的实现部分

  • _IO_OVERFLOW (fp, EOF) == EOF

fp是指向之前用chunk伪装的fake _IO_FILE结构的指针,因此只要将/bin/sh填入该chunk开头部分就可以执行system(“/bin/sh”)来getshell了

待完善……