Chapitre 4 — Techniques de rétro-ingénierie de base¶
Présentation de ce chapitre
1. Cadre pédagogique et légal de ce chapitre¶
Ce chapitre introduit des notions élémentaires de rétro-ingénierie (reverse engineering), c'est-à-dire l'analyse d'un programme compilé pour en comprendre le fonctionnement interne, sans disposer de son code source. Ces notions sont enseignées ici dans un unique but défensif : elles permettent à un analyste de comprendre plus précisément un comportement déjà observé (par l'analyse statique ou dynamique) et de produire des indicateurs de détection fiables. Comme pour les chapitres précédents, tout exercice pratique s'effectue exclusivement sur des programmes pédagogiques bénins, en environnement isolé, jamais sur du code malveillant réel.
2. Rappels d'architecture : de l'exécutable au processeur¶
Un programme compilé en langage machine est une suite d'instructions binaires interprétées directement par le processeur. L'assembleur est la représentation textuelle lisible par un humain de ces instructions machine : chaque instruction assembleur correspond directement à une instruction machine (à la différence d'un langage de haut niveau comme le C, qui est traduit en plusieurs instructions machine par le compilateur).
L'architecture x86 (et son extension 64 bits, x86-64) reste la plus répandue sur les postes de travail et serveurs, ce qui en fait la référence pédagogique la plus utile pour une première initiation.
Registres principaux (x86-64)¶
Les registres sont de petites zones de mémoire directement intégrées au processeur, utilisées pour les calculs et le passage de données entre instructions :
| Registre | Rôle usuel |
|---|---|
RAX |
Registre accumulateur ; contient souvent la valeur de retour d'une fonction |
RBX, RCX, RDX |
Registres généraux, utilisés selon les conventions d'appel et les besoins du compilateur |
RSP |
Pointeur de pile (stack pointer) : sommet de la pile d'exécution courante |
RBP |
Pointeur de base de pile (base pointer) : repère fixe du cadre de pile de la fonction courante |
RIP |
Pointeur d'instruction (instruction pointer) : adresse de la prochaine instruction à exécuter |
Instructions assembleur de base¶
; Extrait pédagogique en syntaxe Intel, à but strictement illustratif
mov rax, 5 ; charge la valeur 5 dans le registre RAX
add rax, rbx ; RAX <- RAX + RBX
cmp rax, 10 ; compare RAX à 10 (met à jour les indicateurs)
je etiquette_egal ; saut conditionnel si l'égalité est vérifiée
call fonction_x ; appel de fonction (empile l'adresse de retour)
ret ; retour de fonction (dépile l'adresse de retour)
Cette poignée d'instructions (déplacement de données mov, arithmétique add/sub, comparaison cmp, sauts conditionnels je/jne/jg, appel et retour de fonction call/ret) suffit à lire une part importante d'un désassemblage simple. L'objectif de ce chapitre n'est pas la maîtrise complète du jeu d'instructions x86, mais la capacité à reconnaître ces motifs récurrents et à en déduire la logique de haut niveau qu'ils implémentent.
3. Structure d'un exécutable : notions générales PE et ELF¶
Comprendre la structure d'un exécutable, déjà abordée au chapitre 2 du point de vue de l'analyse statique, prend ici une importance pratique directe : c'est cette structure qui indique à l'outil de désassemblage où se trouve le code à analyser.
- Un exécutable PE (Windows) ou ELF (Linux) contient un en-tête décrivant le format du fichier, suivi d'une table de sections. La section
.text(ou équivalent) contient le code exécutable proprement dit ; c'est elle qui est désassemblée. - Le point d'entrée (entry point) indique l'adresse de la première instruction exécutée au lancement du programme. Dans un exécutable non packé, il correspond en général au début de la fonction principale du programme (ou d'une routine d'initialisation du runtime qui l'appelle). Dans un exécutable packé (chapitre 2), il pointe vers la petite routine de décompression, et non vers le code réel du programme.
- La table d'importation relie les appels de fonctions du code à des bibliothèques externes (système ou tierces) : un outil de désassemblage s'en sert pour afficher des noms de fonctions lisibles (par exemple une fonction de création de fichier) plutôt qu'une simple adresse mémoire.
4. Lecture d'un désassemblage simple¶
Reprenons un exemple pédagogique de fonction en C, puis son désassemblage simplifié, pour illustrer la démarche de lecture :
/* Fonction pédagogique en C, à but strictement illustratif */
int verifier_code(int code) {
if (code == 1234) {
return 1; /* code correct */
}
return 0; /* code incorrect */
}
; Désassemblage simplifié et annoté (syntaxe Intel, illustratif)
verifier_code:
cmp edi, 1234 ; compare l'argument (edi) à 1234
jne code_incorrect ; si différent, saute vers le cas d'échec
mov eax, 1 ; sinon, valeur de retour = 1
ret
code_incorrect:
mov eax, 0 ; valeur de retour = 0
ret
La démarche de lecture typique consiste à repérer les comparaisons (cmp) et les sauts conditionnels qui les suivent, car ils traduisent directement la logique conditionnelle (if/else) du code source. Une constante comme 1234 apparaissant en dur dans une comparaison — ici un exemple pédagogique de « code d'accès » — est un motif classique à repérer : dans un contexte réel d'analyse défensive, une telle constante correspond souvent à une valeur significative (mot de passe codé en dur, identifiant de licence, condition d'activation).
5. Repérage de fonctions suspectes¶
Face à un désassemblage complet, souvent volumineux, l'analyste ne lit pas linéairement l'ensemble du code : il concentre son attention sur des indices qui orientent vers les fonctions les plus susceptibles de porter une logique intéressante :
- Fonctions appelant des API sensibles : une fonction qui appelle des primitives de manipulation du registre, de création de processus, ou de chiffrement mérite un examen prioritaire.
- Fonctions à proximité de chaînes de caractères suspectes identifiées lors de l'analyse statique (chapitre 2) : un outil de désassemblage permet généralement de retrouver, à partir d'une chaîne repérée, les fonctions qui la référencent.
- Boucles et comparaisons répétées sur de petites constantes : motif fréquent d'une routine de déchiffrement ou de décodage simple (par exemple un XOR répété avec une clé courte), potentiellement révélatrice d'une tentative d'obfuscation.
- Fonctions rarement appelées ou atteintes seulement sous certaines conditions : peuvent correspondre à une charge malveillante conditionnelle (déclenchement à une date donnée, par exemple), un point à mettre en relation avec les techniques d'évasion étudiées au chapitre 3.
6. Décompilation : un complément utile¶
Au-delà du simple désassemblage, des outils modernes proposent une décompilation partielle, qui reconstruit une approximation de code source de haut niveau (proche du C) à partir du code assembleur. Cette reconstruction n'est jamais parfaitement fidèle au code source original (certaines informations, comme les noms de variables ou les commentaires, sont irrémédiablement perdues à la compilation), mais elle accélère considérablement la compréhension d'une fonction complexe par rapport à la lecture de l'assembleur brut, et constitue aujourd'hui une pratique standard en analyse de malwares professionnelle.
À retenir¶
- La rétro-ingénierie de base permet de comprendre précisément la logique d'un programme compilé, en complément de l'analyse statique et dynamique, dans un but strictement défensif.
- Les registres (
RAX,RSP,RIP, etc.) et une poignée d'instructions clés (mov,cmp, sauts conditionnels,call/ret) suffisent à lire une part importante d'un désassemblage simple. - La structure d'un exécutable (PE ou ELF) détermine où se trouve le code à désassembler et où se situe le point d'entrée du programme.
- La lecture d'un désassemblage se concentre sur les comparaisons et sauts conditionnels, qui traduisent la logique conditionnelle du code source original.
- Le repérage de fonctions suspectes s'appuie sur des indices (API sensibles, proximité de chaînes suspectes, motifs de boucle de déchiffrement) plutôt que sur une lecture linéaire exhaustive.
- La décompilation moderne (reconstruction d'un pseudo-code proche du C) accélère fortement l'analyse, sans jamais restituer parfaitement le code source d'origine.
- Ce chapitre, comme l'ensemble du module, s'exerce exclusivement sur des programmes pédagogiques bénins, jamais sur du code malveillant réel.
Dr. Zakaria Sawadogo — École Polytechnique de Ouagadougou