hxp CTF 2017: misc350 "inception" writeup

Warning: flag format for this challenge: HXP{…}

Now please inspect. Download:

4dd967a4fa319ae9049d4f7e3e7b5fc225e6467aa7c0bffac69335a0ad41c508.tar.xz

250 Basepoints + 100 Bonuspoints * min(1, 3 / 0 Solves) = 350 Points

Idea

The idea of this challenge was to distribute a pcap that contained different layers of encrypted content to show that their typical mode of operation is insecure.

Solution

When opening the pcap in your favorite packet analyzer (for example Wireshark) you will find a lot of DNS traffic. With Iodine you are able to tunnel your data through a restricted network that otherwise requires a login within a captive portal. Usually these networks filter traffic like HTTP or SSH but allow passing of DNS traffic.

Open the Iodine dump

Iodine traffic is not encrypted, authentication is implemented by checking an MD5 hash in the beginning of the protocol. The Upstream data is typically encoded with a custom version of base128 and the downstream is typically raw when used with a DNS record of type NULL.

This was actually not completely new in a CTF. Indeed, there are writeups from previous CTFs, for example:

We used a script (derived from the first writeup) in combination with base128_iodine.py from the second writeup to the code the initial packet dump iodine.py.

This gives us another pcap dump containing a lot of HTTP and OpenVPN traffic. The HTTP traffic was not encrypted and contained a lot of inappropriate content.

Extract OpenVPN traffic

OpenVPN can be deployed in two different modes of operation. Typically one should use its TLS Mode in combination with a PKI and a set of certificates (both for client and server). Alternatively, if setting up an entire PKI is too complicated, as in the case where it only is used to decrypt web surfing traffic through an Iodine tunnel, also a static key setup may be used.

In static key mode, the user has to preconfigure a static key on both ends of the VPN Tunnel. Typically those keys contain 2048 bits of randomness.

If we look at the now extracted pcap dump we see that there is no TLS negotiation in the beginning of the OpenVPN connection, which suggests that in fact static key mode is used.

A little bit of googling (here and here) reveals the standard wire format and the used key material.

Wire format
The encrypted packet is formatted as follows:
    HMAC(explicit IV, encrypted envelope)
    Explicit IV
    Encrypted Envelope

The plaintext of the encrypted envelope is formatted as follows:
    64 bit sequence number
    payload data, i.e. IP packet or Ethernet frame

The HMAC and explicit IV are outside of the encrypted envelope.
Key material

The generated key typically has 2048 bits. When used, these bits are divided into 2 key pairs (encryption key, authentication key), one pair for the server and one pair for the client (see option key-direction). Therefore, each key finally consists of 512 bits.

OpenVPN in standard _statickey configuration uses Blowfish (64bit key) for encryption and HMAC_SHA1 (160bit key) for integrity protection. Also in standard configuration only the first key pair is used in both directions.

In order to decrypt the data, the HMAC key is not required, thus leaving a possibility to brute force the encryption on a decent machine.

Extracting the key material

In order to save us from brute forcing the 64bit encryption key, the user had downloaded his static key from the server prior to opening the VPN connection. If we search for the string static key in the pcap we find the following HTTP connection:

Decrypting the contents

With the extracted key and a python script we are able to decrypt the OpenVPN connection.

Extracting the flag

After extracting the OpenVPN traffic, we again see a lot of inappropriate content. But also if we search for the string HXP{ we find the following HTTP traffic, which contains the flag:

HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.5.3
Date: Fri, 17 Nov 2017 18:43:39 GMT
Content-type: text/html
Content-Length: 78
Last-Modified: Fri, 17 Nov 2017 18:06:13 GMT

FLAG: HXP{b0ttle_4g41n?!_i0D1n3_4nd_51T1c_0p3nVpN_n0t_sO_53cur3_u53_W1th_55h}

Files

Iodine Script

::: python
import os, sys
from scapy.all import *
import zlib
import base128_iodine

def b32_8to5(a):
   return "abcdefghijklmnopqrstuvwxyz012345".find(a.lower())

def up_header(p):
    return {
        "userid": int(p[0],16),
        "up_seq": (b32_8to5(p[1]) >> 2) & 7,
        "up_frag": ((b32_8to5(p[1]) & 3) << 2) | ((b32_8to5(p[2]) >> 3) & 3),
        "dn_seq": (b32_8to5(p[2]) & 7),
        "dn_frag": b32_8to5(p[3]) >> 1,
        "lastfrag": b32_8to5(p[3]) & 1
    }

def dn_header(p):
    return {
        "compress": ord(p[0]) >> 7,
        "up_seq": (ord(p[0]) >> 4) & 7,
        "up_frag": ord(p[0]) & 15,
        "dn_seq": (ord(p[1]) >> 1) & 15,
        "dn_frag": (ord(p[1]) >> 5) & 7,
        "lastfrag": ord(p[1]) & 1,
    }

def uncompress(s):
    if '' == s:
        return False
    return zlib.decompress(s)

def main():
    inp, output = sys.argv[1], sys.argv[2]
    topdomain = ".a.ctf.link."
    upstream_encoding = 128

    p = rdpcap(inp)
    print('Done reading pcap')
    print('Got %i entries' %(len(p)))
    dn_pkt, up_pkt = '', ''
    datasent = False
    E = []
    for i in range(len(p)):
        if i % 1000 == 0:
            print('.'),
            sys.stdout.flush()
        if not p[i].haslayer(DNS):
            continue
        if DNSQR in p[i]:
            if DNSRR in p[i] and len(p[i][DNSRR].rdata)>0: # downstream/server
                d = p[i][DNSRR].rdata
                if datasent: # real data and no longer codec/fragment checks
                    dn_pkt += d[2:]
                    if dn_header(d)['lastfrag'] and len(dn_pkt)>0:
                        u = uncompress(dn_pkt)
                        if not u:
                            raise Exception("Error dn_pkt %i: %r" % (i,dn_pkt))
                        E += [IP(u[4:])]
                        dn_pkt = ''
            else: # upstream/client
                d = p[i][DNSQR].qname
                if d[0].lower() in "0123456789abcdef":
                    datasent = True
                    up_pkt += d[5:-len(topdomain)].replace(".","")
                    if up_header(d)['lastfrag'] and len(up_pkt)>0:
                        decoded = base128_iodine.b128decode(up_pkt)
                        if decoded != '':
                            u = uncompress(decoded)
                            if not u:
                                raise Exception("Error up_pkt %i: %r" % (i,up_pkt))
                            E += [IP(u[4:])]
                        up_pkt = ''

    wrpcap(output, E)
    print "Successfully extracted %i packets into %s" % (len(E), output)

if __name__ == '__main__':
    main()

OpenVPN Static Key

GET /hxp.key HTTP/1.1
Host: 192.168.99.1:8000
User-Agent: curl/7.56.1
Accept: */*

HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.5.3
Date: Fri, 17 Nov 2017 18:40:03 GMT
Content-type: application/pgp-keys
Content-Length: 636
Last-Modified: Thu, 16 Nov 2017 14:40:11 GMT

#
# 2048 bit OpenVPN static key
#
-----BEGIN OpenVPN Static key V1-----
b2332924f72b57061e71311c8cde3e16
1990829b54628650a1695ed8418713c2
6acde171efad74bf214b880d0a0a0400
531ab9c9141b652678add9945aef58a1
a86c1c35c832dd06b68e4441f82b319d
b98ec7a4ea2cfd65f102e942bf03a94d
b32871f059a6bdd6f424fe97a2794477
13b2a15206cf3402d09ab5b6274f1a03
46dd7daae3bca1dcc65fbf80225dbb62
ddf037bd4afcbdf37ba9b927f750763d
e9de60d45265693c357deee69afb202c
fe634248a24640966d6732f37287005a
fe4ba4eee7a0741a6dc563724d375850
565ac001057ca680337cfd1713babbf6
227742faff4e1d8132ce2c2b5ede0afd
1ed70cb877b6137201169f937c8fbd9d
-----END OpenVPN Static key V1-----

OpenVPN Script

::: python
import os, sys
import binascii
from Crypto.Cipher import Blowfish
from scapy.all import *

from hashlib import sha1
import hmac

import pprint

def chunks(l, n):
    n = max(1, n)
    return (l[i:i+n] for i in range(0, len(l), n))

def import_key(filename):
    with open(filename) as f:
        keyfile = f.readlines()
    key = b''
    for line in keyfile:
        if line.startswith('#') or line.startswith('-'):
            continue
        key += binascii.unhexlify(line.strip())
    return key

def main():
    if len(sys.argv) < 2:
        print('RTFM')
    inp = sys.argv[1]
    out = sys.argv[2]
    key = list(chunks(import_key('hxp.key'), 64))
    key = [(key[0], key[1]), (key[2], key[3])]
    bf_key = key[0][0][:16]
    hmac_key = key[0][1][:20]
    p = rdpcap(inp)

    E = []
    bs = Blowfish.block_size
    for i in range(len(p)):
        if p[i].haslayer(UDP) and \
            (p[i].getlayer(UDP).sport == 1194 or \
             p[i].getlayer(UDP).dport == 1194):
            mac = p[i].load[:20]
            rest = p[i].load[20:]
            hashed = hmac.new(hmac_key, rest, sha1)
            if mac != hashed.digest():
                print('\o/ MAC passt -- nicht')
                continue
            iv = rest[:bs]
            ciphertext = rest[bs:]
            cipher = Blowfish.new(bf_key, Blowfish.MODE_CBC, iv)
            msg = cipher.decrypt(ciphertext)
            last_byte = msg[-1]
            msg = msg[:- (last_byte if type(last_byte) is int \
                                    else ord(last_byte))]
            if len(msg[8:]) < 40:
                continue
            E += [IP(msg[8:])]

    wrpcap(out, E)
    pass

if '__main__' == __name__:
    main()