Hack.Lu CTF 2013 – Packed

Hack.Lu CTF 2013 - Packed - Task Description

Die Challenge “Packed” beschreibt den Fund verwürfelter Daten, in denen sich irgendwo noch etwas Nützliches befinden soll. Zur Bearbeitung erhalten wir nur eine Datei, die wir zunächst etwas genauer untersuchen:

Hack.Lu CTF 2013 - Packed - File Content

Neben vielen Bytes, die sich nicht wirklich zuordnen lassen, sind doch drei Dinge relatisch schnell erkennbar. Im oberen Bereich befindet sich ein PDF, das im Magick zu erkennen ist (nicht abgebildet). Darunter befindet sich ein Text aus ASCII-Zeichen, der jedoch unverständlich ist. Zusammen mit dem Wort “rot”, das in der Datei vorhanden ist, könnte es sich jedoch um einen ROT13-Cipher handeln.

Im unteren Bereich befindet sich ein langer Abschnitt aus Base64-kodierten Daten. Wenn wir die Abschnitte nun einzeln untersuchen, stellen wir Folgendes fest. Die PDF-Datei kann direkt ausgeschnitten und dann geöffnet werden. Als Ergebnis erhalten wir diese Ausgabe:

Hack.Lu CTF 2013 - Packed - PDF File Part

Sowohl optisch als auch forensisch sind keine weiteren Informationen aus dieser Datei zu entnehmen.

Sehen wir uns also als Nächstes den Base64-Teil der Datei an. Auch dieser kann direkt ausgeschnitten und mit dem Werkzeug “base64” umgewandelt werden. Man erhält eine Openoffice-Datei, die folgendes darstellt:

Hack.Lu CTF 2013 - Packed - Base64 Openoffice File Part

Auch hier lässt sich weder optisch noch anderweitig eine besondere Information zum Lösen der Challenge gewinnen. Bleibt noch der vermutlich mit ROT13 chiffrierte, mittlere Teil der Datei. Wenn wir diesen extrahieren und mit dem Werkzeug “rot13” umwandeln, erhalten wir zunächst dieses Ergebnis:

rup0rt@lambda:~/HackLu2013$ rot13 < packed-rot13.txt
cipher="H51\\\'Ux2J&+(3Z;Uxcx0Xxs\x13h\x014$V!R($R>\t/)R!\x01<.\x13,N-aP4M4aRuG1-VuU0 GuH+a@0W=3R9\x01>(_0\x01,8C0Rx GuN6\"V|\x1ezKZ3\x014$]}R!2\x1d4S?7\x1au\x1fxs\t_\x01xa\x13<Gx)R&Ip2J&\x0f93T#zj\x1c\x1ap\x13rk\x00g\x01e|\x13g\x19ju\x0ba\x18jt\x02o+xa\x13u\x01xa\x13%S1/Gu\x03\x1b.\\:N7.\\:N4o\x13\x0cN-3\x133M9&\x13<Rx A2WjiZ{DvaX0Xjh\x136N6\"R!\x01\x07rC0p\x138a\x1dc22ieu\x161Fw+=-@0\x1bRa\x13u\x01(3Z;UxcR\'F.s\x1c>D!s\x13<Rx,Z&R1/Tw+R"
n =0 ;import hashlib ,sys ;
try :key =sys .argv [1 ]        
except IndexError :sys .exit ("x\x9c\xf3N\xadT0T\xc8\xcd,.\xce\xccKW\xc8\xccSH,J/\x03\x00M\x97\x07\\".decode ("mvc"))
f =getattr (hashlib ,"x\x9c\xcbM1\x05\x00\x02G\x01\x07".decode ("mvc"))
while n <(5 *10 **6 ):key =(f (key ).digest ());n =n +1 
key =key [:5 ].upper ()
while len (key )<len (cipher ):key =key *2 
plain ="".join (map (chr ,[ord (a )^ord (b )for a ,b in zip (cipher ,key )]))
try :exec plain 
except :print "x\x9c\x0b/\xca\xcfKW\xf0N\xadT\x04\x00\x14d\x03x".decode ("mvc"), repr(plain)

Das sieht auf den ersten Blick schon sehr nach Python aus, muss jedoch noch ein wenig angepasst werden. Zunächst müssen die Anweisungen noch korrekt eingerückt werden und anschließend die Anweisung ‘decode(“mvc”)’, die offensichtlich doppelt rotiert wurde, wieder in ‘decode(“zip”)’ umgewandelt werden. Wenn man nun die ‘decode(“zip”)’ Anweisungen noch durch deren Klartext ersetzt, erhält man folgendes gut lesbare Python-Skript:

#!/usr/bin/python

cipher="H51\\\'Ux2J&+(3Z;Uxcx0Xxs\x13h\x014$V!R($R>\t/)R!\x01<.\x13,N-aP4M4aRuG1-VuU0 GuH+a@0W=3R9\x01>(_0\x01,8C0Rx GuN6\"V|\x1ezKZ3\x014$]}R!2\x1d4S?7\x1au\x1fxs\t_\x01xa\x13<Gx)R&Ip2J&\x0f93T#zj\x1c\x1ap\x13rk\x00g\x01e|\x13g\x19ju\x0ba\x18jt\x02o+xa\x13u\x01xa\x13%S1/Gu\x03\x1b.\\:N7.\\:N4o\x13\x0cN-3\x133M9&\x13<Rx A2WjiZ{DvaX0Xjh\x136N6\"R!\x01\x07rC0p\x138a\x1dc22ieu\x161Fw+=-@0\x1bRa\x13u\x01(3Z;UxcR\'F.s\x1c>D!s\x13<Rx,Z&R1/Tw+R"
n =0 ;
import hashlib ,sys ;

try :
	key =sys .argv [1 ]	
except IndexError :
	sys .exit ("Key 1 missing in argv")

md5 =getattr (hashlib ,"md5")

while n <(5 *10 **6 ):
	key =(md5 (key ).digest ());
	n =n +1 

key =key [:5 ].upper ()

while len (key )<len (cipher ):
	key =key *2 

plain ="".join (map (chr ,[ord (a )^ord (b )for a ,b in zip (cipher ,key )]))

try :
	exec plain 
except :
	print "Wrong Key!", repr(plain)

Bei Ausführung liest das Skript das Passwort als Parameter, wendet 5*10^6 mal die MD5-Hashfunktion darauf an und verwendet die ersten 5 Bytes des Ergebnisses um mit der XOR-Verknüpfung die Variable “cipher” zu entschlüsseln. Ein direkter Angriff auf das Passwort mittels Brute-Force kommt auf der Anzahl von MD5 Aufrufen nicht in Frage. Die 5 Bytes, mit denen die XOR-Operation durchgeführt wird, stellen jedoch ein lohnenswertes Ziel dar. Für einen Angriff verwenden wir das xortool. Diesem übergeben wir den Inhalt der Variable “cipher” in Binärform.

rup0rt@lambda:~/xortool$ ./xortool.py cipher 
The most probable key lengths:
   2:   8.5%
   5:   21.4%
   8:   7.8%
  10:   15.7%
  13:   5.8%
  15:   11.8%
  18:   4.9%
  20:   9.1%
  25:   8.9%
  30:   6.0%
Key-length can be 5*n

Die Analyse der XOR-Schlüssellänge ist bereits erfolgreich und meldet uns eine hohe Wahrscheinlichkeit für fünf Zeichen. Dies wissen wir zwar bereits, es bestätigt aber, dass das xortool unseren Ciphertext korrekt erkannt hat. Nun lassen wir das Werkzeug mit der Option -b einen Brute-Force auf einen 5-Zeichen-Schlüssel durchführen.

rup0rt@lambda:~/xortool$ ./xortool.py -l 5 -b cipher 
256 possible key(s) of length 5:
\x01xa\x13u
\x00y`\x12t
\x03zc\x11w
\x02{b\x10v
\x05|e\x17q
...
Found 41 plaintexts with 95.0%+ printable characters

Wenige Sekunden später ist auch dieser Vorgang abgeschlossen und wir können im Verzeichnis “xortool_out” alle erfolgreich dechiffrierten Klartexte einsehen. In der Datei “032.out” finden wir dieses weitere Python-Skript:

import sys
print "Key 2 = leetspeak(what do you call a file that is several file types at once)?"
if len(sys.argv) > 2:
    if hash(sys.argv[2])%2**32 == 2824849251:
        print "Coooooooool. Your flag is argv2(i.e. key2) concat _3peQKyRHBjsZ0TNpu"
else:
    print "argv2/key2 is missing"

Die Lösung zur Challenge scheint greifbar nah, wenn wir doch nur wüssen, wie man Dateien nennt, die mehrere Dateitypen gleichzeitig sind bzw. beinhalten. Selbst nach mehreren Rateversuchen, konnte ich das “Rätsel” nicht lösen, weshalb nur Brute-Force als letzter Ausweg blieb. Wir verwenden das obige Python-Skript als Basis und erweitern es um die Möglichkeit, eine Wörterliste zu übergeben und die einzelnen Zeichen in LeetSpeak umzuwandeln, so wie es sie Aufgabe von uns verlangt. Als Ergebnis ist folgendes Skript entstanden:

#!/usr/bin/python

import sys
import re

f=open(sys.argv[1])
for key in f:
        key = key[:-1].lower()
        key = re.sub('i', '1', key)
        key = re.sub('o', '0', key)
        key = re.sub('e', '3', key)
        key = re.sub('a', '4', key)
        print key

        if hash(key)%2**32 == 2824849251:
                print "Coooooooool. Your flag is argv2(i.e. key2) concat _3peQKyRHBjsZ0TNpu"
                exit(0)

print "NOPE"

Wenn wir dieses Skript nun mit der bekannten Rockyou-Wörterliste ausführen, erhalten wir diese Ausgabe:

rup0rt@lambda:~$ ./packed-crack.py rockyou.txt
[...]
ch4m3l30n
Coooooooool. Your flag is argv2(i.e. key2) concat _3peQKyRHBjsZ0TNpu

Die Lösung lautet somit “ch4m3l30n_3peQKyRHBjsZ0TNpu“.

Leave a Reply

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