Solution de 4idenP pour Guessy

intro reverse linux x86/x64

5 janvier 2024

Avant-propos

Nous allons à travers cette solution résoudre l’épreuve Guessy en réalisant une analyse statique du fichier via l’outil Radare2.

Reconnaissance

Avant tout, nous commençons par déterminer le type du fichier qui nous est fourni :

$ file guessy
guessy: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, not stripped

On voit qu’on a affaire à un éxecutable, mais rien de bien probant. Essayons alors de lancer le programme pour voir ce qu’il fait :

chmod +x guessy

image-3

Ainsi le programme semble attendre de nous qu’on lui fournisse le flag, voyons ce qu’il se passe si on lui fournit le peu qu’on connaisse du drapeau attendu :

image-4

Super ! Maintenant il ne nous reste plus qu’à deviner les 8 prochains caractères du drapeau. Malheureusement, je ne suis pas magicien et le brute force n’est pas autorisé.

Alors faisons les choses proprement et lançons notre analyse statique avec Radare2.

Analyse statique via Radare2

Nous commençons la rétro-ingénierie de notre éxecutable en précisant l’option aaaa pour révéler le plus de détails possibles de l’architecture du programme.

image-1

1. Fonction main

On affiche ensuite le code désassemblé de la fonction main :

image-2

On voit dans la fonction que le programme attend une entrée de l’utilisateur via la fonction fgets et que suite à ça une fonction sym.validate est appelée pour vraisemblablement valider l’entrée de l’utilisateur.

Il n’y a rien d’autre de très intéressant, nous passons alors à l’analyse de la fonction sym.validate.

2. Fonction sym.validate

Voyons ce qu’elle contient :

image-6

On se retrouve face à un bon nombre de lignes mais on peut grossièrement en tirer le fonctionnement principal de la fonction.

On peut voir que la fonction effectue 5 comparaisons à l’aide de l’instruction cmp.

  1. Si celles-ci échouent le programme saute alors à l’instruction située en 0x40150b qui affiche le message “Well it does not begin well for you.” avant de sauter à la fin du programme.

  2. Si elles réussissent on passe à celle d’après, sauf pour la dernière qui en cas de réussite saute à l’instruction située en 0x401532 et qui n’est autre qu’un appel de la fonction sym.difficul_part. Ce qui nous intéressera fortement pour la suite.

On remarque qu’à chaque fois, la comparaison est opérée entre le registre al qui semble à tour de rôle prendre la valeur de chaque caractère fourni par la dernière saisie utilisateur et une valeure décimale.

Une horloge doit alors faire tic dans votre esprit : allons voir la table des caractères ASCII.

Nous retrouvons alors la correspondance :

Décimal Symbole
70 F
67 C
83 S
67 C
123 {

C’est bien le début du drapeau que nous recherchons ! C’est bon signe pour la suite, nous avons décelé la façon dont sont vérifiés les caractères saisis par l’utilisateur.

Cette partie étant faite, attaquons-nous à la suite de l’énigme : la fonction sym.difficult_part.

3. Fonction sym.difficult_part

Nous désassemblons la fonction et observons ce qu’elle fait :

Ouch ! Le désassemblage nous présente 256 lignes de code, analysons-le par morceaux.

image-7

Caractères 6-13

Voici le premier bloc d’instructions situés juste après la déclaration des variables.

Premièrement, on remarque que le message “Now you can try to guess the next eight characters of the flag” est affiché comme vu à l’étape de reconnaissance et qu’après ça une saisie est attendue de l’utilisateur via la fonction fgets.

On peut voir qu’en cas d’une saisie plus grande que 9 octets (8 caractères + caractère nul de fin de chaîne), le message Well it seems that someone has trouble counting to eight est affiché et le programme est arrêté.

Pour ces 8 caractères, la logique utilisée est la même que pour la première étape, alors après un rapide coup d’oeil à notre bonne vieille table des caractères ASCII, nous retrouvons la correspondance :

Décimal Symbole
101 e
55 7
53 5
53 5
50 2
99 c
102 f
54 6

Notre flag avance ! Pour l’instant nous avons : FCSC{e7552cf6.

Caractères 14-21

Maintenant, jetons un oeil au prochain bloc :

image-8

Cette fois, on peut voir qu’il y a une instruction en plus avant chaque comparaison : add eax, eax. Cette instruction a pour effet d’additionner le registre à lui-même avant que celui-ci ne soit comparé aux valeurs affichées en rouge.

Sachant que le registre eax contient à tour de rôle les 8 caractères saisis par l’utilisateur, c’est leur valeur qui est multipliée par deux avant d’être comparée.

Ainsi pour obtenir les caractères du drapeau, rien de plus simple, il nous suffit juste de diviser par deux les valeurs visibles en rouge dans r2. Après une recherche de correspondance dans la table ASCII nous obtenons donc ces valeurs :

Décimal Symbole
52 4
55 c
101 e
50 2
101 e
53 5
102 a
100 d

Une fois de plus notre flag avance ! Désormais nous avons : FCSC{e7552cf64ce2e5ad.

Caractères 22-29

Voyons ce que nous réserve le prochain bloc :

image-9

Cette fois ce n’est pas une addition qui est réalisé mais un décalage (shift) de 3 bits vers la gauche via l’instruction : shl eax, 3.

Le triple décalage vers la gauche des bits aura pour effet de multiplier la valeur du registre par 8 (2^3).

Ainsi en inversant le calcul, il nous faut diviser par 8 les valeurs attendues pour obtenir les caractères du drapeau.

De cette manière, nous obtenons :

Valeur décalée Valeur attendue Symbole
384 48 0
784 98 b
784 98 b
384 48 0
456 57 9
424 53 5
416 52 4
816 102 f

Nous y sommes presque ! Pour l’instant, notre flag est : FCSC{e7552cf64ce2e5ad0bb0954f.

Caractères 30-37

Le bloc se présente ainsi :

image-10

Cette fois nous sommes confrontés à un xor logique effectué entre les variables testées à l’étape précédente et nos caractères saisis.

Or comme le dit le dicton : le xor de mon xor est mon xor, donc pour retrouver le bout de flag, rien de plus simple : il suffit de faire le xor entre les valeurs attendues (en rouge) et nos variables précédentes (0bb0954f).

Pour cela, on sort python :

previous = b"0bb0954f"
expected = b"\x01\x54\x55\x51\x09\x07\x57"
res = bytes([x^y for x,y in zip(previous, expected)])

Ce qui nous donne b'167a02cf' et donc pour le flag : FCSC{e7552cf64ce2e5ad0bb0954f167a02cf.

Ainsi notre flag est complet… ou presque !

Un appel est fait à la fonction sym.most_difficult_part à la fin du programme, nous ne sommes pas au bout de nos peines…

Dernière étape

Jetons-y un coup d’oeil :

image-11

Ouf ! C’était une blague, il manquait simplement l’accolade fermante (}) de code ASCII 125, comme nous l’indique le “Can you guess the LAST character of the flag ?” et l’instruction de comparaison en 0x0040118e.

Validation

Notre drapeau est désormais complet : FCSC{e7552cf64ce2e5ad0bb0954f167a02cf}.

Rentrons-le désormais dans le programme pour vérifier :

image-12

L’épreuve est terminée.