Avant propos
A consulter :
Pour ce tutoriel, un peu de pratique avant tout. On ne va pas découvrir de nouveaux aspects du reversing mais utiliser ce que l'on connaît déjà (plus ou moins) et on va s'intéresser a un aspect particulier : l'analyse statique de binaire.
Avant toute chose, je vous conseille de mettre en place une VM quand vous faites du reversing, il est pratique par le biais de snapshots de maintenir un système saint pour faire tous les tests que l'on souhaite.
Analyse
Le contexte :
Un binaire nous est donné, celui-ci est à reverser
Dans un premier temps, on va procéder à une identification du fichier.
C'est je pense la première chose à faire. Vérifier de quel type de fichier il s'agit.
Ici on s'attaque un à binaire classique. On remarque que celui-ci n'est pas "stripped" donc il dispose encore de ses symboles. Ce qui facilite le debug s'il doit y en avoir.
Passons à l'analyse. Pour cela on va utiliser gdb, même si objdump est un bon désassembleur, gdb ajoute des éléments en plus qui facilitent légèrement le travail. On utilisera gdb non pas pour faire du debug mais juste pour désassembler et retrouver certains éléments plus simples à recueillir lorsque le binaire est en mémoire.
Comme vous le constatez, gdb nous ajoute des décalages <+N> par rapport au début de la fonction ce que ne fait pas objdump.
Première chose, un bref coup d'œil sur les appels de fonction à partir de la fonction « main ». Étant donné que le binaire contient toujours les symboles, on peut commencer le reversing à partir de la fonction main.
On y trouve des appels aux fonctions de la libc fprintf, puts, strlen ... celles-ci sont facilement repérables car on retrouve le symbole @plt en fin de leur nom. On y voit aussi un appel à la fonction « check_password » qui est locale cette fois.
A priori main et « check_password » sont les 2 fonctions dont nous devrions nous intéresser.
C'est parti, étape 1 : commenter le code pour éviter d'avoir à réinterpréter dans notre petit cerveau chaque ligne à chaque lecture du code.
Pour la fonction main :
Bien quelques réglages s'imposent. Pour y voir plus clair, on va reconstituer un système labélisé. C'est à dire définir des labels pour les sauts et appels relatifs (on en a pas ici).
Cela va nous aider à y voir plus clair en séparant la fonction par blocs de code. On précisera ainsi où vont les sauts plutôt qu'une adresse.
Un peu mieux ?
Ensuite, on va chercher les chaines de caractères pour les ajouter en commentaire lors de leur utilisation.
Comme le binaire est en mémoire (car lance avec gdb) on va pouvoir afficher ces chaines directement depuis gdb.
On peut les ajouter en commentaire.
Ce n’est pas un peu plus compréhensible à la lecture ?
Ainsi on peut en déduire que la fonction « check_password » doit retourner une valeur différente de zéro pour nous afficher le message "Well done !".
Faisons de même pour « check_password » :
Une dernière étape qui parfois est bien utile, la retranscription dans un langage de plus haut niveau (style C). Ainsi on pourra lire plus facilement le code et comprendre ce qu'il fait.
Pour la fonction « main », on aurait donc quelque chose comme:
Et pour « check_password » :
On a pu déduire var_2 grâce a gdb. En effet, on a vu que cette adresse est incrémentée et qu'elle se finie par la valeur 0. Avec gdb on affiche donc son contenu :
Ici le 0 est situé au 12e bit on récupère donc :
Qui une fois passe en big-endian pour pouvoir l'utiliser dans un code c sous forme de char* :
On a tout ce qu'il faut maintenant, il ne reste plus qu'à lire. Ici on voit que notre paramètre subit un xor et est comparé à une chaine de bits.
Voir : http://en.wikipedia.org/wiki/Exclusive_or
Lorsque l'on fait un xor sur une valeur, on récupère une nouvelle valeur. Si on xor cette nouvelle valeur, on retrouve alors l'originale.
Ainsi il nous suffit de faire un xor sur cette chaine pour récupérer le contenu originale et donc trouver ce que le binaire attend pour comparer. Ici la valeur du xor est de 42, on utilisera donc cette même valeur.
Let's go.
Bon ce n’était pas si complique si ?
Essayons quand même pour être certain :
Bon comme vous l'avez constaté c'est assez long et fastidieux comme travail et là on à même pas 20 lignes de code en C alors imaginez un programme entier de plusieurs MB. Ce serait juste impossible. Voilà pourquoi ce type d'analyse est à privilégier pour des portions de code qui nécessitent réellement un besoin de compréhension absolu.
Souvent on utilisera cette méthode pour reverser un algorithme spécifique pour ensuite pouvoir l'implémenter dans un code tel que des algorithmes de chiffrements ...
Next time : analyse de binaire mais avec d'autres outils cette fois.
A consulter :
- Reversing tutorials - level 1 - Quelques outils avant de démarrer
- Reversing tutorials - level 2 - Le format ELF
- Reversing tutorials - level 2bis - L'assembleur
Pour ce tutoriel, un peu de pratique avant tout. On ne va pas découvrir de nouveaux aspects du reversing mais utiliser ce que l'on connaît déjà (plus ou moins) et on va s'intéresser a un aspect particulier : l'analyse statique de binaire.
Avant toute chose, je vous conseille de mettre en place une VM quand vous faites du reversing, il est pratique par le biais de snapshots de maintenir un système saint pour faire tous les tests que l'on souhaite.
Analyse
Le contexte :
Un binaire nous est donné, celui-ci est à reverser
Dans un premier temps, on va procéder à une identification du fichier.
Code:
$ ll total 8.0K -rwxr-xr-x 1 death death 7.4K Aug 7 19:36 reverseme* $ file reverseme reverseme: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=cd634133eb48748f8ce861ad28f3be5591e31ba7, not stripped
Ici on s'attaque un à binaire classique. On remarque que celui-ci n'est pas "stripped" donc il dispose encore de ses symboles. Ce qui facilite le debug s'il doit y en avoir.
Passons à l'analyse. Pour cela on va utiliser gdb, même si objdump est un bon désassembleur, gdb ajoute des éléments en plus qui facilitent légèrement le travail. On utilisera gdb non pas pour faire du debug mais juste pour désassembler et retrouver certains éléments plus simples à recueillir lorsque le binaire est en mémoire.
Code:
$ gdb reverseme Reading symbols from /home/death/tuto/binary/reverseme...(no debugging symbols found)...done. (gdb) disass main Dump of assembler code for function main: 0x00000000004006c8 <+0>: push rbp 0x00000000004006c9 <+1>: mov rbp,rsp 0x00000000004006cc <+4>: sub rsp,0x10 0x00000000004006d0 <+8>: mov DWORD PTR [rbp-0x4],edi 0x00000000004006d3 <+11>: mov QWORD PTR [rbp-0x10],rsi 0x00000000004006d7 <+15>: cmp DWORD PTR [rbp-0x4],0x2 0x00000000004006db <+19>: jne 0x400720 <main+88> 0x00000000004006dd <+21>: mov rax,QWORD PTR [rbp-0x10] 0x00000000004006e1 <+25>: add rax,0x8 0x00000000004006e5 <+29>: mov rax,QWORD PTR [rax] 0x00000000004006e8 <+32>: mov rdi,rax 0x00000000004006eb <+35>: call 0x40061c <check_password> 0x00000000004006f0 <+40>: test eax,eax 0x00000000004006f2 <+42>: je 0x400700 <main+56> 0x00000000004006f4 <+44>: mov edi,0x400800 0x00000000004006f9 <+49>: call 0x4004d0 <[email protected]> 0x00000000004006fe <+54>: jmp 0x400740 <main+120> 0x0000000000400700 <+56>: mov rax,QWORD PTR [rip+0x200481] # 0x600b88 <[email protected]@GLIBC_2.2.5> 0x0000000000400707 <+63>: mov rcx,rax 0x000000000040070a <+66>: mov edx,0xc 0x000000000040070f <+71>: mov esi,0x1 0x0000000000400714 <+76>: mov edi,0x40080c 0x0000000000400719 <+81>: call 0x400520 <[email protected]> 0x000000000040071e <+86>: jmp 0x400740 <main+120> 0x0000000000400720 <+88>: mov rax,QWORD PTR [rbp-0x10] 0x0000000000400724 <+92>: mov rdx,QWORD PTR [rax] 0x0000000000400727 <+95>: mov rax,QWORD PTR [rip+0x20045a] # 0x600b88 <[email protected]@GLIBC_2.2.5> 0x000000000040072e <+102>: mov esi,0x400819 0x0000000000400733 <+107>: mov rdi,rax 0x0000000000400736 <+110>: mov eax,0x0 0x000000000040073b <+115>: call 0x400500 <[email protected]> 0x0000000000400740 <+120>: mov eax,0x0 0x0000000000400745 <+125>: leave 0x0000000000400746 <+126>: ret End of assembler dump. (gdb)
Première chose, un bref coup d'œil sur les appels de fonction à partir de la fonction « main ». Étant donné que le binaire contient toujours les symboles, on peut commencer le reversing à partir de la fonction main.
On y trouve des appels aux fonctions de la libc fprintf, puts, strlen ... celles-ci sont facilement repérables car on retrouve le symbole @plt en fin de leur nom. On y voit aussi un appel à la fonction « check_password » qui est locale cette fois.
A priori main et « check_password » sont les 2 fonctions dont nous devrions nous intéresser.
C'est parti, étape 1 : commenter le code pour éviter d'avoir à réinterpréter dans notre petit cerveau chaque ligne à chaque lecture du code.
Pour la fonction main :
Code:
0x00000000004006c8 <+0>: push rbp # prologue 0x00000000004006c9 <+1>: mov rbp,rsp # ... 0x00000000004006cc <+4>: sub rsp,0x10 # réservation d'un espace sur la pile 0x00000000004006d0 <+8>: mov DWORD PTR [rbp-0x4],edi # on place argc sur la pile 0x00000000004006d3 <+11>: mov QWORD PTR [rbp-0x10],rsi # on place argv sur la pile 0x00000000004006d7 <+15>: cmp DWORD PTR [rbp-0x4],0x2 # on compare argv avec la valeur 2 0x00000000004006db <+19>: jne 0x400720 <main+88> # jump si argc diffèrent de 2 0x00000000004006dd <+21>: mov rax,QWORD PTR [rbp-0x10] # on place argv dans rax 0x00000000004006e1 <+25>: add rax,0x8 # on ajoute 8 à rax (décalage d'une adresse donc d'une case mémoire comme argv est un char**) 0x00000000004006e5 <+29>: mov rax,QWORD PTR [rax] # déréférencement de rax pour récupérer l'argument passe au binaire 0x00000000004006e8 <+32>: mov rdi,rax # on place l'argument dans rdi 0x00000000004006eb <+35>: call 0x40061c <check_password> # on appelle la fonction check_password 0x00000000004006f0 <+40>: test eax,eax # on test la valeur de retour de la fonction 0x00000000004006f2 <+42>: je 0x400700 <main+56> # jump si elle est égale a zéro 0x00000000004006f4 <+44>: mov edi,0x400800 # on place 0x400800 dans edi 0x00000000004006f9 <+49>: call 0x4004d0 <[email protected]> # appel de puts avec 0x400800 en paramètre 0x00000000004006fe <+54>: jmp 0x400740 <main+120> # on jump 0x0000000000400700 <+56>: mov rax,QWORD PTR [rip+0x200481] # on place stderr (fd d'erreur défini par la libc) dans rax 0x0000000000400707 <+63>: mov rcx,rax # on place rax dans rcx 0x000000000040070a <+66>: mov edx,0xc # on place la valeur 12 dans edx 0x000000000040070f <+71>: mov esi,0x1 # on place la valeur 1 dans esi 0x0000000000400714 <+76>: mov edi,0x40080c # on place 0x40080c dans edi 0x0000000000400719 <+81>: call 0x400520 <[email protected]> # appel de fwrite 0x000000000040071e <+86>: jmp 0x400740 <main+120> # on jump 0x0000000000400720 <+88>: mov rax,QWORD PTR [rbp-0x10] # on place argv dans rax 0x0000000000400724 <+92>: mov rdx,QWORD PTR [rax] # on déréférence puis place dans rdx, on obtient donc le chemin de lancement du binaire 0x0000000000400727 <+95>: mov rax,QWORD PTR [rip+0x20045a] # on place stderr dans rax 0x000000000040072e <+102>: mov esi,0x400819 # on place 0x400819 dans esi 0x0000000000400733 <+107>: mov rdi,rax # on place rax dans rdi 0x0000000000400736 <+110>: mov eax,0x0 # on mets la valeur 0 dans eax 0x000000000040073b <+115>: call 0x400500 <[email protected]> # appel de fprintf 0x0000000000400740 <+120>: mov eax,0x0 # on mets 0 dans eax (valeur de retour du programme) 0x0000000000400745 <+125>: leave # epilogue 0x0000000000400746 <+126>: ret # ...
Cela va nous aider à y voir plus clair en séparant la fonction par blocs de code. On précisera ainsi où vont les sauts plutôt qu'une adresse.
Code:
_main: 0x00000000004006c8 <+0>: push rbp # prologue 0x00000000004006c9 <+1>: mov rbp,rsp # ... _label_1: 0x00000000004006cc <+4>: sub rsp,0x10 # réservation d'un espace sur la pile 0x00000000004006d0 <+8>: mov DWORD PTR [rbp-0x4],edi # on place argc sur la pile 0x00000000004006d3 <+11>: mov QWORD PTR [rbp-0x10],rsi # on place argv sur la pile 0x00000000004006d7 <+15>: cmp DWORD PTR [rbp-0x4],0x2 # on compare argv avec la valeur 2 0x00000000004006db <+19>: jne 0x400720 <main+88> # jump si argc différent de 2 -> _label_5 _label_2: 0x00000000004006dd <+21>: mov rax,QWORD PTR [rbp-0x10] # on place argv dans rax 0x00000000004006e1 <+25>: add rax,0x8 # on ajoute 8 a rax (décalage d'une adresse donc d'une case mémoire comme argv est un char**) 0x00000000004006e5 <+29>: mov rax,QWORD PTR [rax] # déréférencement de rax pour récupérer l'argument passe au binaire 0x00000000004006e8 <+32>: mov rdi,rax # on place l'argument dans rdi 0x00000000004006eb <+35>: call 0x40061c <check_password> # on appelle la fonction check_password 0x00000000004006f0 <+40>: test eax,eax # on test la valeur de retour de la fonction 0x00000000004006f2 <+42>: je 0x400700 <main+56> # jump si elle est égale a zéro -> _label_4 _label_3: 0x00000000004006f4 <+44>: mov edi,0x400800 # on place 0x400800 dans edi 0x00000000004006f9 <+49>: call 0x4004d0 <[email protected]> # appel de puts avec 0x400800 en paramètre 0x00000000004006fe <+54>: jmp 0x400740 <main+120> # on jump -> _label_6 _label_4: 0x0000000000400700 <+56>: mov rax,QWORD PTR [rip+0x200481] # on place stderr (fd d'erreur défini par la libc) dans rax 0x0000000000400707 <+63>: mov rcx,rax # on place rax dans rcx 0x000000000040070a <+66>: mov edx,0xc # on place la valeur 12 dans edx 0x000000000040070f <+71>: mov esi,0x1 # on place la valeur 1 dans esi 0x0000000000400714 <+76>: mov edi,0x40080c # on place 0x40080c dans edi 0x0000000000400719 <+81>: call 0x400520 <[email protected]> # appel de fwrite 0x000000000040071e <+86>: jmp 0x400740 <main+120> # on jump -> _label_6 _label_5: 0x0000000000400720 <+88>: mov rax,QWORD PTR [rbp-0x10] # on place argv dans rax 0x0000000000400724 <+92>: mov rdx,QWORD PTR [rax] # on déréférence puis place dans rdx, on obtient donc le chemin de lancement du binaire 0x0000000000400727 <+95>: mov rax,QWORD PTR [rip+0x20045a] # on place stderr dans rax 0x000000000040072e <+102>: mov esi,0x400819 # on place 0x400819 dans esi 0x0000000000400733 <+107>: mov rdi,rax # on place rax dans rdi 0x0000000000400736 <+110>: mov eax,0x0 # on mets la valeur 0 dans eax 0x000000000040073b <+115>: call 0x400500 <[email protected]> # appel de fprintf _label_6: 0x0000000000400740 <+120>: mov eax,0x0 # on mets 0 dans eax (valeur de retour du programme) 0x0000000000400745 <+125>: leave # epilogue 0x0000000000400746 <+126>: ret # ...
Ensuite, on va chercher les chaines de caractères pour les ajouter en commentaire lors de leur utilisation.
Comme le binaire est en mémoire (car lance avec gdb) on va pouvoir afficher ces chaines directement depuis gdb.
Code:
(gdb) x/s 0x400800 0x400800: "Well done !" (gdb) x/s 0x40080c 0x40080c: "Try again !\n" (gdb) x/s 0x400819 0x400819: "USAGE %s <password>\n" (gdb)
Code:
_main: 0x00000000004006c8 <+0>: push rbp # prologue 0x00000000004006c9 <+1>: mov rbp,rsp # ... _label_1: 0x00000000004006cc <+4>: sub rsp,0x10 # réservation d'un espace sur la pile 0x00000000004006d0 <+8>: mov DWORD PTR [rbp-0x4],edi # on place argc sur la pile 0x00000000004006d3 <+11>: mov QWORD PTR [rbp-0x10],rsi # on place argv sur la pile 0x00000000004006d7 <+15>: cmp DWORD PTR [rbp-0x4],0x2 # on compare argv avec la valeur 2 0x00000000004006db <+19>: jne 0x400720 <main+88> # jump si argc différent de 2 -> _label_5 _label_2: 0x00000000004006dd <+21>: mov rax,QWORD PTR [rbp-0x10] # on place argv dans rax 0x00000000004006e1 <+25>: add rax,0x8 # on ajoute 8 a rax (décalage d'une adresse donc d'une case mémoire comme argv est un char**) 0x00000000004006e5 <+29>: mov rax,QWORD PTR [rax] # déréférencement de rax pour récupérer l'argument passe au binaire 0x00000000004006e8 <+32>: mov rdi,rax # on place l'argument dans rdi 0x00000000004006eb <+35>: call 0x40061c <check_password> # on appelle la fonction check_password : check_password(argv[1]) 0x00000000004006f0 <+40>: test eax,eax # on test la valeur de retour de la fonction 0x00000000004006f2 <+42>: je 0x400700 <main+56> # jump si elle est égale a zéro -> _label_4 _label_3: 0x00000000004006f4 <+44>: mov edi,0x400800 # on place 0x400800 dans edi 0x00000000004006f9 <+49>: call 0x4004d0 <[email protected]> # appel de puts avec 0x400800 en parametre : puts("Well done !\n") 0x00000000004006fe <+54>: jmp 0x400740 <main+120> # on jump -> _label_6 _label_4: 0x0000000000400700 <+56>: mov rax,QWORD PTR [rip+0x200481] # on place stderr (fd d'erreur défini par la libc) dans rax 0x0000000000400707 <+63>: mov rcx,rax # on place rax dans rcx 0x000000000040070a <+66>: mov edx,0xc # on place la valeur 12 dans edx 0x000000000040070f <+71>: mov esi,0x1 # on place la valeur 1 dans esi 0x0000000000400714 <+76>: mov edi,0x40080c # on place 0x40080c dans edi 0x0000000000400719 <+81>: call 0x400520 <[email protected]> # appel de fwrite : fwrite("Try again !\n", 1, 12, stderr) 0x000000000040071e <+86>: jmp 0x400740 <main+120> # on jump -> _label_6 _label_5: 0x0000000000400720 <+88>: mov rax,QWORD PTR [rbp-0x10] # on place argv dans rax 0x0000000000400724 <+92>: mov rdx,QWORD PTR [rax] # on déréférence puis place dans rdx, on obtient donc le chemin de lancement du binaire 0x0000000000400727 <+95>: mov rax,QWORD PTR [rip+0x20045a] # on place stderr dans rax 0x000000000040072e <+102>: mov esi,0x400819 # on place 0x400819 dans esi 0x0000000000400733 <+107>: mov rdi,rax # on place rax dans rdi 0x0000000000400736 <+110>: mov eax,0x0 # on mets la valeur 0 dans eax 0x000000000040073b <+115>: call 0x400500 <[email protected]> # appel de fprintf : fprintf(stderr, "USAGE %s <password>\n", argv[0]) _label_6: 0x0000000000400740 <+120>: mov eax,0x0 # on mets 0 dans eax (valeur de retour du programme) 0x0000000000400745 <+125>: leave # epilogue 0x0000000000400746 <+126>: ret # ...
Ainsi on peut en déduire que la fonction « check_password » doit retourner une valeur différente de zéro pour nous afficher le message "Well done !".
Faisons de même pour « check_password » :
Code:
_check_password: 0x000000000040061c <+0>: push rbp # prologue 0x000000000040061d <+1>: mov rbp,rsp # ... 0x0000000000400620 <+4>: push rbx # ... _label_1: 0x0000000000400621 <+5>: sub rsp,0x28 # réservation de mémoire sur la pile 0x0000000000400625 <+9>: mov QWORD PTR [rbp-0x28],rdi # on place le paramètre sur la pile -> var_1 0x0000000000400629 <+13>: mov QWORD PTR [rbp-0x20],0x4007f4 # on place l'adresse 0x4007f4 sur la pile -> var_2 0x0000000000400631 <+21>: mov DWORD PTR [rbp-0x14],0x0 # on place 0 sur la pile -> var_3 0x0000000000400638 <+28>: jmp 0x400682 <check_password+102> # jump jump ! _label_2: 0x000000000040063a <+30>: mov eax,DWORD PTR [rbp-0x14] # on place var_3 dans eax 0x000000000040063d <+33>: movsxd rdx,eax # on place eax dans rdx soit var_3 0x0000000000400640 <+36>: mov rax,QWORD PTR [rbp-0x28] # on place var_1 dans rax 0x0000000000400644 <+40>: add rax,rdx # ajout var_3 a var_1 -> incrémentation du pointeur de var_3 0x0000000000400647 <+43>: movzx eax,BYTE PTR [rax] # déréférencement de rax 0x000000000040064a <+46>: mov edx,eax # on place eax dans edx 0x000000000040064c <+48>: xor edx,0x2a # xor de edx avec 42 0x000000000040064f <+51>: mov eax,DWORD PTR [rbp-0x14] # on place var_3 dans eax 0x0000000000400652 <+54>: movsxd rcx,eax # on place eax dans rcx 0x0000000000400655 <+57>: mov rax,QWORD PTR [rbp-0x20] # on place var_2 dans rax 0x0000000000400659 <+61>: add rax,rcx # ajout de var_3 a var_2 0x000000000040065c <+64>: movzx eax,BYTE PTR [rax] # déréférencement de rax 0x000000000040065f <+67>: cmp dl,al # comparaison de dl et al ( (*(var_1 + var_3) ^ 42) et *(var_2 + var_3)) 0x0000000000400661 <+69>: jne 0x400677 <check_password+91> # jump si different -> _label_4 _label_3: 0x0000000000400663 <+71>: mov eax,DWORD PTR [rbp-0x14] # on place var_3 dans eax 0x0000000000400666 <+74>: movsxd rdx,eax # on place eax dans rdx 0x0000000000400669 <+77>: mov rax,QWORD PTR [rbp-0x20] # on place var_2 dans rax 0x000000000040066d <+81>: add rax,rdx # on ajoute var_3 a var_2 0x0000000000400670 <+84>: movzx eax,BYTE PTR [rax] # déréférencement de rax 0x0000000000400673 <+87>: test al,al # test la valeur 0x0000000000400675 <+89>: jne 0x40067e <check_password+98> # jump si différent de 0 -> _label_5 _label_4: 0x0000000000400677 <+91>: mov eax,0x0 # on place 0 dans eax 0x000000000040067c <+96>: jmp 0x4006c1 <check_password+165> # jump -> _label_7 _label_5: 0x000000000040067e <+98>: add DWORD PTR [rbp-0x14],0x1 # incrémentation de var_3 0x0000000000400682 <+102>: mov eax,DWORD PTR [rbp-0x14] # on place var_3 dans eax 0x0000000000400685 <+105>: movsxd rbx,eax # on place eax dans rbx 0x0000000000400688 <+108>: mov rax,QWORD PTR [rbp-0x28] # on place var_1 dans rax 0x000000000040068c <+112>: mov rdi,rax # on place rax dans rdi 0x000000000040068f <+115>: call 0x4004e0 <[email protected]> # appel de strlen : strlen(var_1) 0x0000000000400694 <+120>: cmp rbx,rax # on compare la taille retournée a var_3 0x0000000000400697 <+123>: jb 0x40063a <check_password+30> # jump si inferieur -> _label_2 _label_6: 0x0000000000400699 <+125>: mov eax,DWORD PTR [rbp-0x14] # on place var_3 dans eax 0x000000000040069c <+128>: movsxd rdx,eax # on place eax dans rdx 0x000000000040069f <+131>: mov rax,QWORD PTR [rbp-0x28] # on place var_1 dans rax 0x00000000004006a3 <+135>: add rax,rdx # ajout de var_3 a var_1 0x00000000004006a6 <+138>: movzx edx,BYTE PTR [rax] # déréférencement de rax 0x00000000004006a9 <+141>: mov eax,DWORD PTR [rbp-0x14] # on place var_3 dans eax 0x00000000004006ac <+144>: movsxd rcx,eax # on place eax dans rcx 0x00000000004006af <+147>: mov rax,QWORD PTR [rbp-0x20] # on place var_2 dans rax 0x00000000004006b3 <+151>: add rax,rcx # ajout de var_3 a var_2 0x00000000004006b6 <+154>: movzx eax,BYTE PTR [rax] # déréférencement de rax 0x00000000004006b9 <+157>: cmp dl, al # comparaison de dl et al (*(var_1 + var_3) et *(var_2 + var_3)) 0x00000000004006bb <+159>: sete al # si égale, alors mets a zéro 0x00000000004006be <+162>: movzx eax,al # place al dans eax _label_7: 0x00000000004006c1 <+165>: add rsp,0x28 # libère l'espace de la pile _label_8: 0x00000000004006c5 <+169>: pop rbx # epilogue 0x00000000004006c6 <+170>: pop rbp # ... 0x00000000004006c7 <+171>: ret # ...
Pour la fonction « main », on aurait donc quelque chose comme:
Code:
int main(int argc, char *argv[]) { if (argc == 2) { if (check_password(argv[1])) puts("Well done !\n"); else fwrite("Try again !\n", 1, 12, stderr); } else fprintf(stderr, "USAGE %s <password>\n", argv[0]); return (0); }
Code:
int check_password(char * var_1) { char * var_2 = "\x7e\x42\x4f\x7a\x4b\x59\x59\x5d\x45\x58\x4e"; int var_3; for (var_3 = 0; var_3 < strlen(var_1); var_3++) { if ((var_1[var_3] ^ 42) != var_2[var_3] || var_2[var_3] == 0) return (0); } return (var_1[var_3] == var_2[var_3]); }
Code:
(gdb) x/32x 0x4007f4 0x4007f4: 0x7a4f427e 0x5d59594b 0x004e5845 0x6c6c6557 0x400804: 0x6e6f6420 0x00212065 0x20797254 0x69616761 0x400814: 0x0a21206e 0x41535500 0x25204547 0x703c2073
Code:
0x7a4f427e 0x5d59594b 0x004e5845
Code:
\x7e\x42\x4f\x7a\x4b\x59\x59\x5d\x45\x58\x4e\x00
Voir : http://en.wikipedia.org/wiki/Exclusive_or
Lorsque l'on fait un xor sur une valeur, on récupère une nouvelle valeur. Si on xor cette nouvelle valeur, on retrouve alors l'originale.
Ainsi il nous suffit de faire un xor sur cette chaine pour récupérer le contenu originale et donc trouver ce que le binaire attend pour comparer. Ici la valeur du xor est de 42, on utilisera donc cette même valeur.
Let's go.
Code:
[email protected]:~$ python Python 2.7.5+ (default, Jun 2 2013, 13:26:34) >>> s = "\x7e\x42\x4f\x7a\x4b\x59\x59\x5d\x45\x58\x4e" >>> result = "" >>> for i in s: ... result += chr(ord(i) ^ 42) ... >>> print result ThePassword >>>
Essayons quand même pour être certain :
Code:
$ ./reverseme ThePassword Well done ! $
Souvent on utilisera cette méthode pour reverser un algorithme spécifique pour ensuite pouvoir l'implémenter dans un code tel que des algorithmes de chiffrements ...
Next time : analyse de binaire mais avec d'autres outils cette fois.
Commentaire