PWN-堆基础之House of Orange
House of Orange
1.IO_FILE
在看House of Orange的例子之前,我们首先了解一个Linux中的利用机制_IO_FILE
可以使用ptype /o struct _IO_FILE
查看这个结构的定义,在pwndbg中可以直接写为dt FILE
_IO_FILE
这个结构体中有一个_chain
可以看到它的类型是一个struct _IO_FILE *
,也就是说是一个指向其他_IO_FILE
结构体的指针;在使用fopen
之类的函数打开一个文件之后,实际上系统会在堆上创建一个这样的_IO_FILE
结构体,并将其加入到一个单链表中,单链表的头部为_IO_list_all
,这个单链表可以理解为是一个专门存储_IO_FILE
的fastbin。另外vtable为虚函数表指针,实际上是为了与C++的streambuf
类兼容才会出现的,但是这里兼容性上的问题,让GLIBC的file stream有可能存在虚函数表劫持的漏洞
另一方面,我们知道Linux的文件系统中一切皆为文件
每一个进程创建时都会有三个标准I/O File Stream:
stdin
stdout
stderr
即使程序没有输出、没有输入,它也会存在这三个标准的I/O
还是以前面gdb打开的/bin/sh
为例,查看它的_IO_list_all
可以看到上面的三项内容
分别就是这三个标准的I/O
这个程序甚至还没有运行起来,所以即使一个程序没有用到文件,我们仍然是有可能利用到I/O File Stream的
2.House of orange
原理
House of orange的核心思想主要是这样几部分:
- 不使用free函数而得到一个free chunk
- 伪造vtable
具体而言,获取free chunk的实现方法是修改Top Chunk
。在House of Force中使用的方法是修改Top Chunk
为一个特别大的值,之后我们申请一个特别大的chunk,循环一遍内存之后就可以访问到原本Top Chunk
上方的内容
那如果将Top Chunk
修改为一个很小的值呢?
假如我们把Top Chunk
修改为一个很小的数,这时再申请一个更大的chunk
内存认为的Top Chunk
是无法满足申请空间的需求的,因此堆管理器后续会再使用brk
申请一块新的区域。
正常来说堆管理器会直接将通过brk
分配的新内存直接并入到Top Chunk中(即让Top Chunk变大)
但是由于我们改小了Top Chunk,堆管理器认为Top Chunk与堆的尾部并不相邻
因此会将原本的Top Chunk Free掉,这样一来,我们就没有通过Free函数得到了一个Free chunk
3.利用
首先,申请一块小的chunk,并修改top_chunk大小字段为一个很小的值。
1 | small_malloc() |
运行后可以看到Top Chunk被我们改写为了0x101
这时我们再申请一个 large chunk,会发现报错了。
报错的原因是:Top Chunk
的prev_inuser
位一定需要为1,并且Top Chunk是否在一个页的边界结尾,即是否按页对齐了。这里显然没有按页对齐。由于在Top Chunk之前我们申请了一个0x20大小的chunk(top chunk_addr为0x7020),因此这个大小我们需要伪造为0x1000-0x20。
其实就是原本old top chunk的地址加上其size之后的地址要与页对齐 也就是address&0xfff=0x000
即
1 | small_malloc() |
这样,程序就能顺利通过并且不借助free函数得到了一块free chunk。
有了该free chunk我们就可以进行unsorted bin attack了,因此将bk值修改为_IO_list_all-0x10,这样unlink后_IO_list_all指针就会指向main_arena了。
但是之后还是会报错,这里错误是因为main_arena
被当作File Stream
对待
我们希望触发的是_IO_OVERFLOW(fp,EOF)
这一行
在这之前第一句进行了两个检查
1 | fp->mode <=0 && fp-> _IO_write_ptr > fp-> _IO_write_base |
这里被解析时_mode
被认为是一个负数,并且这里write_base == wirte_ptr 所以不满足执行_IO_OVERFLOW条件,程序认为这个File Stream不需要被关闭,于是直接去看下一个FileStream了
下一个File Stream是去找了_chain
这个对象指向的位置,我们可以看到这里仍然在main_arena
中
这里是main_arena+168
,值为0x7ffff7dd4bc8
的位置
对应main_arean结构图发现该位置正好是small bins 0x60的位置,所以在第一次_IO_flush_all_lockp
执行失败后会顺着_chain
找到这个0x60的smallbins进一步处理
那我们如果将一个伪造的FileStream数据填到这里就可以执行对应的内容了
如果我们将unsortedbin的大小伪造为0x60,在申请一个0x20大小的chunk时,这个unsortedbin就会被sort到这个0x60的smallbins中
也就是说我们直接在这个bins中伪造数据,就会被解析为FileStream了
1 | small_malloc() |
那么伪造File Stream的数据都需要填哪些呢?
在wirte_end
及之后的内容直到mode
都是对我们没用的数据,直接先填0
这部分数据长度从0x30
到0xc0
,用p64(0)*18
来填充,其余部分我们可以构造为:
1 | payload = b"Y"*0x10 |
1 | payload = b'a'*0x10 |
以上内容就构造完毕了_IO_FILE
结构,但是关键的vtable
指针还没有修改
可以发现,overflow位于vtable的偏移为24位置处,因此我们需要向该位置填入system的地址。那么计算一下vtable的值,从heap开始的位置,首先加上是0x20
大小的一个small chunk,加上0xd8
大小的整个伪造的_IO_FILE
结构减去重合的8字节,最后是减去overflow函数的地址偏移24即0x18
1 | vtable = heap + 0x20 + 0xd8 - 0x8 - 0x18 |
最后就是overflow
的值了
这里有两个选择,一是可以直接填写一个one_gadget,另一个简单方法就是使用system(“/bin/sh”),实际上调用的参数是fp
,也就是最开始的flags,因此可以修改flags为”/bin/sh”。
因此,完整exp:
1 | #----- 修改Top Chunk得到一个Free chunk |