FCSC 2021 - Intro - guessy

Table des matières :

Il va probablement vous falloir beaucoup de chance pour deviner le flag. Ou quelques compétences en rétro-ingénierie.

SHA256(guessy) = f9388d92cbf5de8c92831555608162c672a99d164aed789638fcc91a66ab13e5.

Fichiers joints :


Résolution

Tout d’abord, jouons un peu avec le binaire du challenge. Lorsque nous rentrons n’importe quoi en entrée, nous avons Well it does not begin well for you. :

$ ./guessy
Give me the flag:
wow_such_flag
Well it does not begin well for you.

Mais lorsque notre entrée commence par FCSC{ nous avons un autre message, et le binaire nous demande une nouvelle entrée :

Give me the flag:
FCSC{
Ok so I see we have an understanding. Let's begin the difficult part now.
Now you can try to guess the next eight characters of the flag.

Well it seems that someone has trouble counting to eight.

Analyse générale

Ce binaire permet de valider un flag entré en paramètre. Il y a 5 vérifications successives correspondant aux 5 parties du flag suivant :

FCSC{aaaaaaaabbbbbbbbccccccccdddddddd}

Nous allons analyser les fonctions qui réalisent ces vérifications dans l’ordre.

Analyse de la fonction main

La fonction main est très basique, elle appelle simplement la fonction validate sur l’entrée lue avec fgets juste avant :

Désassemblons maintenant la fonction validate

Analyse de la fonction validate

La fonction validate vérifie les 5 première lettres du flag:

Nous avons donc :

FCSC{________________________________}

Elle appelle ensuite la fonction difficult_part pour la suite des vérifications.

Analyse de la fonction difficult_part

La fonction difficult_part contient 4 vérifications opérant chacune différement.

Partie 1

La première vérification est une simple comparaison des lettres du flag dans l’ordre que nous avons entré avec les valeurs ASCII attendues pour ces caractères :

Nous avons donc maintenant :

FCSC{e7552cf6________________________}

Partie 2

La seconde vérification fonctionne un peu différement et introduit un léger calcul sur les valeurs comparées :

Nous remarquons que chaque bloc permettant de vérifier un caractère est composé comme ceci :

0x000000000040128c <+221>:	movzx  eax,BYTE PTR [rbp-0x10]
0x0000000000401290 <+225>:	movsx  eax,al
0x0000000000401293 <+228>:	add    eax,eax
0x0000000000401295 <+230>:	cmp    eax,0x68
0x0000000000401298 <+233>:	jne    0x401306 <difficult_part+343>

La valeur ASCII des caractères du flag que nous avons rentré est lue, puis placée dans eax. Ensuite, cette valeur est multipliée par 2 (add eax,eax est équivalent à eax += eax dans un langage de programmation) avant d’être comparée. Pour reconstruire les valeurs attendues, nous pouvons le faire simplement en python :

>>> cmp_values = [0x68,0xc6,0xca,0x64,0xca,0x6a,0xc2,0xc8]
>>> ascii_vals = [c//2 for c in cmp_values]
>>> bytes(ascii_vals)
b'4ce2e5ad'
>>>

Nous avons donc maintenant :

FCSC{e7552cf64ce2e5ad________________}

Partie 3

Dans cette vérification, la valeur ASCII des caractères du flag que nous avons rentré est lue, puis placée dans eax puis l’opération shl est effectuée.

L’instruction shl en assembleur permet de réaliser un décalage à gauche (shift left) d’un nombre de bits indiqué (ici shl eax,0x3 décale eax de 3 bits vers la gauche, ce qui revient à une multiplication par ).

0x0000000000401358 <+425>:	movzx  eax,BYTE PTR [rbp-0x10]
0x000000000040135c <+429>:	movsx  eax,al
0x000000000040135f <+432>:	shl    eax,0x3
0x0000000000401362 <+435>:	cmp    eax,0x180
0x0000000000401367 <+440>:	jne    0x4013e0 <difficult_part+561>

Pour retrouver les valeurs attendues, nous pouvons le faire simplement en python et réalisant une division par 8 ou un décalage à droite variable >> 3 :

>>> cmp_values = [0x180,0x310,0x310,0x180,0x1c8,0x1a8,0x1a0,0x330]
>>> ascii_vals = [c//(2**3) for c in cmp_values]
>>> bytes(ascii_vals)
b'0bb0954f'
>>>

Nous avons maintenant presque tout le flag:

FCSC{e7552cf64ce2e5ad0bb0954f________}

Partie 4

La dernière partie réalise un xor de deux valeurs dans la stack, et compare leur résultat.

Pour pouvoir récupérer ces valeurs, il suffit de mettre un breakpoint au niveau du xor :

0x0000000000401432 <+643>:	movzx  edx,BYTE PTR [rbp-0x10]
0x0000000000401436 <+647>:	movzx  eax,BYTE PTR [rbp-0x20]
0x000000000040143a <+651>:	xor    eax,edx
0x000000000040143c <+653>:	cmp    al,0x1
0x000000000040143e <+655>:	jne    0x4014a0 <difficult_part+753>

Et de le répéter pour les 8 vérifications des caractères du flag :

b * 0x000000000040143a
b * 0x0000000000401448
b * 0x0000000000401456
b * 0x0000000000401464
b * 0x0000000000401472
b * 0x0000000000401480
b * 0x000000000040148e

Nous n’avons plus qu’a lire à chaque fois la valeur présente dans edx et effectuer un xor avec la valeur attendue par la comparaison, pour retrouver les caractères ASCII du flag.

>>> flag_part_4 = [0x30^0x1, 0x62^0x54, 0x62^0x55, 0x30^0x51, 0x39^0x9, 0x35^0x7, 0x34^0x7, 0x66]
>>> bytes(flag_part_4)
b'167a023f'
>>>

Nous avons maintenant tout le flag (sans l’accolade de fin }):

FCSC{e7552cf64ce2e5ad0bb0954f167a023f

Mais la fonction difficult_part fait ensuite appel à la fonction most_difficult_part, que nous allons analyser juste après.

Analyse de la fonction most_difficult_part

La most_difficult_part est la plus dure de tout ce challenge (just kidding) c’est une fonction toute simple qui vérifie uniquement le dernier caractère du flag (que nous pouvons guess, c’est forcément un }):

Et nous avons le flag :

FCSC{e7552cf64ce2e5ad0bb0954f167a02cf}