使用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进行实验。