Introduction au reverse

De UnixWiki
Aller à la navigation Aller à la recherche

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.

Reverse1.1.jpg

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).

Reverse1.3.jpg

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?

Reverse1.4.jpg

Le mot de passe est écrit en clair, il a dû être seter en dur dans le code source pour qu'il apparaisse ainsi.

Reverse1.5.jpg

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, ...)

Ida1.JPG

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

Ida2.JPG

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:

Ida3.JPG

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:

Ida4.JPG

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:

Ida5.JPG

Même chose, on compare le quatrième caractère à 0x30, 0 en ACSII. Le mot de passe devient donc EOo0???

Cinquième test:

Ida6.JPG

Exactement la même chose que précédemment. Le mot de passe devient donc EOo00??

Dernier test:

Ida7.JPG

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.

Ida8.JPG

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.

Simple1 1.jpg

La fonction prends deux paramètres arg0 qui est le fichier en question, et arg4 sa taille.

La boucle servant au cryptage du fichier:
Simple1 2.jpg

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: Simple1 3.jpg

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()

Voila l'image cachée:
Simple1.png

Binaire encrypt

Ce binaire fait la même chose que le précédent, seule la routine d'obfuscation est différente:
Encrypt1.jpg

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()

Le fichier caché est le même:
Simple1.png