Présentation d’un write-up de résolution du challenge « Crypto – SuperCipher » du WARGAME de la Nuit du Hack 2016.
Le weekend du 02-03 juillet 2016 se déroulait le WARGAME de la Nuit du Hack 2016 sous forme d’un CTF Jeopardy. Ayant eu l’occasion et le temps d’y participer avec quelques collègues et amis, voici un write-up de résolution d’un des challenges auquel nous avons pu participer.
- Catégorie : Crypto
- Nom : SuperCipher
- Description : So easy
- URL : sp.wargame.ndh
- Points : 100
tl;dr : Télécharger et décompiler le fichier home.pyc, puis récupérer sa date de création pour définir le « seed » de la fonction de déchiffrement
Ce challenge se présentait sous la forme d’une interface web, permettant de :
- Uploader un fichier en vue de le chiffrer : une archive ZIP en sortie avec deux fichiers « secret » et « key »
- Uploader un fichier chiffré, indiquer une clé, et récupérer la version en claire
- Télécharger « flag.zip » qui avait été chiffré via ce service
C’est donc un service de chiffrement pur et simple en-ligne.
Observons le contenu du « flag.zip » :
root@kali 12:22 [~/ndh2k16/SuperCipher] # unzip flag.zip Archive: flag.zip creating: secret/ inflating: secret/key extracting: secret/secret
Serait-ce si simple? Voyons le contenu de ces fichiers :
secret/secret :
/8bAieboX5pFq1sI6js92nrI6huZoxLZ5A==
secret/key :
QUhBSEFILVRISVMtSVMtTk9ULVRIRS1LRVk=
Décodons la clé :
root@kali 12:24 [~/ndh2k16/SuperCipher] # cat secret/key | base64 -d AHAHAH-THIS-IS-NOT-THE-KEY
Ok, on a bien un « secret » mais la clé nous est pas encore connue…
La page réalisant les traitements de chiffrement / déchiffrement se nomme « home.py ». Ça semble être du Python. Tentons de récupérer la version compilée (home.pyc) du script : bingo ! On a la version compilée.
Avec la version compilée (*.pyc) du script, il est nécessaire de récupérer le code source Python originel. Pour cela différentes solutions existent comme « uncompyle2« , « decompyle++« , etc. Dans notre cas nous avons opté pour une solution sous Windows « Easy Python Decompiler » (qui au final utilise l’un ou l’autre des binaires précédents).
Lancer l’outil, drag’n’drop du fichier *.pyc et un fichier « home.pyc_dis » avec le code Python en clair est automatiquement généré.
Code source du script Python résultant :
# Embedded file name: home.py from bottle import route, run, template, request, static_file import time import random import base64 import zipfile from StringIO import StringIO import os, sys from Crypto.Cipher import AES from Crypto import Random class AESCipher: def __init__(self): self.key = 'FOOBARBAZU123456' self.pad = lambda s: s + (16 - len(s) % 16) * chr(16 - len(s) % 16) self.unpad = lambda s: s[:-ord(s[len(s) - 1:])] def encrypt(self, raw): raw = str(raw) raw = self.pad(raw) iv = Random.new().read(AES.block_size) cipher = AES.new(self.key, AES.MODE_CBC, iv) return base64.b64encode(iv + cipher.encrypt(raw)) def decrypt(self, enc): enc = base64.b64decode(enc) iv = enc[:16] cipher = AES.new(self.key, AES.MODE_CBC, iv) return self.unpad(cipher.decrypt(enc[16:])) @route('/') @route('/home.py') def hello(): return '\n<h1>SuperCipher</h1>\n<h2>Chiffrer :</h2>\n<form action="/home.py/secret.zip" method="post" enctype="multipart/form-data">\n Select a file: <input type="file" name="upload" />\n <input type="submit" value="cipher" />\n</form>\n<br />\n<h2>Dechiffrer</h2>\n<form action="/home.py/uncipher" method="post" enctype="multipart/form-data">\n Password: <input type="password" name="key" />\n Select a file: <input type="file" name="upload" />\n <input type="submit" value="uncipher" />\n</form>\n' @route('/home.py/secret.zip', method='POST') def cipher(): seed = int(time.time()) random.seed(seed) upload = request.files.get('upload') upload_content = upload.file.read() content_size = len(upload_content) mask = ''.join([ chr(random.randint(1, 255)) for _ in xrange(content_size) ]) cipher = ''.join((chr(ord(a) ^ ord(b)) for a, b in zip(mask, upload_content))) b64_cipher = base64.b64encode(cipher) aes = AESCipher() key = aes.encrypt(seed) secret = StringIO() zf = zipfile.ZipFile(secret, mode='w') zf.writestr('secret', b64_cipher) zf.writestr('key', key) zf.close() secret.seek(0) return secret @route('/home.py/uncipher', method='POST') def cipher(): key = request.forms.get('key') upload = request.files.get('upload') try: aes = AESCipher() key = aes.decrypt(key) random.seed(int(key)) upload_content = base64.b64decode(upload.file.read()) content_size = len(upload_content) mask = ''.join([ chr(random.randint(1, 255)) for _ in xrange(content_size) ]) plain = ''.join((chr(ord(a) ^ ord(b)) for a, b in zip(mask, upload_content))) return plain except: return 'Uncipher error.' @route('/<filename:path>') def download(filename): return static_file(filename, root=os.path.join(os.path.dirname(sys.argv[0])), download=filename) run(host='0.0.0.0', port=8080)
On remarque qu’une clé est définie en dur dans la classe d’objet « AESCipher » :
self.key = 'FOOBARBAZU123456'
De plus, la méthode de « chiffrement » (qui produit le « secret.zip » en sortie), initialise son processus d’encryption via le timestamp courant :
@route('/home.py/secret.zip', method='POST') def cipher(): seed = int(time.time()) random.seed(seed)
Nous avons également accès à la fonction de déchiffrement complète :
@route('/home.py/uncipher', method='POST') def cipher(): key = request.forms.get('key') upload = request.files.get('upload') try: aes = AESCipher() key = aes.decrypt(key) random.seed(int(key)) upload_content = base64.b64decode(upload.file.read()) content_size = len(upload_content) mask = ''.join([ chr(random.randint(1, 255)) for _ in xrange(content_size) ]) plain = ''.join((chr(ord(a) ^ ord(b)) for a, b in zip(mask, upload_content))) return plain except: return 'Uncipher error.'
L’idée est donc à partir du contenu du « flag.zip » et notamment de son fichier « secret » pour le déchiffrer. Le « seed » qui avait servit à l’époque pour chiffrer ce message contenant le flag, peut être déduit de la « date de dernière modification du fichier secret » (commande « stat » sous Linux), à savoir :
« secret » date donc du 30 juin 2016 à 17h 17min 52s. Il nous faut convertir cette date en timestamp et cette valeur permettra d’initialiser arbitrairement le « seed » de la fonction de déchiffrement :
t = datetime.datetime(2016, 06, 30, 17, 17, second=52) seed = int(time.mktime(t.timetuple())) #seed = 1467299872
Pour la résolution finale, on crée un nouveau script Python « flag.py » s’inspirant que la fonction de déchiffrement issue de la décompilation, tout en indiquant de manière arbitraire notre « seed timestamp » :
import random import base64 import time import datetime from Crypto.Cipher import AES from Crypto import Random class AESCipher: def __init__(self): self.key = 'FOOBARBAZU123456' self.pad = lambda s: s + (16 - len(s) % 16) * chr(16 - len(s) % 16) self.unpad = lambda s: s[:-ord(s[len(s) - 1:])] def encrypt(self, raw): raw = str(raw) raw = self.pad(raw) iv = Random.new().read(AES.block_size) cipher = AES.new(self.key, AES.MODE_CBC, iv) return base64.b64encode(iv + cipher.encrypt(raw)) def decrypt(self, enc): enc = base64.b64decode(enc) iv = enc[:16] cipher = AES.new(self.key, AES.MODE_CBC, iv) return self.unpad(cipher.decrypt(enc[16:])) t = datetime.datetime(2016, 06, 30, 17, 17, second=52) seed = int(time.mktime(t.timetuple())) #seed = 1467299872 aes = AESCipher() key = aes.encrypt(seed) aes = AESCipher() key = aes.decrypt(key) random.seed(int(key)) upload_content = base64.b64decode("/8bAieboX5pFq1sI6js92nrI6huZoxLZ5A==") content_size = len(upload_content) mask = ''.join([chr(random.randint(1,255)) for _ in xrange(content_size)]) plain = ''.join(chr(ord(a)^ord(b)) for a,b in zip(mask, upload_content)) print "Flag : " + plain
A l’exécution :
root@kali 12:16 [~/ndh2k16] # python flag.py Flag : ndh_crypt0sh1tn3v3rch4ng3
Flag : ndh_crypt0sh1tn3v3rch4ng3
Merci à toute l’équipe de la NDH2K16 pour cet événement et pour toute l’organisation !
Salutations à nj8, St0rn, Emiya, Mido, downgrade, Ryuk@n et tout ceux dont je n’ai hélas pas le pseudo :), on remet ça quand vous voulez ? // Gr3etZ
Sources & ressources :