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’ob fuscation du code produit afin de compliquer les tentatives de rétro-ingénierie. Il paraît donc source à formatage normalisé Arbre de syntaxe abstrait Abstraction Normalisation Arbre de syntaxe abstrait normalisé Résolution/suppression de liens d’appel Détermination des dépendances Graphe de syntaxe avec liens d’appel Graphe de dépendance Fig. 3.1– Représentations et opérations de transformation Analyse lexicale
Séquences de lexèmes
Calcul de la moyenne */ javadoc[Calcul de la moyenne] static int calculeMoyenne(double[] tab) { double somme = 0.0; for (int i=0; i < tab.length; i++) somme += tab[i]; return somme / tab.length; } Code source static, int, functionName[calculeMoyenne], lpar, double, lbra cket, rbracket, identifier[tab], rpar, lbra double, identifier[somme], eq, constant[0.0], semi for, lpar, int, identifier[i], eq, constant[0], semi, identifier[i], lt, identifier[tab], dotlength, semi, identifier[i], postinc, rpar identifier[somme], assignplus, identifier[tab], lbracket, identi f ier[i], rbracket, semi return, identifier[somme], div, identifier[tab], dotlength, semi rbra Forme lexemisée
Fig. 3.2– Un extrait de code source en Java (calcul de la moyenne des termes d’un tableau) et sa forme lexemisée préférable d’utiliser le code source original et de contrôler les opérations de normalisation sur celui-ci. Lorsque seule une forme compilée intermédiaire est disponible (étude d’un logiciel à source fermée),
des opérations de décompilation peuvent être tentées, avec plus ou moins de succès, ou alors des méthodes dynamiques de recherche de similarité sur des flux d’exécutions peuvent être menées. Dans le cadre de notre travail, nous nous intéressons exclusivement à des méthodes de recherche statique de similarité