使用vdso/vsyscall 绕过 PIE

在ubuntu16.04中查看vsyscall地址

cat /proc/self/maps
00400000-0040c000 r-xp 00000000 fd:00 101615781                          /bin/cat
0060b000-0060c000 r--p 0000b000 fd:00 101615781                          /bin/cat
0060c000-0060d000 rw-p 0000c000 fd:00 101615781                          /bin/cat
024ad000-024ce000 rw-p 00000000 00:00 0                                  [heap]
7fb818b9a000-7fb818d5a000 r-xp 00000000 fd:00 34635806                   /lib/x86_64-linux-gnu/libc-2.23.so
7fb818d5a000-7fb818f5a000 ---p 001c0000 fd:00 34635806                   /lib/x86_64-linux-gnu/libc-2.23.so
7fb818f5a000-7fb818f5e000 r--p 001c0000 fd:00 34635806                   /lib/x86_64-linux-gnu/libc-2.23.so
7fb818f5e000-7fb818f60000 rw-p 001c4000 fd:00 34635806                   /lib/x86_64-linux-gnu/libc-2.23.so
7fb818f60000-7fb818f64000 rw-p 00000000 00:00 0 
7fb818f64000-7fb818f8a000 r-xp 00000000 fd:00 34635786                   /lib/x86_64-linux-gnu/ld-2.23.so
7fb81915c000-7fb819181000 rw-p 00000000 00:00 0 
7fb819189000-7fb81918a000 r--p 00025000 fd:00 34635786                   /lib/x86_64-linux-gnu/ld-2.23.so
7fb81918a000-7fb81918b000 rw-p 00026000 fd:00 34635786                   /lib/x86_64-linux-gnu/ld-2.23.so
7fb81918b000-7fb81918c000 rw-p 00000000 00:00 0 
7ffe0a727000-7ffe0a748000 rw-p 00000000 00:00 0                          [stack]
7ffe0a75f000-7ffe0a761000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

vsyscall简单的介绍

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

more /proc/`pidof 1000levels`/maps |grep vsyscall
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

我们发现vsyscall这个地址总是不变的

gdb中查看vsycall

gdb-peda$ x/3i 0xffffffffff600000
   0xffffffffff600000:	mov    rax,0x60
   0xffffffffff600007:	syscall 
   0xffffffffff600009:	ret    
gdb-peda$ x/3i 0xffffffffff600400
   0xffffffffff600400:	mov    rax,0xc9
   0xffffffffff600407:	syscall 
   0xffffffffff600409:	ret    
gdb-peda$ x/3i 0xffffffffff600800
   0xffffffffff600800:	mov    rax,0x135
   0xffffffffff600807:	syscall 
   0xffffffffff600809:	ret     

漏洞代码

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // eax@2

  init();
  banner();
  while ( 1 )
  {
    while ( 1 )
    {
      print_menu();
      v3 = read_num();
      if ( v3 != 2 )
        break;
      hint();                                   // 由于栈帧开辟的原理,hint和go的rbp应该是同一个地址
    }
    if ( v3 == 3 )
      break;
    if ( v3 == 1 )
      go();                                     // 由于栈帧开辟的原理,hint和go的rbp应该是同一个地址
    else
      puts("Wrong input");
  }
  give_up();
  return 0;
}

由hint中汇编代码可知,system地址在rbp+var_110这个位置

.text:0000000000000D16                 mov     rax, [rbp+var_110]
.text:0000000000000D1D                 lea     rdx, [rbp+var_110]
.text:0000000000000D24                 lea     rcx, [rdx+8]
.text:0000000000000D28                 mov     rdx, rax
.text:0000000000000D2B                 lea     rsi, aHintP     ; "Hint: %p\n"
.text:0000000000000D32                 mov     rdi, rcx        ; s
.text:0000000000000D35                 mov     eax, 0
.text:0000000000000D3A                 call    _sprintf

由go中的汇编代码可知,v5和v6的地址就是system的地址

.text:0000000000000BB9                 mov     rax, [rbp+var_120]
.text:0000000000000BC0                 mov     [rbp+var_110], rax
.text:0000000000000BC7
.text:0000000000000BC7 loc_BC7:                                ; CODE XREF: go(void)+3Bj
.text:0000000000000BC7                 lea     rdi, aAnyMore?  ; "Any more?"
.text:0000000000000BCE                 call    _puts
.text:0000000000000BD3                 call    _Z8read_numv    ; read_num(void)
.text:0000000000000BD8                 mov     [rbp+var_120], rax
.text:0000000000000BDF                 mov     rdx, [rbp+var_110]
.text:0000000000000BE6                 mov     rax, [rbp+var_120]
.text:0000000000000BED                 add     rax, rdx
.text:0000000000000BF0                 mov     [rbp+var_110], rax
.text:0000000000000BF7                 mov     rax, [rbp+var_110]
.text:0000000000000BFE                 test    rax, rax
.text:0000000000000C01                 jg      short loc_C14
.text:0000000000000C03                 lea     rdi, aCoward    ; "Coward"
.text:0000000000000C0A                 call    _puts

利用vdso绕过PIE

由于vsyscall地址的固定性,这个本来是为了节省开销的设置造成了很大的隐患,因此vsyscall很快就被新的机制vdso所取代。与vsyscall不同的是,vdso的地址也是随机化的,且其中的指令可以任意执行,不需要从入口开始,这就意味着我们可以利用vdso中的syscall来干一些坏事了。

由于64位下的vdso的地址随机化位数达到了22bit,爆破空间相对较大,爆破还是需要一点时间的。但是,32位下的vdso需要爆破的字节数就很少了。同样的,32位下的ASLR随机化强度也相对较低,读者可以使用附件中的题目~/NJCTF 2017-233/233进行实验。