典型的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_base
fp->_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);fp
start 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,还没搞清楚原因。