Since it's March, which is of course the anniversary (plus a couple months...) of the Boston Tea Party, I should probably talk about the Boston Key Party. That's right, it's time for

BkPctf Post-mortem

I'd like to talk about two problems, and then I'll share some general thoughts at the end. Unfortunately, my problems this CTF weren't quite what I wanted; I had some interesting networks challenges in development, but between uncooperative libraries and other factors they didn't see the light of day. Expect them next year!

Riverside

This is dannyc's problem, so I'm not going to talk too much about it. The problem idea originates in staring at the PS3/PS4 (i.e., Dark Souls / Bloodborne boxes) - or rather, having to explicitly look away while people wrestle with the onscreen keyboard to log in. The design of this system is fantastic: anyone who looks at the screen can see the password, anyone who looks at the controller can get the password, and, since the PS3/PS4 controllers are all wireless, anyone listening to that gets the password too.

That eventually evolved into mouse sniffing, and you can read more about the problem at the CTF writeups github. I did really enjoy Samurai taping their laptop screen, and I also really liked the video a person on IRC sent me of the keystrokes rendered in real-time as part of a question. One thing that seemed to really trip people up was what happened while the mouse was over the 'g' key; a noncompliant packet was sent, which caused many people's interpreter scripts to register a double click. Unfortunately, since this was recorded with real hardware and Linux handled it correctly (i.e., single-click), there's not too much to be done about it.

Approximately 90 teams solved this problem, placing it just barely in the top third of problems solved.

Central Square

Since this is entirely my problem, I can talk more about it. At 200 points, this should be on the easy side of pwnables, but require some legwork. This problem was released accompanied by the following text:

Check out our new image filter!

and a link to the problem server website.

My hope was that by using the phrase "image filter", people would think of Instagram, or at the very least, send it... images. Based on the traffic I saw during the CTF, that seems not to have been the case.

Especially entertaining to me was the dirbuster/sqlmap traffic, which I believe prompted this tweet. The server never went about 7% CPU load, despite a near-constant stream of both kinds of traffic.

Anyway, the server works by downloading the supplied image and translating it to XPM format. Once it's in XPM, it performs a buggy transformation (see C source below), and feeds the transformed XPM file back to the user.

Let's explore that a bit more. The transformation involves what is quite possibly the absolute worst parser I could have written: I #include the image. That is, directly controlled user input. Depending on how well you know C, this is either trivial or complex. You can see some people's solutions at the CTF writeups github.

It concerned me that most of the questions on IRC had absolutely no idea what this server even was. It makes sense to me that the browser wouldn't display the content type "text/xpm" I set (after all, there really isn't a good reason to use this format anymore), but many people didn't try to work outside their browser (except for the person who wrote automated image generation and managed to figure out that I was using Imagemagick - you're awesome, and I'm glad you had fun).

The problem had about 40 solves, which puts it almost exactly in the middle of problems for this CTF.

I had some difficulties with this challenge during the CTF, and am grateful to Lan for helping diagnose and resolve them. Originally, I was not calling out to curl for downloading images; this proved to be an issue, since too many servers did not support my (compliant!) HTTP requests. This led to more confusion than I had intended. There was also a bug (thanks, StackOverflow!) in my RunCmd class, which has been fixed in this version. As the first pwnable challenge I've done, the experience was rocky at best; more on that later.

Oh, and for the interested, here's the C file it's being compiled against:

#include <stdio.h>

#include "image.xpm" /* provides `static char *image[]` */

int main() {
  int i;

  printf("static char *image[] = {\n"); {
    for (i = 0; image[i]; i+=2) {
      printf("\"%s\",\n", image[i+1]);
      printf("\"%s\",\n", image[i]);
    }
  } printf("};\n");
  return 0;
}

And the problem server itself:

#!/usr/bin/python

from gevent.wsgi import WSGIServer

from subprocess import *
from threading import *

from tempfile import mkdtemp
from urllib2 import unquote

import os
import shutil
import socket
import pipes

large = 5000000

class RunCmd(Thread):
  def __init__(self, cmd, timeout):
    Thread.__init__(self)

    self.cmd = cmd
    self.timeout = timeout

    return

  def run(self):
    self.p = Popen(args=self.cmd,
           stdin=None,
           stdout=PIPE,
           stderr=STDOUT,
           shell=True,
           env=None,
    )
    return

  def Run(self):
    self.start()
    self.join(self.timeout)

    if self.is_alive():
      self.p.kill() # 9
      self.join()
      pass

    return self.p.stdout.read(large)
  pass

def application(env, start_response):
  tmpdir = None # on exception, we wipe state
  method = env["REQUEST_METHOD"].upper()
  if method == "POST":
    try:
      dat = env["wsgi.input"].read(max(env["CONTENT_LENGTH"], 400))
      dat = dat.split("location=", 1)[1]
      dat = dat.split("&", 1)[0]
      dat = unquote(dat)
      name = dat.split("/")[-1]
      [nonext, ext] = name.split(".", 1)

      tmpdir = mkdtemp(prefix="/tmp/585-")
      shutil.copytree("/home/frozencemetery/CTF2015/cs585hw1.hpp",
              tmpdir+'/dat')
      os.chdir(tmpdir+'/dat')

      cmd = "curl -O --max-filesize 5000000 -k %s" % pipes.quote(dat)
      a = RunCmd(cmd, 2)
      a.Run()

      if name != "image.xpm":
    if ext != "xpm":
      a = RunCmd("convert %s image.xpm" % pipes.quote(name), 2)
      a.Run()
      os.remove(name)
      name = "image.xpm"
      pass
    else:
      d = open(name, 'r').read()
      d = d.replace(nonext, "image")
      os.remove(name)
      name = "image.xpm"
      with open(name, "wb") as f:
        f.write(d)
        pass
      pass
    pass

      os.chown(tmpdir+'/dat/image.xpm', 0, 1001)
      os.chmod(tmpdir+'/dat/image.xpm', 0o770)
      os.chown(tmpdir+'/dat/template.c', 0, 1001)
      os.chmod(tmpdir+'/dat/template.c', 0o770)
      os.chown(tmpdir+'/dat', 0, 1001)
      os.chmod(tmpdir+'/dat', 0o770)
      os.chown(tmpdir, 0, 1001)
      os.chmod(tmpdir, 0o770)
      os.chown(tmpdir+'/dat/key', 0, 1001)
      os.chmod(tmpdir+'/dat/key', 0o770)

      # from remote_pdb import set_trace
      # set_trace()

      a = RunCmd("su frozencemetery -c 'gcc template.c; ./a.out'", 2)
      a = a.Run()

      shutil.rmtree(tmpdir, ignore_errors=True)
      start_response("200 OK", [("Content-type", "text/xpm")])
      return [a]
    except:
      if tmpdir is not None:
    shutil.rmtree(tmpdir, ignore_errors=True)
    pass
      pass
  elif method == "GET":
    start_response("200 OK", [("Content-type", "text/html")])
    return ["""
<html><head>
  <title>Acme scrambling service</title>
</head><body>
  <form method="post" name="form">
    Where? <input name="location" type="text">
   <input name="Submit" type="submit" value="Submit">
  </form>
</body></html>
    """]

  start_response("402 pay me", [("Content-type", "text/plain")])
  return ["I wasn't paid enough to implement that method."]

http_server = WSGIServer(('', 80), application)
http_server.serve_forever()

Conclusion

On the whole, BkPctf went off quite well. It helped that a nontrivial portion of us share a house, so we were able to quickly talk to each other (and it was a lot more comfortable sleeping in my bed than in the CIC!). Despite that, I want to hilight some problems.

  1. Very visibly, we did not write our scoreboard infrastructure to scale up properly. This would have been caught if we'd looked more closely at its architecture (I'm talking mostly about the sqlite decision), or tested it under load.

  2. In a similar vein, we did not QA challenges as well as we should have. By requiring that challenges be solvable (e.g., solution script for pwnables), we ensured that all challenges were indeed solvable, but many had far easier, unintended solutions, or didn't scale properly (very vulnerable to DOS was a common theme here). The problems in my challenge were caught when another pair of eyes took a look - but that didn't happen until during the CTF.

  3. We did not have a key format. I continue to push for this in CTFs because I think it makes the CTF more approachable to non-native speakers (and I've played sufficiently many CTFs from other countries to appreciate it when it happens), and also reduces load on organizers by making it extremely clear in most cases when a flag has been found.

  4. Problems were not clearly associated with their authors. This meant that every time someone had a question on IRC, there was an additional dispatch step where an organizer had to determine what problem they were curious about, and point them at the appropriate person.

(2) and (3) are policy issues that I hope we will resolve going forward. For (1) and (4), I'm working on designing an infrastructure for managing pwnables before and during a CTF. Hopefully, we will see it in time for me to be able to write about it next year, but for now, I'm working on PlaidCTF.

Thanks to everyone who played; congratulations especially to my own, PPP, for taking first, and to HITCON who as I understand should now be qualified for Defcon CTF.