Skip to content

Instantly share code, notes, and snippets.

@grnd
Last active January 4, 2016 03:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save grnd/c1c4cd03cb06212fa8e6 to your computer and use it in GitHub Desktop.
Save grnd/c1c4cd03cb06212fa8e6 to your computer and use it in GitHub Desktop.

32C3 CTF 2015 : config.bin

Category: Forensics Points: 150 Solves: 27 Description:

We have obtained what we believe is a configuration backup of an embedded device. However, it seems to be encrypted. Maybe you can help us with decryption?

Write-up

It's pretty clear that the file has a short header, and the rest of the data is either encrypted or compressed. Searching for the magic CFG1 brings up few interesting results. It seems like this is a configuration file for Sphairon routers. There are few decryptors available (here's one), but they all fail to decrypt our file. The md5 hash does not match, meaning that the key used (dummy1) is wrong. From the source code we learn that the header is:

struct header
{
	char magic[4]; 			// magic bytes \x43 \x46 \x47 \x31 (CFG1)
	uint32_t payload_size; 	// length of ciphertext = length of padded plaintext (big endian)
	uint8_t header_md5[8];		// first 8 bytes of MD5 computed over header (assuming the 8 bytes of "header_md5" are \x00)
	char etl[7]; 			// blank electronic label (etl), always "000000" (null-terminated char array)
	uint8_t unused1; 			// not used at the moment
	uint16_t password_len; 	// length of the password used in AES encryption (big endian)
	uint16_t padding_len; 	// number of padding bytes added to plaintext (big endian)
	uint8_t unused2[4];		// not used at the moment
	uint8_t plaintext_md5[16]; // MD5 hash of the plaintext
};

It's time for bruteforcing the key. Enumerating over five character long alphanumeric key is slightly less than 30 bits. Python can totally be enough here. The file is encrypted with AES, in ECB mode, so the naive approach would be to do something like:

all = open('config.bin', 'r').read()
data = all[48:]
expected_md5 = all[32:48].encode('hex')
charset = string.ascii_lowercase + string.ascii_uppercase + string.digits

for key in bruteforce(charset, 5, 5):
    plain = AES.new(key + '\x00' * 27 , AES.MODE_ECB, "").decrypt(data)    
    h = hashlib.md5(plain).hexdigest()
    if (h == expected_md5):
        print 'Found key: ' + key
        open('plain.tar.gz', 'wb').write(plain)
        break

The above code gives us 8500 passwords per second (30hours) on my laptop. But we can improve that dramatically, by decrypting the first block only, and checking if the first 3 bytes correspond to gzip header 1f8b08. If they do, only then we decrypt the whole data and calculate the md5. This little optimization gets us to 220000pps and takes about an hour to complete on my old macbook air.

all = open('config.bin', 'r').read()
data = all[48:]
first_block = data[0:16]
expected_md5 = all[32:48].encode('hex')
charset = string.ascii_lowercase + string.ascii_uppercase + string.digits

for key in bruteforce(charset, 5, 5):
    plain = AES.new(key + '\x00' * 27, AES.MODE_ECB, "").decrypt(first_block)
    
    # identify gzip 1f8b08
    if (plain[0:3] == '\x1f\x8b\x08'):
        plain = AES.new(key, AES.MODE_ECB, "").decrypt(data)
        h = hashlib.md5(plain).hexdigest()
        if (h == expected_md5):
            print 'Found pwd: ' + pwd
            open('plain.tar.gz', 'wb').write(plain)
            break

After about 10 minutes we find the password: oVX09 There are two files in the archive. Inside rc.conf, line 344 we find a base64 encoded string. Flag is inside.

$ echo MzJDM19jNDQ2ZWRlMjMzY2RmY2IxNzdmNGQwZTU2NzQ0NjU0Mjg5YzhkZWE0YzRlZTY1MTI2NGU4\nNWU5YWU2MmFiZjc3 | base64 -D
32C3_c446ede233cdfcb177f4d0e56744654289c8dea4c4ee651264e8
from Crypto.Cipher import AES
import string
import itertools
import time
import hashlib
def bruteforce(charset, minlength, maxlength):
return (''.join(candidate)
for candidate in itertools.chain.from_iterable(itertools.product(charset, repeat=i)
for i in range(minlength, maxlength + 1)))
data = open('config.bin', 'r').read()
header = data[:48]
expected_md5 = header[32:48].encode('hex')
first_block = data[48:64]
all_data = data[48:]
charset = string.ascii_lowercase + string.ascii_uppercase + string.digits
progress = 0
progress_interval = 1000000
total = len(charset)**5
start = time.time()
for key in bruteforce(charset, 5, 5):
progress = progress + 1
plain = AES.new(key + '\x00'*27, AES.MODE_ECB, "").decrypt(first_block)
# identify gzip 1f8b08
if (plain[0:3] == '\x1f\x8b\x08'):
plain = AES.new(key + '\x00'*27, AES.MODE_ECB, "").decrypt(all_data)
if (hashlib.md5(plain).hexdigest() == expected_md5):
print 'Found key: ' + key
open('plain.tar.gz', 'wb').write(plain)
break
if (progress % progress_interval == 0):
took = time.time() - start
start = time.time()
pps = progress_interval/took
mins_left = (total-progress)/pps/60
print '%s: %d pps; %d minutes left' % (key, pps, mins_left)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment