[WARGAME NDH 2016] Write-Up – Crypto: SuperCipher

07
juil.
2016
  • Google Plus
  • LinkedIn
  • Viadeo
Posted by: Yann C.  /   Category: Cryptanalyse / Cryptographie / Cryptologie / CTF / Events / NDH / NDH2k16 / Reverse-Engineering / Wargame   /   Aucun commentaire

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é.

decompyle

decompyle

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 :

timestamp

timestamp

« 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 :

  • Google Plus
  • LinkedIn
  • Viadeo
Author Avatar

About the Author : Yann C.

Consultant en sécurité informatique et s’exerçant dans ce domaine depuis le début des années 2000 en autodidacte par passion, plaisir et perspectives, il maintient le portail ASafety pour présenter des articles, des projets personnels, des recherches et développements, ainsi que des « advisory » de vulnérabilités décelées notamment au cours de pentest.