/ hack.lu

hack.lu - Cryptolocker

Enoncé :
Oh no! Cthulhu's laptop was hit by ransomware and an important document was encrypted! But you have obtained the encryption script and it seems like the encryption is vulnerable...
Even tough you don't know the encryption password, can you still help recover the important ODT file?

Write up:

Nous avons une archive contenant 3 fichiers :

  • flag.encrypted
  • AESCipher.py
  • cryptoloc.py

Regardons de plus près le fichier cryptoloc.py :

#!/usr/bin/env python3
import sys
import hashlib
from AESCipher import *

class SecureEncryption(object):
    def __init__(self, keys):
        assert len(keys) == 4
        self.keys = keys
        self.ciphers = []
        for i in range(4):
            self.ciphers.append(AESCipher(keys[i]))

    def enc(self, plaintext): # Because one encryption is not secure enough
        one        = self.ciphers[0].encrypt(plaintext)
        two        = self.ciphers[1].encrypt(one)
        three      = self.ciphers[2].encrypt(two)
        ciphertext = self.ciphers[3].encrypt(three)
        return ciphertext

    def dec(self, ciphertext):
        three      = AESCipher._unpad(self.ciphers[3].decrypt(ciphertext))
        two        = AESCipher._unpad(self.ciphers[2].decrypt(three))
        one        = AESCipher._unpad(self.ciphers[1].decrypt(two))
        plaintext  = AESCipher._unpad(self.ciphers[0].decrypt(one))
        return plaintext

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("Usage: ./cryptolock.py file-you-want-to-encrypt password-to-use")
        exit()

    # Read file to be encrypted
    filename = sys.argv[1]
    plaintext = open(filename, "rb").read()

    user_input = sys.argv[2].encode('utf-8')
    assert len(user_input) == 8
    i = len(user_input) // 4
    keys = [ # Four times 256 is 1024 Bit strength!! Unbreakable!!
        hashlib.sha256(user_input[0:i]).digest(),
        hashlib.sha256(user_input[i:2*i]).digest(),
        hashlib.sha256(user_input[2*i:3*i]).digest(),
        hashlib.sha256(user_input[3*i:4*i]).digest(),
    ]
    s = SecureEncryption(keys)

    ciphertext = s.enc(plaintext)
    plaintext_ = s.dec(ciphertext)
    assert plaintext == plaintext_

    open(filename+".encrypted", "wb").write(ciphertext)

Nous savons que la clé de chiffrement/déchiffrement fait 8 caractères :

assert len(user_input) == 8

Puis que finalement cette clé de 8 caractères est divisée en 4 clés de 2 caractères :

i = len(user_input) // 4
    keys = [ # Four times 256 is 1024 Bit strength!! Unbreakable!!
        hashlib.sha256(user_input[0:i]).digest(),
        hashlib.sha256(user_input[i:2*i]).digest(),
        hashlib.sha256(user_input[2*i:3*i]).digest(),
        hashlib.sha256(user_input[3*i:4*i]).digest(),
    ]

La méthode de chiffrement utilisée se déroule en 4 étapes : chiffrement du fichier en clair via la première clé, puis chiffrement du résultat obtenu avec la seconde clé, etc. (x4) --> cf la fonction enc() ci-dessus.

Le déchiffrement utilise donc le même principe: déchiffrement du cipher via la 4ème clé, puis déchiffrement du résultat obtenu via la troisième clé, etc. (x4) --> cf la fonction dec() ci-dessus.

Etant donné que chaque clé fait seulement 2 caractères, une attaque brute-force ne prendra pas beaucoup de temps, mais nous obtiendrons le résultat clair qu'au bout du 4ème déchiffrement. Comment savoir que notre résultat est le bon lors des 3 premiers BF ?

Regardons de plus près la fonction encrypt() dans AESCipher.py :

def _pad(self, s):
        return s + (self.bs - len(s) % self.bs) * AESCipher.str_to_bytes(chr(self.bs - len(s) % self.bs))

def encrypt(self, raw):
        raw = self._pad(AESCipher.str_to_bytes(raw))
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return iv + cipher.encrypt(raw)

Le texte chiffré est en réalité constitué du texte à chiffrer basique + un padding ajouté à la fin pour avoir un multiple de 32.

C'est donc ce padding ajouté à la fin qui va nous permettre de déterminer lors du BF que notre résultat est le bon, et donc de trouver chaque clé.

Voici le script permettant de déchiffrer le fichier flag.encrypted :

#!/usr/bin/env python3
from cryptolock import *

ciphertext = open("flag.encrypted", "rb").read()

all_keys = []

# Génération de toutes les clés à 2 caractères UTF-8 possibles
for x in range(32, 128):
	for y in range(32, 128):
		all_keys.append(chr(x)+chr(y))


last_cipher = ciphertext

for x in range(4):
	for element in all_keys:
		tmp_key = hashlib.sha256(element).digest()
		instance = AESCipher(tmp_key)
		clear = instance.decrypt(last_cipher)

		hexa_result = binascii.hexlify(clear)

		# Comparaison des 4 derniers caractères
		# Si identiques, cela correspond au padding 
		# Nous avons donc la bonne clé
		if hexa_result[-8:-6] == hexa_result[-6:-4] == hexa_result[-4:-2] == hexa_result[-2:]:
			last_cipher = AESCipher._unpad(clear)
			break

print (last_cipher)

Nous savons par l'énoncé que c'est un fichier odt, il suffit d'exécuter le script ainsi :

$ ./script.py > flag.odt

En moins d'1 minute environ, nous récupérons notre fichier odt flag.odt avec le flag en bas de la page :

flag

flag{v3ry_b4d_crypt0_l0ck3r}