ebCTF 2013 - Net 400 (Figuurzagers)

01 August 2013 by mf

The problem provides us with an esoteric file format containing a traffic dump. However, the packets are encrypted with an unknown key.

Description

The Eindbazen 'Forensic Unit Computer Knowledge & Advanced Persistent Threats' and the 'Special Unit Cracking Kiddies Investigations Team' are currently investigating a new Scriptkiddie Group working under the name of 'The Figuurzagers'. This team is suspected of hacking various crappy protected environments to place flags on these systems. The NFI provided us with a network tap of one of the attacks. Could you recover the flag for us?

Note: The file is in TIIT format
Encryption key: hashlib.sha256("OHM2013 X Y!").digest()[0:24] where X and Y represent lowercase English words that start with an r
Target Identifier: md5(eindbazen)
Provider Identifier: 51040
Hint: Remember, the first PDU will be "HI2: Start Session"

Prior to this problem, we never heard about the TIIT packet format before, so we found the specs downloadable here. The document outlines which encryption was used to protect the encapsulated frames as well as the various fields of the TIIT file format itself. First step is to build a TIIT parser.

The following script opens the file and prints rudimentary information about the frames.

import struct

def parse_frame(f):
   # Read version
   v = struct.unpack("!B", f.read(1))[0]
   v_maj = v >> 4 & 0xF
   v_min = v >> 0 & 0xF

   print "Major: %d, Minor: %d" % (v_maj, v_min)
   if v_maj == 2:
      print "\tTIIT 1.1.0 Data, or later version"
   if v_min == 3:
      print "\tAES Enc"

   l = struct.unpack("!I", "\x00" + f.read(3))[0]
   print "Len", hex(l)

   # Read target id
   t_id = f.read(16)

   print "\ttargetid", t_id.encode("hex")

   # Read sniffer id
   s_id = f.read(4)

   # Read sequence number
   s_num = f.read(8)

   # Read the rest
   enc_data = f.read(l-32)

   # As per TIIT specs, if major_ver == 2 and minor_ver == 3 IV for AES is 
   # composed of copying the sequence number field two times
   iv = s_num + s_num

   # Return the PDU encrypted blob, as well as the IV
   return (enc_data, iv)

f = open("net400.tiit", "rb")

for x in range(0, 111):
   parse_frame(f)

Now, with knowledge of the file format, we can access the encrypted ciphertexts of each frame. One useful fact to know is that every number in the format is written in big-endian notation. According to the specs, the frames are encrypted using AES-192-CBC where the IV is constructed from the sequence number field of each frame. With this, we can now proceed to brute-force the AES key.

We know what the key format looks like, and we know that the two unknowns are lowercase English words, beginning with an “r”. After downloading a simple English word list, we brute force the key with the following script.

import struct
from Crypto.Cipher import AES
import hashlib

def parse_frame(f):
   # Read version
   v = struct.unpack("!B", f.read(1))[0]
   v_maj = v >> 4 & 0xF
   v_min = v >> 0 & 0xF

   #print "Major: %d, Minor: %d" % (v_maj, v_min)
   #if v_maj == 2:
   #  print "\tTIIT 1.1.0 Data, or later version"
   #if v_min == 3:
   #  print "\tAES Enc"

   l = struct.unpack("!I", "\x00" + f.read(3))[0]
   #print "Len", hex(l)

   # Read target id
   t_id = f.read(16)

   #print "\ttargetid", t_id.encode("hex")

   # Read sniffer id
   s_id = f.read(4)

   # Read sequence number
   s_num = f.read(8)

   # Read the rest
   enc_data = f.read(l-32)

   # As per TIIT specs, if major_ver == 2 and minor_ver == 3 IV for AES is 
   # composed of copying the sequence number field two times
   iv = s_num + s_num

   # Return the PDU encrypted blob, as well as the IV
   return (enc_data, iv)

f = open("net400.tiit", "rb")

# Fetch a single frame
(ctext, iv) = parse_frame(f)

# Open an English word list
f = open("wordlist.txt", "r")
words = f.read().split("\n")

# Use only words beginning with the letter "r"
words_filt = []

for word in words:
   word = word.strip()

   if word[:1] != "r":
      continue

   words_filt.append(word)

for x in range(0, len(words_filt)):

   print "\rProgress %d/%d\t\t\t" % (x, len(words_filt)),

   for y in range(x + 1, len(words_filt)):
      # Prepare the key template
      key = hashlib.sha256("OHM2013 %s %s!" % (words_filt[x], words_filt[y])).digest()[0:24]
      aes = AES.new(key, AES.MODE_CBC, iv)

      ptext = aes.decrypt(ctext)

      # Extract the two fields: provider id and payload id (page 36 of specs)
      prov_id = struct.unpack("!H", ptext[0:2])[0]
      payload_id = struct.unpack("!H", ptext[2:4])[0] &  0x3fff

      # We know that the decrypted first frame will have the provider_id == 51040 and payload_id == 0x3000
      # (a HI2: Start Session frame)

      # Do the two fields match after frame decryption?
      if prov_id ==  51040 and payload_id == 0x3000:
         print "\nBoom"
         print "Decrypted Frame:", ptext.encode("hex")
         print "Key:", key.encode("hex")
         print "Key hint: OHM2013 %s %s!" % (words_filt[x], words_filt[y])
         exit(0)

After only 15-20 seconds, we get the correct AES key. The two sought after words were “really” and “rocks”.

Progress 716/4276
Boom
Decrypted Frame: c760300051cf3b20000b77d1000ac76051cf3b20000b77d10000000000000000
Key: d3b3cce38a32e4a49ff35fe2c2c9436601aa2240a3a62f8f
Key hint: OHM2013 really rocks!

We are now able to decrypt all of the frames and each frame is encrypted using the same key. The IV is frame-dependent, but we know how to construct it correctly. The next step is to decrypt the frames, see which ones contain encapsulated IP traffic and then export that traffic into a pcap file for easier viewing.

The following script walks the frames, decrypts them, extracts the IP packets and saves them to test.cap.

import struct
from Crypto.Cipher import AES
import hashlib
from scapy.all import *

def dec_frame(f, key, iv):
   aes = AES.new(key, AES.MODE_CBC, iv)
   return aes.decrypt(ctext)

def parse_frame(f):
   # Read version
   v = struct.unpack("!B", f.read(1))[0]
   v_maj = v >> 4 & 0xF
   v_min = v >> 0 & 0xF

   print "Major: %d, Minor: %d" % (v_maj, v_min)
   if v_maj == 2:
      print "\tTIIT 1.1.0 Data, or later version"
   if v_min == 3:
      print "\tAES Enc"

   l = struct.unpack("!I", "\x00" + f.read(3))[0]
   print "Len", hex(l)

   # Read target id
   t_id = f.read(16)

   print "\ttargetid", t_id.encode("hex")

   # Read sniffer id
   s_id = f.read(4)

   # Read sequence number
   s_num = f.read(8)

   # Read the rest
   enc_data = f.read(l-32)

   # As per TIIT specs, if major_ver == 2 and minor_ver == 3 IV for AES is 
   # composed of copying the sequence number field two times
   iv = s_num + s_num

   # Return the PDU encrypted blob, as well as the IV
   return (enc_data, iv)

f = open("net400.tiit", "rb")

# AES key
key = "d3b3cce38a32e4a49ff35fe2c2c9436601aa2240a3a62f8f".decode("hex")

packets = []

# There are 111 frames
for x in range(0, 111):
   (ctext, iv) = parse_frame(f)

   # Decrypt the frame
   ptext = dec_frame(ctext, key, iv)

   # Parsing the header looks ugly, but is very simple (page 37, Figure 7)
   payload_id = struct.unpack("!H", ptext[2:4])[0] & 0x3fff
   d = struct.unpack("!H", ptext[2:4])[0] >> 14
   timestamp_sec = struct.unpack("!I", ptext[4:8])[0]
   timestamp_usec = struct.unpack("!I", ptext[8:12])[0]
   l = struct.unpack("!H", ptext[12:14])[0]
   pl = struct.unpack("!H", ptext[14:16])[0]

   # Payload id of 0x00001 means "HI3: IPv4 packet in IPPDU". We ignore any 
   # other kind of frame.
   if payload_id == 0x0001:
      data = ptext[16:][:l]
      print "len:", l
      print "payload_id:", hex(payload_id)
      print "payload len:", pl
      print "=========="
      print data.encode("hex")

      tot_len = struct.unpack("!H", data[0:2])[0]

      # Extract the IP packet, and prepend the two missing fields so scapy likes it    
      ip_enc = "\x45\x10" + data

      # Use scapy to dissect the packet
      p = IP(ip_enc)

      # Manually adjust the checksum. Not needed, but just so the traffic isn't all
      # red in wireshark
      p.chksum -= 0x10
      packets.append(p)

      print p.summary()
      print p[0].payload

      if p.proto != 6:
         # Just in case...
         print "Not tcp"
         exit(0)

# Write the reconstructed packets in a pcap file
wrpcap("temp.cap", packets)

At this point you can open temp.cap with your sniffer of choice and see some simple HTTP requests being made. In one of the requests we see the following piece of data:

00000000  7f 7f 20 2f 90 90 90 90  90 90 90 90 90 90 90 90 .. /.... ........
00000010  90 90 90 90 90 90 90 90  90 90 90 90 90 90 90 90 ........ ........
00000020  90 90 90 90 90 90 90 90  90 90 90 90 90 90 90 90 ........ ........
00000030  90 90 90 90 90 90 90 90  90 90 90 90 90 90 90 90 ........ ........
00000040  90 90 90 90 90 90 90 90  90 90 90 90 90 90 90 90 ........ ........
00000050  90 90 90 90 90 90 90 90  90 90 90 90 90 90 90 90 ........ ........
00000060  90 90 90 90 90 90 90 90  90 90 90 90 90 90 90 90 ........ ........
00000070  90 90 90 90 90 90 90 90  90 90 90 90 90 90 90 89 ........ ........
00000080  e0 db d8 d9 70 f4 5f 57  59 49 49 49 49 49 49 49 ....p._W YIIIIIII
00000090  49 49 49 43 43 43 43 43  43 37 51 5a 6a 41 58 50 IIICCCCC C7QZjAXP
000000A0  30 41 30 41 6b 41 41 51  32 41 42 32 42 42 30 42 0A0AkAAQ 2AB2BB0B
000000B0  42 41 42 58 50 38 41 42  75 4a 49 51 76 6e 61 49 BABXP8AB uJIQvnaI
000000C0  5a 6b 4f 56 6f 77 32 42  72 61 7a 34 42 31 48 4a ZkOVow2B raz4B1HJ
000000D0  6d 34 6e 67 4c 76 65 33  6a 72 54 58 6f 4d 68 31 m4ngLve3 jrTXoMh1
000000E0  58 70 61 63 47 42 4b 4e  6b 78 7a 6c 6f 72 55 4b XpacGBKN kxzlorUK
000000F0  5a 6c 6f 42 55 38 67 59  6f 58 67 41 41 42 42 42 ZloBU8gY oXgAABBB
00000100  42 d8 87 41 00 0d 0a 0d  0a 48 61 77 6b 48 61 77 B..A.... .HawkHaw
00000110  6b 89 e2 d9 e1 d9 72 f4  58 50 59 49 49 49 49 49 k.....r. XPYIIIII
00000120  49 49 49 49 49 43 43 43  43 43 43 37 51 5a 6a 41 IIIIICCC CCC7QZjA
00000130  58 50 30 41 30 41 6b 41  41 51 32 41 42 32 42 42 XP0A0AkA AQ2AB2BB
00000140  30 42 42 41 42 58 50 38  41 42 75 4a 49 78 59 7a 0BBABXP8 ABuJIxYz
00000150  4b 4d 4b 59 49 63 44 34  64 49 64 65 61 58 52 58 KMKYIcD4 dIdeaXRX
00000160  32 72 57 50 31 68 49 31  74 4e 6b 50 71 54 70 6e 2rWP1hI1 tNkPqTpn
00000170  6b 44 36 36 6c 6c 4b 33  46 47 6c 4c 4b 31 56 66 kD66llK3 FGlLK1Vf
00000180  68 6e 6b 73 4e 45 70 4e  6b 55 66 50 38 50 4f 56 hnksNEpN kUfP8POV
00000190  78 73 45 69 63 32 79 53  31 5a 71 39 6f 39 71 73 xsEic2yS 1Zq9o9qs
000001A0  50 4e 6b 50 6c 55 74 54  64 4e 6b 72 65 55 6c 6e PNkPlUtT dNkreUln
000001B0  6b 42 74 36 48 31 68 75  51 7a 4a 6e 6b 33 7a 35 kBt6H1hu QzJnk3z5
000001C0  48 4c 4b 31 4a 65 70 35  51 48 6b 79 73 70 34 30 HLK1Jep5 QHkysp40
000001D0  49 6c 4b 57 44 6c 4b 45  51 78 6e 66 51 6b 4f 46 IlKWDlKE QxnfQkOF
000001E0  51 69 50 59 6c 6c 6c 4c  44 69 50 32 54 36 67 4a QiPYlllL DiP2T6gJ
000001F0  61 68 4f 34 4d 66 61 39  57 78 6b 79 64 35 6b 51 ahO4Mfa9 Wxkyd5kQ
00000200  6c 54 64 66 48 52 55 69  71 4c 4b 31 4a 54 64 43 lTdfHRUi qLK1JTdC
00000210  31 78 6b 42 46 4e 6b 74  4c 72 6b 6c 4b 50 5a 37 1xkBFNkt LrklKPZ7
00000220  6c 65 51 38 6b 4c 4b 33  34 6e 6b 46 61 4d 38 4b leQ8kLK3 4nkFaM8K
00000230  39 73 74 46 44 75 4c 73  51 4a 63 38 32 76 68 55 9stFDuLs QJc82vhU
00000240  79 4e 34 4e 69 4b 55 4b  39 48 42 55 38 6c 4e 52 yN4NiKUK 9HBU8lNR
00000250  6e 76 6e 68 6c 51 42 6d  38 4d 4f 6b 4f 69 6f 6b nvnhlQBm 8MOkOiok
00000260  4f 4c 49 73 75 74 44 4d  6b 63 4e 58 58 48 62 62 OLIsutDM kcNXXHbb
00000270  53 6d 57 67 6c 31 34 32  72 38 68 4c 4e 69 6f 79 SmWgl142 r8hLNioy
00000280  6f 59 6f 6d 59 37 35 73  38 73 58 50 6c 42 4c 35 oYomY75s 8sXPlBL5
00000290  70 37 31 62 48 55 63 70  32 34 6e 33 54 73 58 31 p71bHUcp 24n3TsX1
000002A0  65 33 43 42 45 50 72 6b  38 63 6c 65 74 64 4a 4f e3CBEPrk 8cletdJO
000002B0  79 58 66 32 76 79 6f 72  75 34 44 4b 39 49 52 30 yXf2vyor u4DK9IR0
000002C0  50 4f 4b 39 38 6f 52 32  6d 6d 6c 6c 47 37 6c 66 POK98oR2 mmllG7lf
000002D0  44 73 62 48 68 61 71 39  6f 59 6f 4b 4f 31 78 55 DsbHhaq9 oYoKO1xU
000002E0  6a 66 4d 62 64 66 38 71  78 34 70 36 50 45 71 55 jfMbdf8q x4p6PEqU
000002F0  70 71 78 50 4e 73 75 51  44 54 74 33 58 55 39 62 pqxPNsuQ DTt3XU9b
00000300  4e 65 37 37 50 65 38 42  53 50 6f 72 4c 53 46 70 Ne77Pe8B SPorLSFp
00000310  68 51 30 42 4f 32 4e 71  30 70 68 63 42 55 31 43 hQ0BO2Nq 0phcBU1C
00000320  44 63 43 50 68 63 73 52  4f 50 6e 70 67 46 51 6b DcCPhcsR OPnpgFQk
00000330  6b 6b 38 71 4c 66 44 47  6f 6f 79 48 63 62 48 45 kk8qLfDG ooyHcbHE
00000340  36 56 58 71 6d 31 48 32  48 76 57 63 55 50 31 64 6VXqm1H2 HvWcUP1d
00000350  70 62 48 70 66 34 75 50  34 30 37 45 38 54 74 47 pbHpf4uP 407E8TtG
00000360  47 46 53 70 65 62 48 46  59 65 69 45 62 74 79 63 GFSpebHF YeiEbtyc
00000370  58 56 56 30 39 66 57 70  30 30 68 67 45 45 69 71 XVV09fWp 00hgEEiq
00000380  73 35 69 71 78 46 53 50  31 30 66 70 37 42 48 77 s5iqxFSP 10fp7BHw
00000390  36 31 6b 76 59 72 45 33  58 65 35 35 32 62 63 31 61kvYrE3 Xe552bc1
000003A0  44 75 38 63 51 62 47 65  6a 75 70 45 38 55 35 67 Du8cQbGe jupE8U5g
000003B0  50 70 66 62 4c 61 78 61  63 47 50 32 54 75 38 43 PpfbLaxa cGP2Tu8C
000003C0  58 34 32 73 55 51 30 33  59 35 38 51 31 65 70 57 X42sUQ03 Y58Q1epW
000003D0  38 43 55 50 68 37 50 50  6a 52 4f 32 42 33 58 72 8CUPh7PP jRO2B3Xr
000003E0  6e 61 79 50 63 52 45 70  31 79 59 6b 38 70 4c 61 nayPcREp 1yYk8pLa
000003F0  34 73 73 4f 79 58 61 45  61 48 52 63 5a 43 70 73 4ssOyXaE aHRcZCps
00000400  63 56 31 43 62 6b 4f 5a  70 66 51 6b 70 66 30 59 cV1CbkOZ pfQkpf0Y
00000410  6f 53 65 36 68 41 41                             oSe6hAA

The W YIIIIIII and XPYIIIII sequences are ASCII shellcode decoder stubs, and the rest of the data is ASCII-encoded x86 code. We put the code inside an executable and notice that the first of the two decoders seems to be fake, so we just remove the first 0×110 bytes of the output and single-step through the code. Once the decoder finishes, we can see a big set of PUSH instructions.

0xf745c104: push $0x58442d3a
0xf745c109: push $0x20213030
0xf745c10e: push $0x3454454e
0xf745c113: push $0x20676e69
0xf745c118: push $0x766c6f73
0xf745c11d: push $0x206e6f20
0xf745c122: push $0x73746172
0xf745c127: push $0x676e6f43
0xf745c12c: xor %ebx,%ebx
0xf745c12e: mov %bl,0x1f(%esp)
0xf745c132: mov %esp,%ebx
0xf745c134: push $0x587d3866
0xf745c139: push $0x30316537
0xf745c13e: push $0x37343566
0xf745c143: push $0x65333734
0xf745c148: push $0x39323939
0xf745c14d: push $0x30373936
0xf745c152: push $0x39633935
0xf745c157: push $0x37663133
0xf745c15c: push $0x65397b46
0xf745c161: push $0x54436265
0xf745c166: push $0x203a6761
0xf745c16b: push $0x6c662065
0xf745c170: push $0x68742073
0xf745c175: push $0x69206572
0xf745c17a: push $0x65482021
0xf745c17f: push $0x626f6a20
0xf745c184: push $0x6563694e

Once executed, the instructions create the string:

Done and done.

Flag: ebCTF{9e31f759c969709929473ef5477e10f8}


Comments