Dans cet article nous allons voir comment cracker notre 1er binaire ELF32 avec le decompileur gdb.
NIVEAU : DÉBUTANT
PRÉ-REQUIS :
- Des bases en C
- Des bases en assembleur
- Une boite de Doliprane si pas ces bases
- Une distribution Linux
Pour démarrer créons notre binaire en C qui nous servira d’exemple. N’importe quel éditeur de texte fera l’affaire.
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
int main()
{
char saisie[50] = "";
printf("Entrez le mot de passe...: ");
scanf("%s",&saisie);
if (strcmp(saisie,"MonSuperMotDePasse")==0)
{
printf("Mot de passe correct \n");
}
else
{
printf("Mot de passe incorrect \n");
}
return 0;
}
Rien de très compliqué dans ces quelques lignes de code. Nous définissons une variable saisie qui sera comparée au mot de passe à trouver, à savoir MonSuperMotDePasse.
Enregistrons ce bout de code et compilons le pour le rendre exécutable :
root@kali:/home/crackme# gcc -m32 -o crackme crackme.c
On le teste :
root@kali:/home/crackme# ./crackme
Entrez le mot de passe…: toto
Mot de passe incorrect
root@kali:/home/crackme# ./crackme
Entrez le mot de passe…: MonSuperMotDePasse
Mot de passe correct
root@kali:/home/crackme#
Le programme fonctionne correctement. Il nous renvoie Mot de passe incorrect si le mot de passe saisit n’est pas MonSuperMotDePasse.
Allez il est temps de contourner cette sécurité !
1ère Méthode: Afficher les strings
La 1ère méthode est celle des fainénants ! Elle ne fonctionne pratiquement jamais mais pour débuter le cracking c’est un réflexe que vous devrez avoir.
On fait donc un strings sur notre binaire. Pour ceux qui ne saurait pas ce qu’est la commande strings, elle permet d’afficher les chaines imprimables d’un fichier. Pour plus de renseignement je vous laisse consulter le MAN de strings.
root@kali:/home/crackme# strings crackme
tdh
/lib/ld-linux.so.2
libc.so.6
IO_stdin_used
isoc99_scanf
puts
printf
__cxa_finalize
strcmp
__libc_start_main GLIBC_2.7
GLIBC_2.1.3
GLIBC_2.0
_ITM_deregisterTMCloneTable
__gmon_start
_ITM_registerTMCloneTable WVSQ Y[^]
UWVS
[^]
Entrez le mot de passe…:
MonSuperMotDePasse
Mot de passe correct
Mot de passe incorrect
;*2$"
GCC: (Debian 8.3.0-6) 8.3.0
crtstuff.c deregister_tm_clones
do_global_dtors_aux
completed.6886
__do_global_dtors_aux_fini_array_entry
frame_dummy __frame_dummy_init_array_entry
crackme.c
__FRAME_END init_array_end
_DYNAMIC
__init_array_start
__GNU_EH_FRAME_HDR
_GLOBAL_OFFSET_TABLE
__libc_csu_fini
strcmp@@GLIBC_2.0
_ITM_deregisterTMCloneTable
__x86.get_pc_thunk.bx
printf@@GLIBC_2.0
_edata
__x86.get_pc_thunk.dx
__cxa_finalize@@GLIBC_2.1.3
__data_start
puts@@GLIBC_2.0
__gmon_start
dso_handle
_IO_stdin_used
__libc_start_main@@GLIBC_2.0
__libc_csu_init
_fp_hw
__bss_start
main
__isoc99_scanf@@GLIBC_2.7
__TMC_END
_ITM_registerTMCloneTable
.symtab
.strtab
.shstrtab
.interp
.note.ABI-tag
.note.gnu.build-id
.gnu.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_r
.rel.dyn
.rel.plt
.init
.plt.got
.text
.fini
.rodata
.eh_frame_hdr
.eh_frame
.init_array
.fini_array
.dynamic
.got.plt
.data
.bss
.comment
Et là que voit-on parmi ces strings (je l’ai mis en gras), notre mot de passe !
Bon cette technique ne fonctionne que lorsque les variables sont non obfusquées mais ca vaut le coup d’essayer.
2ème méthode: debugger avec gdb.
gdb est un debugger pour linux permettant de debugger un binaire en cours d’execution via une analyse des registres de la mémoire. Il dispose d’une multitudes d’options qui sortent du cadre de cet article. Pour notre part nous n’en utiliserons que 4 que nous verrons plus tard
Lançons gdb :
root@kali:/home/crackme# gdb crackme
GNU gdb (Debian 8.2.1-2) 8.2.1
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later http://gnu.org/licenses/gpl.html
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
gdb-peda$
Comme vous pouvez le voir mon prompt m’affiche gdb-peda$. En effet j’ai rajouté à gdb une extension, très pratique et qui offre plus de visibilité lors de l’analyse des registres d’un binaire.
Pour installer peda (optionnel) :
git clone https://github.com/longld/peda.git ~/peda
echo "source ~/peda/peda.py" >> ~/.gdbinit
Il est temps de désassembler notre crackme : Pour ce faire nous allons utiliser la commande disas, en précisant la fonction que nous souhaitons désassembler : la fonction main (c’est la que vous sortez le Doliprane)
gdb-peda$ disas main
Dump of assembler code for function main:
0x000011c9 <+0>: lea ecx,[esp+0x4]
0x000011cd <+4>: and esp,0xfffffff0
0x000011d0 <+7>: push DWORD PTR [ecx-0x4]
0x000011d3 <+10>: push ebp
0x000011d4 <+11>: mov ebp,esp
0x000011d6 <+13>: push edi
0x000011d7 <+14>: push esi
0x000011d8 <+15>: push ebx
0x000011d9 <+16>: push ecx
0x000011da <+17>: sub esp,0x48
0x000011dd <+20>: call 0x10d0 <__x86.get_pc_thunk.bx>
0x000011e2 <+25>: add ebx,0x2e1e
0x000011e8 <+31>: mov DWORD PTR [ebp-0x4a],0x0
0x000011ef <+38>: lea eax,[ebp-0x46]
0x000011f2 <+41>: mov ecx,0x2e
0x000011f7 <+46>: mov esi,0x0
0x000011fc <+51>: mov DWORD PTR [eax],esi
0x000011fe <+53>: mov DWORD PTR [eax+ecx*1-0x4],esi
0x00001202 <+57>: lea edx,[eax+0x4]
0x00001205 <+60>: and edx,0xfffffffc
0x00001208 <+63>: sub eax,edx
0x0000120a <+65>: add ecx,eax
0x0000120c <+67>: and ecx,0xfffffffc
0x0000120f <+70>: shr ecx,0x2
0x00001212 <+73>: mov edi,edx
0x00001214 <+75>: mov eax,esi
0x00001216 <+77>: rep stos DWORD PTR es:[edi],eax
0x00001218 <+79>: sub esp,0xc
0x0000121b <+82>: lea eax,[ebx-0x1ff8]
0x00001221 <+88>: push eax
0x00001222 <+89>: call 0x1040 <printf@plt>
0x00001227 <+94>: add esp,0x10
0x0000122a <+97>: sub esp,0x8
0x0000122d <+100>: lea eax,[ebp-0x4a]
0x00001230 <+103>: push eax
0x00001231 <+104>: lea eax,[ebx-0x1fdc]
0x00001237 <+110>: push eax
0x00001238 <+111>: call 0x1070 <__isoc99_scanf@plt>
0x0000123d <+116>: add esp,0x10
0x00001240 <+119>: sub esp,0x8
0x00001243 <+122>: lea eax,[ebx-0x1fd9]
0x00001249 <+128>: push eax
0x0000124a <+129>: lea eax,[ebp-0x4a]
0x0000124d <+132>: push eax
0x0000124e <+133>: call 0x1030 <strcmp@plt>
0x00001253 <+138>: add esp,0x10
0x00001256 <+141>: test eax,eax
0x00001258 <+143>: jne 0x126e <main+165>
0x0000125a <+145>: sub esp,0xc
0x0000125d <+148>: lea eax,[ebx-0x1fc6]
0x00001263 <+154>: push eax
0x00001264 <+155>: call 0x1050 <puts@plt>
0x00001269 <+160>: add esp,0x10
0x0000126c <+163>: jmp 0x1280 <main+183>
0x0000126e <+165>: sub esp,0xc
0x00001271 <+168>: lea eax,[ebx-0x1fb0]
0x00001277 <+174>: push eax
0x00001278 <+175>: call 0x1050 <puts@plt>
0x0000127d <+180>: add esp,0x10
0x00001280 <+183>: mov eax,0x0
0x00001285 <+188>: lea esp,[ebp-0x10]
0x00001288 <+191>: pop ecx
0x00001289 <+192>: pop ebx
0x0000128a <+193>: pop esi
0x0000128b <+194>: pop edi
0x0000128c <+195>: pop ebp
0x0000128d <+196>: lea esp,[ecx-0x4]
0x00001290 <+199>: ret
End of assembler dump.
Pour ceux qui découvrent l’assembleur rassurez vous, seules quelques lignes nous intéressent. Le désassemblage de notre fonction main nous a retourné 4 colonnes :
- L’adresse mémoire de l’instruction. Exemple : 0x00001280
- Une adresse relative dans la fonction en cours. Exemple <+188> qui est égal à main+188 (nous nous en servirons plus tard)
- Un OPCODE. Exemple call
- Une opération à effectuer avec les registres. Exemple esp,[ebp-0x10]
Pour plus de précisions sur les OPCODES je vous invite à suivre ce lien
https://www.gladir.com/CODER/ASM8086/referenceeopcode.htm
Il est temps de regarder notre programme de plus près. Rappelez vous, que fait notre crackme ? il compare une chaine saisie par l’utilisateur avec une variable. Ca tombe bien car si on regarde nos lignes assembleur une instruction nous saute aux yeux !
0x0000124e <+133>: call 0x1030 <strcmp@plt>
strcmp@plt, = string compare. Ok c’est donc là que notre programme vérifie notre entrée pour la comparer. Continuons :
0x00001256 <+141>: test eax,eax
0x00001258 <+143>: jne 0x126e <main+165>
test eax,eax. Ici il teste si le registre EAX est égal à 0. Il vérifie donc que la variable saisie est égale à la variable attendue. Et si ce n’est pas le cas (mauvais mot de passe) il saute en main+165 avec un jne 0x126e <main+165> (Jump If Not Equal)
Le but du jeu dans cette 2ème méthode va donc être de sauter par dessus ce test pour que le programme ignore ce jne. Pour ce faire nous allons utiliser 3 commandes :
- run : Permet de lancer le programme
- b* : (raccourcis pour break). Permet d’inserer un breakpoint à un endroit du programme
- jump : permet de sauter à l’instruction voulue.
Tout d’abord créons un breakpoint sur notre test :
gdb-peda$ b* main+133
Breakpoint 1 at 0x124e
OK. Lançons maintenant notre programme :
gdb-peda$ run
Starting program: /home/crackme/crackme
Entrez le mot de passe...: toto
[----------------------------------registers-----------------------------------]
EAX: 0xffffd23e ("toto")
EBX: 0x56559000 --> 0x3efc
ECX: 0x1
EDX: 0xf7f8189c --> 0x0
ESI: 0x0
EDI: 0xffffd270 --> 0x1
EBP: 0xffffd288 --> 0x0
ESP: 0xffffd220 --> 0xffffd23e ("toto")
EIP: 0x5655624e (<main+133>: call 0x56556030 <strcmp@plt>)
EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x56556249 <main+128>: push eax
0x5655624a <main+129>: lea eax,[ebp-0x4a]
0x5655624d <main+132>: push eax
=> 0x5655624e <main+133>: call 0x56556030 <strcmp@plt>
0x56556253 <main+138>: add esp,0x10
0x56556256 <main+141>: test eax,eax
0x56556258 <main+143>: jne 0x5655626e <main+165>
0x5655625a <main+145>: sub esp,0xc
Guessed arguments:
arg[0]: 0xffffd23e ("toto")
arg[1]: 0x56557027 ("MonSuperMotDePasse")
[------------------------------------stack-------------------------------------]
0000| 0xffffd220 --> 0xffffd23e ("toto")
0004| 0xffffd224 ("'pUV")
0008| 0xffffd228 --> 0xf7ffd000 --> 0x28f2c
0012| 0xffffd22c --> 0x565561e2 (<main+25>: add ebx,0x2e1e)
0016| 0xffffd230 --> 0x0
0020| 0xffffd234 --> 0xffffd334 --> 0xffffd4c4 ("/home/crackme/crackme")
0024| 0xffffd238 --> 0xf7f80000 --> 0x1d9d6c
0028| 0xffffd23c --> 0x6f740000 ('')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x5655624e in main ()
gdb-peda$
Nous voyons ici l’utilité de peda qui nous affiche nos différents registres ! Comme prévu le programme à breaké sur l’instruction strcmp et nous rend la main.
[-------------------------------------code-------------------------------------]
0x56556249 <main+128>: push eax
0x5655624a <main+129>: lea eax,[ebp-0x4a]
0x5655624d <main+132>: push eax
=> 0x5655624e <main+133>: call 0x56556030 <strcmp@plt>
0x56556253 <main+138>: add esp,0x10
0x56556256 <main+141>: test eax,eax
0x56556258 <main+143>: jne 0x5655626e <main+165>
0x5655625a <main+145>: sub esp,0xc
La fleche => nous indique que nous sommes à l’instruction main+133. Nous voyons qu’ensuite le programme ajoute 0x10 en esp puis teste eax. Et saute en main+165 si not equal. Nous allons donc le faire sauter juste après le jne pour qu’il ne puisse lire cette condition
gdb-peda$ jump* main+145
Continuing at 0x5655625a.
Mot de passe correct
[Inferior 1 (process 11278) exited normally]
Et bingo ! malgré un mauvais de passe “toto”, notre programme l’a accepté. Bon c’est déjà bien mais ce qui nous intéresse nous c’est de connaitre le vrai mot de passe ! Pas de problème nous allons fouiller dans la stack ! 3ème méthode. relancons notre programe :
gdb-peda$ run
Starting program: /home/crackme/crackme
Entrez le mot de passe...: toto
[----------------------------------registers-----------------------------------]
EAX: 0xffffd23e ("toto")
EBX: 0x56559000 --> 0x3efc
ECX: 0x1
EDX: 0xf7f8189c --> 0x0
ESI: 0x0
EDI: 0xffffd270 --> 0x1
EBP: 0xffffd288 --> 0x0
ESP: 0xffffd220 --> 0xffffd23e ("toto")
EIP: 0x5655624e (<main+133>: call 0x56556030 <strcmp@plt>)
EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x56556249 <main+128>: push eax
0x5655624a <main+129>: lea eax,[ebp-0x4a]
0x5655624d <main+132>: push eax
=> 0x5655624e <main+133>: call 0x56556030 <strcmp@plt>
0x56556253 <main+138>: add esp,0x10
0x56556256 <main+141>: test eax,eax
0x56556258 <main+143>: jne 0x5655626e <main+165>
0x5655625a <main+145>: sub esp,0xc
Guessed arguments:
arg[0]: 0xffffd23e ("toto")
arg[1]: 0x56557027 ("MonSuperMotDePasse")
[------------------------------------stack-------------------------------------]
0000| 0xffffd220 --> 0xffffd23e ("toto")
0004| 0xffffd224 ("'pUV")
0008| 0xffffd228 --> 0xf7ffd000 --> 0x28f2c
0012| 0xffffd22c --> 0x565561e2 (<main+25>: add ebx,0x2e1e)
0016| 0xffffd230 --> 0x0
0020| 0xffffd234 --> 0xffffd334 --> 0xffffd4c4 ("/home/crackme/crackme")
0024| 0xffffd238 --> 0xf7f80000 --> 0x1d9d6c
0028| 0xffffd23c --> 0x6f740000 ('')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x5655624e in main ()
Il break de nouveau sur le strcmp. Nous savons qu’a ce stade il va effectuer une comparaison. Il a donc empilé sur la stack les variables nécessaires à cette comparaison. Regardons un peu ce qui se trouve en esp (Extended Stack Pointer) :
gdb-peda$ x/2x $esp
0xffffd220: 0xffffd23e 0x56557027
Ok donc il nous a empilé des trucs en 0xffffd23e et 0x56557027. Allons voir ce qu’il y a mis :
gdb-peda$ x/2x $esp
0xffffd220: 0xffffd23e 0x56557027
gdb-peda$ x/s 0xffffd23e
0xffffd23e: "toto"
gdb-peda$ x/s 0x56557027
0x56557027: "MonSuperMotDePasse"
Bingo ! Nous retrouvons ce que nous avons saisi : toto et avec quoi il est comparé : MonSuperMotDePasse.
Comment patcher le binaire pour accepter n’importe quel mot de passe
Allez en bonus patchons notre binaire pour qu’il accepte n’importe quel mot de passe. Pour ce faire nous allons modifier avec un éditeur hexadecimal l’opcode jne par un NOP. En l’équivalent hexa de l’opcode jne s’ecrit 7514. et NOP qui signifie ne rien faire s’écrit 9090. Il nous suffit donc de remplacer dans un editeur hexa la chaine 7514 par 9090. J’utilise GHex.
Comment sais-ton que jne s’écrit 75 14 et NOP 90 90 ? Via objump entre autre :
root@kali:/home/crackme# objdump -d crackme | grep jne
1189: 75 27 jne 11b2 <__do_global_dtors_aux+0x42>
1258: 75 14 jne 126e <main+0xa5>
12eb: 75 e3 jne 12d0 <__libc_csu_init+0x30>
root@kali:/home/crackme# objdump -d crackme | grep nop
1117: 90 nop
129f: 90 nop
Le jne qui nous intéresse est celui du main donc sa transcription hexa est bien 75 14

On recherche la chaîne 75 14 et on la remplace place 90 90


On sauvegarde, on autorise l’exécution et on teste :
root@kali:/home/crackme# chmod +777 crackme2
root@kali:/home/crackme# ./crackme2
Entrez le mot de passe...: motdepassequidevraitpas marcher
Mot de passe correct
root@kali:/home/crackme#
Et voila. Maintenant notre programme acceptera n’importe quel mot de passe comme correct.
6 thoughts on “Débuter en CrackMe: contourner l’authentification en 3 méthodes”
Super intéressant merci pour cet article.
Je reste seulement sur ma faim par rapport aux moyen de protéger son binaire de ces attaques justement.
Quelles auraient été les méthodes pour protéger notre application C de cette attaque ?
Chiffrer le mdp avant comparaison ?
Cela n’empêche pas le hack de l’instruction jne
Bonjour, lu cet article par pure curiosité, mes quelques cours d’assembleurs remontent à plus de 30 ans (sur des 8086), mais j’ai trouvé ça super intéressant, et même génial ! Ça me donne presque envie de m’y remettre ! Merci beaucoup.
Merci c’est sympa.
c’est ce genre de commentaires qui donne envie de faire plus d’articles 😉
Donc, en plus des couleurs, peda est réellement super pratique!update-alternatives –config x-session-manager
Dans la partie desassemblage on voit les arguments de strcmp compare, donc “toto” et “MonSuperMotdePasse”
Guessed arguments:
arg[0]: 0xffffd23e (“toto”)
arg[1]: 0x56557027 (“MonSuperMotDePasse”)
C’est fiable aussi cette methode ?
Hello,
Oui c’est tout aussi fiable.
En fait les arg[0] et arg[1] sont générés par PEDA qui regardent de lui meme dans les registres.
Sur la version de base de GDB tu n’auras pas cette information.