Introduction
Le reverse engineering, ou rétro-ingénierie, consiste à étudier un système existant afin d'en comprendre le fonctionnement interne.
Dans le domaine logiciel, cela signifie généralement partir d'un programme compilé, d'un exécutable, d'une bibliothèque, d'un pilote ou d'un autre composant logiciel sans disposer nécessairement de son code source, puis tenter de reconstruire son comportement, son architecture et sa logique interne.
Le but n'est donc pas de créer un programme à partir d'une spécification, mais de faire le chemin inverse :
- observer le programme ;
- analyser son comportement ;
- étudier son code machine ;
- identifier ses structures internes ;
- comprendre les choix réalisés par le développeur ;
- reconstruire progressivement une représentation compréhensible du programme.
- assembleur ;
- architecture processeur ;
- systèmes d'exploitation ;
- compilation ;
- programmation bas niveau ;
- analyse logique ;
- débogage ;
- analyse mémoire.
1. Qu'est-ce que le reverse engineering ?
Définition générale
Le reverse engineering consiste à partir d'un produit fini afin d'en retrouver le fonctionnement.
Dans un programme classique, le développement suit approximativement ce chemin :
Code: Select all
idée
↓
algorithme
↓
code source
↓
compilation
↓
programme exécutable
Code: Select all
programme exécutable
↓
code machine
↓
assembleur
↓
logique du programme
↓
algorithmes
↓
comportement général
Le reverse engineer reconstruit plutôt une représentation équivalente permettant de comprendre ce que fait le programme.
Par exemple, un programme C++ peut contenir :
Code: Select all
if (password == 1234)
{
AccessGranted();
}
Code: Select all
cmp eax, 1234
jne failed
call AccessGranted
2. Pourquoi faire du reverse engineering ?
Le reverse engineering peut avoir de nombreux objectifs légitimes.
Comprendre un logiciel
Il est parfois nécessaire d'étudier un logiciel lorsque :
- le code source n'est plus disponible ;
- le développeur original n'est plus présent ;
- la documentation est insuffisante ;
- le programme utilise un format inconnu ;
- on souhaite comprendre son architecture.
Le reverse engineering peut être utilisé afin de permettre à deux logiciels ou systèmes différents de communiquer.
Un programme peut par exemple utiliser :
- un format de fichier propriétaire ;
- un protocole réseau non documenté ;
- une structure de données interne ;
- une API non documentée.
Sécurité informatique
Le reverse engineering est extrêmement important en sécurité informatique.
Il permet notamment :
- d'analyser une vulnérabilité ;
- de comprendre un crash ;
- d'étudier un programme inconnu ;
- de rechercher un comportement dangereux ;
- de vérifier les protections d'un programme ;
- d'analyser du code malveillant.
Les analystes de malware utilisent fréquemment le reverse engineering pour comprendre :
- comment le programme démarre ;
- quels fichiers il crée ;
- quelles clés de registre il modifie ;
- quelles fonctions Windows il utilise ;
- comment il communique ;
- comment il persiste sur le système ;
- comment il cache son comportement.
Audit de sécurité
Un binaire peut également être analysé afin d'identifier des erreurs de programmation ou des vulnérabilités.
Quelques exemples :
- mauvaise validation de taille ;
- buffer overflow ;
- mauvaise gestion mémoire ;
- droits excessifs ;
- erreurs cryptographiques ;
- logique d'authentification incorrecte.
Le reverse engineering peut servir à examiner la manière dont une application utilise la cryptographie.
Il est important de distinguer deux choses :
- le secret d'une clé cryptographique ;
- le secret d'un algorithme.
En revanche, certaines applications utilisent des algorithmes propriétaires ou des implémentations particulières qu'un analyste peut chercher à comprendre.
DRM et protections logicielles
Les systèmes de Digital Rights Management cherchent à contrôler l'utilisation ou la copie de contenu numérique.
Le reverse engineering peut être utilisé pour comprendre le fonctionnement de ces mécanismes.
Cela ne signifie pas que leur contournement soit nécessairement légal.
Le cadre juridique dépend du pays, du type de logiciel, du contrat de licence et de l'objectif de l'analyse.
3. Reverse engineering logiciel
Le reverse engineering logiciel peut être séparé en plusieurs niveaux.
Reverse de haut niveau
À haut niveau, on cherche surtout à comprendre :
- l'architecture générale ;
- les composants ;
- les interactions entre modules ;
- les fichiers utilisés ;
- les dépendances ;
- le fonctionnement global.
Des outils d'observation système peuvent déjà fournir beaucoup d'informations.
Reverse de bas niveau
À bas niveau, on étudie directement :
- le code machine ;
- l'assembleur ;
- les registres ;
- la pile ;
- la mémoire ;
- les appels de fonctions ;
- les structures binaires.
4. System-Level Reversing
Le system-level reversing consiste à observer le programme depuis l'extérieur.
On cherche à comprendre son comportement vis-à-vis du système d'exploitation.
On peut par exemple surveiller :
- les fichiers ouverts ;
- les fichiers créés ;
- les clés de registre consultées ;
- les processus créés ;
- les threads ;
- les DLL chargées ;
- les connexions réseau ;
- les handles ;
- les accès mémoire.
Par exemple, avant même d'ouvrir un programme dans un désassembleur, on peut découvrir qu'il :
Code: Select all
ouvre config.dat
charge winhttp.dll
crée un thread
contacte un serveur
écrit une valeur dans le registre
5. Code-Level Reversing
Le code-level reversing consiste à analyser directement le code du programme.
On travaille alors principalement avec :
- le code machine ;
- l'assembleur ;
- un désassembleur ;
- un debugger ;
- éventuellement un décompilateur.
Par exemple :
Code: Select all
mov eax, [rcx]
add eax, 5
cmp eax, 25
jne failed
Code: Select all
value = *ptr + 5;
if (value != 25)
{
goto failed;
}
6. Fondations nécessaires
Assembleur
L'assembleur est indispensable pour comprendre le code produit par le compilateur.
Les notions importantes comprennent :
- registres ;
- mov ;
- lea ;
- push / pop ;
- cmp ;
- test ;
- jmp ;
- jcc ;
- call ;
- ret ;
- arithmétique ;
- opérations binaires ;
- accès mémoire.
Code: Select all
RAX
RBX
RCX
RDX
RSI
RDI
RSP
RBP
R8
R9
R10
R11
R12
R13
R14
R15
Un programme ne fonctionne pas seul.
Il dépend du système d'exploitation.
Pour faire du reverse sous Windows, il est utile de comprendre :
- processus ;
- threads ;
- mémoire virtuelle ;
- DLL ;
- handles ;
- fichiers ;
- registre ;
- API Windows ;
- exceptions ;
- permissions ;
- tokens ;
- chargement des exécutables.
Compilateurs
Le code analysé a généralement été transformé par un compilateur.
Le reverse engineer doit donc comprendre que :
Code: Select all
code source != assembleur direct
- réorganiser le code ;
- supprimer des variables ;
- utiliser des registres ;
- fusionner des expressions ;
- dérouler des boucles ;
- inline des fonctions ;
- supprimer du code inutile.
Optimisations
Un programme compilé en Debug est généralement plus facile à analyser.
Un programme compilé en Release peut contenir de nombreuses optimisations.
Exemple simple :
Code: Select all
int x = 5;
int y = x * 2;
return y;
Code: Select all
mov eax, 10
ret
7. Machines virtuelles et bytecode
Tous les programmes ne sont pas directement compilés en code machine natif.
Certains langages produisent du bytecode exécuté par une machine virtuelle.
Exemples :
- Java ;
- .NET ;
- certaines VM propriétaires.
Le bytecode conserve parfois davantage d'informations proches du code source.
Un programme natif C/C++ peut être plus difficile à reconstruire car beaucoup d'informations disparaissent lors de la compilation.
8. Le processus de reverse engineering
Il n'existe pas une seule méthode universelle.
Cependant, un processus classique ressemble à ceci.
Étape 1 : observation
Avant de lire le code, observer le programme.
Questions utiles :
- que fait-il ?
- quels fichiers utilise-t-il ?
- quelles DLL charge-t-il ?
- crée-t-il des processus ?
- utilise-t-il le réseau ?
- demande-t-il des privilèges ?
L'analyse statique consiste à analyser le programme sans l'exécuter.
On peut examiner :
- les chaînes de caractères ;
- les imports ;
- les sections ;
- les fonctions ;
- les références croisées ;
- le code désassemblé.
L'analyse dynamique consiste à observer le programme pendant son exécution.
On peut :
- placer des breakpoints ;
- exécuter instruction par instruction ;
- observer les registres ;
- observer la pile ;
- inspecter la mémoire ;
- suivre les appels de fonctions.
On transforme progressivement l'assembleur en logique haut niveau.
Par exemple :
Code: Select all
cmp ecx, 0
je empty
mov eax, 1
ret
empty:
xor eax, eax
ret
Code: Select all
if (value == 0)
return 0;
else
return 1;
Une hypothèse doit être testée.
On peut par exemple :
- placer un breakpoint ;
- modifier une entrée ;
- observer une valeur ;
- comparer plusieurs exécutions ;
- suivre une fonction.
9. Les outils du reverse engineer
Outils de monitoring système
Ces outils observent le comportement global du programme.
Ils permettent notamment d'étudier :
- fichiers ;
- registre ;
- processus ;
- threads ;
- réseau ;
- DLL ;
- handles.
Désassembleurs
Un désassembleur transforme du code machine en assembleur lisible.
Le programme contient réellement des octets.
Par exemple :
Code: Select all
48 89 C8
Code: Select all
mov rax, rcx
Exemples d'outils :
- Ghidra ;
- IDA ;
- Binary Ninja ;
- radare2.
Un debugger permet d'exécuter le programme sous contrôle.
Il permet notamment :
- de mettre des breakpoints ;
- de faire du step into ;
- de faire du step over ;
- d'observer les registres ;
- de lire la mémoire ;
- de voir la pile ;
- de suivre les appels ;
- d'arrêter l'exécution à une adresse précise.
WinDbg est particulièrement puissant pour Windows et le kernel.
Décompilateurs
Un décompilateur tente de reconstruire du pseudo-code haut niveau à partir du binaire.
Exemple :
Code: Select all
mov eax, ecx
imul eax, eax, 2
add eax, 5
ret
Code: Select all
return value * 2 + 5;
Le pseudo-code n'est pas le code source original.
Le décompilateur peut :
- se tromper sur les types ;
- mal identifier certaines structures ;
- mal nommer les variables ;
- mal reconstruire certaines conditions.
10. Différence entre désassembleur, debugger et décompilateur
Désassembleur
Il transforme le code machine en assembleur.
Code: Select all
machine code
↓
assembleur
Il exécute le programme sous contrôle et permet d'observer son état.
Code: Select all
programme en cours d'exécution
↓
registres / mémoire / pile / breakpoints
Il tente de reconstruire un pseudo-code plus proche du C/C++.
Code: Select all
code machine
↓
analyse
↓
pseudo-code
11. Analyse statique et analyse dynamique
Analyse statique
Le programme n'est pas exécuté.
Avantages :
- vue globale ;
- analyse des fonctions ;
- navigation dans les références ;
- observation des chaînes ;
- lecture des imports.
- certains comportements n'apparaissent qu'à l'exécution ;
- les données peuvent être calculées dynamiquement ;
- le programme peut être obfusqué.
Le programme est exécuté dans un debugger ou surveillé par des outils.
Avantages :
- on voit les vraies valeurs ;
- on observe les branches réellement prises ;
- on suit la mémoire ;
- on confirme rapidement une hypothèse.
- on ne voit qu'un chemin d'exécution à la fois ;
- certaines fonctions peuvent ne jamais être exécutées ;
- le programme peut détecter le debugger.
12. Le rôle de la curiosité et du raisonnement
Le reverse engineering demande beaucoup de raisonnement.
Il faut constamment poser des questions :
- pourquoi cette valeur est-elle chargée ?
- d'où vient ce pointeur ?
- pourquoi cette fonction est-elle appelée ?
- que représente cette structure ?
- pourquoi cette comparaison existe-t-elle ?
- qu'est-ce qui déclenche cette branche ?
Exemple :
Code: Select all
cmp byte ptr [rax], 0
je end
Code: Select all
rax pointe peut-être vers une chaîne
et le programme teste le caractère '\0'
13. Reverse engineering et légalité
Le reverse engineering n'est pas automatiquement illégal.
La légalité dépend :
- du pays ;
- du logiciel ;
- de l'objectif ;
- du contrat de licence ;
- des protections techniques ;
- des droits d'auteur ;
- des secrets industriels.
Le copyright protège notamment le code source et certaines parties d'un logiciel.
Comprendre le fonctionnement d'un programme n'est pas forcément équivalent à copier son code.
Secrets industriels
Certaines informations techniques peuvent être considérées comme des secrets industriels.
La manière dont ces informations ont été obtenues peut avoir une importance juridique.
Brevets
Certaines techniques peuvent être protégées par des brevets.
Le reverse engineering peut révéler leur fonctionnement, mais cela ne signifie pas nécessairement que leur reproduction soit autorisée.
Contrats de licence
Certaines licences logicielles contiennent des clauses interdisant ou limitant le reverse engineering.
Il faut donc vérifier les conditions d'utilisation du logiciel étudié.
DMCA
Aux États-Unis, le Digital Millennium Copyright Act contient notamment des dispositions concernant le contournement de certaines protections techniques.
Il existe cependant des exceptions et des situations particulières, notamment autour :
- de l'interopérabilité ;
- de la recherche en sécurité ;
- de certains audits ;
- de certaines activités réglementées.
14. Ce qu'un reverse engineer cherche réellement
Lorsqu'il analyse un programme, le reverse engineer cherche rarement à comprendre chaque instruction.
Le but est surtout d'identifier les éléments importants.
Par exemple :
- où commence la logique intéressante ;
- quelles fonctions traitent les données ;
- où sont effectuées les vérifications ;
- quelles API sont appelées ;
- quelles structures sont manipulées ;
- où sont les entrées utilisateur ;
- où se trouvent les décisions importantes.
Dans un gros programme, des millions d'instructions peuvent être exécutées.
Il faut apprendre à retrouver les quelques centaines ou quelques dizaines d'instructions réellement intéressantes pour l'objectif recherché.
15. Exemple simple de démarche
Imaginons un programme qui affiche :
Code: Select all
Access denied
Puis suivre les références vers cette chaîne.
On peut tomber sur quelque chose comme :
Code: Select all
cmp eax, 1
jne denied
call success
jmp end
denied:
lea rcx, "Access denied"
call print
Code: Select all
if (result == 1)
{
success();
}
else
{
print("Access denied");
}
Code: Select all
information visible
↓
référence dans le programme
↓
code assembleur
↓
reconstruction de la logique
Le reverse engineering consiste à comprendre un programme existant sans nécessairement disposer de son code source.
Il existe deux grandes approches complémentaires :
- observer le comportement du programme ;
- analyser directement son code.
- assembleur ;
- architecture processeur ;
- systèmes d'exploitation ;
- compilation ;
- mémoire ;
- debugging.
- outils de monitoring système ;
- désassembleurs ;
- debuggers ;
- décompilateurs.
Le debugger montre leur exécution réelle.
Le décompilateur tente de reconstruire une logique haut niveau.
Un bon reverse engineer ne lit pas simplement les instructions une par une.
Il cherche à comprendre :
- les données ;
- les décisions ;
- les fonctions ;
- les interactions système ;
- les algorithmes ;
- la structure générale du programme.
Avant d'analyser ou de modifier un logiciel tiers, il faut toujours vérifier les droits, licences et lois applicables.
Conclusion
Le reverse engineering est une discipline située entre le développement bas niveau, l'architecture processeur, les systèmes d'exploitation et l'analyse logicielle.
L'assembleur fournit le langage permettant de lire ce que le processeur exécute réellement.
Les outils de monitoring permettent d'observer le programme depuis l'extérieur.
Les désassembleurs permettent de lire son code.
Les debuggers permettent de suivre son exécution.
Les décompilateurs permettent d'obtenir une représentation plus abstraite.
Mais l'outil le plus important reste le raisonnement.
Le but n'est pas seulement de savoir lire :
Code: Select all
mov
cmp
jmp
call
ret
Code: Select all
"voilà ce que le programme est en train de faire"
