Introduction au reverse
Aux Rencontres Mondiales du Logiciel Libre, RootBSD consultant et chercheur en sécurité informatique mainteneur du projet Malware.lu tenait un atelier de reverse engineering. À la suite de cet atelier, il a publié les crackmes/fichiers utilisés durant celui-ci. Ces fichiers étant parfait pour débuter, nous allons donc les utiliser pour découvrir cette discipline.
Mais qu'est ce donc que le Reverse Engineering ?
Dans la langue de Molière, le Reverse Engineering se dit rétroingénierie ou ingénierie inversé.
Plus simplement, il s'agit de l'étude du fonctionnement ou de la conception d'un objet fini afin de comprendre son fonctionnement interne ou sa méthode de fabrication. Par la suite il s'agira pourquoi pas tenter de modifier cet objet voire même de tenter de le recréer.
Lorsque l'on observe les objets qui nous entoure et que l'on cherche à deviner les constituants d'un tel objet nous effectuons sans le savoir de la rétroingénierie : Prenez par exemple une tarte aux pommes, si l'on observe cet "objet" dans sa globalité on a un succulent dessert qui n'attend qu'une chose : être dévoré. Mais si maintenant l'on s’intéresse à ses constituants on observe que cette tarte est composée de pâte brisée, éventuellement de cannelle et ... de pommes ! C'est ça, la rétroingénierie !
Et légalement ?
C'est la partie compliquée, le reverse est dans certains cas illégale. Tout ce qui est soumis à des brevets, droits d'auteur, protections.... c'est hors la loi et cela dépend de la législation du pays.
Concernant tout ce qui est logiciels libres, aucun soucis, le code source est fournis, on peut faire ce que l'on veut.
Et cequi est code fermé, en France, c'est légal dans certaines conditions. Pour les savoir, il faut se référer à la licence du logiciel si elle l'autorise, vous savez le truc que personne ne lit et clique sur "J’accepte les termes et conditions d’utilisation".
Une des conditions pour lequel on peut le faire, c'est l'intéropérabilité. Par exemple, le faite d'adapter un programme s'exécutant sous Windows pour le rendre compatible avec MacOS ou Linux.
Dans tout les cas, il est interdit de reverser un programme et le modifier pour le revendre. Ca sera considéré comme de la contrefaçon.
Pour plus de détails, vous pouvez lire le Code de la propriété intellectuelle
A l'attaque!
Les binaires du cours sont disponibles à cette adresse ou Fichier:Workshop.tgz
L'archive est composé de deux premiers binaires servant à crypter l'image éponyme. Le but du jeux, c'est de trouver l'algorithme de cryptage, le reverser, et décrypter les images. Le dossier binaries contient cinq binaires dans lesquels il faut trouver le mot de passe. On va commencer par la.
Binaire exam1
La première chose, faire connaissance avec ce binaire. Pour ce faire, nous allons utiliser la commande file qui permet de déterminer le type d'un fichier et éventuellement d'autres informations comme les dimensions pour une image ou les codecs.
On apprend qu'on est en présence d'un binaire ELF 32 bits un format de fichiers exécutables sur les OS Linux-Unix. Que celui-ci à été conçus pour utiliser les processeurs de type 80386. qu'il utilise des librairies dynamiques, et qu'il possède les symboles de débogages (not stripped). C'est la fête !
Ensuite on utilise la commande nm qui permet de lister la table des symboles importés et exportés. T pour pour ceux écrit dans le code, et U/u pour undefined (ce qui est le cas pour les fonctions appelés dans les librairies dynamiques).
On retrouve la fonction _start qui s'occupe de préparer l'environnement d'exécution et de lancer le main, dans lequel est ensuite exécuter un printf, un puts et surtout un strcmp qui va nous intéresser.
A partir de la, on constate qu'il y a pas de fonctions louches, on peut lancer notre binaire sans craintes :) (on est jamais trop prudent)
Autre commande importante, strings. Méthode un peu à la sauvage qui va afficher les chaînes de caractère présentes dans le fichier. Et la que voit t'on?
Le mot de passe est écrit en clair, il a dû être seter en dur dans le code source pour qu'il apparaisse ainsi.
Binaire exam2
Deuxième binaire. Au départ le principe est le même.
$ file exam2 exam2: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.15, not stripped
Toujours non strippé ca va bien nous aider.
$ strings exam2 /lib/ld-linux.so.2 __gmon_start__ libc.so.6 _IO_stdin_used puts __stack_chk_fail printf strcmp __libc_start_main GLIBC_2.4 GLIBC_2.0 PTRh D$,1 D$$A D$&c D$'6 T$,e3 [^_] Usage: %s key Bad key Good key
Cette fois-ci le mot de passe n'apparaît pas en clair, mais on voit bien qu'un strcmp est appelé.
Utilisons objdump pour désassembler notre programme en recherchant le strcmp.
$ objdump -d exam2 | grep strcmp 080483dc <strcmp@plt>: 8048539: e8 9e fe ff ff call 80483dc <strcmp@plt>
Ok il se trouve à l'adresse 0x8048539
Sortons gdb et debuggont:
$ gdb -q exam2 Reading symbols from /home/futex/Bureau/to_student/binaries/exam2...(no debugging symbols found)...done. (gdb)
On pose un breakpoint à l'adresse du strcmp:
(gdb) b *0x8048539 Breakpoint 1 at 0x8048539
On lance le programme avec en paramètre un mot de passe bidon (AAAA):
(gdb) r AAAA Breakpoint 1, 0x08048539 in main ()
Ensuite on regarde ce que contient le registre eax:
(gdb) x/10s $eax 0xffffd3d4: "AFc6mcw" 0xffffd3dc: "" .......
Intéressant :)
$ ./exam2 AFc6mcw Good key
Une autre façon intéressante de le cracker est d'utiliser la variable d'environnement LD_PRELOAD. Cette variable permet de charger des librairies dynamiques en priorité à l'exécution d'un programme. On va donc l'utiliser pour usurpé la fonction strcmp du programme. Écrivons ce petit bout de code en C:
$ cat strcmp.c #include <stdio.h> #include <string.h>
int strcmp (const char *s1, const char *s2) { printf ("argument1: %s, argument2: %s\n", s1, s2); return 0; }
Notre fonction se contente d'afficher les deux chaîne passé en paramètre, le mot de passe qu'on rentrera et le bon mot de passe. Puis retourne 0, ainsi la comparaison sera toujours juste. On compile notre librairie de la façon suivante:
$ gcc -m32 strcmp.c -shared -fPIC -o /tmp/strcmp.so
Les binaires étant en 32 bit, si vous êtes en 64 bit il faut mettre l'option -m32, -shared pour spécifier qu'on compile une librairie dynamique, -fPIC (Position Independent Code). On set la variable LD_PRELOAD
$ export LD_PRELOAD="/tmp/strcmp.so"
Et le résultat final:
$ ./exam2 123 argument1: AFc6mcw, argument2: 123 Good key
Une troisième façon encore plus simple d'avoir le mot de passe, le tracer:
$ ltrace -s128 ./exam2 123 __libc_start_main(0x80484a4, 2, 0xffb133f4, 0x8048590, 0x8048580 <unfinished ...> strcmp("AFc6mcw", "123") = 1 puts("Bad key"Bad key ) = 8 +++ exited (status 0) +++
Binaire exam3
Comme d'habitude on commence par faire connaissance avec ce binaire:
$ file exam3 exam3: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.15, not stripped
$ strings exam3 /lib/ld-linux.so.2 __gmon_start__ libc.so.6 _IO_stdin_used exit puts printf strlen __libc_start_main GLIBC_2.0 PTRh [^_] Bad key Usage: %s key Good key
Un objdump -d exam3 nous apprendra que le mot de passe fait 9 caractères.
$ objdump -d -j .text exam3 | grep -A 2 strlen 80484dc: e8 a7 fe ff ff call 8048388 <strlen@plt> 80484e1: 83 f8 09 cmp $0x9,%eax 80484e4: 74 05 je 80484eb <main+0x49> 80484e6: e8 99 ff ff ff call 8048484 <loose>
On notera aussi que le call (adresse 80484e6) sur la fonction loose sera appelé en cas d'échec.
On va faire un grep sur les appels à cette fonction
$ objdump -d -j .text exam3 | grep -B 2 loose 8048483: 90 nop
08048484 <loose>: -- 80484e1: 83 f8 09 cmp $0x9,%eax // Test de la longueur du mot de passe, vu juste avant 80484e4: 74 05 je 80484eb <main+0x49> 80484e6: e8 99 ff ff ff call 8048484 <loose> -- 80484f6: 3c 41 cmp $0x41,%al //Test du premier caractère 0x41 est le code ascii en hexadécimal de A 80484f8: 74 05 je 80484ff <main+0x5d> 80484fa: e8 85 ff ff ff call 8048484 <loose> -- 8048521: 39 c2 cmp %eax,%edx //A voir plus loin 8048523: 74 05 je 804852a <main+0x88> 8048525: e8 5a ff ff ff call 8048484 <loose> -- 8048538: 3c 31 cmp $0x31,%al //Troisième caractère, 0x31 est le 1 804853a: 74 05 je 8048541 <main+0x9f> 804853c: e8 43 ff ff ff call 8048484 <loose> -- 804854f: 3c 33 cmp $0x33,%al //Quatrième caractère, 0x33 est le 3 8048551: 74 05 je 8048558 <main+0xb6> 8048553: e8 2c ff ff ff call 8048484 <loose> -- 804857d: 39 d0 cmp %edx,%eax //A voir plus loin 804857f: 74 05 je 8048586 <main+0xe4> 8048581: e8 fe fe ff ff call 8048484 <loose> -- 8048594: 3c 78 cmp $0x78,%al //Sixième caractère, 0x78 est le x 8048596: 74 05 je 804859d <main+0xfb> 8048598: e8 e7 fe ff ff call 8048484 <loose> -- 80485ab: 3c 68 cmp $0x68,%al //Septième caractère, 0x68 est le h 80485ad: 74 05 je 80485b4 <main+0x112> 80485af: e8 d0 fe ff ff call 8048484 <loose> -- 80485d9: 39 d0 cmp %edx,%eax //A voir plus loin 80485db: 74 05 je 80485e2 <main+0x140> 80485dd: e8 a2 fe ff ff call 8048484 <loose>
Notre mot de passe ressemble à cela pour le moment: A.13.xh..
Alors maintenant on va manipuler un peu gdb :) On charge le binaire:
$ gdb -q exam3
On pose des breackpoints sur les cmp inconnues
(gdb) b *0x8048521 (gdb) b *0x804857d (gdb) b *0x80485d9
Et on l'exécute:
(gdb) r A.13.ND.. Breakpoint 1, 0x8048521 in main ()
Regardons ce qu'il y a dans les registre eax et edx
Breakpoint 1, 0x08048521 in main () (gdb) print $eax $1 = 68 //code décimal ascii de D (gdb) print $edx $2 = 46 //code décimal ascii de notre .
Pour pouvoir continuer on force edx à 68
(gdb) set $edx=68 (gdb) print $edx $3 = 68
MDP: AD13.xh..
Même manip au cmp suivant:
Breakpoint 2, 0x0804857d in main () (gdb) print $edx $4 = 61 //code décimal ascii de = (gdb) print $eax $5 = 46 //code décimal ascii de notre .
Pour pouvoir continuer on force eax à 61
(gdb) set $eax=61 (gdb) print $eax $6 = 61
MDP: AD13=xh..
Toujours la même manip au troisième breackpoint:
Breakpoint 3, 0x080485d9 in main () (gdb) print $eax $7 = 46 (gdb) print $edx $8 = 99 //code décimal ascii de c
On force eax
(gdb) set $eax=99 (gdb) continue Good key
Le mot de passe final est donc AD13=xhc. Le dernier caractère n'étant pas testé, on peut mettre ce que l'on veut.
Binaire exam4
Hash md5: $ md5sum exam4 c5ada58ca8731be76bc275df4b2feb6d exam4
$ file exam4 exam4: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, stripped
Différence avec les précédents, ce binaire est strippé.
$ strings exam4 UPX! >(nW h6hI PTRh h`QVh ... $Info: This file is packed with the UPX executable packer http://upx.sf.net $ $Id: UPX 3.07 Copyright (C) 1996-2010 the UPX Team. All Rights Reserved. $ PROT_EXEC|PROT_WRITE failed. (/proc/self/exe [jUX POl< jZ^[ nEf} UPX!u @bQs 55|k ;P(2) jH/M UPX!
On apprends qu'il est aussi packer via UPX. Les intérêts de packer un binaire est de réduire la taille de l'exécutable et de compliquer la tache du réverseur.
Pour le dépacker, c'est très simple avec la commande upx:
$ upx -d exam4 Ultimate Packer for eXecutables Copyright (C) 1996 - 2010 UPX 3.05 Markus Oberhumer, Laszlo Molnar & John Reiser Apr 27th 2010wbr> File size Ratio Format Name -------------------- ------ ----------- ----------- 578492 <- 251396 43.46% linux/elf386 exam4
Unpacked 1 file.
Nous voila avec un autre binaire
$ md5sum exam4 e2892e36f8d052aa315fb09c6411a0b3 exam4
$ file exam4 exam4: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, for GNU/Linux 2.6.15, not stripped
$ strings exam4 | more PTRh <[^_] ,[^_] [^_] [^_] t!e3 [^_] H$[] ~hxX [^_] $[^_] SH9z [^_] ([^_] [^_] [^_] ....
On apprend rien d'intéressant via cette commande.
Ouvrons le binaire dans IDA. IDA est un désassembleur commercial (une version gratuite existe), reconnaissant plusieurs type d'exécutable (PE, ELF, XCOFF, ...) sur plusieurs type de CPU (x86, ARM, PowerPC, ...)
A droite dans la fenêtre 'Names Window' on voit notre fonction 'main' et une 'check' qui semble plus intéressante.
Jetons un oeil à cette fonction
On voit qu'elle commence par un appelle à la fonction strlen, puis compare le retour de la fonction qui rappelons le se retrouve toujours dans eax à 7. On peut en déduire la taille de notre mot de passe. Si le test échoue on part dans la branche à gauche qui va afficher à l'écran "Bad key" et met fin au programme. Si le test est bon, on va comparer les caractères.
On voit tout de suite une comparaison à 45h code ACSII de E. Ca sera le premier caractère de notre mot de passe.
Dans le test suivant:
Le programme va charger dans eax l'adresse sur la pile du mot de passe qu'on a rentré, puis va incrémenter l'adresse de 1 pour pointer sur le second caractère et copier ce caractère dans edx. Ensuite il va charger une adresse dans eax, adresse qui pointe sur une autre adresse pointant sur le mot de passe qu'on a rentré et copier le premier caractère dans eax, soit donc 0x45 E en ACSII et ajouter 0xA, 10 en décimal. eax vaudra alors 0x4f, O en ACSII.
Ca sera le second caractère du vrai mot de passe EO?????
Troisième test:
On charge de nouveau l'adresse de notre mot de passe dans eax, on incrémente l'adresse de 2 pour pointer sur le troisième caractère (le premier étant en position 0 pour ceux qui suivent pas au fond). Puis on le compare à 0x6f, o en ACSII. Le mot de passe devient donc EOo????
Quatrième test:
Même chose, on compare le quatrième caractère à 0x30, 0 en ACSII. Le mot de passe devient donc EOo0???
Cinquième test:
Exactement la même chose que précédemment. Le mot de passe devient donc EOo00??
Dernier test:
Comme d'hab, on charge le 5+1 ème caractère du mot de passe rentré à la mano dans eax. Et comme dans le troisième test, on charge aussi son adresse dans edx, puis on l'incrémente de 4, il va donc pointé sur 0 (0x30). Et il sera incrémenter de 0x14, soit 0x68, D en ACSII.
Notre mot de passe sera donc: EOo00D?, on peut mettre ce qu'on veut pour le dernier caractère il n'est pas testé.
$ ./exam4 EOo00DA Good key
Binaire exam5
Binaire simple1
Ce petit binaire sert à obfusquer des fichiers, tels que l'image "simple1.png" dans l'archive. L'idée et de réverser la routine de d'obfusquation pour en refaire un programme et desobfusquer l'image.
On ouvre le binaire dans IDA, on y trouve plusieurs fonctions.
main, ca on connait :)
syntaxe, qui va afficher l'utilisation du binaire.
super_encrypt, qui est bien plus intéressante.
La fonction prends deux paramètres arg0 qui est le fichier en question, et arg4 sa taille.
La boucle servant au cryptage du fichier:
Qui ne fait que de lire notre fichier octet par octet et sur chacun va utiliser octet par octet le contenu de la variable key_2193 pour y faire un XOR.
Le contenu de la variable key_2193:
Ce qui n'est autre que les valeurs hexa de la chaîne "rootbsd". Les caractères sont à l'envers, n'oubliez pas qu'on est en little endian.
L'opération XOR est souvent utiliser dans des opérations d'obfuscage simple (on peut pas vraiment parler d'un cryptage), son fonctionnement est super simple:
Si par exemple, on fait un XOR de A et de B on obtient la valeur C.
La même opération, entre A et C on obtient B.
Et bien sur B XOR C = A.
Donc pour desobfusquer notre fichier, il suffit de faire XOR octet par octet de son contenu avec la chaîne "rootbsd".
On fait un petit programme "superdecrypt.py" en python qui va nous faire ça pour nous:
#!/usr/bin/python
# encoding: utf-8
key_2193 = "rootbsd\x00"
crypt_file=open("./simple.png","rb").read()
crypt_file_size=len(crypt_file)
uncrypt_content=""
for i in range(crypt_file_size):
tmp = ord(crypt_file[i]) ^ ord(key_2193[i%len(key_2193)])
uncrypt_content += chr(tmp)
uncrypt_file=open("./simple1.png","w")
uncrypt_file.write(uncrypt_content)
uncrypt_file.close()
Binaire encrypt
Ce binaire fait la même chose que le précédent, seule la routine d'obfuscation est différente:
Au début de la routine un add eax, 0AH est fait. A ce moment la eax contient un octet de notre fichier. On lui ajoute 0xA (10 en décimal).
Puis son contenu est copier dans edx, et un caractère de la chaîne "rootbsd" est chargé dans eax.
Et on a de nouveau un XOR de fait entre les registres eax et edx.
Suffit de modifier légèrement notre script:
#!/usr/bin/python # encoding: utf-8
key_2193 = "rootbsd\x00" crypt_file=open("./encrypt.png","rb").read() crypt_file_size=len(crypt_file) uncrypt_content=""
for i in range(crypt_file_size): tmp = ord(crypt_file[i]) ^ ord(key_2193[i%len(key_2193)]) tmp -= 0xa if tmp < 0: tmp = 256 + tmp uncrypt_content += chr(tmp)
uncrypt_file=open("./encrypt1.png","w") uncrypt_file.write(uncrypt_content) uncrypt_file.close()