vsyscall 到底应该怎么用?

syscall 前面加个V,v是virtual的意思,顾名思义,把syscall放在一个特殊的地方,而这个位置是一个比较特殊的虚拟地址区域,简称fixed-map.

PIE保护详解和常用bypass手段 一文中提到了用vsyscall来当ret gadget,可以用来跳栈,因为vsyscall的位置是固定的,可以在/root/linux/Documentation/x86/x86_64/mm.txt找到它的位置:

1
ffffffffff600000 - ffffffffff600fff (=4 kB) legacy vsyscall ABI

可以看到这其实一个已经被废弃的ABI. 在上述文章评论有一些疑惑,跳到vsyscall上去执行相关系统调用的时候,还是出错了,文中也提到了只能跳到syscall起始的位置才能执行不出错。这是为什么呢?

在 linux inside 一书中的linux-syscall-3.html 的章节已经阐述的很清楚了。我再给它搬一遍

vsyscall是一个预先设定好的地址,就是上面的说的fixed-map,当然只能保证这个位置不被其他过程占用,这个地址里面是否存在相应的syscall,取决于一个参数vsyscall_mode

vsyscall的设定有三种模式:

  • native
  • emulate
  • none

最后一个模式好说,不专门去为每个用户态的进程里面映射vsyscall的区域。前两种模式的最大的区别在于,vsyscall的这个内存区域的属性。

1
2
#define __PAGE_KERNEL_VSYSCALL  (__PAGE_KERNEL_RX | _PAGE_USER)
#define __PAGE_KERNEL_VVAR (__PAGE_KERNEL_RO | _PAGE_USER)

第一个宏对应着native ,第二宏对应着emulate,可以看到他们都是内核和用户都可以访问的区域,但是第一个允许页执行。这也就对应了不同模式。

native下就可以直接在vsyscall区域上执行syscall,syscall过程不在这里阐述,可以看看我前一篇总结。

而在emulate下这个时候是不允许去执行,这个时候就会引发page fault,内核在处理page fault的时候,其中有一步会去判断这个地址是不是属于vsyscall ,可能就需要做额外的操作:

1
2
3
4
5
if (unlikely((error_code & X86_PF_INSTR) &&
((address & ~0xfff) == VSYSCALL_ADDR))) {
if (emulate_vsyscall(regs, address))
return;
}

regs是一个pt_reg结构保存着发生page fault的时候当时的寄存器状态。所以这个emulate 模式的具体实现 ,其实在emulate_vsyscall这个函数里面

这个函数做了下面几件事:

  • vsyscall_mode 是否为emulate
  • addr_to_vsyscall_nr(address),这是一个从地址映射到系统调用号的过程,因为syscall调用起始地址都是1024对齐的。所以编号可以很容易的 >>12,编号从0开始。
  • 判断系统调用号是否正确
  • 判断系统调用过程中传递的参数地址是否可以访问和写。例如其中的gettimeofday调用过程,需要传递2个参数timevaltimezone,根据C ABI规则,需要验证rdirsi这两个地址是否可以访问。
  • 完成系统调用返回结果
  • 错误处理,返回SIGSEGV

因为第二步的映射过程,所以我们必须要跳到syscall的起始,由于在还需要验证参数的合法性,所以在用vsyscall的时候,还要取决于rdi,rsi这些寄存器里面的值合法性的问题,就是能不能写的问题。

思考

这可能也就是上面文章评论下面出现的问题。vsyscall已近乎被废弃,被vdso替代,其实也很难去利用。但是vsyscall地址是固定的,但是vdso相当于动态加载库,那glibc是怎么实现调用里面函数的呢? 很简单,先在glibc里面定义一个,把它设置为弱符号函数,在vdso加载时候gettimeofday也就重新定义了。

1
2
3
4
5
6
7
8
9
int
__gettimeofday (struct timeval *tv, struct timezone *tz)
{
__set_errno (ENOSYS);
return -1;
}
libc_hidden_def (__gettimeofday)
weak_alias (__gettimeofday, gettimeofday)
libc_hidden_weak (gettimeofday)