2022强网杯的house of cat。高版本下UAF的直接利用。看了很多wp发现在高版本下,基本所有攻击都聚焦于FSOP方向了。看来这才是大势所趋。
注意到前几天dice_hope也有一个一模一样的题,简单做法是改libc的got表为system,也可以FSOP泄露一个栈地址来劫持返回地址。但是这道题开了沙箱,也就是只能用第二种方法了。
house of cat攻击手法
目前这种攻击手法适用于任何版本。包括最新的2.35.其利用条件为
- 能够任意写一个可控地址
- 能够泄露堆地址和libc基址
- 能够触发IO流(FSOP或触发__malloc_assert),执行IO相关函数
本质上来说FSOP的核心还是可控地址写一个可控数据,之后再FSOP的作用下完成调用vtable里面的函数,完成的攻击。
注意vtable在glibc2.24之后就有了地址检查,也就是判断vtable整体位置。防止伪造整个vtable。这也就直接导致了house of orange在2.23之后的版本中无法使用。以下是具体的检查代码。
1 | void _IO_vtable_check (void) attribute_hidden; |
具体看看是怎么检查的。可以看到第七行是检查当前vtable和libc的vtable开始地址之间的偏移,如果大于一个预定好的大小范围久做更细致的vtable检查。也就是说我们难以伪造整个vtable。但是对于其中的函数指针检查还是相对宽松的。因此我们可以通过改vtable中的函数指针来执行函数。
我们这里修改的函数为_IO_WOVERFLOW
。可以具体看一下它的代码和汇编代码
1 |
1 | ► 0x7f4cae745d30 <_IO_switch_to_wget_mode> endbr64 |
这里的汇编源代码如下
1 | int |
可以从上面的汇编中很清楚的看到以下关键的内容
1 | 0x7f4cae745d34 <_IO_switch_to_wget_mode+4> mov rax, qword ptr [rdi + 0xa0] |
这里的寄存器rdi(fake_IO的地址)、rax和rdx都是我们可以控制的,在开启沙箱的情况下,假如把最后调用的[rax + 0x18]设置为setcontext,把rdx设置为可控的堆地址,就能执行srop来读取flag;如果未开启沙箱,则只需把最后调用的[rax + 0x18]设置为system函数,把fake_IO的头部写入/bin/sh字符串,就可执行system(“/bin/sh”)
下面看一下house of cat的模板
1 | fake_io_addr=heapbase+0xb00 |
house of cat题目
这里就选择2022强网杯pwn里面的house of cat作为例子。这也是解出人数最多的一个pwn题目。
首先看看这题是存在沙箱保护的。并且限制了文件描述符只能为0.
这道题给我们的条件是可以分配大小为size <= 0x417 || size > 0x46F的chunk,最多分配16个,可以edit两次,并且每次只能修改30字节。free的时候存在UAF。另外还有show()函数。
首先这道题需要逆一下怎么输入能够通过输入条件的检验,我比赛的时候就卡在这个地方就卡了两个小时。。。输入的结构是这样的
1
2 LOGIN | r00t QWB QWXFadmin
CAT | r00t QWB QWXF$\xff
之后就可以类似堆题一样,做上述的四种操作。
step1 泄露地址
完成上述的FSOP我们首先需要泄露堆地址和libc地址。由于UAF,这两者泄露比较容易。堆地址可以直接从largebin中拿到,而堆地址需要建立两个largebin的group,之后一并输出就能拿到一个包含下一个largebin group地址的chunk,也就能拿到堆地址。
1 | input_str1 = b"LOGIN | r00t QWB QWXFadmin" |
step2 largebin attack
关于largebin attack的细节可以参考我之前学习的文章。注意在2.31之后对于largebin的利用也有一些变化。这里就要利用最后一种方式。
可以看到程序给了我们两个edit的机会。第一次:我们要用largebin attack改掉libc中stderr的指针,改向一个chunk,里面是伪造的FILE table。第二次:我们要用largebin attack改掉top chunk的size,从而触发malloc_assert的IO流错误。回顾一下largebin attack在2.31之后的利用手段,总结如下。
分配两个chunk,他们两个大小需要在一个largebin范围内,但是不能相同(例如0x418和0x428)。记小的chunk为A,大的为B。
首先free(B),B会进入unsorted bin,之后add一个更大的块,将B放入largebin中。
修改B的bk_nextsize为target-0x20。
释放A,A进入unsortedbin
add一个更大的块,让A进入largebin。此时target中就会被写上A所在的地址。
如果把target写成stderr,A中布置好一个fake IO_FILE结构体,那么就可以完成一次FSOP。
第一次攻击
我们首先来看这道题已经对堆做的相关操作。(也就是到后面泄露堆为止的操作)
操作 | index | 大小 |
---|---|---|
add | 0 | 0x420 |
add | 1 | 0x430 |
add | 2 | 0x418 |
delete | 0 | 0x420 |
add | 3 | 0x440 |
现在0在largebin中,我们第一步就是修改0号chunk的next_size部分为target-0x20。这里设置为stderr-0x20即可。
之后我们将2释放,也就是delete(2),现在chunk2就在unsortedbin中。我们申请一个大的块,将2放入largebin,就能完成这次的攻击。具体的代码如下。这里一开始变成了delete(6),是一样的,先把2取出来给6,因此2和6相同了。
1 | add(6,0x418,payload1) # 申请一个比原先chunk小的块,但是要在一个size map里面 |
第二次攻击
第二次攻击的目标是top chunk的size字段。方法和之前类似。不过现在的target address变成了堆的基地址加上一个偏移量。代码如下所示
1 | add(8,0x430,'eee') # 小的chunk |
此时如果我们再去add一个大块,将会触发以下内容
- 对unsortedbin进行检查,将小的chunk放到largebin
- 触发largebin attack,修改top chunk的size
- 分配一个块的时候触发top chunk size的检查,触发FSOP。而此时stderr正是我们在第一次攻击中布置好的伪造的chunk。此时通过chunk中不同位置判断条件触发
_IO_switch_to_wget_mode
在跳转到setcontext
就可以完成攻击了。
step3 setcontext
由于这道题存在沙箱,我们需要使用set_context+0x3d。具体使用方法是找到libc中的如下函数。蓝色框中的位置
跳转到的位置
因此我们可以将rdx+0xa8作为ROP的开始,后面这个地址将作为新的栈顶指针。之后布置一系列寄存器作为参数传递。当然在我们已经拿到libc之后,也不用管理参数,后面ROP自行修改即可。
构造ROP和FILE
伪造FILE
这里是这道题最难的地方。我们怎么构造伪造的FILE结构体,布置好参数,跳转到set context执行命令呢?我们重点分析以下payload,这是fake file的具体内容。
1 | fake_IO_FILE = p64(0)*4 |
结合以下汇编分析。回忆一下我们首先通过一些flags的位置变化来让stderr执行到这个函数。此时我们只有rdi
是可控的。rdi
也就是这个fake file的指针。指向一开始的四个p64(0)。
首先看我们把[rdi+0xa0]移动到rax。在上面的伪造FILE中,也就是heap_base+0xb30
。而这个地址,实际上指向的是什么呢?我们动态调试一下。用以下命令下断点。
1 | gdb.attach(io,'b* (_IO_wfile_seekoff)') |
这一步之后,就是上面wget相关函数。我们在运行完将结果给到rax之后,看一看rax。发现其实是伪造FILE的第二行。
接下来两句汇编
1 | 0x7fd903968d3f <_IO_switch_to_wget_mode+15> mov rdx, qword ptr [rax + 0x20] |
执行之后rdx是
这个地址里面存的内容是什么呢?不着急,我们往下看。
不过接下来,到达了以下汇编部分
这里经过cmp之后,并没有在jmp跳转。直接进入了后面。好,在这里我们重新设置了rax。而这里的rax也和我们最重要调用的函数息息相关。我们原本的rax是
之后,加上0xe0并解引用之后的结果如下。而这部分在Fake_file中的体现也就是第18行,在未解引用的时候位于FAKE_FILE第四行1的位置,解引用之后加上0x18的偏移量,正好是setcontext的地址。
好,在进入setcontext之前我们看一眼寄存器。
setcontext的具体构造
在setcontext中,我们将要经过两次跳转。第一次的jump是必然发生的。但是第一次跳转我们其实只需要其中的一条汇编,也就是修改rsp为ROP的起始位置(注意看现在rsp为pop rdi了)。注意看这里是根据rdx的偏移来放置内容的。好,到这里就明确了,之前我们在fake_file中放置的rdx的值(见FAKE_FILE旁的注释)实际上和最终的ROP链息息相关。怎么计算呢?看到这里取出的是[rdx+0xa0],那么实际上就是在第9个chunk中的ROP链==所在的地址==减去0xa0。
如下所示。注意不是ROP链的地址,是所在的地址。而这个地址我们可以人为的写在某个地方。在这里,就是FAKE_FILE最后末尾的p64(heap_base+0x2050)
接下来的几个赋值并不重要。重要的是我们一定会完成这个跳转。注意这里push了一个rcx。这个也是当前rdx偏移计算的。**这个值正是我们伪造的FILE的最后一个字段内容p64(ret)**。其实就是一个ret。
其实跳转之后的汇编也可以用来布置参数(当GADGET比较少的情况下)。我们此时rdx依然是之前的位置没有变。可以通过此处布置参数(可以看到rsi,rdi都在里面了)也可以在后面ROP的时候自己构造。这里提前防止参数的好处就是在无法泄露堆地址的情况下,可以直接获取堆对应位置字符串。如果后续使用ROP需要知道堆地址。
终于,到了ROP
ROP
这里由于只允许文件描述符0,因此先关闭0,之后打开文件的时候文件描述符自动会变成0。
1 | payload = p64(pop_rdi)+p64(0)+p64(close)+p64(pop_rdi)+p64(flag_addr)+\ |
其实之后也就不复杂了。先close(0)之后就是正常的ORW。由于我们当前有储存在chunk的文件名“flag”,也有堆地址,可以任意指定一个地方储存flag内容。因此剩下的就不复杂了。我们也可以在这里布置参数,就不需要setcontext布置了。
这里还学到一个就是libc的read write等参数调用和系统调用是一模一样的。其实这里用了glibc的,用系统调用也完全没有问题。(并且好像glibc也就是在外面套了一层壳)
至此这道题结束了。
总结
构造方法
这道题最重要的是提供了一种IO_FILE中,控制其重要寄存器rdx,并且调用set context的一种手法。首先这道题用largebin attack改了stderr,写成了一个chunk,之后触发malloc_assert,在chunk中写FAKE_FILE,触发set_context,完成ROP。如果这道题没有开沙箱,那么直接将chunk开头的部分写成/bin/sh(将会存在rdi中),调用的函数写成system即可。
回顾一下构造的IO_FILE。看看要改什么地方。
1 | fake_IO_FILE = p64(0)*4 |
从上往下看,这里的第5行,需要改成存放ROP指令的chunk的地址减去0xa0之后的数值存放的地址。用一张图表示,不然太拗口了。就是填0x210。而地址A-0xa0的数值,可以填在这个FILE结构体后面(就相当于下面绿色的块,不过填在哪里都可以)
第六行填写要调用的函数的地址。
第12行填写指向第二行p64(0)所在的地址
第16行写入上面这个固定的内容,绕过vtable的检验。可以通过以下方式找到。
第18行rax2将会指向第四行的p64(1)的位置,影响到后面call的函数。
最后一行中末尾还需要加一个ret的gadget,通过在set_context中rdx的偏移找到,并且用于在ROP中(因为一开始会push一个rcx,就是这个值)
利用条件
- 泄露libc和堆(很多地方用到了偏移)
- 能够通过largebin attack或者tcache stashing unlink等修改stderr(可控地址写入可控值)
- 能够触发malloc_assert流(例如top chunk的size出错,基本上所有malloc相关的出错都会调用assert)