Problèmes de digestion
Le Breizh CTF 2018 s’est tenu dans la nuit de vendredi à samedi dernier, dans le grand hall de l’Université de Rennes 1.
Sponsor de l’évènement, et contributeur de trois rumps, ACCEIS participait également à la compétition. Ce write-up présente la solution du challenge de Reverse nommé « tube digestif », dont la résolution s’est avérée intéressante. Au final, l’épreuve représentait un enchaînement d’opérations cryptographiques en série, qui explique son nom étrange.
C’est quoi ce binaire ?
L’épreuve se présente de façon très simple, sous la forme d’un fichier unique, sans extension, nommé td. Un file nous renseigne rapidement sur sa nature :
Il s’agit d’un ELF 64 bits. Un ldd nous apporte quelques informations supplémentaires en listant les librairies utilisées :
On remarque que la librairie « libcrypto » est nécessaire. Intéressant.
On commence par exécuter le binaire, pour comprendre comment il fonctionne et ce qu’il attend :
Manifestement, il faut un paramètre (et un seul) qui semble être le flag à trouver. On note les différents messages, ils vont probablement nous servir à nous y retrouver une fois dans l’assembleur.
Fibroscopie de chez HexRays
On utilise IDA pour explorer le binaire. La liste des imports nous renseigne et quelques indices nous font comprendre qu’on va probablement avoir à faire à du SHA1 et du HMAC. La piste de la cryptographie semble se confirmer.
En affichant le main, on retrouve tout de suite les chaînes de caractères affichées pendant l’essai en live. C’est rassurant, il ne semble pas y avoir d’obfuscation de code ou de données qui compliqueraient la recherche.
Au passage, on remarque la chaîne de caractère « Flag is correct » qu’on n’avait pas encore vue, et IDA nous indique que ce chemin de code n’est atteint que si la fonction sub_10CB renvoie eax null. Allons regarder par là.
Avant, on repère rapidement les paramètres passés à cette fonction, qui sont tout simplement le pointeur vers le premier argument de l’exécutable et la longueur de cet argument. Il s’agit bien de notre flag passé en paramètre, qui va être digéré.
« Nettoyage » du flag
La fonction sub_10CB est une petite fonction qui retourne soit -1 ou -2, soit la valeur de retour d’une autre fonction nommée sub_FD9, que l’on n’a pas encore explorée.
Un rapide coup d’œil sur ce qui précède l’appel à cette fonction inconnue nous montre qu’elle prend en paramètre notre flag expurgé du préfixe « bzhctf{ » et du suffixe « } », ainsi que la taille de cette sous-chaîne en deuxième paramètre.
Par curiosité, on va essayer l’exécutable avec ces préfixes/suffixes, pour voir si ça change quelque chose.
Bingo ! Vue de la relecture, ça ne se voit pas, l’exécutable met environ 5 secondes avant de rendre son verdict au lieu de répondre immédiatement sa terrible sentence. Ça confirme qu’il faut bien un flag qui commence par « bzhctf{ » et finisse par « } ».»
Un bout de tube, ça a deux bouts ?
On se penche sur la fonction sub_FD9 :
On vérifie rapidement que la valeur de retour de cette fonction, testée à zéro en cas de succès, peut en particulier être rendue par un appel à memcmp() de 0x14 octets par rapport à une chaîne constante à l’adresse unk_12F0. On remarque que 20 octets correspondent à la taille d’une empreinte SHA-1. Intéressant.
Pour arriver à ce memcmp(), il faut passer par une comparaison à 0 d’une variable locale (renommée ici var_pid pour simplifier la compréhension), qui est la valeur de retour d’une fonction encore inexplorée sub_CBA.
Cette fonction n’est pas détaillée ici, mais elle crée une paire de pipe puis fork() le process. La valeur retournée de sub_CBA est le PID du process fils (pour le père) et zéro pour le processus fils. C’est donc le processus père qui exécute le memcmp() entre une chaîne constante et une chaîne récupérée en entrée standard via un pipe, juste après avoir envoyé une autre chaîne constante, nommée unk_12D0 (de 20 octets aussi) dans un pipe en sortie standard.
A cette étape, on peut raisonnablement faire l’hypothèse (qui va se révéler exacte) que le processus père donne à manger cette chaîne constante unk_12D0 au processus fils, duquel il récupère la sortie, et la compare à la chaîne unk_12F0.
La vue de la fonction en mode graphique est la suivante :
On voit immédiatement (par surlignement du nom de la fonction sub_DDA ) qu’elle s’appelle elle-même. Le « tube » est donc récursif. On verra ça plus tard.
D’un autre côté, sur la partie droite, on voit une boucle qui s’applique 20 fois (d’après le compteur var_idx) sur deux buffers qui sont xorés avant que le résultat ne soit envoyé dans un pipe pour sortir brutalement de la fonction.
En s’intéressant à l’origine de ces deux buffers (s et buf), on retrouve facilement que s est issue d’un read() provenant d’un pipe, et que buff vient d’être « poussé » dans le tube. Il faut donc remonter plus haut pour savoir d’où vient le contenu de buff.
En suivant le code, tout s’éclaircit : buff est le résultat d’une opération HMAC (SHA1) sur notre flag. Pour embrouiller un peu les esprits, on observe que la clé utilisée pour appliquer la signature est dans le même buffer que le résultat, et ce buffer provient de la lecture d’un pipe. Encore un petit effort et on va bientôt pouvoir reconstituer cet intestin.
En effet, la variable var_iteration_num (0x400 = 0d1024 au départ, à l’appel de sub_DDA) est comparée à 1 pour savoir si le chemin de code part à droite (du coté du xor), ou à gauche, du coté d’une fonction sub_CBA. Celle-là même qui crée une paire de pipes puis fork() le process. La boucle est bouclée.
A cette étape, on peut reconstituer de façon schématique la structure du binaire :
Un peu de citrate de bétaïne…
A travers cette itération de hash salés, on reconnaît l’algorithme PBKDF2, initialement conçu pour dériver des clés de chiffrement, et qui est aujourd’hui utilisé comme fonction de hachage. Il répète donc 1024 fois SHA-1 sur le flag, en utilisant la valeur de unk_12D0 comme sel. Son résultat (d’une taille de 20 octets, évidemment), est comparée à unk_12F0, qui est le hashmac attendu.
Pour résoudre le challenge, on utilise donc hashcat, en fournissant la liste rockyou comme entrée. Le résultat ne tarde pas (trop) à arriver.
On teste la validité du flag en le fournissant en entrée au binaire :
Challenge résolu. Une épreuve sympa mélangeant cryptographie et reverse.