Introduction Reverse Engineering

Kaddate |

Avant de rentrer dans le dur de l'analyse de malware, on va poser les bases du reverse engineering. L'idée ici, c'est simple : on prend un programme fini, on l'ouvre, et on essaye de comprendre ce qu'il fait sans avoir accès à son code source.

Les grandes familles d'outils

Quand on fait du reverse on utilise plusieurs outils en parallèle qui nous permettent d'analyser les programme avec différents points de vue. Parmis tous ces outils, on va retrouver 4 grandes catégories :

Famille Rôle
Désassembleur Traduis les instructions processeurs en Assembly
Décompilateur Essaye de transformer l'assembly en du code source lisible
Debugger Permet d'exécuter le programme pas à pas et d'analyser la mémoire pendant l'exécution
Émulateur / sandbox Reproduit un environnement d'exécution sans lancer ça directement sur la machine hôte

L'idée, c'est de comprendre qu'ils ne servent pas tous à la même chose et qu'ils permettent de répondre à des questions différentes:
- Désassembleur = "qu'est-ce que le CPU lit ?"
- Décompilateur = "qu'est-ce que le développeur voulait dire ?"
- Debugger = "qu'est-ce qu'il fait vraiment à l'exécution ?"
- Émulateur = "qu'est-ce qu'il ferait dans un environnement différent ?"

Vous avez peut-être déjà entendu parler d'outils comme: gdb, Ghidra, IDA, x64dbg, Qemu, Unicorn, objdump, ndisasm. Ils rentrent tous plus un moins dans une catégorie plus haute, même si ils remplissent souvent plus d'un rôle.

Ghidra et IDA sont des décompilateur. gdb et x64dbg sont des debuggers. Qemu et Unicorn sont des émulateurs. objdump et ndisasm sont des désassembleurs.

Quelques outils pour comprendre l'analyse statique

Quand un programme est compilé il ne contient plus le code source du programme mais les instructions processeurs seulement... Mais en vrai ça suffit pour comprendre comment un marche un programme :D . Au final un programme compilé est obligé de contenir l'information de ce qu'il fait pour la donner au processeur, un programme compilé est donc TOUJOURS reversible. C'est juste plus chronophage en fonction des niveaux d'obfuscations.

L'avantage c'est que les système d'exploitations définissent clairement un standard pour les programme executable, cela permet à l'OS de récupérer les informations comme il faut, sous Windows ça va être le format PE (tous vos .exe sous windows ce sont des fichiers au format PE), et sous linux le format ELF. L'avantage de ces format c'est qu'ils décrivent parfaitement où est le code du programme, où sont les variables, où sont les informations de debug si il y'en a. Et donc pour nous c'est aussi une première approche pour commencer à reverse.

Le désassemblage

Le désassemblage, c'est l'action inverse de l'assemblage.

L'objectif est de convertir un fichier binaire en assembly, c'est-à-dire en instructions bas niveau lisibles par un humain. Bien que l'assembly est compliqué à lire, il est la représentation exacte de ce que le CPU exécute.

Définition

Le désassemblage consiste à convertir un code machine en instructions assembleur, afin de retrouver une représentation lisible de ce que le processeur exécute.

C'est généralement la première étape quand on ouvre un binaire, simplement parce que l'assembly est la représentation exacte de ce qui est utilisé et parce que les autres outils d'analyses vont ensuite utiliser le code désassemblé pour analyser le programme.

Le désassemblage permet voir à la fois la structure globale du programme, les appels système, les fonctions utilisées, les chaînes de caractères, et parfois déjà pas mal de logique.

Exemple de programme de référence

Pour commencer, on va prendre un petit Hello World en C compilé avec les informations de debug.

#include <stdio.h>

int main()
{
    puts("Hello world");
    return 0;
}

Compilation :

gcc -g -o hello hello.c

Le fait d'ajouter -g est important : ça ajoute des symboles de debug. Les outils de reverse vont avoir beaucoup plus d'infos pour nous aider car ils vont récupérer les informations de debug pour nous.

Avec un désassembleur

Si on ouvre ce binaire dans un désassembleur, voilà le résultat:

  /tmp objdump -M intel -j .text -d ./hello

./a:     file format elf64-x86-64


Disassembly of section .text:
...

0000000000001139 <main>:
    1139:   55                      push   rbp
    113a:   48 89 e5                mov    rbp,rsp
    113d:   48 8d 05 c0 0e 00 00    lea    rax,[rip+0xec0]        # 2004 <_IO_stdin_used+0x4>
    1144:   48 89 c7                mov    rdi,rax
    1147:   e8 e4 fe ff ff          call   1030 <puts@plt>
    114c:   b8 00 00 00 00          mov    eax,0x0
    1151:   5d                      pop    rbp
    1152:   c3                      ret

À ce stade, on ne lit plus du C, on lit de l'assembleur.

Mais même comme ça, on comprend déjà plusieurs choses :

on voit qu'il y a une fonction main
on voit un appel à la fonction puts
on voit que la fonction retourne 0

Je vais pas trop rentrer dans le détail et vite passer à la décompilation

La décompilation

La décompilation, c'est l'étape d'après. L'idée est de partir du binaire et d'essayer de reconstruire un pseudo-code proche du C. Dans le cas de notre programme, les outils vont nous générer un pseudo code très propre.

Définition

La décompilation consiste à reconstruire une représentation de plus haut niveau d'un programme à partir de son binaire, pour retrouver une représentation proche du code source.

Attention avec le décompilateur !

Les décompilateurs INTERPRETE l'assembleur mais se trompent souvent ! Le décompilateur vous ment ! Ne vous y fiez pas trop.

Le décompilateur fait une reconstruction intelligente à partir du binaire, il va tenter de retrouver les types des variables, reconstruire les appels de fonctions, les boucles logiques, il va donner des noms aux variables temporaires... etc...

Mais il y a toujours une limite. Les compilateurs très optimisés créés des codes décompilés illisibles, les malwares arrivent facilemment à faire mentir les décompilateurs et à leur faire générer du code illislble.

Exemple sur notre Hello World

Toujours avec le même programme, voici ce que le décompilateur de ghidra en fait :

../../img/ghidra_decomp.png

Là on voit tout de suite la logique du programme clairement sans même avoir son code source.

décompilation et désassemblage

Souvent en tant que reverser, on va jongler entre les deux outils. Le Décompilateur facilite beaucoup la vie et fait gagner du temps mais parfois se trompe, et c'est là qu'on va regarder l'assembly. Dans le cas de programmes potentiellement obfusqués, les décompilateurs vont souvent très vite être perdus et il va falloir revenir à l'assembly rapidemment.

Vous avez un petit exemple sur un cas pratique ici : ./exemple_reverse.md