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 |
第一个宏对应着native
,第二宏对应着emulate
,可以看到他们都是内核和用户都可以访问的区域,但是第一个允许页执行。这也就对应了不同模式。
在native
下就可以直接在vsyscall区域上执行syscall,syscall过程不在这里阐述,可以看看我前一篇总结。
而在emulate
下这个时候是不允许去执行,这个时候就会引发page fault,内核在处理page fault的时候,其中有一步会去判断这个地址是不是属于vsyscall ,可能就需要做额外的操作: 1
2
3
4
5if (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个参数timeval
和timezone
,根据C ABI规则,需要验证rdi
和rsi
这两个地址是否可以访问。 - 完成系统调用返回结果
- 错误处理,返回SIGSEGV
因为第二步的映射过程,所以我们必须要跳到syscall的起始,由于在还需要验证参数的合法性,所以在用vsyscall的时候,还要取决于rdi
,rsi
这些寄存器里面的值合法性的问题,就是能不能写的问题。
思考
这可能也就是上面文章评论下面出现的问题。vsyscall已近乎被废弃,被vdso替代,其实也很难去利用。但是vsyscall地址是固定的,但是vdso相当于动态加载库,那glibc是怎么实现调用里面函数的呢? 很简单,先在glibc里面定义一个,把它设置为弱符号函数,在vdso加载时候gettimeofday
也就重新定义了。 1
2
3
4
5
6
7
8
9int
__gettimeofday (struct timeval *tv, struct timezone *tz)
{
__set_errno (ENOSYS);
return -1;
}
libc_hidden_def (__gettimeofday)
weak_alias (__gettimeofday, gettimeofday)
libc_hidden_weak (gettimeofday)