Revers(ibl)e Engineering [2/2]

404CTF{4df8110da3b4c5c1e87e564418fab97f}

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

Challenge

circle-info

Description


Il est recommandé de finir Revers(ibl)e Engineering [1/2] avant d'essayer ce challenge.Du fait de la nature des binaires générés, vous pourriez vouloir les executer dans une VM.

"Je vois que mon enseignement a porté ses fruits, augmentons le niveau. Quand lors d'un combat anticiper devient impossible, seul celui qui sait danser sur le fil de l'imprévisible peut triompher dans ce ballet de l'inconnu."

Même principe que l'épreuve précèdente, mais cette fois vous avez dix minutes. La chance ! Par contre, les crackme sont à usage unique. Pas très eco-friendly, je sais..

Connexion : nc challenges.404ctf.fr 31990 > chall.zip nc challenges.404ctf.fr 31991

circle-exclamation
circle-exclamation

Préparation

Ici, nous utiliserons le même script de préparation que dans la partie 1 du challenge pour les mêmes raisons.

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

HOST = 'challenges.404ctf.fr'
PORT_BINARY = 31990
PORT_SOLVE = 31991

class Binary:
    FOLDER = 'out'
    
    # Sauvegarde de toutes les instances de binaires téléchargés
    INSTANCES: typing.Dict['str', 'Binary'] = {}
    
    @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)

    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)
        
        # Récupération des données envoyées par le serveur
        self.responses = self.get_server_data()
        
        # Ajout de notre objet à la liste des instances existances
        self.INSTANCES[self.token] = self
        
        self.save()
    
    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 get_server_data(self):
        # A compléter
        pass
    
    def solve(self):
        # A compléter
        pass

Analyse des binaires reçus

circle-info

Cette partie liste les fonctions du binaire, si vous avez la flemme, les explications de résolution commence après

Commençons par une décompilation d'un binaire (avec Ghidra pour ma part). J'ai renommé toutes les variables, modifié un peu le code pour le simplifier et ajouté des commentaires histoire de mieux comprendre ce qu'il se passe.

main

recv_functions

connect_and_send_token

recv_function

recv_key_and_message

read_password

verify

apply_functions

encrypt


Compréhension et stratégie

Pour résumer, voici les étapes depuis le lancement du binaire :

  1. Une connexion s'ouvre vers le serveur 162.19.101.153:31992 pour récupérer des fonctions compilées jusqu'à recevoir le message endfunc

  2. Tant que la boucle n'est pas cassée

    1. Une nouvelle connexion s'ouvre pour récupérer une clé de 16 octets et un message de taille variable

    2. Demande le mot de passe en affichant le message récupéré

    3. Applique toutes les fonctions sur notre mot de passe

    4. Chiffre la clé

    5. Compare la clé chiffrée avec notre mot de passe modifié

La petite chose à noter est que le serveur auquel se connecte notre binaire n'envoie les réponses qu'une seule fois. Autrement dit, on ne peut lancer le binaire qu'une seule fois.


L'astuce utilisée ici est de contacter le serveur distant qui envoie les fonctions et clés en amont, son IP et son port ne changent d'un binaire à l'autre.

Ainsi, avant de lancer notre binaire, on va récupérer les réponses qu'il était censé recevoir du serveur et les enregistrer. Ensuite, on viendra lancer un serveur local qui imitera le comportement du serveur distant en renvoyant ces données dans le même ordre. Il ne reste alors plus qu'à rediriger les requêtes faites par le binaire vers le serveur distant sur notre serveur local pour qu'il n'y voie que feu et qu'on puisse le lancer sans limites.

Résolution

Redirection des flux

L'objectif est de capturer ce qui part vers le serveur distant sur le port 31192 et de le rediriger vers notre serveur local sur le port 3000. Mais nous devons aussi pouvoir récupérer les données du serveur distant, pour ça, on peut mapper un autre port libre (ici le 40000) vers le vrai port 31992 car si on utilise directement le vrai port, il sera capturé par notre première règle.

Il nous faut donc 2 règles iptables :

  • 162.19.101.153:31992 redirigé vers localhost:3000

  • 162.19.101.153:xxxx redirigé vers 162.19.101.153:31992

Voici un petit script qui permet d'ajouter les règles et les supprimer (tout en vérifiant si elles sont déjà en place).


Récupération des données

On va compléter la fonction get_server_data de notre classe Binary, elle va servir à récupérer les données envoyées par le serveur distant en se faisant passer pour le binaire.


Serveur local

A chaque fois que le binaire fait une requête avec son token, on doit envoyer les données suivantes. Le serveur va être lancé dans un thread pour ne pas bloquer l'exécution du binaire ensuite.


GDBScript et solve

On fait la même technique que dans la version 1 du challenge, on met un breakpoint sur le memcmp@plt et l'on regarde les registres. Petite variation sur le fait qu'au total on nous demande 3 mots de passe à la suite.


Script complet

Mis à jour