Les chatons ont la réponse – FCSC 2020 // Revision


Intro

Déjà à l’école, on me le disait, “prends ton temps, lis bien le problème, c’est avant tout une question de français”. J’ai perdu énormément de points car je n’ai vu le bouton comparator.py qu’au bout d’une semaine !

oui bon c’était mon premier CTF en mode compétiteur !

Révision, où comment gagner un repas au resto offert par ton sysadmin

Le principe est simple, on envoie deux documents, via le site web, et cela les archive. Jusque là tout va bien, mais très vite cela va devenir le drame.

comparator.py

# coding: utf-8
import hashlib
from web.services.database import Database
from web.services.mailer import Mailer


class ComparatorError(Exception):
    """Base class for all Comparator exceptions"""
    pass


class DatabaseError(ComparatorError):
    """Exception raised for errors in database operations."""


class StoreError(ComparatorError):
    """Exception raised for errors in store function.

    Attributes:
        message -- explanation of the error
    """

    def __init__(self, files, message):
        self.files = files
        self.message = message


class Comparator(object):
    """A class for Comparator"""

    BLOCK_SIZE = 8*1024

    def __init__(self, f1=None, f2=None):
        """
        Set default parameters

        Required parameters :
            f1: open file handler
            f2: open file handler
            db: database
            m : mailer

        """
        self.f1 = f1
        self.f2 = f2
        self.db = Database()
        self.m = Mailer()

    def compare(self):
        self._reset_cursor()
        return self.f1.read() == self.f2.read()

    def store(self):
        self._reset_cursor()
        f1_hash = self._compute_sha1(self.f1)
        f2_hash = self._compute_sha1(self.f2)

        if self.db.document_exists(f1_hash) or self.db.document_exists(f2_hash):
            raise DatabaseError()

        attachments = set([f1_hash, f2_hash])
        # Debug debug...
        if len(attachments) < 2:
            raise StoreError([f1_hash, f2_hash], self._get_flag())
        else:
            self.m.send(attachments=attachments)

    def _compute_sha1(self, f):
        h = hashlib.sha1()
        buf = f.read(self.BLOCK_SIZE)
        while len(buf) > 0:
            h.update(buf)
            buf = f.read(self.BLOCK_SIZE)
        return h.hexdigest()

    def _reset_cursor(self):
        self.f1.seek(0)
        self.f2.seek(0)

    def _get_flag(self):
        with open('flag.txt', 'r') as f:
            flag = f.read()
        return flag

Si l’on analyse le code rapidement, il sert à comparer deux documents, et si tout va bien il les envoie par mail, si une grosse erreur arrive, il lève l’exception qui nous donne le flag :

    def store(self):
        self._reset_cursor()
        f1_hash = self._compute_sha1(self.f1)
        f2_hash = self._compute_sha1(self.f2)

        if self.db.document_exists(f1_hash) or self.db.document_exists(f2_hash):
            raise DatabaseError()

        attachments = set([f1_hash, f2_hash])
        # Debug debug...
        if len(attachments) < 2:
            raise StoreError([f1_hash, f2_hash], self._get_flag())
        else:
            self.m.send(attachments=attachments)

OK, mais comment obtenir cette exception alors ? La comparaison se fait par le Hash des fichiers, et le bloc juste après, nous voyons qu’il va utiliser une somme SHA1 :

    def _compute_sha1(self, f):
        h = hashlib.sha1()
        buf = f.read(self.BLOCK_SIZE)
        while len(buf) > 0:
            h.update(buf)
            buf = f.read(self.BLOCK_SIZE)
        return h.hexdigest()

Et vous savez quoi mes chatons ? Il existe une collision SHA1, deux documents craftés peuvent avoir la même somme SHA1 (alors que théoriquement, cela était impossible).
Je vous laisse regarder ce site qui explique le souci, et ce qui est bien c’est qu’il fournit deux PDF crafté avec la collision, c’est parfait !

Logique !

Let’s forge our file !

Le site a déjà ce hash en mémoire, ce n’est pas grave, cela nous retarde juste un petit peu. Nous avons deux PDF avec la même somme SHA1 :

Nous allons donc les modifier afin de changer la signature ! Et si nous le faisons bien, nous aurons la même propriété. Pour cela, nous allons ajouter un padding à la fin du document, des random bytes qui modifieront la somme SHA1 et qui nous validerons le défi. Pour ce faire, nous avons besoin de créer un fichier dummy avec notre padding dedans :

dd bs=64 if=/dev/urandom of=./padding-pseudo-random.tmp count=23

Pourquoi 23 ? Car c’est mon jour de naissance. Et l’on reste dans la limitation des 2 Mo par document (23 blocs de 64 Ko).

On réalise une copie des documents de Google, et nous allons les modifier comme cec i:

Nous vérifions que nous avons bien gardé notre propriété magique :

parfait !

Nous les passons dans l’outil de révision, et nous obtenons notre flag !

FCSC{8f95b0fc1a793e102a65bae9c473e9a3c2893cf083a539636b082605c40c00c1}

Pourquoi ton sysadmin devrait te payer le resto

Va voir ton sysadmin, et demande lui s’il est possible qu’une somme SHA1 soit identique sur deux documents différents. S’il est cultivé, il saura te répondre que oui c’est possible, s’il n’est pas réellement intéressé, il te soutiendra que non, et là tu pourras mettre en place ton pari !


Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.