Notions & Fondamentaux en vrac
Avant de faire du reverse engineering ou de l’analyse de malware, il faut comprendre une idée fondamentale : un programme n’est pas une entité abstraite, mais une suite d’instructions machine exécutées dans un environnement matériel et système très structuré. Toute la complexité vient uniquement des couches d’abstraction entre le code humain et l’exécution CPU.
Le pipeline réel d’un programme est toujours le même : du code source lisible par un humain, on passe à une représentation intermédiaire, puis à du code machine regroupé dans un binaire, lequel est chargé en mémoire par le système, puis exécuté instruction par instruction par le processeur.
Vous trouverez ci-dessous une suite de notions fondamentales utiles, vous pouvez piocher dans ce document quand une notion ne vous semble pas claire.
De code source à binaire
Un code source comme celui écrit en C est une abstraction de haut niveau. Il décrit une logique, mais n’a aucune correspondance directe avec ce que le CPU exécute réellement.
#include <stdio.h>
int main() {
puts("Hello world");
return 0;
}
Pour transformer ce code en instructions exécutables, on utilise le compilateur. Il passe par plusieurs étapes : génération d’un code intermédiaire (IR), compilation en assembleur, puis assemblage en code machine.
gcc -o hello hello.c
Le résultat final est un binaire contenant des instructions machine, mais aussi des références vers des symboles externes qui seront résolus plus tard.
Le binaire
Un binaire est un fichier structuré qui combine du code machine, des données et des informations nécessaires à son chargement. Il ne s’agit pas d’un simple bloc d’instructions, mais d’un format organisé que le système d’exploitation sait interpréter. Sous Linux, ce format est ELF (Executable and Linkable Format).
Format ELF
Le format ELF décrit un programme avec des sections, chaque sections contient les différents de données dont un programme a besoin. les sections les plus connues sont : .text, .data, .bss et .rodata
La section .text contient le code machine exécuté par le CPU. La .rodata contient les constantes comme les strings. .data et .bss gèrent les variables globales initialisées ou non.
Le format ELF décrit aussi des segments qui indiquent au système comment charger les différentes sections de données en mémoire et avec les bon droits, par exemple la section .text va être chargé dans un segment mémoire avec les droits d'exécutions puisque c'est le code.
La section .rodata veut dire read only data et contient les constantes d'un programme, elle va donc venir être chargée dans un segment avec des droits read only.
Enfin en reverse engineering on va aussi souvent analyser les sections .plt et .got qui servent à gérer les appels vers des fonctions externes le programme est dynamique.
Compilation statique vs dynamique
Lors de la compilation, un programme peut être lié dynamiquement ou statiquement. En mode dynamique, le binaire contient des références vers des bibliothèques partagées comme la libc, qui seront chargées au runtime.
Cela permet des binaires plus petits et une meilleure réutilisation des bibliothèques, mais introduit une dépendance forte au système et à ses versions de librairies.
En mode statique, toutes les dépendances sont intégrées directement dans le binaire au moment du linking. Le programme devient autonome mais beaucoup plus volumineux.
Le loader
Lorsqu’un programme est exécuté, le système ne lance pas réellement notre programme mais lance d'abord un loader, comme son nom l'indique il va venir charger notre programme comme il faut en mémoire avant de lui passer la main.
C'est aussi là que le format de fichier ELF (et les autres formats sur les autres OS) prennent sens, c'est le loader qui va lire les sections et créer les segments correspondants en mémoire.
Si le programme est dynamique, c'est aussi lui qui va charger en mémoire les libraries dont le programme a besoin.
Il initialise aussi certaines structures critiques comme la stack, les arguments du programme et les variables d’environnement avant de transférer l’exécution au point d’entrée (_start).
Les registres
Les registres sont des emplacements mémoire directement intégrés au CPU, utilisés pour stocker des valeurs temporaires pendant l’exécution.
Sur l'architecture de processeur x86_64, on va retrouver suivants par exemple : rdi, rsi, rdx, rcx, r8, r9. Ces registres sont très petits et ne contiennent que 64 bits de données chacun. C'est de là que viennent les termes Architecture 32-bits, Architecture 64-bits, etc...
D’autres registres comme rsp (stack pointer) et rbp (base pointer) servent à structurer les frames de stack.