EBCTF Teaser 2013 – CRY100

EBCTF Teaser 2013 - Cry100 - task description

Bei der CRY100 (“Espionage”) Challenge erhalten wir den Auftrag, Nachrichten zu entschlüsseln. Als Ressourcen wird uns dafür ein Archiv bereit gestellt, das wir direkt entpacken.

cry100_espionage/
cry100_espionage/crypto.py
cry100_espionage/msg001.enc
cry100_espionage/msg002.enc
cry100_espionage/README

Wir erhalten ein Python-Skript, zwei verschlüsselte Nachrichten und einen Hinweis-Text, der wie folgt lautet:

We suspect an employee of one of the embassies has leaked
confidential information to a foreign intelligence agency.

We've managed to capture an individual whom we assume to be the
recipient of the info. Our forensics department has managed to
recover two messages from his outbox, which appear to be encrypted
using some crypto tool. Along with each email our suspect also
received an SMS message containing a password, however we were
only able to recover one - "SieR1mephad7oose".

Could you help us decrypt both messages?

Eine Nachricht konnte demnach offenbar bereits entschlüsselt werden, um die zweite müssen wir uns selbst kümmern. Zunächst testen wir, ob das mitgelieferte Python-Skript die Nachricht wirklich entschlüsseln kann und das darin enthalten ist.

rup0rt@lambda:~/EBCTF2013/CRY100$ ./crypto.py 
Usage:  crypto.py encrypt   
        crypto.py decrypt   

rup0rt@lambda:~/EBCTF2013/CRY100$ ./crypto.py decrypt SieR1mephad7oose msg001.enc msg001.plain 

rup0rt@lambda:~/EBCTF2013/CRY100$ cat msg001.plain 
From: Vlugge Japie <vl.japie@yahoo.com>
To: Baron van Neemweggen <b.neemweggen@zmail.com>
Subj: Weekly update

Boss,

Sorry, I failed to get my hands on the information you
requested. Please don't tell the bureau - I'll have it
next week, promise! 

Vlugge Japie

Die Nachricht wurde also korrekt entschlüsselt und enthält eine Email, inklusive Email-Header. Um nun an den Inhalt der zweiten Nachricht zu gelangen, sollten wir uns zunächst das Skript zum Ver- und Entschlüsseln genauer ansehen. (Ich konzentriere mich hier auf den Teil zum Entschlüsseln.)

inp = open(infile).read()

if op == 'decrypt':
    pt = decrypt(inp, passwd)
    open(outfile, 'w').write(pt)

Die übergebene Datei wird eingelesen und deren Inhalt zusammen mit dem Passwort an die Funktion decrypt() übergeben. Anschließend wird der Rückgabewert in der Zieldatei gespeichert.

def decrypt(msg, passwd):
    msg = crypt(msg.decode('base64'), passwd)

    if verify(msg):
        return msg
    else:
        sys.stderr.write('Looks like a bad decrypt\n')
        sys.exit(1)

Der verschlüsselte Text wird mit BASE64 dekodiert und zusammen mit dem Passwort an die crypt()-Funktion übergeben. Anschließend wird die verify()-Funktion aufgerufen um zu Überprüfen, ob der entschlüsselte Text korrekt ist. (Da Funktion verify() prüft nur, ob der Text allein aus ASCii-Zeichen besteht, sehen wir uns nur die Funktion crypt() weiter an.)

def crypt(msg, passwd):
    k = h(passwd)

    for i in xrange(ROUNDS):
        k = h(k)

    out = ''
    for i in xrange(0, len(msg), 16):
        out += xor(msg[i:i+16], k)
        k = h(k + str(len(msg)))

    return out

Das Passwort wird entsprechend der Rundenanzahl (20000) an die Funktion h() übergeben und anschließend der verschlüsselte Text mit der xor()-Funktion in 16-Byte-Blöcken entschlüsselt. Nach jedem Block wird erneut die Funktion h() aufgerufen.

def h(x):
    x = hashlib.sha256(x).digest()
    x = xor(x[:16], x[16:])
    return x

Bei der Funktion h() handelt es sich um die eigentlische Hash-Funktion. Hier wird ein SHA256-Hash vom übergebenen Parameter erzeugt und dann die beiden (16-Byte-Hälften) miteinander per XOR verknüpft.

Zusammengefasst erzeugt das Skript aus dem Passwort in (mehr oder weniger sinnvollen) Schritten eine Zeichenkette (k), die zur Entschlüsselung der ersten 16 Bytes mit XOR verwendet wird (decrypt(), Zeile 9). Über weiteres Hashing werden auch die übrigen Bytes entschlüsselt.

Wenn es uns nun gelingen sollte, an die 16 Bytes von k zu gelangen, die aus dem Passwort berechnet wurden, könnten wir die gesamte Nachricht entschlüsseln. Dies wäre zum Beispiel über einen known-plaintext attack denkbar.

Da wir die erste Nachricht bereits kennen und diese in Email-Form (mit Header) vorlag, können wir davon ausgehen, dass auch die zweite Nachricht dieses Format haben sollte. Demnach müssten die ersten 16 Bytes der zweiten Nachricht ebenfalls wie folgt lauten:

From: Vlugge Jap

Zum Umsetzung des known-plaintext attacks habe ich das crypto.py-Skript wie folgt angepasst:

def crack(msg):
    plain = "From: Vlugge Japie <vl.japie@yahoo.com>"
    msg = msg.decode('base64')
    key = ""

    for pos in range(16):

        for w in range(256):
            passwd = chr(w) * 32

            k = passwd

            out = ''
            for i in xrange(0, len(msg), 16):
                out += xor(msg[i:i+16], k)
            if out[pos] == plain[pos]:
                print "POS " + str(pos) + ": chr(" + str(w) + ")"
                key = key + chr(w)
    return key

Da wir das Ergebnis der h()-Funktion (nach 20000 Runden) ermitteln wollen, können wir alle Aufrufe von h() entfernen. Dann wird jedes Zeichen k per Brute Force bestimmt. Dazu wird jedes mögliche Zeichen durchprobiert und wenn die entsprechende Position des entschlüsselten Textes mit unserem known-plaintext überein stimmt, das gefundene Zeichen Ausgegeben.

Der Aufruf dieser Funktion liefert nun bei Übergabe des zweiten Schlüsseltextes:

rup0rt@lambda:~/EBCTF2013/CRY100$ ./crack.py
POS 0: chr(234)
POS 1: chr(40)
POS 2: chr(185)
POS 3: chr(168)
POS 4: chr(71)
POS 5: chr(177)
POS 6: chr(157)
POS 7: chr(97)
POS 8: chr(0)
POS 9: chr(204)
POS 10: chr(73)
POS 11: chr(67)
POS 12: chr(242)
POS 13: chr(45)
POS 14: chr(239)
POS 15: chr(241)

Demnach konnte tatsächlich der Vektor k gefunden werden, der zum entschlüsseln der ersten 16 Bytes verwendet wird – vorausgesetzt der Plaintext stimmt. Da es sich hierbei nicht um das eigentliche Passwort, sondern um k handelt, müssen wir das Skript weiter anpassen, um eine vollständige Entschlüsselung zu versuchen.

inp = open("msg002.enc").read()

key = crack(inp)

res = decrypt2(inp, key)
print res

def decrypt2(msg, passwd):
    msg = crypt2(msg.decode('base64'), passwd)

    if verify(msg):
        return msg
    else:
        sys.stderr.write('Looks like a bad decrypt\n')
        sys.exit(1)

def crypt2(msg, k):
    out = ''
    for i in xrange(0, len(msg), 16):
        out += xor(msg[i:i+16], k)
        k = h(k + str(len(msg)))

    return out

Hierbei nutzen wir den zurückgegebenen Wert von crack (k) und rufen decrypt2() auf. Nun wird crypt2() aufgerufen und k direkt zum Entschlüsseln der ersten 16 Bytes des Textes verwendet. Anschließend werden die originalen Funktionen zur weiteren Verarbeitung von k beibehalten (Zeile 21).

Bei korrekt gewähltem Klartext sollte nun eine korrekte Entschlüsselung durchgeführt werden. Dies probieren wir durch Aufruf des finalen Skriptes aus:

rup0rt@lambda:~/EBCTF2013/CRY100$ ./crack.py
[...]
From: Vlugge Japie <vl.japie@yahoo.com>
To: Baron van Neemweggen <b.neemweggen@zmail.com>
Subj: Found it!

Boss,

I found some strange code on one of the documents.
Is this what you're looking for?

ebCTF{21bbc4f404fa2057cde2adbf864b5481}

Vlugge Japie

Die Lösung lautet somit “ebCTF{21bbc4f404fa2057cde2adbf864b5481}“.

Leave a Reply

Your email address will not be published. Required fields are marked *