CTF Practice

Those who know me know that I might play in the occasional CTF competition. It's a good way to improve my skills, keep my mind sharp, and it's just plain fun. From a defensive security perspective, it's quite amazing to see how code that looks perfectly reasonable is, in fact, quite often very broken.  If you've never done a CTF, you should watch @rogueclown's "If You Can Open A Terminal, You Can Capture the Flag."

I do some extra practice between CTFs -- I'm currently working my way through the challenges on OverTheWire.org, and they've recently added support for scoring via WeChall, a scoring site for a variety of CTF/challenge sites.  In doing those, I've come across some good reading for anyone doing reversing/challenges/CTFs/etc:


Thoughts on NSA Surveillance

I'm going to make this quick -- trying to distill all my thoughts on the NSA into a blog post is impossible, but I feel the need to post something. I believe the actions of the NSA violate my privacy, violate the 4th amendment, and violate the rights of every person on the Internet.  The US Government has Betrayed the Internet, and We Need to Take It Back.  While I don't want to give free reign to terrorists, we have been talking about how our Constitution is what makes America great, and yet we have shredded that very document.  I lose sleep over this not because of the ways the government claims its being used, but over the ways it could be misused -- the next Hoover, the next Nixon, the next McCarthy.  It's time for us to return to a government that respects our rights and our constitution; It's time to return to checks and balances; It's time for America to be free again.  I've been a member of the EFF for several years now, and it (along with organizations like the ACLU and other civil liberties organizations) is the only hope I have left for our country.


Setting Up Kali Linux

I've been meaning to write this up for a while, and it's as much a reminder to me as it's meant to be useful to anyone else, but with DEFCON around the corner, I'm reformatting my laptop for the trip, so now's the best time.  I'm sure everyone has their own "routine" when setting up a new system.  This is my checklist for Kali Linux, which I use for security cons & ctfs, and is separate from my everyday OS installs.

Install with full-disk encryption
Never know when my laptop might go missing.  Even though I try not to have any personal information on it for cons, I figure it's better safe than sorry.   (For example, I typically have a VPN setup, I'd rather those keys not be copied.)

Install updates and missing packages
I enable 32-bit libs (dpkg --add-architecture i386) and make sure I have the latest packages (apt-get update && apt-get dist-upgrade).  Then I install a few packages I feel are "missing" (i.e., I always want):

  • strace
  • ltrace
  • cryptsetup
  • lvm2
  • network-manager-openvpn-gnome
  • byobu
  • cinnamon
  • ia32-libs
  • virtualbox
  • virtualbox-guest-additions-iso
  • gnupg2
  • opensc
  • scdaemon
  • ufw
  • alsa
  • gcc-multilib
  • volatility
  • kpartx
  • yara-python
  • gimp
  • wine-bin:i386
  • ldap-utils
  • ecryptfs-utils
  • python-hachoir-urwid

I also, of course, install Chrome and a few chrome extensions.

Dotfiles
I also install a bunch of dotfiles I keep in a git repository:

  • .gnupg/gpg.conf
  • .bashrc
  • .gdbinit
  • .vimrc
  • .gitconfig
  • .ssh/config

Boston Key Party -- MITM

Boston Key Party is the latest CTF I've played in (this time playing with some local friends as part of our team 'Shadow Cats'). The first challenge we cleared (actually, first blood in the CTF) was MITM.

Now, you might think a challenge named "MITM" was some sort of Man-In-The-Middle exercise, but it's actually crypto! We're given five base-64 encoded messages: two plaintext/ciphertext pairs, and a ciphertext (which we're presumably supposed to decrypt).

message 1:  QUVTLTI1NiBFQ0IgbW9kZSB0d2ljZSwgdHdvIGtleXM=
encrypted:  THbpB4bE82Rq35khemTQ10ntxZ8sf7s2WK8ErwcdDEc=
message 2:  RWFjaCBrZXkgemVybyB1bnRpbCBsYXN0IDI0IGJpdHM=
encrypted:  01YZbSrta2N+1pOeQppmPETzoT/Yqb816yGlyceuEOE=
ciphertext: s5hd0ThTkv1U44r9aRyUhaX5qJe561MZ16071nlvM9U=

Decoding the two plaintexts gives us:

AES-256 ECB mode twice, two keys
Each key zero until last 24 bits

Some useful hints. So our construction is C = EK2(EK1(P)) where K1 and K2 are both 256-bit keys with the first 29 bytes being all nulls. This means the remaining keyspace for each key is 224, or 224 * 224 = 248 for both keys combined. Brute forcing 248combinations seems... unlikely during a CTF.

The good news is, we only actually have to try ~225 possible keys, which is quite doable even on my laptop. (Actually completes in a matter of minutes.) The trick is a Meet in the Middle (MITM again!) attack. Because we have a plaintext/ciphertext pair, we can encrypt the plaintext with all possible keys, store those encryptions, then decrypt the ciphertext with all possible keys and check those keys against the encryptions. They will match when you have found your two keys, which can then be used to decrypt the unknown ciphertext.

from Crypto.Cipher import AES
plain = 'QUVTLTI1NiBFQ0IgbW9kZSB0d2ljZSwgdHdvIGtleXM='.decode('base64')
cipher = 'THbpB4bE82Rq35khemTQ10ntxZ8sf7s2WK8ErwcdDEc='.decode('base64')
unknown = 's5hd0ThTkv1U44r9aRyUhaX5qJe561MZ16071nlvM9U='.decode('base64')
 
KEY_PADDING = chr(0)*29
 
def NewAES(key):
  return AES.new(key, mode=AES.MODE_ECB)
 
def Encrypt(short_key, text=plain):
  return NewAES(KEY_PADDING+short_key).encrypt(text)
 
def Decrypt(short_key, text=cipher):
  return NewAES(KEY_PADDING+short_key).decrypt(text)
 
def KeyGen():
  """Generator for all possible 24 bit keys."""
  for a in xrange(0, 256):
    for b in xrange(0, 256):
      for c in xrange(0, 256):
        yield chr(a)+chr(b)+chr(c)
 
def EncryptTable():
  """Map of encryptions to keys."""
  table = {}
  for short_key in KeyGen():
    table[Encrypt(short_key)] = short_key
  return table
 
table = EncryptTable()
for short_key in KeyGen():
  decrypted = Decrypt(short_key)
  if decrypted in table:
    # Have a match, now decrypt the unknown
    print Decrypt(short_key, Decrypt(table[decrypted], unknown))
    break

The downside to this technique is that it has 224 memory complexity, as you store an entire hash table of encryption->key pairs for the inner encryption. However, even with the overhead in python, 224 memory seems to amount to ~2.5GB, so a small tradeoff here.


PlaidCTF Compression

PlaidCTF 2013 had a level called "Compression". Here's the provided code for this level:

#!/usr/bin/python
import os
import struct
import SocketServer
import zlib
from Crypto.Cipher import AES
from Crypto.Util import Counter
 
# Not the real keys!
ENCRYPT_KEY = '0000000000000000000000000000000000000000000000000000000000000000'.decode('hex')
# Determine this key.
# Character set: lowercase letters and underscore
PROBLEM_KEY = 'XXXXXXXXXXXXXXXXXXXX'
 
def encrypt(data, ctr):
    aes = AES.new(ENCRYPT_KEY, AES.MODE_CTR, counter=ctr)
    return aes.encrypt(zlib.compress(data))
 
class ProblemHandler(SocketServer.StreamRequestHandler):
    def handle(self):
        nonce = os.urandom(8)
        self.wfile.write(nonce)
        ctr = Counter.new(64, prefix=nonce)
        while True:
            data = self.rfile.read(4)
            if not data:
                break
 
            try:
                length = struct.unpack('I', data)[0]
                if length > (1<<20):
                    break
                data = self.rfile.read(length)
                data += PROBLEM_KEY
                ciphertext = encrypt(data, ctr)
                self.wfile.write(struct.pack('I', len(ciphertext)))
                self.wfile.write(ciphertext)
            except:
                break
 
class ReusableTCPServer(SocketServer.ForkingMixIn, SocketServer.TCPServer):
    allow_reuse_address = True
 
if __name__ == '__main__':
    HOST = '0.0.0.0'
    PORT = 4433
    SocketServer.TCPServer.allow_reuse_address = True
    server = ReusableTCPServer((HOST, PORT), ProblemHandler)
    server.serve_forever()

So there's a few interesting things of note here:

  • They take user-supplied input, concatenate the flag, and then encrypt and return the value.
  • Input is limited to 1MB (1
  • We're compressing with gzip and then encrypting with AES in CTR mode.
  • It's a 128 bit nonce: 8 bytes of urandom, followed by a 64-bit counter.
My first thoughts were to attack CTR mode: reuse of the IV in a CTR mode cipher is quite fatal. But given that the nonce is of reasonably high entropy (64-bit) and our input length limit prevents wrapping the counter (if we could even send that much data, which we can't) it becomes quite obvious we won't be getting a reused IV anytime soon.

So I start thinking about the fact that AES (or any block cipher) in CTR mode is really a stream cipher -- they encrypt the counter with the key, to produce a keystream, then XOR with the plaintext to get the ciphertext. In particular, pycrypto guarantees that len(input) == len(output). Given the name of the level (Compression) I start thinking about approaches to get information out of the ciphertext length.

At this point, it's worth revisiting the design of the DEFLATE algorithm (used by the zlib.compress call in the compression.py program). DEFLATE uses a combination of Huffman coding and LZ77/LZ78-style duplicate string elimination. In this context, I believe the duplicate string elimination plays the bigger role -- this part takes repeated sections of the input and, for the 2nd and later instance, includes a pointer back to first instance that is shorter than the original string. For our purposes, that means if we provide input that contains substrings of the unknown key, we will get a shorter response than if our string is completely different than the flag. To test my theory, I fired off 27 tries to the server, each containing one of the valid ([a-z_]) characters repeated 3 times: all responses, save one, were the same length (30 bytes). Only the repeated 'c' string came back at 29 bytes. This led me to believe that 'c' probably started the flag. (If it only needed to be in the flag, more than one character would likely have returned a different length.)

I put together a script to go through character by character and look for lengths that were shorter from the rest. During my first couple of runs, it would frequently get stuck until I hit upon the idea of putting the test string multiple times, increasing the likelihood that duplicate string elimination would use the entire thing. Eventually, I had a few candidate flags, but from glancing at them, it was clear what the answer was...

import struct
import socket
import sys
import collections
 
HOST = 'ip.add.res.s'
PORT = 4433
 
def try_val(val):
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  sock.connect((HOST, PORT))
  nonce = sock.recv(8)
  sock.send(struct.pack('I', len(val)))
  sock.send(val)
  data = sock.recv(4)
  recv_len = struct.unpack('I', data)[0]
  data = sock.recv(recv_len)
  return (nonce, data)
 
 
def get_candidate_len(can):
  nonce, data = try_val(can)
  return len(data)
 
def try_layer(prefix):
  if len(prefix) == 20:
    print "Found candidate %s!" % prefix
    return
 
  candidates = 'abcdefghijklmnopqrstuvwxyz_'
  print "Trying %s" % prefix
  sys.stdout.flush()
  samples = {}
  for c in candidates:
    val = prefix + c
    samples[val] = get_candidate_len(val*2 if len(val)>9 else val*5)
  m = mode(samples.values())
  for k in samples:
    if samples[k] == m:
      continue
    try_layer(k)
 
def mode(l):
  c = collections.Counter(l)
  return c.most_common(1)[0][0]
 
try_layer('')