vsdo和vsyscall的前世今生
一、vsyscall:一般只有ubuntu16.04,libc-2.23中有了。
1.vsyscall的作用:
现代的Windows/*Unix操作系统都采用了分级保护的方式,内核代码位于R0,用户代码位于R3。许多对硬件和内核等的操作都会被包装成内核函数并提供一个接口给用户层代码调用,这个接口就是我们熟知的int 0x80/syscall+调用号模式。当我们每次调用这个接口时,为了保证数据的隔离,我们需要把当前的上下文(寄存器状态等)保存好,然后切换到内核态运行内核函数,然后将内核函数返回的结果放置到对应的寄存器和内存中,再恢复上下文,切换到用户模式。这一过程需要耗费一定的性能。对于某些系统调用,如gettimeofday来说,由于他们经常被调用,如果每次被调用都要这么来回折腾一遍,开销就会变成一个累赘。因此系统把几个常用的无参内核调用从内核中映射到用户空间中,这就是vsyscall。
2.vsyscall的特点:
(1)某些版本存在,需要用到gdb来查看,IDA中默认不可见。
(2)地址不受到ASLR和PIE的影响,固定是0xffffffffff600000-0xffffffffff601000。
(3)不能从中间进入,只能从函数开头进入,意味着不能直接调用里面的syscall。这里vsyscall分为三个函数,从上到下依次是
1 | #注释头 |
(4)gettimeofday函数执行成功时返回值就是0,保存在rax寄存器中。这就为某些one_gadget创造了条件。
(5)有R,X权限,但是没有W权限,不可写,不能进行shellcode布置。
(6)只有三个系统调用的函数,其余内存都以”int3”指令填充。
3.vsyscall的利用:
(1)调整栈帧,下拉rsp:
vsyscall直接进行syscall,并没有利用栈空间,所以在处理栈溢出,但是由于PIE没有别的地址可以用时,而栈上又有某个有用的地址的时候,可以通过vsyscall构造一个rop链来ret,每次ret都会消耗掉一个地址,将rsp下拉一个单位,这样就可以逐渐去贴近想要的那个地址,最后成功ret到相应的位置。
(2)SROP利用:
syscall ret;指令可用于构造SROP,只需要在其面前放置一个”pop rax;ret”的gadget,通过栈溢出将rax赋值为0xf即可调用__kernel_rt_sigreturn,从而打SROP。
二、vdso:大多版本用vdso取代了vsyscall
1.vdso作用:
与vsyscall差不多,本来就是为了取代vsyscall用的,不过是通过共享库进行映射,和动态加载有点类似。
2.vdso特点:
(1)vdso的地址随机化的,且其中的指令可以任意执行,不需要从入口开始。
(2)相比于栈和其他的ASLR,vdso的随机化非常的弱,对于32的系统来说,有1/256的概率命中。
(3)不同的内核随机程度不同:
A.较旧版本:0xf76d9000-0xf77ce000
B.较新版本:0xf7ed0000-0xf7fd0000
可利用pwn中Linux知识中的文件来查看具体范围。注意偏移需要更改一下,可以调试查看,一般在200以内。用stack 200,查到的地方除以4之后-2就是偏移,不行就再调试。或者直接pwndbg的fmtarg addr来查看偏移。
▲范围测量:
编译一个打印vdso的程序,编译代码为:
gcc -g -m32 vdso_addr.c -o vdso_addr
如果是64位程序则将m32改成m64即可
1 | #注释头 |
然后用脚本即可打印确定范围,将范围调大更加精确:
1 | #注释头 |
3.vdso的利用:
与vsyscall差不多,32位中有现成的__kernel_rt_sigreturn可以打SROP。也可以用gettimeofday来创造rop,绕过PIE,滑过空间。
4.读取靶机的vdso:
(1)如果给了libc,那么根据libc版本使用qemu或者docker创建一个环境,随便运行一个程序,用gdb命令dump下来。(dump memory vdso32.so addr_start addr_end)
(2)如果没给,那么只能先泄露地址再考虑其它。
(3)之后就可以用
1 | #注释头 |
但是这里还是需要加上while循环爆破使用,因为不确定随机化的基地址。
参考资料: