Revers(ibl)e Engineering [1/2]

404CTF{e9d749db81e9f8caf745a5547da13579}

Catégorie: Reverse Difficulté: medium Flag: -

Challenge

circle-info

Description


Après une année éprouvante marquée par les compétitions, vous décidez de rentrer dans votre village natal. C'est avec beaucoup d'émotion que vous apercevez le dojo de votre enfance et décidez de vous y rendre. Votre ancienne sensei vous y attend, le sourire aux lèvres.

"La clairvoyance est l'arme la plus redoutable du combattant. Anticiper chaque mouvement avant qu'il ne soit lancé, voilà la véritable maîtrise du combat. Relève mon défi et prouve ta valeur."


Récupérer une archive zip avec netcat contenant un crackme et un token, renvoyer le token avec la solution du crackme à un deuxième serveur, recevoir un flag... Facile. Petit détail : vous avez vingt secondes pour faire tout ça, et le binaire change à chaque essai.

Connexion : nc challenges.404ctf.fr 31998 > chall.zip nc challenges.404ctf.fr 31999

circle-exclamation

Préparation

On va commencer par construire, en Python, une classe pour faciliter la réception et l'étude des binaires. On fera évoluer ce code au fil de notre progression

import socket
import os
from io import BytesIO
from zipfile import ZipFile


HOST = 'challenges.404ctf.fr'
PORT_BINARY = 31998
PORT_SOLVE = 31999

class Binary:
    FOLDER = 'out'
    
    @staticmethod
    def fetch() -> 'Binary':
        client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        client.connect((HOST, PORT_BINARY))
        archive = b''
        while True:
            data = client.recv(4096)
            if len(data) == 0:
                break
            archive += data
        archive = ZipFile(BytesIO(archive))
        crackme = archive.read('crackme.bin')
        token = archive.read('token.txt').decode()
        return Binary(crackme, token)

    @staticmethod
    def open(filename):
        token = filename.split('_')[1][:-4]
        with open(os.path.join(Binary.FOLDER, filename), 'rb') as f:
            crackme = f.read()
        return Binary(crackme, token)

    def __init__(self, crackme: bytes, token: str) -> None:
        self.crackme = crackme
        self.token = token
        self.filename = f'crackme_{self.token}.bin'
        self.filepath = os.path.join(self.FOLDER, self.filename)
    
    def save(self):
        if not os.path.exists(self.FOLDER):
            os.mkdir(self.FOLDER)
        with open(self.filepath, 'wb') as f:
            f.write(self.crackme)
        os.system(f'chmod +x {self.filepath}')
    
    def solve(self) -> str:
        # A compléter
        pass


def main() -> None:
    binary = Binary.fetch()
    print(f'Token: {binary.token}')
    


if __name__ == '__main__':
    main()

Ce script permet de récupérer l'archive depuis le serveur, extraire le crackme et le token associé, puis de les enregistrer localement. On peut également ouvrir un crackme déjà enregistré.

La fonction solve sera implémentée plus tard et retournera le mot de passe qui résout le challenge


Analyse des binaires reçus

On peut s'amuser à enregistrer 2-3 crackme et les décompiler pour comparer leurs différences.

La structure est toujours la même :

  • On doit rentrer un mot de passe en argument.

  • Le mot de passe doit faire 16 caractères.

Mais 2 choses changent constamment :

  • les opérations faites dans la fonction encrypt qui est utilisée sur notre mot de passe

  • la valeur de key à laquelle le résultat de notre mot de passe chiffré est comparé

On ne va pas s'amuser à reverse la fonction encrypt mais on peut voir sa sortie en exécutant un des binaires avec GDB (ou autres débugueurs) et en plaçant un breakpoint juste avant la comparaison. Le breakpoint à placer est sur memcmp@plt (je précise "plt" car nous voulons break avant de rentrer dans la résolution de l'adresse de la vraie fonction memcmp et ainsi pouvoir regarder la valeur des paramètres situés dans les registres RAX et RCX).

Dans RAX, on voit bien les valeurs croisées pendant la décompilation. Et dans RCX, sans oubliler que c'est du Little Endian, on a :

En relançant avec un mot de passe différent, on obtient :

circle-check

On peut donc essayer de faire un mapping en testant toute une liste de caractères et regarder en quoi ils sont transformés. Une fois ce mapping fait, il suffira alors de regarder la valeur de key pour regarder les correspondances attendues.


Automatisation de GDB

circle-exclamation

Ici, on va utiliser un script python que l'on passera avec l'option --command à GDB. Ce script va pouvoir utiliser l'API de GDB pour exécuter des commandes (comme ajouter un breakpoint et lire les registres).

On peut tester en lançant le binaire avec gdb et le script en option

Maintenant si on lance le binaire avec le mot de passe

Bingo ! Plus qu'à compléter notre fonction solve dans notre classe Binary et envoyer le résultat au serveur.


Résolution

Voici le script python au complet pour résoudre le challenge (ne pas oublier gdbscript.py)

Mis à jour