FCSC 2021 - Intro - guessy
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 2³
).
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}