典型的House of orange + unsorted bin attack + _IO_FILE,之前在melody_center中有写过,基本是一样的,除了一些利用的细节。权当是练习了,毕竟第一次接触的时候,写起来有点吃力。
题目描述
x64程序,保护除了PIE全开:
1 | Arch: amd64-64-little |
题目提供了五个功能:
1 | ---------------------- |
最开始的时候会要求输入author name。add功能就是添加一个page,地址和大小分别存在bss上两个数组里面。view功能就是输出一个给定page的内容。edit功能就是修改一个给定page的内容,但是这里会重新根据strlen(content)来修改之前提到的bss上存大小的数组里,从而影响下一次可以读入的字节数。info功能就是输出author name, page数量,以及提供更改author name的选择。
相关知识点
House of orange
House of orange攻击的核心,就是在没有free函数的情况下,释放出一个unsorted bin供利用,而主要的原理就是利用top chunk。
一般在可以overwrite top chunk size的情况下,请求malloc一个比伪造的top chunk size更大的空间时。在_int_malloc检验fastbin, smallbin, unsorted bin, large bin没有找到符合需求的块时,接下俩_int_malloc会试图从top chunk切,如果top chunk的空间也不够,那么会执行:
1 | /* |
此时ptmalloc已经不能满足用户申请堆内存的操作,需要执行sysmalloc来向系统申请更多的空间。但是对于堆来说有mmap和brk两种分配方式,我们需要让堆以brk的形式拓展,之后原有的top chunk会被置于unsorted bin中。
综上,我们要实现brk拓展top chunk,但是要实现这个目的需要绕过一些libc中的check。首先,malloc 的尺寸不能大于mmp_.mmap_threshold:
1 | if ((unsigned long)(nb) >= (unsigned long)(mp_.mmap_threshold) && (mp_.n_mmaps < mp_.n_mmaps_max)) |
如果所需分配的chunk大小大于mmap分配阈值,默认为128K,并且当前进程使用mmap()分配的内存块小于设定的最大值,将使用mmap()系统调用直接向操作系统申请内存。
此外,在sysmalloc函数中存在对top chunk size的check,如下:
1 | assert((old_top == initial_top(av) && old_size == 0) |
这里检查了top chunk的合法性:
- 如果第一次调用本函数,top chunk可能没有初始化,所以可能
old_size为 0。 - 如果top chunk已经初始化了,那么top chunk的大小必须大于等于
MINSIZE,因为top chunk中包含了 fencepost,所以top chunk的大小必须要大于MINSIZE。 - 其次top chunk必须标识前一个chunk处于inuse状态.
- 并且top chunk的结束地址必定是页对齐的。
- 此外top chunk除去fencepost的大小必定要小于所需chunk的大小,否则在
_int_malloc函数中会使用top chunk分割出chunk。
因此伪造的top chunk size需要满足: - 伪造的size必须要对齐到内存页(0x1000)
- size要大于
MINSIZE(0x10) - size要小于之后申请的
chunk size + MINSIZE(0x10) - size的
prev inuse位必须为 1
unsorted bin attack
unsorted bins是一个双向链表,它采用的遍历顺序是FIFO,即插入时插在头部,取出时从尾部取。
当响应内存分配请求时,若small bin中不含符合要求的块,那么此时就会从unsorted bins中查找,如果有符合要求的,就会被取出来使用,其余的根据大小分别放在不同的bin中。
从unsorted bins中取块时,会将改该unsorted bin的bck->fd写为unsorted bins的地址(glibc2.23的版本下是main_arena+0x58,以后提及“unsorted bins的地址”,均用main_arena+0x58代替)。
1 | /* remove from unsorted list */ |
于是只要伪造unsorted bin的bk的值,就能将main_arena+0x58写到bk的fd处。

事实上,一般在配合House of orange的情况下,一般会选择将_IO_list_all更改为main_arena+0x58,而此时_IO_list_all->_chains正好指向main_arena中size为0x60的smallbin。
fake _IO_FILE and vtable
前面提到_IO_list_all->_chains正好指向main_arena中size为0x60的smallbin,那么只要伪造出对应的smallbin为struct _IO_FILE以及伪造出相应的vtable地址以及vtable内容,就可以达到getshell的目的。
因为这题是glibc2.23,所以不存在对vtable的check,所以vtable也可以随意构造。至于更高的glibc版本(如glibc2.24),存在一些相应的对vtable的检查机制,构造vtable会有困难,解决方案可以参考melody_center。
对于_IO_FILE的构造,需要满足以下条件:
fp->_mode= 0fp->_IO_write_ptr<fp->_IO_write_basefp->_IO_read_ptr= 0x61 smallbin with size 0x60fp->_IO_read_base=_IO_list_all-0x10_IO_list_all ==> main_arena+0x58vtable->_IO_OVERFLOW=system_IO_OVERFLOW(fp);fpstart with “/bin/sh\x00”
显然对于glibc2.24中利用_IO_str_jumps的方法,对于glibc2.23同样适用。
利用思路
edit功能中由于会根据strlen重新设置可读入的长度,因此可以在长度包含next chunk的size域的时候,可以完成对next chunk的size字段的修改,这里是更改top chunk的size,使其在下一次分配更大的空间的时候,被free到unsorted bin中。- 由于malloc出的chunk里的内容不会被清0,因此在从unsorted bin中分配内存时,只要将原fd字段用字符填满,即可利用
view功能leak出bk处的main_arena相关地址,从而得到libc的基地址。 add功能中对个数判断存在漏洞,即bss上的储存chunk的空间只有8个,而这里可以分配第九个也就是chunk_array[8],而chunk_array[8]其实就是chunk_size[0],只要利用edit功能中对size的改写将chunk_size[0]变为0(通过输入’\x00’开头可以做到),就可以将chunk_size[0]overwrite为堆地址,从而可以接受更多输入,这一点很重要,因为后面修改伪造_IO_FILE,vtable以及smallbin需要用到。1
2
3
4
5
6
7for ( i = 0; ; ++i )
{
if ( i > 8 )
return puts("You can't add new page anymore!");
if ( !chunk_array[i] )
break;
}- 接下来就是构造
_IO_FILE结构体了,因为这里没有选择leak heap address,也就没有选择在堆块中伪造vtable,还是选择利用_IO_str_jumps其实就是偷个懒,用一下之前写过的脚本。其实可以通过author name把紧跟着的chunk_array[0]打印出来,然后伪造vtable,然后利用_IO_OVERFLOW(fp)…
exp
1 | context.log_level = 'debug' |
小结
- 很奇怪,本地还是打不通,明明都断在
system里面了,参数rdi也是对的,而且远程也是概率性打通。 - 调试的时候,leak出来的libc地址不是预期的
main_arena+0x58的值,而是会大0x610,还没搞清楚原因。