Représentations intermédiaires du code source
Code source
Le code source d’un programme est une représentation textuelle humainement intelligible dont la construction obéit à un ensemble de règles définies par un dictionnaire de lexèmes, une grammaire ainsi qu’une sémantique. Après avoir été élaboré par un développeur, le code source fait généralement l’objet de transformations en représentations intermédiaires successives dans l’optique d’obtenir un code objet binaire directement interprétable par la machine. Un code source est généralement différencié d’un texte en langue naturelle par le fait que la grammaire et la sémantique rattachées au langage sont définies formellement, sans aucune possibilité d’ambiguïté. Cependant, l’existence de dictionnaires, tables de lexique-grammaires et graphes d’étiquetage lexical et sémantique encore incomplets pourrait être mis à profit pour la recherche de similitudes sur des langues naturelles. Un mot sur les formes compilées intermédiaires Nous nous intéressons à la recherche de similitudes au sein du code source original. Dans certains cas, il pourrait être judicieux de travailler sur des formes compilées intermédiaires disposant d’informations de liaison avec le code source original. Ainsi, par exemple, nous pouvons utiliser le code assembleur dérivé d’un code source C ou alors le bytecode obtenu par la compilation d’une unité de compilation Java. Ces représentations intermédiaires peuvent également utiliser des langages de haut niveau (tel que par exemple un compilateur de langage Eiffel [120] produisant des sorties en langage C ou Java). De telles formes compilées intermédiaires présentent l’avantage d’avoir subi certaines opérations de normalisation par le compilateur et peuvent permettre de supprimer certaines formes d’obfuscation du code source tel que l’ajout de code trivialement inutile. Toutefois, certaines formes d’optimisation du code (inlining, déroulage de boucle) peuvent à l’inverse avoir des effets obfuscatoires. Certains compilateurs ont même pour principal objectif l’obfuscation du code produit afin de compliquer les tentatives de rétro-ingénierie.
Séquences de lexèmes
Séquence de lexèmes bruts
Étant donnée la définition du lexique d’un langage, l’obtention d’une séquence de lexèmes à partir d’un code source est réalisée par une étape d’analyse lexicale. Généralement les lexèmes d’un langage peuvent être reconnus par des expressions rationnelles et donc par l’usage d’automates finis déterministes (AFD). Les analyseurs lexicaux à base d’AFD peuvent être générés automatiquement à partir d’une description des termes du lexique à l’aide d’outils tels que, par exemple, (F)lex [117] (génération d’analyseurs lexicaux en C) ou J(F)lex [118] (génération en Java). La figure 3.2 présente la forme lexemisée d’une fonction Java.
Abstraction
Les opérations d’abstraction sur les séquences portent principalement sur la suppression des noms d’identificateurs. Il est possible également d’abstraire les noms de types voire également certains types primitifs. Les commentaires peuvent aussi être supprimés. Ces opérations permettent de détecter des similitudes malgré le renommage de variables, le changement de types ou l’édition de commentaires. Toutefois, plutôt que de définir une fonction booléenne de similarité sur des lexèmes, il peut être intéressant de conserver des lexèmes paramétrés par ces informations concrètes afin de définir une distance de similarité plus précise entre lexèmes paramétrés.