Rétro-ingénierie

Un article de Haypo.

Retour à la page précédente Retour à l'accueil sécurité

La rétro-ingénierie (ou reverse engineering en anglais) est une technique consistant à analyser un programme dont on ne possède pas le code source. L'analyse peut se faire en statique ou dynamique.

Sommaire

[modifier] Analyse statique

Vous trouverez dans cette section des informations spécifiques à la rétro-ingéniérie. Mais consultez également l'article sur l'analyse statique de code qui est plus général.

[modifier] Premier aperçu

Vous aurez besoin des outils :

  • file : Paquet file sous Debian
  • nm : Fait parti du paquet binutils sous Debian
  • strings : Fait parti du paquet binutils sous Debian
  • ldd : Fait parti du paquet libc6 sous Debian

La première étape est de voir le type du fichier avec la commande file :

$ file hello
hello: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
for GNU/Linux 2.2.0, dynamically linked (uses shared libs), not stripped

Yahoo ! C'est la fête ! Les symboles de débogage sont encore présent (not stripped), ce qui facilite grandement la compréhension du code. Sinon, c'est un binaire Linux (ELF) classique avec une liaison dynamique des symboles (dynamically linked), c'est-à-dire que les fonctions comme celles de la libc sont "chargées" au démarrage.

Autre exemple sans les symboles :

$ file hello-strip
hello-strip: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
for GNU/Linux 2.2.0, dynamically linked (uses shared libs), stripped

Exemple de programme lié statiquement :

$ file hello-static
hello-static: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
for GNU/Linux 2.2.0, statically linked, not stripped

Ensuite, pour un programme à liaison dynamique, utilisez le programme ldd pour lister les librairies qu'il utilise :

$ ldd hello
   libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7eb1000)
   /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0xb7feb000)

Ce programme utilise par exemple la libc version 6 (libc.so.6), qui elle-même va charger « /lib/ld-linux.so.2 » (bibliothèque obligatoire sous Linux : c'est elle qui gêre la liaison dynamique et va être chargée en premier).

Le programme nm est également pratique : il liste les symboles importés/exportés. Les symboles importés sont marqué du préfixe "U" (Unknow je suppose) :

$ nm hello
080495cc A __bss_start
080482f4 t call_gmon_start
080495cc b completed.1
         w __gmon_start__
0804831b T __i686.get_pc_thunk.bx
08048278 T _init
         U __libc_start_main@@GLIBC_2.0
08048394 T main
080494d0 d p.0
         U printf@@GLIBC_2.0
080482d0 T _start

J'ai abrégé la sortie car ce n'est pas intéressant. On voit ici une fonction main et la fonction classique en C : _start (qui s'occupe d'appeler main et préparer l'environnement).

Enfin, l'outil "goret" est strings qui va rechercher des chaînes de caractères dans un fichier binaire :

$ strings hello-strip
/lib/ld-linux.so.2
_Jv_RegisterClasses
__gmon_start__
libc.so.6
printf
_IO_stdin_used
__libc_start_main
GLIBC_2.0
PTRh
[^_]
Hello World!

Le résultat n'est pas toujours joyeux, et un vaut mieux ajouter "| less" à la commande pour pouvoir se promener dans les résultats et faire des recherches (via "/motcle").

[modifier] Décompilation

La décompilation est une opération utopique qui vise à retrouver le code source d'un programme après qu'il ait été compilé. Selon le langage utilisé pour programmer le logiciel, on peut espérer un code source plus ou moins exploitable. Les langages Java et Visual Basic (et Python ?) donnent par exemple d'excellents résultats, car les programmes "compilés" sont en fait écrit dans en "byte-code" qui se décode facilement.

  • Java : Jode fonctionne plutôt bien
  • Delphi : DeDe, DevendePro, EXE 2 DPR, MultiRipper, UnDelphi, RevendPro, EMS Source Rescuer, ...
  • Visual Basic : Apparement il y en a un paquet :-)
  • Python : decompyle permet de retrouver le code source à partir du "byte-code" (fichiers .pyc)
  • C : Essayez uncc (semble très basique)
  • Programme C++ : Je n'en connais aucun ...

[modifier] Désassembleur

Dans la majorité des cas, on se rabat sur les désassembleurs, car la décompilation est impossible. La décompilation traduit du code machine en code assembleur, ce qui est en gros la même chose mais écrit dans un langage plus facilement lisible par un humain.

Désassembleurs :

  • IDA Pro : le meilleur selon moi :-) Supporte de nombreux types de binaires (allant du programme PE Windows à la Playstation), permet d'agir sur le code (ajout de commentaire, mais aussi modifier le code machine), a son propre langage de programmation, etc. (programme commercial et cher)
  • Multi : Apparement génial selon un ami ... (programme commercial et cher)
  • objdump permet un désassemblagage bête et méchant
  • Gdb permet un désassemblage un peu interactif (on peut spécifier les adresses de début et fin)

Je suis à la recherche d'un bon déassembleur interactif sous Linux.

[modifier] objdump

L'outil objdump permet d'analyser les entêtes et les sections d'un binaire ELF, et comporte également un désassembleur.

Options :

  • « -f programme » affiche le format du fichier, son architecture, et le point d'entrée du programme.
  • « -p programme » affiche l'entête du programme, les sections dynamiques, et les références de version (pour les librairies externes).
  • « -h programme » affiche les sections du programme.
  • « -d programme » désassemble toutes les sections exécutables.
  • « -d programme -j section » désassemble la section demandée du programme.
  • « -s programme -j section » affiche le contenu d'une section en hexadécimal
  • « -t programme » affiche les symboles d'un programme. Pour information, la commande strip permet de supprimer cette table des symboles.
  • « -T programme » affiche les symboles dynamiques d'un programme (résolus à l'exécution, symboles de librairies dynamiques en particulier)
  • « -R programme » affiche la table de relocation dynamique

[modifier] Analyse dynamique

[modifier] Outils

Outils :

  • DTrace : outil vraiment très puissant permettant de suivre de très près le fonctionnement d'un programme, de la couche noyau jusque dans l'espace utilisateur. Connait les structures de données et le type des arguments d'une fonction. Programme scriptable avec un langage interprété. Bref presque parfait. Presque car il ne tourne que sous Solaris 10 (ou OpenSolaris) ...
  • Subterfuge : Projet similaire à DTrace, mais beaucoup moins performant (lent et peut modifier le fonctionnement d'un programme). Ne semble plus vraiment maintenu malheureusement.
  • Frysk : Surveille les processus et threads (surveille quoi exactement?).
  • SystemTap : Sonde dans le noyau Linux permettant de mesurer les performances d'un programme ou traquer des erreurs.

Outils disponibles sous Unix :

  • strace suit les appels systèmes.
  • ltrace suit les appels aux librairies externes.
  • lsof liste les fichiers ouverts et sockets par processus.

Surveillance orienté sécurité (bloquer plutôt que surveiller) :

  • Systrace: Programme orienté sécurité permettant de tracer tous les appels systèmes et installer des rêgles pour interdire certaines fonctions (avec certains paramètres) pour tel ou tel programme. Fonctionne que sous OpenBSD, NetBSD et Linux (et un portage Mac OSX a existé, mais n'est plus maintenu).
  • User Mode Linux : Permet de lancer un Linux à l'intérieur d'un autre Linux
  • Xen : Projet de virtualisation logicielle, c'est-à-dire lancer plusieurs systèmes d'exploitation simultanément sur le même ordinateur en partageant les ressouces.

[modifier] Utilisation d'un débogueur

L'outil ultime pour la rétro-ingénierie est le débogueur. Il permet de voir le comportement du programme en direct et en plus de pouvoir agir sur son comportement (à utiliser modérément sous peine de plantage :-)).

Sous Linux, on peut utiliser gdb ou une de ses interfaces graphiques (ex: ddd).

[modifier] Table de relocation dynamique sous Linux

Sous Linux, les librairies sont mappées en mémoire. On peut voir ça et faisant un cat /proc/<pid>/maps (utiliser ps ax | grep <programme> pour connaître son identifiant). Exemple :

$ cat /proc/11714/maps
08048000-08049000 r-xp 00000000 03:07 70411      /home/haypo/hello
08049000-0804a000 rw-p 00000000 03:07 70411      /home/haypo/hello
b7e9e000-b7e9f000 rw-p b7e9e000 00:00 0 
b7e9f000-b7fc9000 r-xp 00000000 03:05 227957     /lib/tls/libc-2.3.2.so
b7fc9000-b7fd2000 rw-p 00129000 03:05 227957     /lib/tls/libc-2.3.2.so
b7fd2000-b7fd4000 rw-p b7fd2000 00:00 0 
b7fe9000-b7fea000 rw-p b7fe9000 00:00 0 
b7fea000-b8000000 r-xp 00000000 03:05 163030     /lib/ld-2.3.2.so
b8000000-b8001000 rw-p 00015000 03:05 163030     /lib/ld-2.3.2.so
bffeb000-c0000000 rw-p bffeb000 00:00 0 
ffffe000-fffff000 ---p 00000000 00:00 0 

On voit que le programme hello est mappé dans deux zones mémoire : 0x8048000 à 0x8049000 (code, lecture + exécution) et 0x8049000 à 0x804a000 (donnée, lecture et ecriture) ayant la même taille (70411). On voit aussi les bibliothèques libc et ld qui ont chacune leur partie code et donnée.

Or, avant l'exécution d'un programme, on ne peut pas prédire à quelle à adresse sera chargée telle ou telle fonction. C'est pour cela qu'il y a dans un fichier ELF une table de relocation dynamique. Lors de l'exécution du programme, on y écrira les adresses des fonctions importées dans les différentes librairies.

Exemple :

$ objdump -R programme | grep printf
0804a6b0 R_386_JUMP_SLOT   printf

L'adresse de la fonction printf sera écrite à l'adresse mémoire 0x804a6b0. Un appel à printf se fera en fait de la manière suivante :

(1) (préparation des paramètres suivi de)
    call 0x8048728

(2) (et à l'adresse 0x08048728)
    jmp *0x804a6b0

Un appel à printf revient donc à appeller la fonction 0x8048728 qui est en fait un simple saut à l'adresse stockée dans la zone mémoire 0x804a6b0.

Note : La version CVS du 15/06/2005 des binutils gêrent la table de rellocation dynamique.

[modifier] Attaque d'une boîte noire

Lorsqu'on ne connait rien d'un programme et que l'on n'a pas accès au code source, on peut tester sa fiabilité en lui envoyant des données aléatoires. Apparement, un grand nombre de programme Windows et Unix plantent tout simplement :-) En recherchant l'origine des plantages, on peut trouver des failles exploitables pour injecter du code.

Voir l'article fuzzing pour les détails.

[modifier] Articles connexes

[modifier] Liens externes