系统调用
linux 的系统调用,是 linux 内核一种特殊的运行机制,使得用户空间的应用程序可以请求,像写入文件和打开套接字等特权级下的任务。在 linux 内核中发起一个系统调用是特别昂贵的操作,因为处理器需要中断当前正在执行的任务,切换内核模式的上下文,在系统调用处理完毕后跳转至用户空间。 vsyscall 设计用来加速系统调用的处理。
vsyscall 介绍
vsyscall 或 virtual system call 是一种也是最古老的一种用于加快系统调用的机制。 vsyscall 的工作原则其实十分简单。Linux 内核在用户空间映射一个包含一些变量及一些系统调用的实现的内存页。vsyscall 是由 map_vsyscall 实现的,它的源码如下:
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);
}
我们重点关注最后一行 , BUILD_BUG_ON 宏用于检查 vsyscall 内存页的虚拟地址是否等于变量 VSYSCALL_ADDR ,而 VSYSCALL_ADDR 定义如下:
#define VSYSCALL_ADDR (-10UL << 20) /* VSYSCALL_ADDR=0xFFFFFFFFFF600000 */
也就是说 vsyscall 的内存页的位置在任何时刻都是相同的,值为 0xFFFFFFFFFF600000 。我们可以验证一下,使用命令:
sudo cat /proc/1/maps | grep vsyscall
输出结果如下:
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
vsyscall 内存页中包含了三个系统调用:
__vsyscall_page:
mov $__NR_gettimeofday, %rax
syscall
ret
.balign 1024, 0xcc
mov $__NR_time, %rax
syscall
ret
.balign 1024, 0xcc
mov $__NR_getcpu, %rax
syscall
ret
- gettimeofday:获取当前时间和时区信息。
- time:获取系统时间(秒数)。
- getcpu:确定调用线程正在运行的 CPU 和 NUMA 节点概要。
在 glibc 源码中找到地址定义如下:
#define VSYSCALL_ADDR_vgettimeofday 0xffffffffff600000
#define VSYSCALL_ADDR_vtime 0xffffffffff600400
#define VSYSCALL_ADDR_vgetcpu 0xffffffffff600800
利用方式
从上面的介绍我们可以知道,那怕程序开启了 pie , vsyscall 的地址还是不变的,而且这三个系统调用对程序运行基本没有影响,也就是说我们获得了三个已知地址的 ret gadget ,分别是 0xffffffffff600000 、 0xffffffffff600400 、 0xffffffffff600800 。我们将这个三个地址简单当成 ret 使用即可。
比如现在栈情况如下:
0x7fffffffda60:0xaaaaaaaaaaaaaaaa <-- rbp
0x7fffffffda68:0xbbbbbbbbbbbbbbbb <-- ret_addr
0x7fffffffda70:0xcccccccccccccccc
0x7fffffffda78:0xcccccccccccccccc
0x7fffffffda80:0xdddddddddddddddd <-- get_shell_addr
我们通过栈溢出修改如下:
0x7fffffffda60:0xaaaaaaaaaaaaaaaa <-- rbp
0x7fffffffda68:0xffffffffff600000 <-- ret_addr
0x7fffffffda70:0xffffffffff600000
0x7fffffffda78:0xffffffffff600000
0x7fffffffda80:0xdddddddddddddddd <-- get_shell_addr
这样函数退出时,就类似与执行:
ret ret --> ret ret ---> ret ret --> ret get_shell_addr
最终成功 get shell ,当然这里只是举例了比较简单的利用方式,具体姿势还要根据题目灵活运用。