Avec les langages de machine virtuelle basés sur le bytecode comme Java, VB.NET, C #, ActionScript 3.0, etc., vous entendez parfois à quel point il est facile de télécharger un décompilateur sur Internet, d'exécuter le bytecode à travers lui un bon moment, et souvent, trouver quelque chose pas trop loin du code source d'origine en quelques secondes. Soi-disant ce type de langage est particulièrement vulnérable à cela.
J'ai récemment commencé à me demander pourquoi vous n'en entendez pas plus à ce sujet concernant le code binaire natif, alors que vous savez au moins dans quelle langue il a été écrit à l'origine (et donc, dans quelle langue essayer de décompiler). Pendant longtemps, j'ai pensé que c'était simplement parce que le langage machine natif était tellement plus fou et plus complexe que le bytecode typique.
Mais à quoi ressemble le bytecode? Cela ressemble à ceci:
1000: 2A 40 F0 14
1001: 2A 50 F1 27
1002: 4F 00 F0 F1
1003: C9 00 00 F2
Et à quoi ressemble le code machine natif (en hexadécimal)? Cela ressemble bien sûr à ceci:
1000: 2A 40 F0 14
1001: 2A 50 F1 27
1002: 4F 00 F0 F1
1003: C9 00 00 F2
Et les instructions viennent d'un état d'esprit quelque peu similaire:
1000: mov EAX, 20
1001: mov EBX, loc1
1002: mul EAX, EBX
1003: push ECX
Donc, étant donné le langage pour essayer de décompiler un binaire natif en, disons C ++, qu'est-ce qui est si difficile? Les deux seules idées qui me viennent immédiatement à l'esprit sont 1) c'est beaucoup plus complexe que le bytecode, ou 2) le fait que les systèmes d'exploitation ont tendance à paginer les programmes et à disperser leurs morceaux pose trop de problèmes. Si l'une de ces possibilités est correcte, veuillez expliquer. Mais de toute façon, pourquoi n'en entendez-vous jamais parler?
REMARQUE
Je suis sur le point d'accepter l'une des réponses, mais je veux d'abord mentionner quelque chose. Presque tout le monde fait référence au fait que différentes parties du code source original peuvent correspondre au même code machine; les noms des variables locales sont perdus, vous ne savez pas quel type de boucle a été utilisé à l'origine, etc.
Cependant, des exemples comme les deux qui viennent d'être mentionnés sont plutôt triviaux à mes yeux. Cependant, certaines des réponses tendent à dire que la différence entre le code machine et la source d'origine est considérablement plus que quelque chose d'aussi trivial.
Mais par exemple, lorsqu'il s'agit de choses comme les noms de variables locales et les types de boucles, le bytecode perd également ces informations (au moins pour ActionScript 3.0). J'ai déjà récupéré ces trucs dans un décompilateur auparavant, et je ne me souciais pas vraiment si une variable était appelée strMyLocalString:String
ou loc1
. Je pouvais toujours regarder dans cette petite portée locale et voir comment il était utilisé sans trop de problèmes. Et une for
boucle est à peu près la même chose exacte qu'unwhile
boucle, si vous y pensez. De plus, même lorsque j'exécutais la source via irrFuscator (qui, contrairement à secureSWF, ne fait pas beaucoup plus que simplement randomiser les noms de variables et de fonctions des membres), il semblait toujours que vous pouviez simplement commencer à isoler certaines variables et fonctions dans des classes plus petites, figure comment ils sont utilisés, attribuez-leur vos propres noms et travaillez à partir de là.
Pour que cela soit un gros problème, le code machine devrait perdre beaucoup plus d'informations que cela, et certaines des réponses vont dans ce sens.
la source
Réponses:
À chaque étape de la compilation, vous perdez des informations irrécupérables. Plus vous perdez d'informations de la source d'origine, plus il est difficile de décompiler.
Vous pouvez créer un décompilateur utile pour le code d'octet car beaucoup plus d'informations sont conservées à partir de la source d'origine que lors de la production du code machine cible final.
La première étape d'un compilateur est de transformer la source en une représentation intermédiaire souvent représentée sous forme d'arbre. Traditionnellement, cet arbre ne contient pas d'informations non sémantiques telles que des commentaires, des espaces blancs, etc. Une fois ces informations supprimées, vous ne pouvez pas récupérer la source d'origine de cet arbre.
L'étape suivante consiste à rendre l'arbre dans une certaine forme de langage intermédiaire qui facilite les optimisations. Il y a pas mal de choix ici et chaque infrastructure de compilateur a la sienne. En règle générale, cependant, des informations telles que les noms de variables locales, les grandes structures de flux de contrôle (comme si vous avez utilisé une boucle for ou while) sont perdues. Certaines optimisations importantes se produisent généralement ici, propagation constante, mouvement de code invariant, alignement de fonctions, etc.
Une étape après cela consiste à générer les instructions réelles de la machine qui pourraient impliquer ce que l'on appelle une optimisation "à judas" qui produisent une version optimisée des modèles d'instructions courants.
À chaque étape, vous perdez de plus en plus d'informations jusqu'à ce que, à la fin, vous en perdiez tellement qu'il devient impossible de récupérer quoi que ce soit ressemblant au code d'origine.
Le code octet, en revanche, enregistre généralement les optimisations intéressantes et transformatrices jusqu'à la phase JIT (le compilateur juste à temps) lorsque le code machine cible est produit. Le byte-code contient beaucoup de métadonnées telles que les types de variables locales, la structure de classe, pour permettre au même byte-code d'être compilé en plusieurs codes machine cible. Toutes ces informations ne sont pas nécessaires dans un programme C ++ et sont ignorées dans le processus de compilation.
Il existe des décompilateurs pour divers codes machine cibles, mais ils ne produisent souvent pas de résultats utiles (quelque chose que vous pouvez modifier puis recompiler) car une trop grande partie de la source d'origine est perdue. Si vous disposez d'informations de débogage pour l'exécutable, vous pouvez faire un travail encore meilleur; mais, si vous avez des informations de débogage, vous avez probablement aussi la source d'origine.
la source
La perte d'informations, comme le soulignent les autres réponses, est un point, mais ce n'est pas le casse-tête. Après tout, vous ne vous attendez pas à ce que le programme d'origine revienne, vous voulez juste une représentation dans un langage de haut niveau. Si le code est en ligne, vous pouvez simplement le laisser, ou factoriser automatiquement les calculs courants. Vous pouvez en principe annuler de nombreuses optimisations. Mais il y a certaines opérations qui sont en principe irréversibles (sans une quantité infinie de calcul au moins).
Par exemple, les branches peuvent devenir des sauts calculés. Code comme celui-ci:
pourrait être compilé en (désolé que ce ne soit pas un vrai assembleur):
Maintenant, si vous savez que x peut être 1 ou 2, vous pouvez regarder les sauts et inverser cela facilement. Mais qu'en est-il de l'adresse 0x1012? Devriez-vous en créer un
case 3
également? Vous devrez suivre l'ensemble du programme dans le pire des cas pour déterminer les valeurs autorisées. Pire encore, vous devrez peut-être considérer toutes les entrées utilisateur possibles! Au cœur du problème, vous ne pouvez pas distinguer les données et les instructions.Cela étant dit, je ne serais pas entièrement pessimiste. Comme vous l'avez peut-être remarqué dans `` l'assembleur '' ci-dessus, si x vient de l'extérieur et n'est pas garanti à 1 ou 2, vous avez essentiellement un mauvais bug qui vous permet de sauter n'importe où. Mais si votre programme est exempt de ce type de bogue, il est beaucoup plus facile de le raisonner. (Ce n'est pas par hasard que les langages intermédiaires "sûrs" comme CLR IL ou le bytecode Java sont beaucoup plus faciles à décompiler, même en mettant de côté les métadonnées.) Ainsi, dans la pratique, il devrait être possible de décompiler certains bons comportementsprogrammes. Je pense à des routines de style individuelles et fonctionnelles, qui n'ont pas d'effets secondaires et des entrées bien définies. Je pense qu'il y a quelques décompilateurs qui peuvent donner un pseudocode pour des fonctions simples, mais je n'ai pas beaucoup d'expérience avec de tels outils.
la source
La raison pour laquelle le code machine ne peut pas être facilement converti en code source d'origine est que beaucoup d'informations sont perdues lors de la compilation. Les méthodes et les classes non exportées peuvent être intégrées, les noms de variables locales sont perdus, les noms de fichiers et les structures sont entièrement perdus, les compilateurs peuvent effectuer des optimisations non évidentes. Une autre raison est que plusieurs fichiers source différents pourraient produire exactement le même assemblage.
Par exemple:
Peut être compilé pour:
Mon assemblage est assez rouillé, mais si le compilateur peut vérifier qu'une optimisation peut être effectuée avec précision, il le fera. Cela est dû au fait que le binaire compilé n'a pas besoin de connaître les noms
DoSomething
etAdd
, ainsi que le fait que laAdd
méthode a deux paramètres nommés, le compilateur sait également que laDoSomething
méthode retourne essentiellement une constante, et il pourrait aligner à la fois l'appel de méthode et le méthode elle-même.Le but du compilateur est de créer un assembly, pas un moyen de regrouper des fichiers source.
la source
ret
et dites simplement que vous supposiez la convention d'appel C.Les principes généraux ici sont les correspondances plusieurs à un et le manque de représentants canoniques.
Pour un exemple simple de phénomène plusieurs-à-un, vous pouvez penser à ce qui se passe lorsque vous prenez une fonction avec des variables locales et la compilez en code machine. Toutes les informations sur les variables sont perdues car elles deviennent simplement des adresses mémoire. Quelque chose de similaire se produit pour les boucles. Vous pouvez prendre une boucle
for
ouwhile
et si elles sont structurées correctement, vous pouvez obtenir un code machine identique avec desjump
instructions.Cela soulève également le manque de représentants canoniques du code source d'origine pour les instructions du code machine. Lorsque vous essayez de décompiler des boucles, comment mappez-vous les
jump
instructions sur les constructions en boucle? Faites-vous desfor
boucles ou deswhile
boucles.Le problème est encore exacerbé par le fait que les compilateurs modernes effectuent diverses formes de pliage et de doublure. Donc, au moment où vous arrivez au code machine, il est pratiquement impossible de dire de quelles constructions de haut niveau le code machine de bas niveau provient.
la source