/ crypto

NDH2K16 - SuperCipher

Ce challenge était un service de chiffrement sur lequel on pouvait uploader un fichier et on récupérer une archive zip contenant le fichier chiffré (secret) et un autre fichier contenant la clé (key).
Ce service permettait également de déchiffrer un fichier en l'uploadant et en tapant la clé dans un champ.

En plus de l'accès au service nous avions une archive flag.zip contenant le flag dans un fichier chiffré et un fichier key.

$ unzip flag.zip 
Archive:  flag.zip
   creating: secret/
  inflating: secret/key              
 extracting: secret/secret 

$ cat key 
QUhBSEFILVRISVMtSVMtTk9ULVRIRS1LRVk=

$ cat secret 
/8bAieboX5pFq1sI6js92nrI6huZoxLZ5A==

Même en étant sûr du résultat on essaye de déchiffrer le fichier du flag avec la clé via le service en ligne, et comme attendu ça ne fonctionne pas.

Avec autant de certitude du résultat que précedemment on décode en base64 secret et key :

>>> secret.decode('base64')
'\xff\xc6\xc0\x89\xe6\xe8_\x9aE\xab[\x08\xea;=\xdaz\xc8\xea\x1b\x99\xa3\x12\xd9\xe4'
>>> key.decode('base64')
'AHAHAH-THIS-IS-NOT-THE-KEY'

Ca a le mérite d'être clair ! Il faut trouver autre chose.

On retourne sur le service en ligne et on remarque que les boutons chiffrer et déchiffrer font appel à une page home.py.

On récupère donc le python compilé en accédant à la page home.pyc. Puis on décompile le fichier téléchargé avec uncompyle2 :

$ uncompyle2 -o home.py home.pyc 
# 2016.07.05 00:18:13 CEST
+++ okay decompyling home.pyc 
# decompiled 1 files: 1 okay, 0 failed, 0 verify failed
# 2016.07.05 00:18:13 CEST

Jetons un coup d'oeil au fichier home.py obtenu. Parmi tout le code les fonctions suivantes nous intéressent :

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
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.'
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:]))

En analysant ces fonctions on comprend que si l'on retrouve le seed initialisé avec time.time() pendant le chiffrement on peux retrouver la key nécessaire au déchiffrement.
Puisqu'on a le fichier secret on récupère avec la commande stat la dernière date de modification :

$ stat secret 
  Fichier : 'secret'
   Taille : 36        	Blocs : 8          Blocs d'E/S : 4096   fichier
Périphérique : 803h/2051d	Inœud : 14815408    Liens : 1
Accès : (0644/-rw-r--r--)  UID : ( 1000/ maxisam)   GID : ( 1001/ maxisam)
 Accès : 2016-07-05 00:06:53.840074529 +0200
Modif. : 2016-06-30 17:17:52.000000000 +0200
Changt : 2016-07-02 22:14:20.604259325 +0200
  Créé : -

On convertit la date en timestamp : 1467299872
Puis on modifie légèrement la fonction de déchiffrement :

cipher = base64.b64decode("/8bAieboX5pFq1sI6js92nrI6huZoxLZ5A==")
seed = 1467299872
aes = AESCipher()
key = aes.encrypt(seed)
aes = AESCipher()
key = aes.decrypt(key)
random.seed(int(key))
content_size = len(cipher)
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, cipher))
print plain

ndh_crypt0sh1tn3v3rch4ng3