Dans ma quête d’apprentissage du reverse engineering, je suis tombé sur ce github avec quelques crack-me en x64 dont le niveau augmente au fur et à mesure.
Le niveau 1 étant trop simple, je me suis concentré sur le niveau 2 pour ce writeup.
Après avoir cloné le repo, on se retrouve avec les crack-me en c.
Pour le compiler et ainsi le désassembler (car analyser le code en C ne serait pas drôle), on lance la commande make :
On se retrouve ainsi avec un fichier .64, et la commande file nous donne quelques informations supplémentaires :
On confirme ainsi que c’est un ELF x64, not stripped, ce qui est normal pour un crack-me facile comme celui-ci.
Exécutons ce binaire et voyons ce qu’il nous donne !
On voit que le binaire n’a besoin que d’un seul argument, on va donc essayer un string simple pour commencer : “aaaa”
A ma plus grande surprise, le string “aaaa” ne fonctionne pas, mais on sait que l’on pourra trouver en décompilant ce binaire la condition d’échec en recherchant cette chaîne.
On va enfin démarrer Cutter et inspecter ce binaire plus en profondeur.
On utilise l’analyse basique pour commencer.
La première chose que je fais lorsque j’analyse un binaire est de jeter un œil aux fonctions du programme. On voit ici 2 fonctions entry mais dans ce genre de crack-me, elles ne sont pas utilisées. C’est toujours un bon réflexe de les analyser.
On voit une fonction main, qui sera celle analysée ici.
La deuxième chose que j’ai l’habitude de faire, c’est d’analyser les strings du programme, pour voir un peu ce que contient le binaire, les strings d’échec et de réussite.
On voit premièrement les fonctions utilisées (ce qui peut être très intéressant dans le cadre d’un binaire avec strcmp par exemple, pour trouver le mot de passe comparé), et ensuite on retrouve les strings qu’on a vu lors de l’exécution du programme :
Password1 ! C’est surement le mot de passe du binaire.
Raté, ce serait trop simple..
Assez parlé, on regarde enfin ce que fait cette fameuse fonction main.
La première chose qui me saute aux yeux est la présence d’une boucle (la flèche verte suivant le jump à 0x1163), juste après la validation de la comparaison (cmp eax, edx). Ce que je comprends ici c’est qu’il y a plusieurs conditions de validité qui doivent être remplies avant d’aboutir à la réussite du programme.
On peut également confirmer la présence d‘une boucle avec l’incrémentation du registre rcx à 1 à chaque itération, ce qui correspond à un compteur.
En regardant le decompiler (ce que je fais quasi jamais car je ne comprends pas grand chose en c), on confirme une bonne fois pour toutes la présence d’une boucle avec “while”
L’analyse statique ne nous donne pas vraiment de moyen de deviner le mot de passe, on va lancer le debug en se concentrant sur ce bloc, avec une comparaison qui, si elle est réussie (les valeurs sont égales), rebouclera.
Voici le bloc qui nous intéresse.
Je lance le debug avec password1 comme argument, on va essayer de trouver pourquoi ce mot de passe ne fonctionne pas.
Nous sommes prêts ! Je me suis placé au début de la fonction main et on va passer rapidement les lignes qui ne nous intéressent pas.
Avant de commencer, à la deuxième instruction on voit que le programme vérifie que l’on a mis un argument, en comparant edi (le pointeur) avec 2. rdi étant à 2, le programme continue.
Le registre rax est chargé avec la valeur hexadécimale de p.
Première instruction intéressante, on voit que le programme met ‘p’ dans le registre eax, et remet à zéro le registre ecx, qui va nous servir de compteur. On voit également que l’offset du string ‘password1’ testé plus tôt est chargé dans rdi.
Autre bloc intéressant on voit que le registre edx est chargé avec la valeur de rsi + rcx (rsi étant le regsitre contenant notre mot de passe, et rcx le compteur). On a donc un test du premier index de notre chaîne, soit argc[0].
On voit aussi un test dl, dl qui jump à la fin du programme (la fin réussie !) si la valeur des bits de poids faible de rdx = 0. On sait donc que la chaîne à deviner termine par 0, ce qui arrête la boucle.
Stop! eax a été décrémenté et contient maintenant la valeur 6f, ce qui correspond à ‘o’, le programme compare ensuite cette valeur avec la première lettre du mot de passe que l’on a renseigné, contenue dans rdx.
Il fallait s’en douter.. ‘o’ n’étant pas égal à ‘p’ le programme a tout de suite break et nous affiche que le mot de passe est incorrect.
Relançons donc le programme avec ‘o’ comme première lettre, pour analyser ce qui se passe dans ce cas là.
C’est reparti
J’ai sauté toutes les précédentes étapes car on sait déjà ce qui se passe, on retourne un peu avant notre comparaison. En jetant un œil à nos registres, rdx (notre mot de passe) est à 6f (‘o’, la première lettre). Voyons la comparaison après la décrémentation de eax.
Enfin un nouveau bloc! rax et rdx étant égaux on rentre dans la boucle, rcx s’incrémente à 1 (on peut traduire par “1 comparaison a été faite”). Ensuite, eax va changer et passer à rdi + rcx (rdi étant l’offset du mot de passe “password1”, chargé au début du programme +1, le compteur, on comprend donc eax = chaine_a_trouver[1]).
Encore une fois, le test al, al qui termine le programme si le résultat est 0, confirme une dernière fois que la chaîne à trouver se termine par 0.
On reboucle ensuite sur ce bloc (qui a déjà servi auparavant) et on voit ici que c’est au tour d’edx d’être incrémenté par le compteur. On comprend donc que la prochaine valeur à tester sera argc[1], soit la lettre ‘a’ de notre argument ‘oassword1’.
eax se décremente encore et contient désormais la valeur 60 soit “`"
La je ne comprends plus trop, je croyais au départ qu’a chaque boucle, l’index la chaîne “password1” hardcodée dans le binaire changeait, mais de manière aléatoire. C’est à dire qu’on aurait pu se retrouver avec un mot de passe comme “srwaps1od”, mais la une quote est contenue dans eax, qui ne correspond à aucune lettre de la chaîne “password1”.
Je me souviens ensuite qu’on a la certitude que le chaîne termine par 0, on a donc un changement de valeurs des caractères de la chaîne à chaque itération.
Pas de surprise, ‘a’ n’étant pas égal à “`”, le programme break
Ilaurait été possible de résoudre ce crack-me en relançant le binaire une dizaine de fois avec chaque fois la lettre comparée de eax, en découvrant à chaque itération le prochain caractère et ainsi modifier notre argument pour que la condition soit validée.
Mais avec un peu de réflexion, je remarque que le code hex (soit 60) dans rax correspond au code ASCII 96 du caractère “`”, et, surprise, juste en bas, on se retrouve avec le caractère “a” précédemment chargé dans eax avant d’être décrémenté :
J’en conclus donc qu’a chaque boucle, le binaire compare l’index i (ici représenté par le registre rcx) de l’argument avec celui du mot de passe -1, mais pas -1 dans l’index, -1 dans la valeur héxadécimale, ce qui va faire changer le code ASCII du caractère de la chaîne.
Je me retrouve donc à décomposer la chaîne “password1” en la réécrivant avec la valeur précédente du tableau ASCII. Par exemple, p, qui a une valeur ASCII de 70, se retrouve à 6f, soit la valeur de o.
A noter que “o” était en effet le premier caractère du mot de passe qu’on avait trouvé.
En décomposant cette chaîne j’arrive à “o`rrvnqc0”, ce qui doit être le mot de passe du binaire.
Ici le “\” permet de passer une quote dans un argument
Great! Le mot de passe est donc o`rrvnqc0
Merci d’avoir lu ce premier write-up, qui, mine de rien m’aura demandé quelques heures à rédiger. On se retrouve très bientôt pour le crack-me n3 de cette série.
Crack-me : https://github.com/NoraCodes/crackmes
Cutter : cutter.re