Un décompilateur est un programme informatique qui traduit un fichier exécutable en code source de haut niveau . Il fait donc le contraire d'un compilateur classique , qui traduit un langage de haut niveau en langage de bas niveau. Alors que les désassembleurs traduisent un exécutable en langage assembleur , les décompilateurs vont plus loin et traduisent le code dans un langage de niveau supérieur tel que C ou Java , ce qui nécessite des techniques plus sophistiquées. Les décompilateurs sont généralement incapables de reconstruire parfaitement le code source d'origine, et produisent donc fréquemment du code obscurci . Néanmoins, ils restent un outil important dans la rétro-ingénierie des logiciels informatiques .
Introduction
Le terme décompilateur est généralement appliqué à un programme qui traduit des programmes exécutables (la sortie d'un compilateur ) en code source dans un langage de niveau relativement élevé qui, une fois compilé, produira un exécutable dont le comportement est le même que le programme exécutable d'origine. En comparaison, un désassembleur traduit un programme exécutable en langage assembleur (et un assembleur pourrait être utilisé pour le réassembler en un programme exécutable).
La décompilation est l'acte d'utiliser un décompilateur, bien que le terme puisse également faire référence à la sortie d'un décompilateur. Il peut être utilisé pour la récupération de code source perdu, et est également utile dans certains cas pour la sécurité informatique , l'interopérabilité et la correction d'erreurs . Le succès de la décompilation dépend de la quantité d'informations présentes dans le code décompilé et de la sophistication de l'analyse effectuée sur celui-ci. Les formats de bytecode utilisés par de nombreuses machines virtuelles (telles que la machine virtuelle Java ou le Common Language Runtime .NET Framework ) incluent souvent des métadonnées étendues et des fonctionnalités de haut niveau qui rendent la décompilation tout à fait réalisable. L'application de données de débogage , c'est-à-dire des symboles de débogage, peut permettre de reproduire les noms d'origine des variables et des structures et même les numéros de ligne. Le langage machine sans ces métadonnées ou données de débogage est beaucoup plus difficile à décompiler.
Certains compilateurs et outils de post-compilation produisent du code obscurci (c'est-à-dire qu'ils tentent de produire un résultat très difficile à décompiler ou qui se décompile en un résultat déroutant). Cela permet de rendre plus difficile la rétro-ingénierie de l'exécutable.
Alors que les décompilateurs sont normalement utilisés pour (re)créer du code source à partir d'exécutables binaires, il existe également des décompilateurs pour transformer des fichiers de données binaires spécifiques en sources lisibles et modifiables par l'homme.
Le niveau de réussite atteint par les décompilateurs peut être affecté par divers facteurs. Il s'agit notamment du niveau d'abstraction du langage source, si le code objet contient des informations explicites sur la structure de la classe, cela facilite le processus de décompilation. Les informations descriptives, en particulier avec les détails de nommage, accélèrent également le travail du compilateur. De plus, un code moins optimisé est plus rapide à décompiler car l'optimisation peut entraîner une plus grande déviation par rapport au code d'origine.
Conception
Les décompilateurs peuvent être considérés comme composés d'une série de phases dont chacune contribue à des aspects spécifiques du processus global de décompilation.
Chargeur
La première phase de décompilation charge et analyse le code machine d'entrée ou le format de fichier binaire du programme en langage intermédiaire . Elle doit être capable de découvrir des faits de base sur le programme d'entrée, tels que l'architecture (Pentium, PowerPC, etc.) et le point d'entrée. Dans de nombreux cas, elle doit être capable de trouver l'équivalent de la fonction d'un programme C , qui est le début du code écrit par l'utilisateur . Cela exclut le code d'initialisation d'exécution, qui ne doit pas être décompilé si possible. Si elles sont disponibles, les tables de symboles et les données de débogage sont également chargées. Le frontal peut être capable d'identifier les bibliothèques utilisées même si elles sont liées au code, cela fournira des interfaces de bibliothèque. S'il peut déterminer le ou les compilateurs utilisés, il peut fournir des informations utiles pour identifier les idiomes du code. main
Démontage
La phase logique suivante est le désassemblage des instructions du code machine en une représentation intermédiaire indépendante de la machine (IR). Par exemple, l' instruction machine Pentium
déplacer eax , [ ebx + 0x04 ]
pourrait être traduit en IR
eax : = m [ ebx + 4 ] ;
Expressions idiomatiques
Les séquences de code machine idiomatiques sont des séquences de code dont la sémantique combinée n'est pas immédiatement apparente à partir de la sémantique individuelle des instructions. Que ce soit dans le cadre de la phase de désassemblage ou dans le cadre d'analyses ultérieures, ces séquences idiomatiques doivent être traduites en IR équivalent connu. Par exemple, le code assembleur x86 :
cdq eax ; edx est défini sur l'extension de signe≠edi,edi +(tex)push xor eax , edx sub eax , edx
pourrait être traduit par
eax := abs(eax);
Certaines séquences idiomatiques sont indépendantes de la machine ; d'autres n'impliquent qu'une seule instruction. Par exemple, efface le registre (le met à zéro). Cela peut être implémenté avec une règle de simplification indépendante de la machine, telle que . xoreax,eaxeaxa = 0
En général, il est préférable de retarder la détection des séquences idiomatiques, si possible, à des étapes ultérieures qui sont moins affectées par l'ordre des instructions. Par exemple, la phase de planification des instructions d'un compilateur peut insérer d'autres instructions dans une séquence idiomatique ou modifier l'ordre des instructions dans la séquence. Un processus de recherche de motifs dans la phase de désassemblage ne reconnaîtrait probablement pas le motif modifié. Les phases ultérieures regroupent les expressions d'instructions en expressions plus complexes et les modifient en une forme canonique (standardisée), ce qui augmente la probabilité que même l'idiome modifié corresponde à un motif de niveau supérieur plus tard dans la décompilation.
Il est particulièrement important de reconnaître les idiomes du compilateur pour les appels de sous-routines , la gestion des exceptions et les instructions switch . Certains langages prennent également en charge de manière étendue les chaînes ou les entiers longs .
Analyse du programme
Diverses analyses de programmes peuvent être appliquées à l'IR. En particulier, la propagation d'expressions combine la sémantique de plusieurs instructions dans des expressions plus complexes. Par exemple,
déplacer eax , [ ebx + 0x04 ] ajouter eax , [ ebx + 0x08 ] sous-titrer [ ebx + 0x0C ], eax
pourrait entraîner l'IR suivant après la propagation de l'expression :
m[ebx+12] := m[ebx+12] - (m[ebx+4] + m[ebx+8]);
L'expression résultante ressemble davantage à un langage de haut niveau et a également éliminé l'utilisation du registre machine eax. Des analyses ultérieures pourraient éliminer le ebxregistre.
Analyse des flux de données
Les emplacements où le contenu des registres est défini et utilisé doivent être tracés à l'aide de l'analyse du flux de données . La même analyse peut être appliquée aux emplacements utilisés pour les données temporaires et locales. Un nom différent peut alors être formé pour chaque ensemble connecté de définitions et d'utilisations de valeurs. Il est possible que le même emplacement de variable locale ait été utilisé pour plusieurs variables dans différentes parties du programme d'origine. Pire encore, il est possible que l'analyse du flux de données identifie un chemin par lequel une valeur peut circuler entre deux de ces utilisations même si cela ne se produirait jamais ou n'aurait jamais d'importance en réalité. Cela peut dans les cas graves conduire à devoir définir un emplacement comme une union de types. Le décompilateur peut permettre à l'utilisateur de rompre explicitement ces dépendances non naturelles, ce qui conduira à un code plus clair. Cela signifie bien sûr qu'une variable est potentiellement utilisée sans être initialisée et indique donc un problème dans le programme d'origine.
Analyse de type
Un bon décompilateur de code machine effectuera une analyse de type. Ici, la façon dont les registres ou les emplacements mémoire sont utilisés entraîne des contraintes sur le type possible de l'emplacement. Par exemple, une andinstruction implique que l'opérande est un entier ; les programmes n'utilisent pas une telle opération sur les valeurs à virgule flottante (sauf dans le code de bibliothèque spécial) ou sur les pointeurs . Une addinstruction entraîne trois contraintes, car les opérandes peuvent être à la fois des entiers, ou un entier et un pointeur (avec des résultats d'entier et de pointeur respectivement ; la troisième contrainte provient de l'ordre des deux opérandes lorsque les types sont différents).
Il est possible de reconnaître différentes expressions de haut niveau qui déclenchent la reconnaissance de structures ou de tableaux. Cependant, il est difficile de distinguer de nombreuses possibilités, en raison de la liberté que le code machine ou même certains langages de haut niveau tels que C permettent avec les conversions et l'arithmétique des pointeurs.
L'exemple de la section précédente pourrait donner lieu au code de haut niveau suivant :
struct T1 * ebx ; struct T1 { int v0004 ; int v0008 ; int v000C ; }; ebx -> v000C -= ebx -> v0004 + ebx -> v0008 ;
Structuration
L'avant-dernière phase de décompilation implique la structuration de l'IR en constructions de niveau supérieur telles que whiledes boucles et if/then/elsedes instructions conditionnelles. Par exemple, le code machine
xor eax , eax l0002 : ou ebx , ebx jge l0003 ajouter eax , [ ebx ] déplacer ebx , [ ebx + 0x4 ] jmp l0002 l0003 : déplacer [ 0x10040000 ], eax
pourrait être traduit par :
eax = 0 ; tant que ( ebx < 0 ) { eax += ebx -> v0000 ; ebx = ebx -> v0004 ; } v10040000 = eax ;
Le code non structuré est plus difficile à traduire en code structuré que le code déjà structuré. Les solutions incluent la réplication d'une partie du code ou l'ajout de variables booléennes.
Génération de code
La phase finale est la génération du code de haut niveau dans le back-end du décompilateur. Tout comme un compilateur peut avoir plusieurs back-ends pour générer du code machine pour différentes architectures, un décompilateur peut avoir plusieurs back-ends pour générer du code de haut niveau dans différents langages de haut niveau.
Juste avant la génération du code, il peut être souhaitable de permettre une édition interactive de l'IR, peut-être en utilisant une forme d' interface utilisateur graphique . Cela permettrait à l'utilisateur de saisir des commentaires et des noms de variables et de fonctions non génériques. Cependant, ceux-ci sont presque aussi faciles à saisir lors d'une édition post-décompilation. L'utilisateur peut vouloir modifier des aspects structurels, comme convertir une whileboucle en forboucle. Ceux-ci sont moins facilement modifiables avec un simple éditeur de texte, bien que les outils de refactorisation du code source puissent aider dans ce processus. L'utilisateur peut avoir besoin de saisir des informations qui n'ont pas été identifiées pendant la phase d'analyse du type, par exemple en modifiant une expression de mémoire en une expression de tableau ou de structure. Enfin, un IR incorrect peut devoir être corrigé ou des modifications apportées pour rendre le code de sortie plus lisible.
Autres techniques
Des décompilateurs utilisant des réseaux neuronaux ont été développés. Un tel décompilateur peut être formé par apprentissage automatique pour améliorer sa précision au fil du temps.
Légalité
La majorité des programmes informatiques sont protégés par les lois sur le droit d'auteur . Bien que la portée précise de ce qui est couvert par le droit d'auteur diffère d'une région à l'autre, la loi sur le droit d'auteur confère généralement à l'auteur (le ou les programmeurs ou l'employeur) un ensemble de droits exclusifs sur le programme. Ces droits incluent le droit de faire des copies, y compris des copies dans la mémoire vive de l'ordinateur (à moins que la création d'une telle copie ne soit essentielle à l'utilisation du programme). Étant donné que le processus de décompilation implique de faire plusieurs copies de ce type, il est généralement interdit sans l'autorisation du titulaire du droit d'auteur. Cependant, comme la décompilation est souvent une étape nécessaire pour parvenir à l'interopérabilité des logiciels , les lois sur le droit d'auteur aux États-Unis et en Europe autorisent la décompilation dans une certaine mesure.
Aux États-Unis, la défense de l'utilisation équitable du droit d'auteur a été invoquée avec succès dans des affaires de décompilation. Par exemple, dans l'affaire Sega v. Accolade , le tribunal a jugé qu'Accolade pouvait légalement procéder à une décompilation afin de contourner le mécanisme de verrouillage du logiciel utilisé par les consoles de jeu de Sega. De plus, le Digital Millennium Copyright Act (PUBLIC LAW 105–304 ) prévoit des exemptions appropriées pour les tests et évaluations de sécurité dans l'article 1201(i) et pour l'ingénierie inverse dans l'article 1201(f).
En Europe, la directive sur les logiciels de 1991 prévoit explicitement un droit à la décompilation afin de parvenir à l'interopérabilité. Fruit d'un débat houleux entre, d'un côté, les défenseurs de la protection des logiciels et, de l'autre, les universitaires et les développeurs de logiciels indépendants, l'article 6 autorise la décompilation uniquement si un certain nombre de conditions sont remplies :
- Premièrement, une personne ou une entité doit disposer d’une licence pour utiliser le programme à décompiler.
- Deuxièmement, la décompilation doit être nécessaire pour obtenir l'interopérabilité avec le programme cible ou d'autres programmes. Les informations d'interopérabilité ne doivent donc pas être facilement disponibles, par exemple via des manuels ou la documentation API . Il s'agit d'une limitation importante. La nécessité doit être prouvée par le décompilateur. Le but de cette limitation importante est principalement d'inciter les développeurs à documenter et à divulguer les informations d'interopérabilité de leurs produits.
- Troisièmement, le processus de décompilation doit, si possible, se limiter aux parties du programme cible concernées par l'interopérabilité. L'un des objectifs de la décompilation étant de comprendre la structure du programme, cette troisième limitation peut être difficile à respecter. Là encore, la charge de la preuve incombe au décompilateur.
En outre, l’article 6 prescrit que les informations obtenues par décompilation ne peuvent être utilisées à d’autres fins et qu’elles ne peuvent être communiquées à des tiers.
Dans l'ensemble, le droit de décompilation prévu par l'article 6 codifie ce qui est censé être une pratique courante dans l'industrie du logiciel. On connaît peu de poursuites judiciaires européennes découlant du droit de décompilation. Cela pourrait être interprété comme signifiant l'une des trois choses suivantes :
- ) le droit de décompilation n'est pas utilisé fréquemment et il se peut donc qu'il ait été inutile,
- ) le droit de décompilation fonctionne bien et offre une sécurité juridique suffisante pour ne pas donner lieu à des litiges juridiques ou
- ) la décompilation illégale passe largement inaperçue.
Dans un rapport de 2000 concernant la mise en œuvre de la directive sur les logiciels par les États membres européens, la Commission européenne semble soutenir la deuxième interprétation.