Outils d`analyse statique
Transcription
Outils d`analyse statique
Outils d’analyse statique1 Vincent Mathieu 25 août 2001 1 Cette 1er cycle. recherche a pu être menée à terme grâce à une bourse CRSNG du Table des matières 1 Introduction 1.1 Motivation . . . . . . . . . . . . . 1.2 Code malicieux . . . . . . . . . . 1.2.1 Vers . . . . . . . . . . . . 1.2.2 Virus . . . . . . . . . . . . 1.2.3 Chevaux de Troie . . . . . 1.2.4 Bombes logiques . . . . . 1.2.5 Code mobile hostile . . . . 1.2.6 Portes arrières . . . . . . . 1.2.7 Erreurs de programmation 1.3 Différentes approches d’analyse de 1.3.1 Analyse dynamique . . . . 1.3.2 Analyse statique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . programmes . . . . . . . . . . . . . . . . 2 Analyse statique 2.1 Représentation du programme . . . . . . . . 2.1.1 Arbre syntaxique . . . . . . . . . . . 2.1.2 Graphe de flot de contrôle . . . . . . 2.1.3 Graphe de dépendance de contrôle . 2.1.4 Analyse du flot de données . . . . . . 2.1.5 Graphe de dépendance de données . 2.1.6 Graphe de dépendance de programme 2.2 Découpage . . . . . . . . . . . . . . . . . . . 2.2.1 Résultat du découpage . . . . . . . . 2.2.2 Utilisation . . . . . . . . . . . . . . . 2.2.3 Comment faire . . . . . . . . . . . . 2.3 Vérification . . . . . . . . . . . . . . . . . . i . . . . . . . . . . . . ou . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . du système . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 2 2 2 3 3 3 3 4 4 5 5 . . . . . . . . . . . . 7 7 8 8 9 9 11 11 13 15 15 16 18 3 Choix des outils 3.1 Bibliographie des outils trouvés . . . . . . . . . . . . . . 3.2 Fonctionnalités de ces outils . . . . . . . . . . . . . . . . 3.2.1 Métriques . . . . . . . . . . . . . . . . . . . . . . 3.2.2 Détection d’erreurs pouvant survenir à l’exécution 3.2.3 Graphes . . . . . . . . . . . . . . . . . . . . . . . 3.2.4 Découpage . . . . . . . . . . . . . . . . . . . . . . 3.2.5 Politiques de sécurité . . . . . . . . . . . . . . . . 3.3 Jeu d’essai . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4 Critères de sélection . . . . . . . . . . . . . . . . . . . . 4 Samcots 4.1 Fonctionnalités et méthode 4.2 Guide d’utilisation . . . . 4.3 Essai de l’outil . . . . . . 4.4 Avantages et désavantages utilisée . . . . . . . . . . . . . . . 5 Wasp 5.1 Fonctionnalités . . . . . . . . 5.2 Configuration . . . . . . . . . 5.3 Graphe d’appels de méthodes 5.4 Guide d’utilisation . . . . . . 5.5 Interprétation des messages . 5.6 Essai de l’outil . . . . . . . . 5.6.1 demo.java . . . . . . . 5.6.2 demo2.java . . . . . . 5.6.3 demo3.java . . . . . . 5.6.4 demo4.java . . . . . . 5.6.5 demo5.java . . . . . . 5.6.6 demo6.java . . . . . . 5.6.7 demo7.java . . . . . . 5.7 Avantages et désavantages . . 6 CodeSurfer 6.1 Fonctionnalités . . . 6.2 Efficacité . . . . . . . 6.2.1 Faux négatifs 6.2.2 Faux positifs . . . . . . . . . . . . . . . . . . . . . ii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 21 24 24 24 25 25 26 26 27 . . . . 28 28 29 31 31 . . . . . . . . . . . . . . 34 34 36 36 36 38 39 39 40 41 42 43 43 45 46 . . . . 47 47 50 50 52 6.3 6.4 6.5 6.6 6.7 6.8 6.9 Différents types de points de programme . . . . . . . . . . . . Filtres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Feuilles de propriétés . . . . . . . . . . . . . . . . . . . . . . . Interpréteur Scheme . . . . . . . . . . . . . . . . . . . . . . . 6.6.1 Les informations accessibles à l’utilisateur . . . . . . . 6.6.2 Affichage des graphes de dépendance et de flot de contrôle Guide d’utilisation . . . . . . . . . . . . . . . . . . . . . . . . Essai de l’outil . . . . . . . . . . . . . . . . . . . . . . . . . . Avantages et désavantages . . . . . . . . . . . . . . . . . . . . 54 55 56 57 57 59 69 70 70 7 PolySpace C Verifier 75 7.1 Fonctionalités et méthode . . . . . . . . . . . . . . . . . . . . 75 7.2 Essai de l’outil . . . . . . . . . . . . . . . . . . . . . . . . . . 76 7.3 Avantages et désavantages . . . . . . . . . . . . . . . . . . . . 81 8 Conclusion 8.1 Autres outils . . . . . . . . . . . . . . . . . . . . . . . 8.1.1 Malicious Code Filter . . . . . . . . . . . . . . 8.1.2 Vista . . . . . . . . . . . . . . . . . . . . . . . 8.1.3 Unravel . . . . . . . . . . . . . . . . . . . . . 8.1.4 ITS4 . . . . . . . . . . . . . . . . . . . . . . . 8.2 Utilité des outils pour la détection de code malicieux 8.2.1 Graphes . . . . . . . . . . . . . . . . . . . . . 8.2.2 Découpage . . . . . . . . . . . . . . . . . . . . 8.2.3 Erreurs pouvant survenir à l’exécution . . . . 8.3 Faire la recherche autrement . . . . . . . . . . . . . . iii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 82 82 83 83 83 84 84 84 85 85 Résumé L’analyse statique est une technique qui permet d’analyser un programme sans toutefois l’exécuter. Elle a plusieurs utilités en informatique, mais celle dont il sera surtout question dans ce rapport est la détection de code malicieux. La recherche qui a mené à l’écriture de ce rapport avait pour objectif de trouver et de comparer des programmes qui sont en mesure de faire la détection de code malicieux par analyse statique ou dont les résultats peuvent aider à atteindre ce but. Ces programmes se nomment outils d’analyse statique. Ce rapport décrit les types de code malicieux et les principales approches d’analyse de programmes qui sont l’analyse dynamique et l’analyse statique. Il traite entre autres de la technique du découpage de programme qui peut être utilisée dans la détection de code malicieux. Par la suite, il contient une bibliographie d’outils d’analyse statique ainsi que des critères de sélection permettant de déterminer les plus intéressants. Enfin, les outils correspondant le plus à ces critères sont étudiés et présentés en détail avec leurs forces et faiblesses ainsi qu’un guide d’utilisation. On y retrouve des outils de découpage de programmes ainsi que d’autres qui détectent des erreurs dans les programmes pouvant survenir au cours de l’exécution. Chapitre 1 Introduction 1.1 Motivation Le développement des technologies informatiques est en croissance fulgurante et la popularité de ces dernières fait en sorte que la sécurité est devenue un domaine de recherche important. De plus en plus de gens se sentent concernés par la présence de malices dans les programmes. Un code est dit malicieux s’il affecte la confidentialité et l’intégrité des données ainsi que la disponibilité des ressources d’un système. Les techniques les plus connues ne sont plus suffisantes pour assurer la sécurité aujourd’hui. En effet, les outils qui implantent ces techniques ne savent que reconnaı̂tre le code qu’ils connaissent déjà comme le font la plupart des logiciels anti-virus. Ceci signifie donc qu’ils ne sont pas en mesure de détecter un code malicieux inconnu ou qui est capable de changer d’apparence. Il faut donc se tourner vers des méthodes qui étudient les comportements des programmes. L’objectif de cette recherche est de découvrir des programmes pouvant faire la détection de code malicieux par l’analyse statique d’un code source ou binaire ou dont les résultats peuvent être utiles pour la détection de code malicieux. Ces programmes sont nommés outils d’analyse statique. L’analyse statique consiste à analyser le code des programmes sans les exécuter pour tenter de déterminer leur comportement dynamique. Elle sera décrite en détail dans le chapitre 2. 1 Ce travail est fait au laboratoire de recherche LSFM (Langage, Sémantiques et méthodes formelles) du département d’informatique de l’Université Laval. Le laboratoire a déjà créé un outil d’analyse statique pour la détection de code malicieux dans les programmes exécutables dans le cadre d’un projet en collaboration avec le Centre de Recherche pour la Défense à Valcartier (CRDV). Cet outil se nomme Samcots et il est présenté au chapitre 4. Ce chapitre d’introduction se termine par la présentation des différents types de code malicieux et des deux approches d’analyse de programmes qui sont l’analyse dynamique et l’analyse statique. Le chapitre 2 insiste sur les étapes à franchir pour faire l’analyse statique d’un programme dans le but de détecter du code malicieux. Ensuite, le chapitre 3 présente une bibliographie d’outils d’analyse statique et des critères permettant de choisir les plus intéressants à étudier. Les chapitres qui suivent traiteront des différents outils sélectionnés pour une étude plus approfondie. Enfin, le dernier chapitre a pour but de conclure le rapport. 1.2 Code malicieux Il existe plusieurs types de code malicieux [1]. Le code malicieux peut être inséré au moment de la programmation de façon intentionnelle ou non ou bien plus tard. Voici les différents types. 1.2.1 Vers Un ver est un programme conçu pour se répliquer par lui-même et exécuter la nouvelle version copiée. Il se répand tel quel, sans infecter d’autres fichiers. Certains d’entre eux peuvent se copier via un réseau. Un ver peut mettre en péril la confidentialité des données ou causer des opérations inattendues. 1.2.2 Virus Un virus est un programme qui en infecte d’autres en leur incluant une copie de son code. Il contient des instructions malicieuses et peut tenter d’éviter la détection par certaines techniques. Une de ces techniques est le polymorphisme, c’est-à-dire que le virus va changer son apparence, ce qui 2 le protégera d’un anti-virus qui le connaı̂t, mais ne connaı̂t qu’une seule ou quelques-unes de ses apparences. Les virus se propagent lorsqu’un programme infecté est copié sur un autre système par l’intermédiaire d’un disque ou d’un réseau. Le virus s’active lorsque le programme qui le contient est exécuté ou lorsque le système démarre à partir d’un secteur de démarrage infecté par le virus. 1.2.3 Chevaux de Troie Les chevaux de Troie sont des programmes qui contiennent des fonctions cachées souvent trouvés à l’intérieur d’un autre programme. Ils s’activent au moment où ce programme est exécuté et fait des actes inattendus ou non désirés par l’utilisateur. Ils ne se copient pas d’eux-mêmes, ils comptent sur les utilisateurs pour les installer et les distribuer. 1.2.4 Bombes logiques Une bombe logique est un code malicieux qui commet son acte lorsqu’une certaine condition est rencontrée. Par exemple, les bombes temporelles en forment un type particulier qui font leur action malicieuse à une certaine date. 1.2.5 Code mobile hostile Le code mobile hostile est exécuté à l’intérieur de pages web. Les applets Java forment un exemple de code mobile. Ce code est exécuté sur un ordinateur sans l’accord de l’usager et peut contenir des actions malicieuses dommageables ou inattendues. Il s’active au moment où la page web est chargée. 1.2.6 Portes arrières Les portes arrières sont des accès laissés par le programmeur d’un système comme un mot de passe. Ces accès donnent la possibilité de contrôler un ordinateur à distance et d’accéder à ses données ou ressources par l’intrus qui 3 les utilise. Une fois à l’intérieur, il peut accéder à des données confidentielles ou faire des opérations inattendues. 1.2.7 Erreurs de programmation Les erreurs de programmation sont du code malicieux qui a été inséré dans un programme par le programmeur de façon non intentionnelle la plupart du temps. Elles peuvent causer un arrêt du programme au cours de l’exécution ou faire en sorte que le programme ne donne pas les résultats désirés. Ces erreurs peuvent aussi causer des débordements de tampon. Les débordements de tampon surviennent lorsque l’espace alloué pour les données est dépassé, ce qui peut écraser l’adresse de retour d’une fonction. Ceci permet donc à un utilisateur de modifier cette adresse pour que le programme aille exécuter d’autre code, peut-être malicieux. L’usage de certaines pratiques de programmation peut exposer un système à des vulnérabilités. Ces problèmes sont souvent la cause de l’ignorance ou de la paresse du programmeur. L’utilisation de fonctions de certaines librairies n’est pas totalement sécuritaire. Certaines d’entre elles peuvent écrire en mémoire à des endroits où il ne faudrait pas. C’est le cas de la fonction strcpy() du langage C qui permet de copier une chaı̂ne de caractère d’un tableau à un autre. Si le tableau qui reçoit la copie est plus court, cela cause un débordement de tampon. 1.3 Différentes approches d’analyse de programmes Il existe deux grandes approches pour analyser un programme, soit l’analyse dynamique et l’analyse statique. Cette section a pour objectif de les présenter. Évidemment, cette recherche ayant pour but de trouver des outils d’analyse statique, cette approche sera expliquée beaucoup plus en détail au chapitre 2. 4 1.3.1 Analyse dynamique Cette approche consiste à exécuter le programme et à surveiller la présence de comportements malicieux, ce qui nous intéresse, mais peut aussi avoir d’autres utilités comme évaluer la performance du programme et trouver les erreurs dans un code source en l’exécutant pas à pas. Dans le cas de la détection de code malicieux, la surveillance se fait automatiquement par un logiciel appelé moniteur de surveillance. On lui a préalablement appris un ensemble de règles qui lui permettent de reconnaı̂tre un comportement malicieux. Cet ensemble de règles est appelé politique de sécurité et sera traité à la section 2.3. Malgré toute sa simplicité, l’analyse dynamique possède plusieurs désavantages : – Elle expose le système à des dommages causés par l’exécution d’un programme malicieux si le moniteur de surveillance n’a pas été en mesure de reconnaı̂tre le comportement malicieux. – Elle ne permet que de vérifier le code qui a été exécuté au cours de l’exécution du programme. Un test plus large demande donc l’essai de plusieurs scénarios d’exécution, ce qui demande plus de temps. – La façon d’agir face à un comportement malicieux peut être une décision difficile. Le système peut être arrêté ou l’assistance d’une personne peut être demandée. C’est pourquoi il serait préférable de détecter le comportement malicieux avant l’exécution, si c’est possible. Contrairement à l’analyse dynamique, l’analyse statique ne possède pas les défauts énoncés plus haut. La prochaine section explique cette autre approche. 1.3.2 Analyse statique Comme l’autre approche, l’analyse statique ne sert pas seulement à détecter du code malicieux. D’autres applications faites par les outils trouvés sont énumérées à la section 3.2. Contrairement à l’analyse dynamique, l’analyse statique examine le code sans l’exécuter. Donc, au lieu reconnaı̂tre le comportement du programme à l’exécution, il s’agit ici de découvrir ce qu’il serait par une simple lecture et analyse du code source ou binaire selon le cas. 5 Cette façon de faire offre des avantages sur l’autre approche. Puisqu’il n’y a pas d’exécution, la crainte de dommages n’est plus et il n’y a plus de temps d’exécution. Par contre, il n’est pas possible d’être certain de certaines propriétés avec l’analyse statique à cause du problème de l’indécidabilité. Par exemple, dans un programme assembleur, une instruction indique de sauter à l’adresse contenue dans un registre quelconque. Puisqu’il n’y a pas d’exécution, on ne connaı̂t pas cette adresse et les possibilités de valeurs du registre peuvent être très grandes. On ne pourra donc pas étudier statiquement tous les scénarios d’exécution possibles du programme. L’analyse dynamique reste donc un complément nécessaire à l’analyse statique dans ce cas. Un approfondissement de la technique d’analyse statique est fait au chapitre suivant qui traite des différentes étapes de son application. 6 Chapitre 2 Analyse statique L’analyse statique peut faire ressortir différentes informations sur un programme, souvent représentées sous forme de graphes. Ces informations ou représentations du programme peuvent être utiles pour détecter du code malicieux, mais ne sont pas toutes nécessaires, cela dépend de la technique utiliser pour la détection. Aussi, ce chapitre ne prétend pas décrire toutes les techniques d’analyse statique, mais surtout celles qui faciliterons la compréhension du fonctionnement des outils qui seront analysés plus tard dans le rapport. Dans les sections de ce chapitre, il sera d’abord question des informations utiles qu’on peut tirer du code. Ensuite, la technique du découpage (aussi appelé focalisation par certains) qui est utile entre autres pour la détection de code malicieux sera présentée. Enfin, il sera question de l’étape de vérification du programme en se servant des différentes informations recueillies. 2.1 Représentation du programme Avant de tirer des conclusions sur un programme, il est nécessaire de l’analyser pour en tirer de l’information. Les sections suivantes montrent les informations que l’on peut tirer d’un programme et comment elles sont représentées, souvent sous forme de graphes. 7 2.1.1 Arbre syntaxique Avant de tirer les informations pertinentes d’un programme, il est important, dans le cas d’un code source, de vérifier que ce dernier respecte bien les règles de syntaxe du langage utilisé pour l’écrire. La syntaxe d’un langage étant représentée par une grammaire, il est possible de faire un arbre syntaxique du programme. C’est à partir de cet arbre que les informations seront tirées puisque ce dernier écarte les détails inutiles reliés au code comme les commentaires et l’indentation. Dans le cas du code exécutable, il peut être nécessaire que le programme soit d’abord désassemblé dans le but d’obtenir le code assembleur qui sera analysé. Puisque l’assembleur possède aussi une syntaxe, il est possible d’obtenir un arbre syntaxique pour ce type de code qui sera utilisé par la suite pour l’analyse. 2.1.2 Graphe de flot de contrôle Il s’agit d’un graphe orienté dans lequel les noeuds représentent des instructions. Pour tout noeud, un arc quitte vers chaque noeud pour lequel les instructions peuvent suivre immédiatement celles du noeud courant. Il met en évidence les boucles, instructions conditionnelles et branchements. Un chemin dans ce graphe représente un scénario d’exécution du programme. Le programme suivant servira à donner un exemple pour ce type de graphe (voir figure 2.1) et sera utilisé pour les autres types également. void main() { int x = 0; int y = 1; while (y < 10) { y = 2 * y; x = x + 1; } printf ("%d", x); printf ("%d", y); 8 Entrée F x=0 y=1 y < 10 ? printf (x) printf (y) V y=2*y x=x+1 Fig. 2.1 – Graphe de flot de contrôle } 2.1.3 Graphe de dépendance de contrôle Ce graphe montre quelles instructions seront exécutées en fonction de la valeur d’une expression dans le programme. Les noeuds du graphe sont les mêmes que ceux du graphe de flot de contrôle. Pour deux noeuds p et q, un arc va de p vers q si la valeur de l’expression p a un impact sur le fait que l’instruction q soit exécutée ou non. La figure 2.2 en montre un exemple. Dans cet exemple, on peut noter que la présence de la boucle sur l’expression y < 10 est dû au fait qu’elle devra être évaluée de nouveau si elle est vraie puisque le corps de cette boucle doit s’exécuter tant qu’elle est vraie, c’està-dire jusqu’à ce qu’elle soit fausse. 2.1.4 Analyse du flot de données Le flot de données peut aussi être analysé. Il informe sur le déplacement des données entre le programme, les disques, le réseau, etc. Par exemple, la lecture sur le disque d’un certain fichier et l’envoi de données sur le réseau par la suite peut dans certains cas être considéré comme malicieux. Il s’agit 9 Entrée x=0 y=1 y < 10 ? y=2*y printf (x) printf (y) x=x+1 Fig. 2.2 – Graphe de dépendance de contrôle ici du flot de données à travers les périphériques, mais on peut aussi analyser le flot de données à l’intérieur du programme. Il permet d’avoir de l’information sur l’utilisation des variables dans le temps. Il y a vraiment plusieurs méthodes plus ou moins complexes pour étudier le flot de données et celles-ci mènent à des informations différentes. On peut d’ailleurs consulter [2] dans lequel un chapitre traite de l’analyse du flot de données. Par exemple, une information de base qui peut être très utile est l’ensemble des variables utilisées et celui des variables modifiées pour chaque instruction du programme. Voici l’illustration de cette méthode sur le programme étudié ; les ensembles sont décrits à la suite de chaque ligne de code. void main() { int x = 0; int y = 1; while (y < 10) { y = 2 * y; x = x + 1; } printf ("%d", x); printf ("%d", y); } Utilise={} Définit={x} Utilise={} Définit={x} Utilise={y} Définit={} Utilise={y} Définit={y} Utilise={x} Définit={x} Utilise={x} Définit={} Utilise={y} Définit={} 10 Entrée x=0 y=1 y < 10 ? y=2*y printf (x) printf (y) x=x+1 Fig. 2.3 – Graphe de dépendance de données 2.1.5 Graphe de dépendance de données Un autre graphe dont les noeuds sont les mêmes que celui du graphe de flot de contrôle peut être fait. Dans ce graphe, un arc va de p vers q s’il est possible que la valeur d’une des variables modifiées à l’instruction p soit utilisée à l’instruction q sans qu’elle ne soit modifiée entre temps. La figure 2.3 en montre un exemple. 2.1.6 Graphe de dépendance de programme ou du système Un nouveau graphe est le graphe de dépendance de programme [3]. Il n’est rien d’autre que l’union du graphe de dépendance de contrôle et du graphe de dépendance de données (voir figure 2.4). On peut, pour garder plus de 11 Entrée x=0 y=1 y < 10 ? y=2*y printf (x) printf (y) x=x+1 Fig. 2.4 – Graphe de dépendance de programme précision, différencier dans ce graphe les arcs venant des deux graphes en les étiquetant arc de contrôle et arc de données, c’est pourquoi on entend rarement parler des deux autres. Jusqu’ici, les explications n’ont pas fait mention qu’un programme peut être composé de plusieurs procédures. Si c’est le cas, on commence par faire le graphe de dépendance de programme pour chaque procédures de façon indépendante. Il y a ensuite une façon de relier ces graphes pour faire le graphe de tout le programme que l’on appellera graphe de dépendance du système. Pour chaque appel de fonction, le noeud représentant le point d’appel est relié par un arc de contrôle au point d’entrée de la fonction appelée ainsi qu’à un noeud représentant chaque paramètre effectif et la sortie effective (variable qui accepte la valeur retournée par la fonction). Ce dernier point, s’il existe, possédera des dépendances de données vers les instructions qui utilisent sa valeur. À l’intérieur de la fonction appelée, il y a un arc de dépendance de contrôle qui va du point d’entrée vers un noeud représentant chaque paramètre formel et la sortie formelle (valeur retournée par la fonction). Il y a un arc de dépendance de données de chaque paramètre effectif 12 vers le paramètre formel associé ainsi que de la sortie formelle vers la sortie effective pour chaque point d’appel. De plus, il y aura les arcs de dépendance de données adéquats des paramètres formels vers les instructions de la fonction et des instructions vers la sortie formelle. La figure 2.5 montre un exemple de graphe de dépendance du système pour le programme écrit plus loin. Dans ce graphe, les arcs inter-procédurals sont en pointillés. Parmi ces arcs, ceux en lignes courbes sont des arcs de contrôle, les autres sont des arcs de données. int somme(int a, int b) { int c; c = a + b; return c; } void main() { int x; int y; x = somme(2,3); printf("%d", x); y = somme(x,3); printf("%d", y); } 2.2 Découpage Le découpage [3] est une technique utilisée dans le but de faire ressortir certaines instructions d’un programme en relation avec une propriété. Le résultat du découpage est donc un sous-ensemble du programme. Il en existe deux types, le découpage arrière et le découpage avant dont les résultats seront présenté à la section suivante. Ensuite, il sera question de l’utilité du découpage et d’un exemple d’utilisation. Enfin, la méthode pour obtenir un sous-programme avec la technique sera expliquée et illustrée sur des exemples. 13 Entrée main appel somme Param1 = 2 x = retour printf (x) appel somme Param1 = x Param2 = 3 y = retour Param2 = 3 Entrée somme a = Param1 b = Param2 retour = c c=a+b Fig. 2.5 – Graphe de dépendance du système 14 printf (y) 2.2.1 Résultat du découpage Le découpage se fait sur une variable à un point particulier du programme. Le découpage arrière donne l’ensemble des instructions qui précèdent le point dans le programme et qui ont un impact sur la valeur de la variable. Par exemple, si on choisit comme point dans le programme une instruction qui doit afficher le contenu d’une variable et qu’on demande le découpage sur cette même variable, le sous-programme contiendra seulement les instructions nécessaires pour établir sa valeur correctement. Le découpage avant sur une variable à un point particulier du programme donne l’ensemble des instructions qui suivent ce point et qui sont affectées par cette variable. On parle aussi parfois de découpage sur une instruction sans mentionner de variable. Dans ce cas, s’il est question de découpage avant, le sousprogramme est l’ensemble des instructions qui sont affectées par l’instruction choisie, donc c’est l’union des sous-programmes obtenus par le découpage à ce point du programme pour chaque variable modifiée par l’instruction choisie. Dans le cas du découpage arrière, c’est la même chose sauf qu’il est fait pour les variables utilisées au lieu des variables modifiées. La façon de présenter le résultat de cette deuxième définition en fonction de la première est seulement théorique, puisque la façon de le calculer n’utilise pas la première définition. Les exemples donnés plus tard seront fait sur ce modèle. 2.2.2 Utilisation Cette technique est utile à la réutilisation du code. Par exemple, si un programme calcule plusieurs résultats simultanément en mélangeant les calculs nécessaires à chacun d’eux, mais qu’un seul de ces résultats est utile pour un autre programme, un découpage arrière sur la variable qui contient ce résultat dans le programme donne un sous-programme sans les instructions superflues. Elle sert aussi à comprendre des programmes compliqués puisque son résultat est un programme plus petit, donc plus facile à analyser. Une utilité particulièrement intéressante est la possibilité de réduire le problème de l’indécidabilité de l’analyse statique. Si on reprend l’idée de l’instruction assembleur qui demande de sauter à l’adresse contenue dans un registre, un découpage arrière sur cette instruction pourrait aider à faire diminuer le nombre de valeurs possibles du registre [4]. 15 Voici un exemple plus compliqué qui montre l’intérêt de cette méthode en sécurité informatique. Imaginons un système informatique dans lequel les données sont contenues dans des fichiers qui comportent chacun un niveau de sécurité selon la confidentialité des données qu’ils contiennent. Un problème de sécurité se pose si un programme peut lire un fichier de haut niveau de sécurité et en inscrire le contenu dans un de bas niveau. Il est possible dans le programme suspect de faire un découpage avant sur l’instruction de lecture du premier fichier ainsi qu’un découpage arrière sur l’instruction d’écriture dans le second fichier. Si les deux sous-programmes ont des instructions en commun, il pourrait y avoir dépendance de données entre les deux fichiers et donc le problème de sécurité mentionné plus tôt. En utilisant plusieurs types de découpage et en agençant les résultats comme dans l’exemple ci-haut en utilisant des opérateurs ensemblistes comme l’union et l’intersection, on peut spécialiser le découpage selon les critères voulus. 2.2.3 Comment faire Pour appliquer cette technique sur un programme composé d’une seule fonction, on se sert du graphe de dépendance du programme. Il est aussi possible de faire le découpage selon le flot de données seulement ou le flot de contrôle seulement en ne considérant que les arcs de contrôle ou les arcs de données dans le graphe. Dans le graphe de dépendance du programme, on choisit le noeud correspondant à l’instruction sur laquelle on désire effectuer le découpage. Dans le cas du découpage avant, le sous-programme contient les instructions représentées par les noeuds que l’on peut atteindre à partir du noeud choisi en suivant les arcs. Pour le découpage arrière, il s’agit des noeuds qui peuvent atteindre le noeud choisi, donc les noeuds accessibles en suivant les arcs à contre-sens à partir du noeud choisi. La figure 2.6 montre le graphe de dépendance de programme en mettant en évidence (par des arcs en gras) le découpage arrière sur l’instruction printf(y). Ensuite la figure 2.7 montre le nouveau graphe de flot de contrôle pour le sous-programme résultant de ce découpage. Voici maintenant comme faire pour un programme composé de plusieurs fonctions. Le découpage est plus difficile à faire lorsqu’il nécessite une analyse inter-procédurale. Dans ce cas, il faut utiliser le graphe de dépendance 16 Entrée x=0 y=1 printf (x) y < 10 ? printf (y) x=x+1 y=2*y Fig. 2.6 – Graphe de dépendance de programme montrant le découpage arrière sur l’instruction printf(y) Entrée F y=1 y < 10 ? printf (y) V y=2*y Fig. 2.7 – Graphe de flot de contrôle pour le sous-programme après le découpage 17 du système. La façon de faire est la même que pour le découpage intraprocédural, mais il faut faire attention à une chose. Par exemple, pour le découpage avant, si on entre dans une fonction en suivant les flèches, lorsqu’on arrivera à la sortie formelle qui est le dernier point à l’intérieur de la fonction appelée avant d’en ressortir, il faudra seulement choisir l’arc qui retourne vers le site d’appel et non tous les arcs comme il faut faire d’habitude. Dans le cas du découpage arrière, c’est la même chose, il faut revenir sur les arcs qui vont vers les paramètres effectifs du site d’appel par lequel on était entré (par la sortie) dans la fonction appelée. La figure 2.8 montre un découpage avant sur le deuxième appel de la fonction somme fait à partir du graphe de dépendance du système à la figure 2.5 en mettant les arcs et les noeuds en gras. À la sortie de la fonction somme, une croix est dessinée sur une des flèches pour montrer qu’il ne faut pas l’emprunter, car elle ne retourne pas dans la fonction main à l’endroit où la fonction somme a été appelée. 2.3 Vérification Une fois que toutes les informations pertinentes ont été retrouvées dans le programme à analyser, c’est le temps de passer à l’étape de vérification du programme. Dépendant de la méthode de vérification utilisée, il n’est pas nécessaire de produire tous les graphes qui représentent le programme. Ce n’est pas parce qu’un comportement est considéré comme malicieux dans un programme qu’il l’est dans tous les programmes. Ceci doit être défini par un ensemble de règles qui forme la politique de sécurité. Il existe plusieurs méthodes pour implanter une telle politique. La méthode choisie pour décrire la politique de sécurité dépend des règles qu’elle contient. Les méthodes n’ont pas toutes le même pouvoir d’expression, c’est-à-dire qu’une politique qui se décrit bien avec une pourrait ne pas se faire avec une autre. Aussi, il y a des méthodes qui sont plutôt adaptées à l’analyse dynamique puisqu’elles ne sauraient quoi faire devant l’indécidable. Voici tout de même un exemple qui fonctionne bien avec l’analyse statique. Une façon de faire est l’utilisation d’automates de sécurité. Les transitions de ces automates correspondent aux instructions du programme. Ils comportent un ou plusieurs états qui ne doivent pas être atteints ; s’ils le sont, 18 Entrée main appel somme param1 = 2 printf (x) x = retour appel somme param1 = x param2 = 3 printf (y) y = retour param2 = 3 Entrée somme a = param1 b = param2 retour = c c=a+b Fig. 2.8 – Graphe de dépendance du système montrant le découpage avant sur le deuxième appel de la fonction somme 19 cela signifie que la politique n’a pas été respectée. Si un scénario d’exécution du programme possède une suite d’instructions, donc de transitions de l’automate, qui fait atteindre un état tel que la politique n’est pas respectée, le programme est considéré comme malicieux. 20 Chapitre 3 Choix des outils Les outils d’analyse statique qui sont considérés en premier lieu sont ceux qui font la détection de code malicieux. Puisque de tels outils semblent pratiquement inexistants, sauf ceux qui font la détection d’erreurs de programmation (un des types de code malicieux présenté à la section 1.2), ceux qui ne détectent pas le code malicieux comme tel, mais qui peuvent faire une ou plusieurs des choses présentées au chapitre 2 sont intéressants pour l’étude. Aussi, la recherche se concentre sur les outils qui analysent le code source en C/C++ et Java ainsi que le code exécutable. La raison pour laquelle nous nous intéressons aux outils qui analysent le code exécutable est que nous n’avons pas accès au code source des logiciels commerciaux. Pour le code source, nous avons choisi les langages mentionnés plus haut parce qu’ils sont les plus utilisés actuellement. De plus, lorsqu’il est possible d’avoir le code source, l’analyse est plus facile puisque ce dernier est plus parlant que le code exécutable. 3.1 Bibliographie des outils trouvés La liste des outils trouvés est présentée par les figures 3.1 et 3.2. Il est aussi possible de consulter d’autres listes disponibles sur Internet [5, 6, 7, 8, 9, 10]. 21 Nom de l'outil Bandera Cantata Cantata++ C-Cover CheckMate Cleanscape LintPlus CMT++ CodeSurfer CodeWizard Cscope Extended Static Checking FlexeLint Hindsight Imagix 4D Instant QA IRIS IST4 Java Anayzer JavaWizard Jex Jtest LCLint LDRA Testbed McCabe QA Metamata Panorama PB Code Analyzer PC-lint Plum Hall SQS PolySpace C Verifier Prodag QA C QA C++ QA-C/C++ Qstudio Java Qstudio Java Lite Safer C Toolset Sniff+ STATIC Understand for C++ Unravel VISTA Wasp Langage(s) supporté(s) Java C/C++ C++ C C/C++ C C/C++ C C++ C Java C/C++ C/C++, Fortran C/C++ C/C++ C++ C/C++ Java Java Java Java C Ada, Cocol, C/C++, Fortran, Pascal, Algol, Assembleur ? Java Java, C/C++, VB C/C++ C/C++ C C C++ C/C++ Java Java C C/C++, Java, Ada C C/C++ C C Java, Modula-2 Système(s) d'exploitation Machine virtuelle Java Windows, Linux Windows, Linux Windows, Unix Machine virtuelle Java Linux Windows NT/95, Unix, Linux Solaris, Windows Windows, Unix Unix, Linux Windows, Unix, Linux, Solaris Unix Unix, Linux Windows, Linux, Solaris Unix Windows, Unix Machine virtuelle Java Windows Windows Windows, Linux, Solaris Windows, Linux, Solaris Fontionnalités Découpage M M M, DEE M M, DEE M Découpage M M DEE DEE M GFC DEE GFC DEE DEE DEE DEE M DEE DEE Windows NT/95, Unix, Linux Windows Windows, Dos, OS/2 ? Windows, Linux, Solaris Solaris 5.5 Windows, Solaris Windows, Solaris Windows 95/NT, Unix Windows, Solaris, Linux Windows, Solaris, Linux Windows, Linux Windows NT, Solaris, Linux Windows, Linux, Solaris Unix Unix Windows NT/95 M M, DEE M M DEE M DEE Dépendances M M M M M M, DEE M DEE M Découpage Graphes DEE Fig. 3.1 – Liste d’outils d’analyse statique M : Métriques (ou résultats ayant la même utilité) DEE : Détection d’erreurs pouvant survenir à l’exécution GFC : Graphe de flot de contrôle 22 Nom de l'outil Bandera Cantata Cantata++ C-Cover CheckMate Cleanscape LintPlus CMT++ CodeSurfer CodeWizard Cscope ESC FlexeLint Hindsight Imagix 4D Instant QA IRIS IST4 Java Anayzer JavaWizard Jex Jtest LCLint LDRA Testbed McCabe QA Metamata Panorama PB Code Analyzer PC-lint Plum Hall SQS PolySpace C Verifier Prodag QA C QA C++ QA-C/C++ Qstudio Java Qstudio Java Lite Safer C Toolset Sniff+ STATIC Understand for C++ Unravel VISTA Wasp Évaluation Coût gratuit 0 30 jours 30 jours oui 800 oui oui 15 à 30 jours 10 jours 2495 ou 0* oui gratuit 0 gratuit 0 non 998 non oui non gratuit 0 gratuit 0 gratuit 0 gratuit 0 gratuit 0 oui gratuit 0 non non bientôt 15 jours 1300 à 2450** oui 225 non 239 30 jours oui gratuit 0 non non non non 299 gratuit 0 14 jours oui non oui gratuit 0 non gratuit 0 Page Web www.cis.ksu.edu/santos/bandera/ www.iplbath.com/ www.iplbath.com/ www.bullseye.com www.bluestone-sw.com www.cleanscape.net/stdprod/lplus/index.html www.testwell.fi www.codesurfer.com www.parasoft.com cscope.sourceforge.net/ research.compaq.com/SRC/esc/download.html www.gimpel.com www.integrisoft.com www.imagix.com/products/products.html www.reasoning.com/ laser.cs.umass.edu/tools/process.htm www.cigital.com/its4/ students.cs.byu.edu/~larson/absint/DemoApplet.html csdl.ics.hawaii.edu/Tools/JWiz/JWiz.html www.cs.ubc.ca/~mrobilla/jex/ www.parasoft.com lclint.cs.virginia.edu/ www.ldra.com www.mccabe.com www.webgain.com/products/metamata/ www.softwareautomation.com/statican.htm www.ascensionlabs.com/pbcodeanalyzer.htm www.gimpel.com www.plumhall.com www.polyspace.com www.ics.uci.edu/~softtest/prodag.html www.prqa.co.uk www.prqa.co.uk www.qa-systems.com/products/ www.qa-systems.com/products/ www.qa-systems.com/products/ www.oakcomp.co.uk/SoftwareProducts.html www.wrs.com/products/html/sniff.html www.soft.com/Products/Advisor/static.html www.scitools.com hissa.nist.gov/unravel/ www.cigital.com/VISTA-demo/ www.iis.nsk.su/wasp/ Fig. 3.2 – Liste d’outils d’analyse statique * L’outil est gratuit pour une utilisation universitaire dans un but de recherche ou d’éducation. ** Il s’agit en fait de plusieurs outils, chacun analyse un langage donné et le prix de chacun peut différer de celui des autres (prix des versions pour Windows). 23 3.2 Fonctionnalités de ces outils Dans le grand monde de l’informatique, l’analyse statique ne sert pas qu’à la détection de code malicieux, ce qui implique que la recherche d’outils d’analyse statique ne donnera pas que des résultats intéressants. Les prochaines sections énumèrent ce que sont en mesure de faire les outils trouvés et indiquent si ces fonctionnalités sont intéressantes ou bien quelles sont les particularités qu’elles devraient avoir pour qu’elles le soient. 3.2.1 Métriques L’analyse statique est utilisée en génie logiciel dans le but de calculer certaines métriques sur le code source d’un programme. La proportion de code à l’intérieur de boucles dans le programme et le nombre de scénarios d’exécution possibles en sont des exemples. Cela a pour utilité d’inciter les programmeurs à faire des programmes plus lisibles et faciles à maintenir, mais n’apporte aucun intérêt du point de vue de la sécurité. Les outils de la liste ayant cette fonctionnalité comme mention sont en fait les outils qui aident à écrire des programmes lisibles et compréhensibles ou à verifier s’ils le sont. Il est donc possible que ces outils ne calculent pas nécessairement des métriques, mais leurs résultats a à peu près la même utilité. Il était parfois difficile de classer les outils dans une catégorie particulière. De toute façon, ils ne seront pas essayés puisqu’on a peu d’intérêt pour eux. 3.2.2 Détection d’erreurs pouvant survenir à l’exécution Une autre fonctionnalité des outils d’analyse statique est d’étendre le travail des compilateurs. Il arrive que certaines erreurs provoquent l’arrêt d’un programme en cours d’exécution parce qu’elles n’ont pas pu être détectées au cours de la compilation. Pour cette raison, ce sont ces problèmes de programmation qui prennent le plus de temps à régler. Les outils d’analyse statique peuvent découvrir certaines de ces erreurs en lisant le code source. L’accès à une variable à partir d’un pointeur nul ou l’accès à un tableau à l’extérieur de ses bornes en sont des exemples. Ce type d’information est déjà beaucoup 24 plus intéressant pour nous que les métriques. En effet, ceci éviterait d’utiliser un système qui mettrait la vie de personnes en danger suite à ce genre d’arrêt. Par exemple, nous pouvons imaginer que le programme qui contrôle l’ouverture du train d’atterrissage d’un avion ne fonctionne plus alors que l’avion est en vol aurait des conséquences catastrophiques. Dès qu’un outil offrait des résultats qui pouvaient être utilisés pour détecter des erreurs pouvant survenir à l’exécution, il été classé dans cette catégorie. Par contre, parmi ces outils, certains se contentent de donner plusieurs avertissements qui ne sont que des risques d’erreurs et c’est au programmeur de les vérifier et de constater que la plupart ne sont pas fondés. À l’opposé, d’autres outils, plus rares, essayent de donner un bilan beaucoup plus juste en évitant de donner des erreurs quand il n’y en a pas. Ces derniers sont plus intéressants pour l’essai. 3.2.3 Graphes Certains outils d’analyse statique construisent quelques types de graphes tel qu’ils ont été décrits à la section 2.1. Dans le cas où l’outil en question ne va pas plus loin, c’est-à-dire que son objectif est de faire les graphes associés à un programme, il faudrait être en mesure de récupérer la structure de données des graphes dans le but de l’analyser avec un autre programme par la suite. À cause de cela, la représentation graphique seule est peu intéressante puisqu’elle ne peut pas être analysée par un autre outil. 3.2.4 Découpage Les outils qui font du découpage de programmes sont des outils d’analyse statique. Puisque le découpage utilise le graphe de dépendance d’un programme pour calculer ses résultats, le choix d’un outil de découpage sera fait en fonction de la qualité des dépendances qu’il trouve à l’intérieur du programme analysé. Il serait intéressant entre autre qu’on puisse avoir accès à la structure de données du graphe de dépendance du système et aussi que le résultat du découpage soit présenté sous forme d’un programme prêt à être compilé au lieu d’avoir un résultat du découpage qui est seulement visuel en mettant en évidence dans le code les instructions correspondants au résultat du découpage. 25 3.2.5 Politiques de sécurité Un outil d’analyse statique qui va jusqu’à faire la détection de code malicieux doit se baser sur une politique pour donner son verdict sur le programme analysé. Une politique qui est implantée selon la méthode décrite à la section 2.3 ou une autre méthode doit faire partie de l’outil. Soit que cette politique est ancrée dans l’outil ou ce qui serait plus intéressant encore, l’outil en question utilise une méthode d’implantation de politiques de sécurité et permet à son utilisateur d’écrire ou de modifier une politique de l’outil. Dans ce cas, il faudrait voir la complexité de cette tâche par rapport à la puissance de la méthode utilisée pour définir une politique de sécurité. 3.3 Jeu d’essai L’utilisation d’un jeu d’essai a pour but de découvrir les limites d’un outil ou bien de vérifier si elles correspondent à celles qui sont données dans la littérature venant avec l’outil. Si l’outil a vraiment une large gamme de fonctionnalités, le jeu d’essai vérifiera les plus intéressantes. Il peut aussi servir à comparer entre eux plusieurs outils ayant des fonctionnalités similaires. Puisque les outils essayés n’ont pas tous les mêmes fonctionnalités et qu’ils n’analysent pas des programmes dans le même langage, on ne peut pas utiliser le même jeu d’essai pour tous. En effet, un programme d’essai pour un outil qui détecte des erreurs pouvant survenir à l’exécution devra nécessairement contenir des erreurs tandis que ça ne sera pas le cas pour un outil de découpage. Pour essayer deux outils qui détectent des erreurs pouvant survenir à l’exécution, mais un qui traite le code C et l’autre le code Java, on fera des tests qui se ressemblent comme des boucles infinies et des divisions par zéro semblables. Par contres, les jeux d’essais ne pourront pas être totalement semblables puisque les deux langages n’ont pas les mêmes particularités. Voilà donc les raisons pour lesquelles un nouveau jeu d’essai sera présenté pour chaque outil analysé. 26 3.4 Critères de sélection Les outils d’analyse statique de la liste n’ont pas tous les mêmes fonctionnalités, on choisira des outils qui ont des fonctionnalités différentes ou qui traitent des langages différents. Ceci enlève la possibilité de comparer des outils entre eux pour déterminer vraiment le meilleur, mais fait en sorte que l’ensemble des outils essayés aura un plus grand potentiel. Puisque les outils choisis n’auront pas les mêmes fonctionnalités, il faudra essayer de choisir le meilleur de chaque catégorie en consultant l’information disponible. Pour cela, il faudra se baser sur certains critères. Le travail étant d’essayer des outils, il est important que l’outil qui semble intéressant soit accessible. Une période d’évaluation avant l’achat est intéressante, mais le regard est plutôt porté sur les outils qui sont gratuits puisqu’on pourra les utiliser dans le futur si on le désire. Pour ce qui est des critères propres à chaque type d’outil, ils ont déjà été présentés. 27 Chapitre 4 Samcots Samcots [11] est un outil d’analyse statique servant à la détection de code malicieux dans le code assembleur. Il a été conçu ici au laboratoire de recherche LSFM de l’Université Laval. La raison pour laquelle il traite l’assembleur est qu’il a pour but de vérifier des logiciels commerciaux pour lesquels le code source est habituellement non disponible. 4.1 Fonctionnalités et méthode utilisée Puisque l’outil prend le code assembleur, il faut préalablement utiliser un désassembleur sur le programme exécutable pour avoir le code voulu. Celui qu’il faut utiliser pour faire le travail est le désassembleur commercial IDA32 Pro [12]. Le fichier de sortie du désassembleur pour un programme corresponds au fichier d’entrée de l’outil Samcots. La première étape à faire avec le fichier entré est de vérifier sa syntaxe et de produire un arbre syntaxique du programme sur lequel on fait ensuite l’analyse de flot de contrôle. Un graphe de flot de contrôle tel que définit à la section 2.1.2 est d’abord fait pour chaque procédure. Ensuite, ces graphes sont regroupés ensemble en ajoutant une arrête de chaque appel de procédure vers la première instruction de la procédure appelée et des instructions de retour dans la procédure vers l’instruction qui suit l’appel. Lorsque ce graphe est fait, l’analyse de flot de données correspond à annoter chaque instruction du graphe avec l’ensemble des variable modifiées et l’ensemble des variables utilisées par cette dernière. 28 Samcots utilise quatres politiques de sécurités distinctes, chacune étant représenté par un automate de sécurité. Il est possible de vérifier si le programme est malicieux selon chacune d’entre elles de façon séparée. Les transitions de ces automates correspondent à des appels d’APIs. Il faut donc prendre le graphe de flot de contrôle, y enlever toutes les instructions qui ne sont pas des appels d’APIs puisqu’elles n’auront aucun impact sur le résultat, la politique de sécurité ne tenant compte que des appels d’APIs. Ce graphe est appelé graphe d’appel d’APIs qui peut encore être réduit en graphe d’appel d’APIs critiques. Ce graphe ne garde que les appels qui font partis des automates de sécurité, puisqu’encore une fois, les autres n’ont pas d’impact. C’est à partir de ce dernier graphe que l’outil vérifie si le programme respecte la politique de sécurité en vérifiant s’il existe ou non un scénario d’exécution qui peut amener un des automates dans l’état qui indique que la politique n’est pas respectée, comme l’explique la section 2.3. Pour le programme analysé, les listes contenant les APIs appelés concernant les accès disque, réseau, la base de registres et l’horloge sont produites. Aussi, il est possible de visualiser les différents graphes produits par Samcots en utilisant un logiciel nommé VCG (Visualization of Compiler Graphs) [13]. 4.2 Guide d’utilisation Samcots est outil dont l’utilisation est assez simple. Tout d’abord, tout ce qu’il est possible de faire en utilisant les menus déroulants peut être fait autrement, entre autres avec les boutons. Donc ce sont ceux si qui seront expliqués. Voici donc ce que font les boutons présents en haut de la fenêtre (voir figure 4.1) de gauche à droite : – Parmi les quatre premiers, seul le second a une utilité. Il sert à ouvrir un fichier assembleur dans le but de l’analyser. – Fait l’analyse lexicale et syntaxique ainsi que l’analyse de flot de contrôle et de flot de données. L’arbre syntaxique peut être consulté dans la partie gauche de la fenêtre. – Affiche le graphe de flot de contrôle. Il semble aussi préparer les graphes d’APIs et d’APIs critiques puisqu’on ne peut pas les afficher avant d’avoir appuyé sur ce bouton. – Ce bouton qui devrait être destiné à l’analyse de flot de données ne semble rien faire du tout, puisque cette analyse est déjà faite même si 29 Fig. 4.1 – Interface utilisateur de l’outil Samcots 30 on n’a pas appuyé dessus. – Les deux boutons qui suivent affichent respectivement le graphe d’APIs et le graphe d’APIs critique. – Les quatres boutons qui suivent servent à présenter des rapports d’accès. Ils concernent respectivement le disque, le réseau, la base de registres et l’horloge. Il faut être dans l’onglet Critical API pour voir les rapports. – Le dernier bouton ne sert qu’à afficher le numéro de la version de Samcots qui est utilisé. Il ne reste que le contenu de l’onglet Static Verifier à expliquer (voir figure 4.2). Il faut d’abord choisir une des quatre politiques de sécurité. En choisissant le bouton Static Verifier dans le bas, on peut savoir si la politique de sécurité sélectionnée est respectée ou non. Si oui, un message en bleu indique qu’elle n’est pas violée. Par contre, si ce n’est pas le cas, un message en rouge dit ce que le programme fait et qui fait en sorte que la politique de sécurité n’est pas respectée. L’autre bouton, identifié Security Automata, sert à faire afficher le dessin de l’automate de sécurité correspondant à la politique de sécurité sélectionnée. 4.3 Essai de l’outil Voici une présentation de l’utilisation de Samcots en analysant la version désassemblée du fichier Winipx.exe [14]. Ce fichier est en fait un virus. Lorsqu’il est en opération, il envoie de l’information provenant de la machine infectée vers quelques adresses Internet. L’analyse du fichier Winipx.exe montre que de l’information est envoyée sur le réseau et qu’elle provient bien de fichiers présents sur le disque (voir figure 4.2). 4.4 Avantages et désavantages Les avantages de Samcots : – L’outil est facile à utiliser. – L’analyse d’un fichier est très rapide, elle se fait en une ou deux secondes. – L’outil est original, c’est-à-dire qu’il n’existe probablement pas un autre outil qui a les mêmes fonctionnalités. 31 Fig. 4.2 – Résultat de l’analyse de Winipx.exe avec la politique de sécurité qui concerne les accès disque - réseau 32 Les désavantages de Samcots – Les politiques de sécurité sont programmées dans le code source de l’outil donc il n’est pas possible de les modifier ou d’en ajouter de nouvelles. – Lorsque le programme à analyser atteint une certaine grosseur (fichier assembleur d’environ 1 Mo), il n’est plus possible de voir les graphes. Ce problème doit être attribué à VCG plutôt qu’à Samcots. 33 Chapitre 5 Wasp Wasp 1 est un outil qui permet de détecter statiquement les erreurs pouvant survenir à l’exécution ainsi que le code inaccessible dans un programme Java. Il est conçu par AcademSoft. La version testée date du mois d’avril 2000, mais une nouvelle version est sortie au cours de l’été 2001. Contrairement à l’ancienne version, la nouvelle n’est pas gratuite, mais il est possible d’en avoir une version avec certaines limitations. Ce chapitre présente d’abord les fonctionnalités de l’outil suivies des informations sur la configuration. Il y a ensuite un guide d’utilisation et l’information nécessaire pour comprendre les messages de Wasp pour terminer par l’essai de l’outil et la présentation de ses avantages et désavantages. 5.1 Fonctionnalités L’outil de détection statique d’erreurs Wasp nécessite l’installation du compilateur JDK 2 dont il doit connaı̂tre l’emplacement du fichier qui contient les différentes classes mises à la disposition du programmeur. La méthode utilisée par Wasp se base sur un autre outil de détection statique d’erreurs pouvant survenir à l’exécution nommé OSA3 [15]. Il fait 1 http://www.waspsoft.com http://www.java.sun.com 3 OSA (Oberon-2/Modula-2 Static Analyser ) a été créé par la même organisation que Wasp 2 34 une analyse de flot de données sensible au contexte, c’est-à-dire qui prend en compte pour une méthode, les différents états possibles du programme lorsque celle-ci a été appelée. Selon le manuel de l’utilisateur4 , voici une liste des différentes situations que l’outil peut détecter : – – – – – – – – – – – – – Usage d’une variable non initialisée. Exception due à un pointeur nul. Affectation d’une variable qui n’est jamais utilisée par la suite. Conversion de type non permise pour une valeur dans un certain type. Branche inaccessible dans les instructions if et switch. Clause catch inaccessible. Opérande toujours vraie ou toujours fausse dans les expressions « exp1 && exp2 » et « exp1 || exp2 ». Méthode qui ne se termine pas normalement. Dépassement de capacité d’un type suite à une opération arithmétique. Accès à un tableau à l’extérieur de ses bornes. Division par zéro. Non exécution du corps d’une boucle for ou while. Exception qui n’a pas été prise en compte. Pour chacun de ces messages, Wasp indique s’il s’agit d’une situation qui causera l’arrêt du programme ou non. Si c’est le cas, il indique si cela arrivera en tout temps, sinon dans quelles conditions cela arrivera. Puisque qu’il fait une analyse sensible au contexte, Wasp dit pour une erreur trouvée dans une méthode, lorsqu’il le peut, de quels points du programme cette méthode doit être appelée pour que l’erreur survienne. Si un programme est long, il est possible de faire seulement une partie de l’analyse et d’être en mesure de la poursuivre plus tard ou bien de faire une analyse simplifiée, ce qui demande moins de temps, mais risque fort de diminuer la précision des résultats. Pour en savoir plus à ce sujet, il faut consulter le manuel de l’utilisateur. 4 Le manuel de l’utilisateur est contenu dans le fichier d’aide Windows bin\wasp.hlp 35 5.2 Configuration La configuration de Wasp se fait à l’aide d’options et d’équations. Les options ont une valeur booléenne tandis qu’il s’agit d’une chaı̂ne de caractères pour les équations. Par exemple, considérons une option nommée graph. On l’active par la commande +graph et on la désactive avec -graph. Les équations ont plutôt la forme suivante : -nom = valeur. Par exemple, si on considère une équation nommée CollectMin à laquelle on veut affecter la valeur 101, on utilise la commande -CollectMin = 101. Quelques options et équations seront introduites plus tard dans le rapport. 5.3 Graphe d’appels de méthodes Pour chaque programme qu’il analyse, Wasp fabrique son graphe d’appels de méthodes. La structure de données représentant le graphe est divisé en deux parties. La première section montre, pour chaque méthode, les méthodes qu’elle appelle dans son corps tandis la deuxième section montre, pour chaque méthode, les méthodes appelantes. Le graphe est présenté par niveaux. Chaque méthode appartient à un niveau et ne peut appeler que les méthodes des niveaux inférieurs. Une méthode peut appeler une méthode de même niveau si elle est récursive. L’option graph indique si le graphe d’appel de méthodes est fabriqué. Il ne l’est pas par défaut parce qu’il augmente le temps de l’analyse de façon importante. Pour les analyses qui ne sont pas faites au complet, le symbole ! dans le graphe indique les méthodes dont l’analyse n’est pas terminée. 5.4 Guide d’utilisation Wasp n’offrant pas d’interface graphique, on utilise l’outil en lui donnant une commande dans une console DOS. Lorsque l’installation est complètement réussie, le programme utilise deux répertoires, celui qui contient justement le programme ainsi qu’un répertoire de travail. C’est à partir de ce dernier qu’il faut appeler la commande wasp avec les bons paramètres. Il faut tout d’abord savoir que le répertoire de travail se compose des sousrépertoires prj, msg, grf et irf. Le répertoire prj contient les fichiers servant 36 à décrire un projet à analyser. Ceci est utile lorsque le programme à analyser est constitué de plusieurs fichiers sources. De plus, dans ce fichier, il est possible de donner la configuration de Wasp pour ce projet. Le répertoire msg contient les messages d’erreurs retournés suite à une analyse. Les deux autres répertoires contiennent respectivement le graphe d’appel de méthode créé par Wasp et les informations sur un programme analysé de façon partielle. Les fichiers du répertoire msg portent l’extension mes tandis que dans les autres cas, l’extension est la même que le nom du répertoire. Ces répertoires sont définies par défaut, ils peuvent être changés dans le fichier de redirection wasp.red. Ce fichier contient aussi les endroits où il doit trouver les fichiers java et class. Ces informations peuvent aussi être modifiées dans un fichier de projet pour l’analyse d’un programme particulier en utilisant l’équation lookup. Voici le fichier de redirection par défaut : *.java = c:\wasp\mJDK;src *.class = c:\jdk1.3.1\jre\lib\rt.jar;src *.prj = prj *.mes = msg *.grf = grf *.irf = irf Dans un fichier de projet, en plus de définir les options et équations (s’il y en a), il faut définir la classe de niveau supérieur. Cette classe est celle qui sera analysée. Par exemple, si cette classe se nomme M aClasse, il faut écrire !class MaClasse. Une méthode de niveau supérieur est aussi nécessaire, mais celle-ci est définie par défaut comme étant la méthode main (String[]) de la classe de niveau supérieur. C’est cette méthode qui sera analysée ainsi que toutes celles appelées de façon immédiate ou transitive par cette dernière. Si on désire que ce soit une autre méthode que celle par défaut ou plusieurs méthodes (ce qui est utile pour les applets), on peut les spécifier à l’aide de l’équation top. Voici un exemple de fichier de projet : -lookup = *.java = c:\MonProjet; !class MaClasse Pour lancer l’analyse d’un fichier source java, il suffit donc simplement d’appeler la commande wasp suivie du nom du fichier tandis que pour un 37 projet, on la fait suivre du nom du fichier de projet. Pour que le tout fonctionne, il faut bien entendu que le fichier en paramètre de la commande soit dans un répertoire indiqué dans le fichier de redirection. 5.5 Interprétation des messages Cette section a pour but d’expliquer la signification des différents messages que Wasp écrit dans les fichiers mes. Pour chaque erreur, Wasp donne les informations suivantes : – S’il s’agit d’une erreur absolue (qui arrivera à toutes les exécutions), conditionnelle ou d’un avertissement. – Le fichier ainsi que la ligne et l’endroit dans celle-ci où se trouve l’erreur. La ligne est affichée avec un symbole # servant à attirer l’attention sur l’opération qui cause l’erreur. – Une description textuelle de l’erreur est aussi présente. Il est aussi possible de rencontrer les termes suivants dans la description des erreurs : – #REFERENCES : Un objet ou un tableau est représenté par une variable $newK où K doit être remplacé par un entier. $newK[] représente un élément arbitraire du tableau $newK. Ce qui est écrit après ce terme peut être par exemple : a^=$new0!. Cela signifie que a est une référence sur la variable $new0. Le symbole ! signifie que a est nécessairement une référence sur cette variable. Le symbole ? au même endroit aurait voulu dire que a peut être une référence sur cette variable. – #DECLARATIONS : Ce terme suit toujours le terme présenté plus haut. Pour le même exemple que plus haut, ce qui suit ce terme indique l’endroit dans le code source où est déclaré la référence a et où elle devient une référence de la variable $new0. – #CONTEXT : Ce terme n’est pas en lien avec les deux autres. Il est utilisé pour une erreur présente dans une méthode et indique à partir de quelle autre méthode et de quel endroit dans le code elle doit être appelée pour que l’erreur soit possible. Ce qui est écrit après ce terme peut être par exemple : F(5,14) <- G(8,16 ?) <- main(12,15 ?). Le premier symbole ? dans cette chaı̂ne indique que l’appel de F par G nécessite certaines conditions. Lorsque cet arbre d’appel a plusieurs branches, 38 c’est-à-dire qu’il y a plus d’un contexte à énumérer, les autres branches sont présentés sur des lignes différentes qui commencent par le symbole #. 5.6 Essai de l’outil Cette section montre les résultats retournés par Wasp sur plusieurs exemples. Ces résultats sont analysés et il est indiqué ce qui est correct dans ceuxci ainsi que ce qui ne l’est pas ou bien ce qui est manquant. Pour chaque programme d’essai, on peut d’abord lire son code source, le fichier mes qui contient les résultat de l’analyse et un paragraphe pour expliquer et juger ces résultats. Le temps pris pour analyser chaque programme a été inférieur à cinq secondes. 5.6.1 demo.java class demo { public static void main(String args[]) { int i = 4; while (i < 4) i++; while (i > 0) i++; } } ############################ UNREACHABLE BRANCHES ########################## [W] (demo.java 3,8) method main not completed normally #public static void main(String args[]) [W] (demo.java 6,25) false while-condition - loop body never executed while (i #< 4) i++; [W] (demo.java 7,25) while-condition always = true while (i #> 0) i++; ######################### UNUSED VARIABLES (Strong) ######################## [W] (demo.java 3,39) formal parameter args not used public static void main(String #args[]) 39 Dans ce programme, on peut constater que l’instruction à l’intérieur de la première boucle while ne sera jamais exécutée puisque la condition est toujours fausse. On note aussi que la deuxième boucle est infinie parce que la condition est toujours vraie. Ces deux informations sont bien présentes dans le résultat d’analyse. Il est aussi inscrit que la méthode ne se termine pas normalement (à cause de la boucle infinie) et que le paramètre de la méthode main n’est pas utilisé, ce qui est aussi vrai. 5.6.2 demo2.java class demo2 { public static void main(String args[]) { int i = 4; int[] a = new int[7]; for (int j = 0; j <=7; j++) a[j] = j; i = 5 / (i - 4); } } ###################### Scalar Errors (ABSOLUTE ERRORS) ##################### [E] (demo2.java 9,22) zero divisor 0 i = 5 #/ (i - 4); ########################## Scalar Errors (WARNINGS) ######################## [W] (demo2.java 8,29) range 0:6 overflow on value 0:7 a[j] #= j; ############################ UNREACHABLE BRANCHES ########################## [W] (demo2.java 3,8) method main not completed normally #public static void main(String args[]) ######################### UNUSED VARIABLES (Strong) ######################## [W] (demo2.java 3,39) formal parameter args not used public static void main(String #args[]) [W] (demo2.java 9,18) variable i assigned but not used i #= 5 / (i - 4); 40 ########################## UNUSED VARIABLES (Weak) ######################### [W] (demo2.java 8,29) variable $new5[] assigned but not used a[j] #= j; #REFERENCES a^=$new5! #DECLARATIONS $new5:(6,34) a:(6,22) Il y a deux choses à noter dans cet exemple. Premièrement, on remplit le tableau en dépassant sa limite supérieure. Deuxièmement, la dernière instruction cause une division par zéro et cela à toutes les exécutions. Ces deux erreurs sont détectées. On indique aussi que les valeurs affectées à la variable i et au tableau a ne sont pas utilisés. 5.6.3 demo3.java class demo3 { public static void main(String args[]) { int[] a = new int[7]; for (int j = 0; j <=7; j+=2) a[j] = j; } } ######################### UNUSED VARIABLES (Strong) ######################## [W] (demo3.java 3,39) formal parameter args not used public static void main(String #args[]) ########################## UNUSED VARIABLES (Weak) ######################### [W] (demo3.java 7,29) variable $new5[] assigned but not used a[j] #= j; #REFERENCES a^=$new5! #DECLARATIONS $new5:(5,34) a:(5,22) Dans cet exemple, contrairement au précédent, il n’y a pas d’erreur puisque qu’une valeur est affectée seulement aux cases paires du tableau a. Le fichier du résultat de l’analyse retourne encore une fois l’information à laquelle on pouvait s’attendre. 41 5.6.4 demo4.java class demo4 { public static void main(String args[]) { int i = 0; int j = 15; f(j); while (i < 10) { System.out.println(i); f(i); i++; } } public static void f(int x) { x = 5 / (x - 3); } } ########################## Scalar Errors (WARNINGS) ######################## [W] (demo4.java 18,22) zero divisor -3:6, 12 x = 5 #/ (x - 3); ######################### UNUSED VARIABLES (Strong) ######################## [W] (demo4.java 3,39) formal parameter args not used public static void main(String #args[]) [W] (demo4.java 18,18) variable x assigned but not used x #= 5 / (x - 3); Dans ce programme, l’instruction de la fonction f peut causer une division par zéro si le paramètre x est égal à 3. Ceci arrive une fois lorsque la fonction est appelée avec i comme paramètre effectif, mais pas si ce paramètre est j. Wasp détecte bien la possibilité d’une division par zéro en indiquant que le diviseur peut prendre une valeur entre -3 et 6 (i) et aussi 12 (j). Malheureusement, il aurait dû trouver que cette erreur ne peut survenir 42 qu’au moment du deuxième appel de la fonction dans le code source, ce que l’analyse de contexte ne semble pas avoir révélée. 5.6.5 demo5.java import java.util.Vector; class demo5 { public static void main(String args[]) { int i = 4; Vector a = new Vector(); Vector b; b = a; if (a == b) i = -4; } } ######################### UNUSED VARIABLES (Strong) ######################## [W] (demo5.java 5,35) formal parameter args not used public static void main(String #args[]) [W] (demo5.java 7,22) variable i assigned but not used int i #= 4; [W] (demo5.java 11,30) variable i assigned but not used if (a == b) i #= -4; Ce qu’il faut remarquer dans ce programme est que l’on crée deux références a et b sur le même objet (le type de l’objet n’est pas important). Ceci fait en sorte que la condition a == b est toujours vraie. Malheureusement, il n’en est pas du tout mention dans le rapport d’analyse. Le problème est aussi présent avec un objet de type String (ceci a été testé, mais n’est pas présenté dans le rapport). 5.6.6 demo6.java import java.util.Random; 43 class demo6 { public static void main(String args[]) { int i; int j; Random generateur = new Random(); i = Math.abs(generateur.nextInt()) % 10 + 1; System.out.println(i); j = 1 / i; if (i > 0) while (i < 0) i--; } } ########################## Scalar Errors (WARNINGS) ######################## [W] (demo6.java 12,22) zero divisor -8:10 j = 1 #/ i; ############################ UNREACHABLE BRANCHES ########################## [W] (demo6.java 14,33) false while-condition - loop body never executed while (i #< 0) i--; ######################### UNUSED VARIABLES (Strong) ######################## [W] (demo6.java 5,39) formal parameter args not used public static void main(String #args[]) [W] (demo6.java 12,18) variable j assigned but not used j #= 1 / i; Dans ce programme, il est important de noter que la valeur de i après sa première affectation est comprise entre 1 et 10. Ceci fait en sorte que l’instruction suivante ne peut pas causer de division par zéro et que la condition i > 0 de l’instruction if sera toujours vraie. De plus, on peut noter que la condition i < 0 de la boucle while est toujours fausse puisque la condition précédente doit être vraie pour que celle-ci soit évaluée. D’après les résultats de l’analyse, il semble que Wasp ne tienne pas compte de la valeur absolue, puisqu’il indique que la valeur de i peut être comprise entre -8 et 10. Ce qui est décevant, c’est que cela fait en sorte qu’il conclut qu’il y a une possibilité 44 de division par zéro. C’est la première fois qu’une chose est fausse dans le rapport. Évidemment, il ne peut donc pas savoir non plus que la condition i > 0 est toujours vraie. Par contre, il trouve que l’autre condition est toujours fausse sachant que la précédente doit être vraie pour que celle-ci soit évaluée. 5.6.7 demo7.java class demo7 { public static void main(String args[]) { int i = 1; f(i); } public static void f(int x) { if (x > 0) f(x + 1); } } ############################ UNREACHABLE BRANCHES ########################## [W] (demo7.java 3,4) method #public static void [W] (demo7.java 9,4) method #public static void [W] (demo7.java 11,14) true if (x #> 0) f(x main not completed normally main(String args[]) f not completed normally f(int x) condition - unreachable else-branch + 1); ######################### UNUSED VARIABLES (Strong) ######################## [W] (demo7.java 3,35) formal parameter args not used public static void main(String #args[]) Dans ce programme, après que la fonction f soit appelée pour la première fois, elle s’appelle toujours elle-même sans arrêt. Il s’agit d’une récursivité infinie. Wasp indique que la fonction f ne termine pas normalement, ce qui est vrai puisqu’il s’agit d’une récursivité infinie. Il indique la même chose 45 pour la fonction main. Pour que celle-ci termine, il faudrait d’abord que f termine. Enfin, il détecte que la condition du if est toujours vraie. Même si Wasp ne dit pas explicitement qu’il y a une récursivité infinie, il donne des informations qui sont utiles pour découvrir qu’il y en a une. 5.7 Avantages et désavantages Les avantages de Wasp : – Il est rare qu’il donne de faux avertissements dans les résultats, ce qui arrive souvent avec d’autres outils qui donnent des hypothèses d’erreurs, ce qui n’est pas le cas de Wasp. – L’installation, l’apprentissage et l’utilisation de l’outil sont relativement simples et rapides. – L’analyse d’un programme se fait très rapidement (moins de cinq secondes pour les programmes du jeu d’essai). Les désavantages de Wasp : – L’information venant avec l’outil manque de contenu à propos de la méthode utilisée pour trouver des erreurs. – Il faudrait avoir de l’information pour savoir et comprendre dans quelles conditions Wasp peut détecter un type d’erreur donné et dans quelles conditions il ne le peut pas. 46 Chapitre 6 CodeSurfer CodeSurfer 1 [16] est un outil de découpage de programmes conçu par la compagnie Grammatech et qui traite le code source en langage C. Le découpage est une technique qui a déjà été présentée à la section 2.2. Il est intéressant de prendre en compte que cet outil commercial est gratuit pour une utilisation universitaire dans un but de recherche ou d’éducation. La version 1.5 de l’outil, sortie en juillet 2001, est la dernière version disponible au moment où ce rapport est écrit. Puisqu’elle n’était pas encore sortie au moment du début de l’essai de CodeSurfer, certaines informations traitent de la version 1.4. Ces deux versions sont respectivement nommées nouvelle et ancienne version dans le rapport. Le chapitre présente entre autres les fonctionnalités de base et les limites de l’outil suivies d’autres fonctionnalités plus avancées. Enfin, il présente un guide d’utilisation, des essais ainsi que les avantages et les désavantages de l’outil. 6.1 Fonctionnalités L’outil CodeSurfer permet de calculer le résultat d’un découpage avant ou arrière sur un ensemble de points sélectionnés dans un code source. Le découpage sur un ensemble de points est équivalent à l’union des résultats du 1 http://www.codesurfer.com 47 découpage pour chaque point pris de façon séparé. Les points du programmes correspondent souvent à des instructions. Par exemple, L’instruction z = x ∗ y correspond à un point de programme. Chaque noeud dans le graphe de dépendance correspond à un point de programme. Dans la nouvelle version, il est maintenant possible de faire un découpage sur une variable dans une instruction, ce qui n’était pas possible dans l’ancienne. Il sera question plus loin de cette façon de faire des requêtes ainsi que des autres façons disponibles dans la nouvelle version. De plus, il est possible de calculer l’ensemble des prédécesseurs et des successeurs d’un point du programme ou d’une variable dans un point. L’ensemble des prédécesseurs est l’ensemble des noeuds du graphe de dépendance desquels il part un arc allant vers le point choisi tandis que l’ensemble des successeurs est l’ensemble des noeuds vers lesquels il va un arc à partir du point choisi. En connaissant ces deux nouvelles notions, on peut redéfinir le découpage avant et arrière comme étant respectivement la fermeture transitive de la fonction permettant de calculer l’ensemble des successeurs et des prédécesseurs. Évidemment, en utilisant cette définition dans un cadre interprocédural, il ne faut que tenir compte des chemins valides, c’est-à-dire ceux pour lesquels la sortie d’une fonction retourne à la fonction appelante. CodeSurfer permet également de calculer la tranche (traduction libre de chop) entre deux points du programme. Elle a comme paramètres deux ensembles de points : la source et la destination. Elle décrit comment l’ensemble source affecte l’ensemble de destination. En fait, la tranche est simplement l’intersection entre le découpage avant sur la source et le découpage arrière sur la destination. Comme dans l’exemple présenté plus tôt à la section 2.2, ceci a entre autres pour utilité de vérifier les politiques de sécurité qui disent qu’il ne doit pas y avoir de dépendance de données entre deux variables ou le contenu de deux fichiers. Pour toutes les opérations décrites plus haut, il est toujours possible de la faire en fonction de la dépendance de données seulement, de la dépendance de contrôle seulement ou bien les deux ensemble. La nouvelle version offre une requête supplémentaire appelée fermeture du prédécesseur de contrôle (traduction de control predecessor closure). On peut constater, lorsqu’on fait un découpage arrière, que le résultat correspond toujours à un programme qui est correct. Par contre, pour un découpage avant, ceci n’est pas vrai. On peut s’en convaincre avec un exemple simple : 48 z = 1; if (x) y = z; Cette partie de programme contient trois noeuds : z = 1, if (x) et y = z. Il y a une dépendance de données entre le premier et le troisième noeud et une dépendance de contrôle entre le deuxième et le troisième noeud. Donc, un découpage arrière sur le troisième noeud inclut les trois noeuds dans le résultat. Par contre, un découpage avant sur le premier n’inclut pas le deuxième. Le programme résultant exécute donc l’instruction y = z en tout temps puisque le if n’est plus là, ce qui n’est pas correct. La nouvelle requête a donc pour but d’ajouter les noeuds manquants pour avoir un programme correct. Il y a aussi, dans la nouvelle version, des façons autres que de simplement choisir des points de programme pour les requêtes : – Points et variables : On doit sélectionner des points de programme comme dans la façon traditionnelle, mais, en plus, on choisit des variables. On peut donc avoir un découpage sur une seule variable dans une instruction. Par exemple, on peut avoir le découpage arrière sur la variable x seulement dans l’instruction z = x + y, ce qui n’est pas possible dans le mode point seulement. – Variables : Dans ce mode, on ne choisit pas de point, mais plutôt des variables et les points sont sélectionnés automatiquement selon les variables choisies. Pour un découpage arrière, les points sélectionnés sont ceux qui utilisent les variables choisies tandis que pour un découpage avant, il s’agit des points qui peuvent les modifier. – Fonctions : Encore là, on ne choisit pas de point, mais on indique les fonctions sur lesquels on veut effectuer la requête. On indique aussi ce que l’on veut de ces fonctions comme, par exemple, le point d’entrée ou les sites d’appels. Parmi les autres fonctionnalités de CodeSurfer, on note qu’il y a une calculatrice permettant d’appliquer les opérations ensemblistes sur les ensembles de points d’un programme est aussi disponible. Elle permet aussi d’enregistrer des ensembles dans un projet pour s’en servir plus tard sans avoir à les redéfinir. Il est aussi possible de voir le graphe d’appels de fonctions du programme analysé. 49 6.2 Efficacité La précision des résultats de l’outil dépend de l’exactitude des dépendances établies par ce dernier. Comme il est mentionné dans le guide de l’utilisateur [17], l’outil n’est pas totalement efficace, c’est-à-dire qu’il peut conclure qu’il y a une dépendance entre deux points d’un programme même s’il n’y en a pas, ce qu’on appelle un faux positif ou qu’il n’y en a pas même s’il y en a une, ce qu’on appelle un faux négatif. Ceci est tout de même normal puisqu’une analyse statique totalement efficace n’est généralement pas calculable. 6.2.1 Faux négatifs En ce qui a trait aux faux négatifs, ils arrivent surtout lorsque le code ne respecte pas les conventions. Effectivement, si on voulait être certain de montrer toutes les dépendances possibles, il faudrait donner vraiment beaucoup de dépendances hypothétiques qui seraient sûrement pour la plupart des faux positifs. Il y a quand même un désavantage à ne pas les nommer puisque les résultats données par l’outil serviront à faire la détection de code malicieux et on sait que les personnes malveillantes qui écrivent ce code violent souvent les conventions de programmation dans le but d’en rendre la détection plus difficile. Ces erreurs sont causées entre autres par la réutilisation de la mémoire et les accès à un tableau à l’extérieur de ses bornes. Voici des exemples tirés du manuel de l’utilisateur qui illustrent ces erreurs : – Unions : Le programme suivant affiche 12345. Le même espace mémoire est alloué pour les différents membres d’une structure union. Par contre, la dépendance entre U.g et U.f n’est pas prise en compte. main() { union { int f; int g; } U; U.f = 12345; printf("%d", U.g); } – Pile : Avec certains compilateurs, la variable y de la fonction g occupera le même espace mémoire que la variable x de la fonction f dans la pile. 50 Même si la variable x est dépilée en sortant de f , sa valeur n’est pas effacée de la mémoire. Par conséquent, le programme affiche 12345, mais la dépendance entre x et y n’est pas prise en compte. void f() { int x; x = 12345; } void g() { int y; printf("%d", y); } void main() { f(); g(); } – Tas : Encore une fois, le programme suivant affiche 12345. C’est parce que la mémoire allouée dynamiquement n’est effacée ni au moment où elle est libérée, ni au moment où elle est réallouée. Les pointeurs p et q pointent à des moments différents sur le même espace mémoire. Par contre, la dépendance entre les deux n’est pas prise en compte. main() { int *p, *q; p = (int*)malloc(sizeof(int)); *p = 12345; free(p); q = (int*)malloc(sizeof(int)); printf("%d", *q); } – Tableaux : Le tableau B suit le tableau A en mémoire puisqu’il a été déclaré après. Une écriture dans le tableau A passé sa borne supérieure sera donc faite dans le tableau B. Cette situation se produit dans le 51 programme suivant, ce qui fait en sorte qu’il affiche 12345. Par contre, la dépendance entre l’instruction d’affectation et celle d’affichage n’est pas prise en compte. main() { int A[1]; int B[1]; A[1] = 12345; prtinf("%d", B[0]); } – Interruptions : Supposons que dans le programme suivant, la fonction f soit appelée sur une interruption entre l’exécution des deux instructions de la fonction main. Dans ce cas, le programme afficherait 12345, mais la dépendance entre l’instruction d’affection de la fonction f et l’instruction d’affichage de la fonction main n’est pas prise en compte. int x; void f() { x = 12345; } void main() { x = 0; printf("%d", x); } 6.2.2 Faux positifs Pour ce qui est des faux positifs, ils sont dus pour la plupart à certaines simplification faites par CodeSurfer dans le but de faciliter le travail de recherche de dépendances et l’accélérer dans le cas de gros programmes à analyser. Voici des exemples de faux positifs : – Tableaux : Comme simplification, on considère entre autres un tableau comme étant une seule variable. Donc, on indiquera une dépendance entre deux cases d’un tableau, même si on peut être assuré qu’il s’agit de 52 cases différentes. Voici trois parties de code pour lesquels on indiquera à chaque fois une dépendance de données entre a et b : x[i] = a; b = x[j]; x[2*i] = a; b = x[2*k+1]; x[0] = a; b = x[1]; Dans la première partie, il y a une dépendance possible entre a et b, elle a lieu si i = j. Par contre, dans les deux autres parties, la dépendance est impossible. Dans la deuxième, c’est parce que 2 ∗ i est pair tandis que 2 ∗ k + 1 est impair. Pour l’autre partie, c’est le cas puisque 0 = 1. – Structures : Contrairement aux tableaux, les structures ne sont pas considérées comme étant de simples variables, leurs différents champs sont indépendants. Par contre, certaines circonstances peuvent contredire cet énoncé : 1. Pointeurs : Un pointeur sur une structure ou même sur un champ de celle-ci est considéré comme s’il pointait vers tous les champs de la structure. Dans ce cas, une dépendance est rapportée entre les deux instructions suivantes. P->f = x; y = P->g; 2. Affectation : À cause de l’affectation des structures dans l’exemple suivant, la variable x dépend des deux champs de la structure T . T.f T.g S = x = = 0; = 1; T; S.f; 3. Fonction : Le même genre de problème se produit lorsqu’une structure est passée comme argument d’une fonction. Un champ du pa53 ramètre formel est considéré comme dépendant de tous les champs du paramètre effectif. – Pointeurs : Puisque l’analyse de pointeurs est coûteuse en temps, les dépendances qui mettent en cause des pointeurs se basent sur un ensemble calculé pour chaque pointeur et qui determine l’ensemble des variables qu’il peut pointer au cours de l’exécution. À cause de cela, puisque le pointeur p peut pointer vers x et y dans le programme suivant, les variables r et s dépendent chacune de x et y alors que r devrait seulement dépendre de x tandis que s devrait dépendre de y seulement. if ( b ) { p = &x; r = *p; } else { p = &y; s = *p; } 6.3 Différents types de points de programme Les noeuds du graphe de dépendance et du graphe de flot de contrôle sont des instructions ou points du programmes. Chaque point de programme possède un type qui dépend de l’instruction qu’il représente. En voici une liste avec les explications pour les plus communs : – actual-in : Paramètre effectif (passé en argument dans un appel de fonction). – actual-out : Variable qui accepte la valeur retournée par une fonction qui retourne une valeur. – body : Point unique au graphe de dépendance de chaque procédure qui correspond au premier point exécutable. Ce point a plutôt un usage interne qu’une signification pour l’être humain. – call-site : Point d’appel direct de fonction. – control-point : Instructions conditionnelles if, while, switch, for. 54 – declaration : Variable déclarée par le programmeur ou par un paramètre formel. – entry : Point unique à chaque fonction étant la cible des points d’appels directs. – exit : Point commun que vient rejoindre tous les points return avant de sortir d’une fonction. – expression : Expression ou instruction d’affectation. – formal-in : Paramètre formel. – formal-out : Résultat retourné par une fonction. – global-actual-in : actual-in généré pour une variable globale utilisée ou modifiée par une fonction de façon immédiate ou transitive. – global-actual-out : actual-out généré pour une variable globale modifiée par une fonction de façon immédiate ou transitive. – global-formal-in : formal-in généré pour une variable globale utilisée ou modifiée par une fonction de façon immédiate ou transitive. – global-formal-out : formal-out généré pour une variable globale modifiée par une fonction de façon immédiate ou transitive. – indirect-call : Appel de fonction indirect via un pointeur de fonction. – jump : Instruction goto, break ou continue. – label : Une étiquette dans le programme. – return : Instruction return pour sortir d’une fonction. – switch-case : Commande case ou default dans une structure switch. – variable-initialization : Initialisation d’une variable globale ou statique. 6.4 Filtres Puisqu’un programme est composé de plusieurs types de noeuds et que certains d’entre eux sont peu significatifs pour l’être humain, il existe des filtres dans le but de cacher ses noeuds. Les filtres ont plusieurs usages, mais c’est toujours dans le but de faciliter la compréhension de l’utilisateur en cachant certains des nombreux types de noeuds. On peut empêcher certains types de noeuds d’être sélectionnés pour les requêtes. On peut cacher des points dans les feuilles de propriétés d’ensembles de points (voir la section 6.5). Enfin, on peut modifier le résultat des requêtes de prédécesseurs et de successeurs en empêchant certains types de noeuds de faire partie des résultats. 55 Les filtres permettent de dire, lorsqu’il y a parcours de graphe pour répondre à une requête, pour chaque type de noeuds, s’il faut le considérer, passer au noeud suivant sans l’inclure dans le résultat ou bien arrêter le traitement. Par exemple, pour une requête de successeurs sur le point d’appel d’une fonction, on ne serait peut-être pas intéressé d’avoir comme résultat le point d’entrée de la fonction appelée puisqu’il s’agit d’un résultat évident. Dans ce cas, on spécifie dans le filtre qu’il faut passer ce type de noeud et considérer le suivant. Si d’un autre côté, on ne voudrait pas pour une requête, que le résultat soit cherché dans une fonction appelée, on définirait dans le filtre qu’il faut arrêter la recherche à chaque point d’appel de fonction. 6.5 Feuilles de propriétés Les feuilles de propriétés contiennent de l’information accessible dans CodeSurfer sur les différents éléments du programme analysé. Les informations disponibles sont différentes selon le type de l’élément du programme. Voici une liste des éléments pour lesquels il existe une feuille de propriétés et ce qu’elle peut contenir : – Définition de fonction : les fonctions qui l’appelle, les fonctions qu’elle appelle et les variables présentes dans la fonction. – Variable : les instructions où elle est présente, les pointeurs qui peuvent pointer sur elle, les instructions qui l’utilisent et celles qui la définissent. – Point de programme : les prédécesseurs et successeurs de contrôle et de données et les variables présentes. – Ensemble de points de programme : la liste des points qu’il contient. Il y a aussi plusieurs types de feuilles de propriétés concernant les fichiers et les points d’appels. Avec elles, on peut connaı̂tre entre autres : – Les options de configuration du projet. – Les fichiers inclus dans un fichier particulier et ceux qui incluent ce fichier. – Tous les appels à l’intérieur d’une fonction. – Les fonctions pouvant être appelées par un appel de fonction indirect via un pointeur de fonction. 56 6.6 Interpréteur Scheme CodeSurfer possède un interpréteur Scheme permettant à l’utilisateur d’appeler certaines fonctions définies dans le but d’accéder aux informations tirées d’un programme analysé. Il est aussi possible d’écrire des fonctions utilisant celles déjà définies, ce qui permet d’étendre les fonctionnalités de CodeSurfer. 6.6.1 Les informations accessibles à l’utilisateur Ces fonctions permettent d’accéder au graphe de dépendance et au graphe de flot de contrôle du programme. De plus, pour chaque noeud de ces graphes qui représentent les instructions du programme, on peut connaı̂tre les variables utilisées et celles modifiées ou possiblement modifiées. Pour accéder à ces informations à partir de la console Scheme, on exécute tout d’abord une fonction qui retourne la liste de tous les PDGs du programme. Il s’agit de la fonction sdg-pdgs. Il y a un PDG pour chaque fonction, mais il y en a aussi d’autres, entre autres pour chaque fichiers sources. Pour extraire un PDG de cette liste, on utilise la fonction list-ref. Elle a deux arguments, le premier étant la liste et le second le rang dans cette liste de l’élément désiré, le premier élément d’une liste étant l’élément 0. Malheureusement, on ne peut pas savoir à qu’elle fonction correspond n’importe quel PDG de la liste avant de l’avoir extrait et appelé la fonction qui retourne cette information sur ce dernier. Une fois qu’on a le PDG correspondant à la fonction désirée, on utilise des fonction pour en tirer l’information voulue. La liste des fonctions est dans le guide d’utilisation et elle est un peu longue donc on peut s’y référer. Voici tout de même une liste des principaux types de données présents dans cet API et les informations les plus intéressantes qu’on peut obtenir à partir de chacun d’eux : – PDG : Représente le graphe de dépendance d’une fonction du programme. Il peut aussi contenir de l’information sur le flot de contrôle si on a ajusté l’option concernant le flot de contrôle dans les options du projet. À partir du PDG d’une fonction, on peut entre autres, à l’aide des fonctions de l’API, connaı̂tre son nom, avoir l’ensemble des points de programme (PDG-VERTEX-SET ) de la fonction et accéder à un de ses noeuds en connaissant le numéro d’identification de ce dernier. 57 – PDG-VERTEX : Un point de programme, c’est-à-dire un noeud du graphe de dépendance ou du graphe de flot de contrôle. Les différents types de point de programme ont été définis plus tôt à la section 6.3. À partir d’un élément de ce type, on peut connaı̂tre son numéro d’identification, son type et la fonction à laquelle il appartient. On peut aussi connaı̂tre ses noeuds voisins dans le graphe de dépendance et dans le graphe de flot de contrôle et avoir de l’information sur les variables présentes : lesquelles sont utilisées et lesquelles sont modifiées ou possiblement modifiées. Une option du projet concerne les informations sur les variables et elle doit être correcte pour pouvoir accéder à cet information. – PDG-VERTEX-SET : Il s’agit d’un ensemble d’éléments de type PDGVERTEX. Il est possible de créer un PDG-VERTEX-SET et d’ajouter ou enlever des éléments dans cet ensemble et utiliser des opérations ensemblistes sur ces ensembles. Il est aussi possible de faire un traitement (représenté par une fonction qui prend un PDG-VERTEX en argument) sur tous les éléments de l’ensemble. – PDG-EDGE-SET : C’est un ensemble de couples. Chaque couple représente un arc du graphe de dépendance. Le premier élément du couple est de type PDG-VERTEX. Le deuxième est une chaı̂ne de caractères qui représente la sorte d’arc qui est égale à control pour une dépendance de contrôle ou data pour une dépendance de données. Il est possible d’appliquer un traitement sur chaque élément de ce type d’ensemble. Un seul noeud est mentionné par arc. Logiquement, un arc devrait être représenté par deux noeuds. Un PDG-EDGE-SET est obtenu suite a l’appel d’une fonction qui retourne les voisins d’un noeud. Ce noeud manquant, qui est l’argument de la fonction, n’est pas mentionné de nouveau dans le résultat. – CFG-EDGE-SET : C’est la même chose que le type PDG-EDGE-SET, mais il s’agit ici d’arc du graphe de flot de contrôle. Contrairement au type précédent, le deuxième champ du couple représente l’étiquette de l’arc. De chaque noeud correspondant à une instruction if ou while part deux arcs, une étiquetée #t pour vrai et #f pour f aux. Les arcs qui partent des instructions non conditionnelles ont la valeur #t par défaut. Dans le cas de l’instruction switch, il y a une étiquette différente pour chaque cas. Encore une fois, il est possible d’appliquer un traitement sur chaque élément de ce type d’ensemble. 58 Si on décide d’écrire des fonctions et de les enregistrer dans un fichier ayant pour extension stk, il faut utiliser la fonction load dans la console avant de pouvoir les utiliser. Cette fonction a le nom du fichier en argument. Il faut écrire le nom entre guillemets et ne pas écrire l’extension. En plaçant ce fichier dans le répertoire etc de CodeSurfer, on n’a pas à écrire le chemin d’accès au fichier. On peut aussi exécuter des fonctions de l’API sur un projet existant à partir d’une console DOS sans avoir à utiliser l’interface graphique de CodeSurfer. Pour ce faire, il faut écrire un fichier stk qui contient le code à exécuter. Si on se trouve dans le répertoire contenant le projet nommé bonjour, on que le code se trouve dans le fichier batch.stk, on exécute la commande suivante : csurf -b -l batch.stk Le fichier comme tel doit avoir la forme suivante. Les lignes débutant par deux points-virgules désignent des commentaires. ;; Spécification du fichier qui reçoit les informations ;; sorties comme les messages d’erreurs. (s-set-build-output-file! "messages.txt") ;; Ouverture du fichier qui contient la structure du projet. (s-read-sdg "CSURF.FILES/bonjour.sdg") ;; Ici, on met le code à exécuter. ;; Quitter à la fin. (quit) 6.6.2 Affichage des graphes de dépendance et de flot de contrôle Cette section présente deux fonctions, une servant à afficher de façon textuelle le graphe de dépendance d’une fonction, l’autre faisant la même chose pour le graphe de flot de contrôle. Elles fonctionnent de la façon suivante. On doit leur passer en paramètre une structure de type PDG. À partir de 59 cette structure, elles accèdent à l’ensemble des noeuds et pour chacun de ceux-ci, elles affichent l’instruction correspondant au noeud ainsi que la liste des instructions correspondant à la liste des noeuds accessibles par un arc. Pour le graphe de dépendance, à côté de chaque noeud de la liste, la fonction indique si l’arc qui a permis de l’atteindre représente une dépendance de données ou de contrôle. En ce qui a trait au graphe de flot de contrôle, l’étiquette de l’arc est affichée. Les noeuds accessibles sont présentés en deux blocs séparés. Le premier contient des noeuds de la même fonction que celui étudié tandis que le deuxième contient des noeuds appartenant à d’autres fonction. Autrement dit, il s’agit respectivement d’arcs intra-procédurals et inter-procédurals. Dans ce dernier cas, le nom de la fonction de la cible est indiquée. Voici le code de ces deux fonctions, la première, sd-pdg, est celle qui affiche la structure du graphe de dépendance tandis que la deuxième, sd-cfg, est celle qui affiche celle du graphe de flot de contrôle. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Nom: (sd-pdg x) ;; Arguments: x: PDG ;; Action: ;; Créer une représentation lisible de la structure de données ;; du PDG d’une fonction. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define (arcinter x y) (and ;;affiche l’instruction correspondant au noeud (display (pdg-vertex-characters x)) (display "\t") ;;affiche la sorte de noeud (display (pdg-vertex-kind x)) (display "\t") ;;affiche son identificateur unique (display (pdg-vertex-id x)) (display "\t") ;;affiche le type de dépendance (contr^ ole ou données) (display y) (display "\t") ;;affiche le nom de la fonction du noeud (display (pdg-procedure-name(pdg-vertex-pdg x))) (display "\n") ) ) (define (arc x y) (and ;;affiche l’instruction correspondant au noeud (display (pdg-vertex-characters x)) (display "\t") 60 ;;affiche la sorte de noeud (display (pdg-vertex-kind x)) (display "\t") ;;affiche son identificateur unique (display (pdg-vertex-id x)) (display "\t") ;;affiche le type de dépendance (PDG) ou l’étiquette (CFG) (display y) (display "\n") ) ) (define (noeud x) (and ;;affiche l’instruction correspondant au noeud (display (pdg-vertex-characters x)) (display "\t") ;;affiche la sorte de noeud (display (pdg-vertex-kind x)) (display "\t") ;;affiche son identificateur unique (display (pdg-vertex-id x)) (display "\n-------\n") ;;exécute la fonction arc pour chaque noeud accessible par un arc intra-procédural (pdg-edge-set-traverse (pdg-vertex-intra-targets x) arc) (display "-------\n") ;;exécute la fonction arcinter pour chaque noeud accessible par un arc inter-procédural (pdg-edge-set-traverse (pdg-vertex-inter-targets x) arcinter) (display "\n") ) ) (define (sd-pdg x) (and ;;affiche le nom de la procédure (display (pdg-procedure-name x)) (display "\n\n") ;;exécute la fonction noeud pour chaque noeud du PDG (pdg-vertex-set-traverse (pdg-vertices x) noeud) ) ) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Nom: (sd-cfg x) ;; Arguments: x: PDG ;; Action: ;; Créer une représentation lisible de la structure de données ;; du CFG d’une fonction. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define (cfgnoeud x) (and ;;affiche l’instruction correspondant au noeud (display (pdg-vertex-characters x)) (display "\t") ;;affiche la sorte de noeud (display (pdg-vertex-kind x)) 61 (display "\t") ;;affiche son identificateur unique (display (pdg-vertex-id x)) (display "\n-------\n") ;;exécute la fonction arc pour chaque noeud accessible par un arc intra-procédural (if (not (equal? (cfg-edge-set-cardinality (pdg-vertex-cfg-targets x)) 0)) (cfg-edge-set-traverse (pdg-vertex-cfg-targets x) arc)) (display "-------\n") ;;exécute la fonction arcinter pour chaque noeud accessible par un arc inter-procédural (if (not (equal? (cfg-edge-set-cardinality (pdg-vertex-cfg-inter-targets x)) 0)) (cfg-edge-set-traverse (pdg-vertex-cfg-inter-targets x) arcinter)) (display "\n") ) ) (define (sd-cfg x) (and ;;affiche le nom de la procédure (display (pdg-procedure-name x)) (display "\n\n") ;;exécute la fonction cfgnoeud pour chaque noeud du PDG (les m^ emes que ceux du CFG) (pdg-vertex-set-traverse (pdg-vertices x) cfgnoeud) ) ) Le modèle d’affichage est un choix personnel, il serait possible de refaire les fonctions et de faire un affichage différent. En connaissant la syntaxe des fichiers graphiques d’un programme comme VCG, on pourrait sûrement créer le graphe de flot de contrôle et le graphe de dépendance pour qu’ils puissent être visualisés de façon graphique. Aussi, les fonctions affichent tous les arcs en tenant compte de tous les noeuds, même ceux qui sont peu significatifs pour l’être humain. C’est la raison pour laquelle ils peuvent être gros. Lorsqu’on sélectionne une partie de code dans CodeSurfer et on demande d’afficher les points de programmes correspondant, certains pourraient ne pas être affichés tout dépendant du filtre choisi, question d’alléger le résultat. Le concept de filtre pourrait être réutilisé pour afficher des graphes plus simple en éliminant les noeuds moins significatifs. Voici un programme et ensuite le résultat donné par la fonction qui affiche textuellement le graphe de flot de contrôle pour la fonction main : #include <stdio.h> void f(int x, int y) { int z; 62 if (x) y = y * 2; z = y; y = 3; } void main() { int a; int b; b = 5; a = 0; f(a, b); } main b actual-in ------------- 13 a actual-in ------------- 12 } exit ------------#f return 11 } return ------} exit ------- 10 -48 #t 11 f() call-site ------} return 10 #System_Initialization #t 8 #t 63 ------f() entry 1 #t f a = 0 expression ------f() call-site 8 ------- 7 b = 5 expression ------a = 0 expression ------- 6 main() entry 1 ------b = 5 expression ------- #t 7 #t 6 #t int b declaration -60 ------------int a declaration -61 ------------main() body ------------- 3 Pour bien comprendre comment interpréter chaque bloc (on fait de la même pour un graphe de dépendance), voici un exemple avec le bloc suivant : f() call-site ------} return 10 8 #t 64 entry b=5 a=0 call-site f return exit Fig. 6.1 – Graphe de flot de contrôle dessiné à partir des informations sorties dans CodeSurfer ------f() entry 1 #t f On constate que ce bloc est divisé en trois sous-blocs par une ligne pointillée. Le premier comporte toujours un seul noeud, les autres peuvent en contenir plusieurs. Il y a un arc qui part du noeud dans le premier sous-bloc et qui va vers chaque noeud dans les deux autres. Les noeuds du deuxième sous-bloc font parti de la même fonction que le premier noeud tandis que ceux du troisième font parti d’autres fonctions. Ici, on constate donc que dans le graphe de flot de contrôle, il y a un arc étiqueté vrai qui part du point d’appel de la fonction f et qui va vers la fin du programme (point return de la fonction main). Un autre arc étiqueté vrai partant du même point va vers le point d’entrée de la fonction f . Il peut paraı̂tre bizarre d’avoir deux arcs étiquetés vrai partant du même point puisqu’on peut en considérer qu’une seule comme étant correcte. Cela dépend si on considère un graphe interprocédural ou intra-procédural. Dans le premier cas, on doit aller au point d’entrée de la fonction f . Par contre, dans l’autre cas où on ne parcourt pas l’intérieur de la fonction f , il faut savoir où continuer dans la fonction main, dans ce cas, on considère l’arc return. Comme ici, on n’a que le graphe de flot de contrôle pour la fonction main, on ne considérera que les arcs allant vers des points à l’intérieur de la même fonction. La figure 6.1 montre le résultat du dessin du graphe de flot de contrôle. La seule différence avec la théorie de la section 2.1.2 est que les noeuds return et exit ont été ajoutés. Voici maintenant le résultat de la fonction qui affiche le graphe de dépendance pour la fonction main du même programme. La présentation est la même que pour le graphe de flot de contrôle, sauf qu’au lieu d’indiquer les étiquettes des arcs, on indique si elle représente une dépendance de contrôle (control) ou de données (data). main 65 b actual-in 13 ------------int y formal-in 5 data f a actual-in 12 ------------int x formal-in 3 data f } exit ------------- 11 } return ------------- 10 f() call-site ------b actual-in a actual-in ------f() entry 1 8 13 12 control f a = 0 expression ------a actual-in 12 ------b = 5 expression ------b actual-in 13 ------main() entry ------} exit 11 control control 7 data 6 data 1 control 66 int b declaration -60 control int a declaration -61 control main() body 3 control ------int b declaration -60 ------b actual-in 13 data b = 5 expression 6 data ------int a declaration -61 ------a actual-in 12 data a = 0 expression 7 data ------main() body 3 ------} return 10 control f() call-site 8 control a = 0 expression 7 control b = 5 expression 6 control ------La figure 6.2 représente ce graphe dessiné en considérant tous les arcs intra-procédurals des informations retournées en résultat. Les arcs interprocédurals ne sont pas considérés. La figure 6.3 représente le graphe de dépendance pour la même fonction, mais dessiné selon la théorie présentée à la section 2.1.6. Bien que les deux soient différents, on peut exprimer, à l’aide de la notion de filtre, une méthode permettant de passer de un à l’autre. En parcourant le graphe à partir du point d’entrée de la fonction, on arrête lorsqu’on rencontre, un noeud de type declaration, return ou exit sans l’inclure. Pour le noeud body, on ne l’inclut pas, mais on continue quand même de parcourir ses successeurs. 67 entry body a=0 b=5 exit call-site f actual-in b declaration b declaration a return actual-in a Fig. 6.2 – Graphe de dépendance dessiné à partir des informations sorties dans CodeSurfer 68 Entrée a=0 b=5 appel f Param1 = b Param2 = a Fig. 6.3 – Graphe de dépendance de la figure 6.2 simplifié 6.7 Guide d’utilisation CodeSurfer démarre sur la fenêtre de projet. C’est à l’aide du menu Project qu’on peut créer un nouveau projet, ajouter des fichiers sources et entêtes à l’intérieur et configurer le projet. Une fois le projet construit avec Build Project on peut cliquer sur les fichiers dans la fenêtre de projet pour ouvrir une fenêtre de fichier dans laquelle on pourra choisir des points de programme et demander des requêtes. Il est aussi possible de construire un projet à partir d’une commande DOS (voir le manuel). Les différentes requêtes peuvent être exécutées à partir du menu Queries ou en utilisant les boutons imagés. Avant de faire une requête, il faut sélectionner les instructions sur lesquels on désire la faire. Si on veut choisir des points appartenant à des groupes éloignés les uns des autres, il faut sélectionner chaque groupe un par un et faire à chaque fois Queries / Add Points. On choisit dans ce sous-menu query-points pour les requêtes de découpage, prédécesseur et successeur. Pour les tranches, il faut passer par ce menu et choisir chop-sources et chop-targets pour les points sources et destinations de la tranche respectivement. Il y a un bouton pour chaque requête en haut de la fenêtre. Il y a aussi des boutons (dans la fenêtre de projet) pour choisir les dépendances à considérer pour les requêtes (données, contrôle, les 69 deux) et pour choisir la façon d’entrer la requête (points, variables, points et variables, fonctions). Il y a aussi une liste déroulante qui sert à choisir le type de filtre utilisé. Pour modifier les filtres, il faut choisir Preferences dans le menu Project. En sélectionnant un point ou un ensemble de points, on peut accéder à sa feuille de propriétés en appuyant sur le bon bouton au haut de la fenêtre. Pour voir le graphe d’appel du programme ou utiliser le calculateur d’ensemble, l’appel se fait à l’aide du bouton approprié. Pour ce qui est de l’interpréteur Scheme, on choisit Console dans le menu Project de la fenêtre de projet. 6.8 Essai de l’outil Cette section montre des exemples de requêtes dans CodeSurfer sur de petits programmes. La figure 6.4 montre un découpage arrière sur l’instruction à la dernière ligne (celle qui est soulignée) du programme dans la fenêtre. Le résultat de ce découpage correspond au code en rouge. Il s’agit d’un exemple déjà traité à la section 2.2. On peut constater que le résultat obtenu est le même qu’à ce moment. La figure 6.5 montre un découpage avant sur la déclaration de la variable x. Ceci correspond à l’impact qu’aura cette variable dans le programme. La figure 6.6 montre l’application de la fermeture du prédécesseur de contrôle au découpage avant qui précède. Cela a pour effet d’ajouter l’instruction if au résultat. 6.9 Avantages et désavantages Les avantages de CodeSurfer : – Les limites de l’outil en ce qui a trait au calcul des dépendances sont mentionnées dans le manuel de l’utilisateur. – Les différents types et modes de requêtes liées au fait que l’on peut les exécuter sur des ensembles de plusieurs points et utiliser un calculateur d’ensemble permet une grande expressivité dans les requêtes. 70 Fig. 6.4 – Découpage arrière dans CodeSurfer 71 Fig. 6.5 – Découpage avant dans CodeSurfer 72 Fig. 6.6 – Fermeture du prédécesseur de contrôle sur le résultat de la figure 6.5 73 – L’API de CodeSurfer permet d’accéder aux résultats de l’analyse au niveau le plus bas et de s’en servir de la manière désiré pour calculer tout ce que l’on veut qui peut être déduit de ces derniers. Par exemple, dans [16], on présente qu’il est possible de se servir de d’une fonction pour vérifier qu’un programme analysé respecte un certain modèle. On peut vérifier des choses comme : La fonction a doit être exécutée avant que la fonction b puisse l’être. Ceci est possible puisque les informations nécessaires sont contenues dans le graphe de flot de contrôle que l’on peut avoir en utilisant l’API. Par contre, cette fonction qui semble réellement exister selon l’article n’est pas disponible dans la version actuelle de CodeSurfer. Les désavantages de CodeSurfer : – On ne peut pas utiliser les fonctions de l’API directement dans un autre langage de programmation, ce qui serait très intéressant. Il faut absolument utiliser un fichier script et appeler CodeSurfer en demandant de l’exécuter. Par contre, une alternative à ce problème serait de faire un fichier script qui appelle une fonction qui enregistre dans un fichier les informations d’analyse sur un programme. Par la suite, il faudrait créer une structure de données dans notre programme en chargeant ce fichier et l’utiliser à notre guise. – Le résultat d’un découpage ou d’une autre requête est seulement visible à l’écran et on peut avoir accès à l’ensemble des points de programme qu’il contient. Par contre, on n’a pas le programme correspondant au résultat et prêt à être compilé. 74 Chapitre 7 PolySpace C Verifier PolySpace C Verifier est un outil d’analyse statique conçu par la compagnie PolySpace Technologies 1 . Il détecte les erreurs pouvant survenir à l’exécution dans les programmes en langage C en se servant de l’interprétation abstraite [2]. Cet outil n’est pas gratuit, mais il est possible d’en demander une copie d’évaluation. Contrairement aux autres outils de ce rapport, celui-ci n’a pas été essayé. Un programme a été envoyé à la compagnie pour qu’il soit testé. Il n’y aura donc pas de guide d’utilisation, seulement une présentation de ce que fait l’outil [18] et les résultats de l’analyse du programme envoyé. Par contre, on peut consulter des démonstrations montrant l’outil en action [19, 20] ainsi qu’un document [21] qui est une courte introduction à son utilisation. 7.1 Fonctionalités et méthode Polyspace C Verifier peut détecter les erreurs suivantes : – – – – – 1 Lecture d’une variable non initialisée. Conflit d’accès pour des variables partagées non protégées. Référence via un pointeur nul ou pointeur à l’extérieur des bornes. Accès à un tableau à l’extérieur des bornes. Division par zéro. http://www.polyspace.com 75 – Opérations arithmétiques invalides (ex : racine carrée d’un nombre négatif). – Dépassement de capacité d’un nombre suite à une opération arithmétique. – Code inaccessible. – Conversion de type illégale. Polyspace C Verifier utilise une interface graphique qui permet de bien repérer et distinguer les erreurs dans le code source en utilisant un code de couleurs : – Vert : L’opération ne peut pas causer d’erreur à l’exécution. – Rouge : Il y aura une erreur à l’exécution chaque fois que cette opération sera exécutée. – Gris : Montre le code inaccessible. – Orange : L’opération peut causer une erreur à l’exécution dans certaines circonstances. Pour les erreurs qui surviennent dans un contexte particulier, c’est-à-dire pour une séquence d’appels de fonctions précise, il est possible de voir le graphe d’appels de fonctions qui mènent à l’erreur. Dans le but de détecter des erreurs dans les programmes concurrents, Polyspace C Verifier examine les variables globales et identifie les séquences d’utilisation qui ne sont pas sûres. Il est possible de voir le graphe d’accès concurrents dans le but de comprendre pourquoi il peut y avoir un conflit d’accès sur une variable partagée. 7.2 Essai de l’outil Le programme d’essai (figures 7.1, 7.2, 7.3 et 7.4) a été composé de façon à avoir dans un seul programme, douze petits programmes complètement indépendants. La plupart d’entre eux ne comportent qu’une fonction nommée par une lettre de a à l. La fonction main a pour unique but d’aiguiller vers les autres fonctions. – Fonction a : La chaı̂ne de caractère trop longue pour le tableau tab n’a pas été détectée. – Fonction b : L’accès au tableau tab au-delà de sa limite supérieure a été détecté. – Fonction c : Rien d’anormal a été détecté, la fonction est correcte. 76 #include <stdio.h> #include <string.h> #include <stdlib.h> typedef struct un_noeud { int elem; struct un_noeud* suivant; } noeud; void a() { char tab[10]; strcpy(tab, "un_peu_trop_long"); //dépassement de la borne supérieure } void b() { int tab[7]; int i; for (i = 0; i <= 7; i++) tab[i] = i; //dépassement de la borne supérieure } void c() { int tab[7]; int i; for (i = 0; i <= 7; i+=2) tab[i] = i; //pas de dépassement } void d() { int i; int j = 0; scanf("%d", &i); while (i < 20) j++; //condition de la boucle invariable } void e() { int i = 20; while (i > 20) printf("erreur"); //code inaccessible } void f() { Fig. 7.1 – Programme d’essai pour Polyspace C Verifier 77 int i = 20; while (i >= 20) i++; //boucle infinie } void g() { int i, j; for (i = 0; i < 10; i++) j = 1 / (i - 5); //division par zéro } void h1(int x) { x = 1 / (x - 5); //division par zéro pour le deuxième appel dans le code de h() } void h() { int i; h1(10); //correcte for (i = 0; i < 10; i++) h1(i); //division par zéro pour i = 5 } void i() { noeud* courant; noeud liste; courant = &liste; courant->elem = 1; courant->suivant = NULL; courant = courant->suivant; //pointeur nul printf("%d", courant->elem); //arrêt du programme } void j() { int i; noeud* courant; noeud liste; courant = &liste; courant->elem = 1; for (i = 0; i < 10; i++) { courant->suivant = (noeud*)malloc(sizeof(noeud)); if (courant->suivant == NULL) exit(1); courant = courant->suivant; courant->elem = 1; Fig. 7.2 – Programme d’essai pour Polyspace C Verifier 78 } courant->suivant = &liste; courant = &liste; while (courant->suivant != NULL) courant = courant->suivant; //boucle infinie, liste circulaire } void k() { int *p, *q; p = (int*)malloc(sizeof(int)); q = p; *p = 3; free(q); printf("%d", *p); //pointeur vers de la mémoire effacée } void l1(int i) { if (i > 0) l1(i + 1); //récursivité infinie } void l() { l1(1); } int main() { int choix; do { scanf("%d", &choix); switch (choix) { case 1: a(); break; case 2: b(); break; case 3: c(); break; case 4: d(); break; case 5: e(); break; case 6: f(); break; case 7: g(); break; case 8: h(); Fig. 7.3 – Programme d’essai pour Polyspace C Verifier 79 break; case 9: i(); break; case 10: j(); break; case 11: k(); break; case 12: l(); break; } } while (choix >= 1 && choix <= 12); return 0; } Fig. 7.4 – Programme d’essai pour Polyspace C Verifier – Fonction d : Dans cette fonction, la condition de la boucle est invariable. Ceci fait en sorte que le corps de la boucle n’est pas exécuté ou bien la boucle ne termine pas. On voit qu’un problème est détecté puisque le while est en rouge. Normalement, il ne devrait pas être rouge puisqu’il n’y a pas d’erreur en tout temps. Par contre, ceci est normal si on ne considère pas les cas où le corps de la boucle n’est pas exécuté. Cette hypothèse semble bonne puisque l’appel à la fonction dans le main n’est pas en rouge alors qu’il l’est pour toutes les fonctions qui ne terminent jamais (lorsque c’est détecté). On détecte aussi que la variable j peut dépasser sa capacité supérieure, ce qui est logique puisque la boucle est infinie si elle est exécutée. – Fonction e : Le i dans la condition de la boucle est en gris, ce qui montre qu’il y a du code inaccessible en rapport avec cette boucle. – Fonction f : La boucle est infinie, ce qui est bien détecté. – Fonction g : La division possible par zéro est détectée ainsi que la boucle f or qui ne termine pas (à cause de la division par zéro). – Fonction h : La division possible par zéro dans la fonction h1 est détectée, mais les informations données ne mentionnent pas que cela peut seulement arriver lors d’un appel provenant de la dernière ligne de la fonction h et non de la deuxième. – Fonction i : Cette fonction ne termine pas parce qu’il y a une référence via un pointeur nul, ce qui cause un arrêt du programme. Par contre rien n’indique, de façon claire du moins (il n’y a pas de rouge), que cette erreur est détectée. Aussi, dans la fonction main, l’appel à cette 80 fonction n’est pas en rouge. – Fonction j : Ici, on essai de parcourir au complet une liste qui est circulaire, ce qui fait en sorte que ça boucle indéfiniment. Ce problème est plus difficile que le précédent et, encore une fois, rien n’indique de façon claire qu’il a été détecté. L’appel dans la fonction main n’est pas en rouge non plus. – Fonction k : Le pointeur vers la mémoire effacée est bien détecté. – Fonction l : On détecte que la fonction l1 ne termine pas, ce qui est normal puisqu’il y a récursivité infinie. La qualité de ces résultats est très bonne puisque la majorité des erreurs ont été trouvées. On peut aussi noter que le temps pour analyser ce programme a été de quinze minutes, ce qui quand même long. 7.3 Avantages et désavantages Voici les avantages de PolySpace C Verifier : – La présentation des résultats est agréable. – La qualité des résultats est très bonne par rapport au nombre d’erreurs trouvées. – Le fait d’avoir des liens verts, c’est-à-dire pour ce qui est correct, permet de savoir ce qui a été testé. Voici les désavantages de PolySpace C Verifier : – Le temps d’analyse semble très long. Avec un temps de quinze minutes pour le petit programme d’essai, on devrait s’attendre à un temps énorme pour un véritable programme de quelques dizaines de milliers de lignes. – Les informations sur les erreurs détectées pourraient être plus complètes. Par exemple, au lieu de dire simplement que le dénominateur peutêtre différent de zéro, on pourrait donner les valeurs qu’il peut prendre lorsque c’est possible. 81 Chapitre 8 Conclusion Pour terminer, il sera question d’autres outils qui n’ont pas été testés. On traitera aussi de l’utilité que les outils trouvés peuvent avoir pour détecter du code malicieux ainsi que d’une nouvelle piste de recherche. 8.1 Autres outils Cette section présente des outils qui n’ont pas été testés, mais qui semblaient très intéressants avec les raisons pourquoi ils ne l’ont pas été. 8.1.1 Malicious Code Filter Malicious Code Filter [22] est un outil de détection de code malicieux. Il se sert entre autre du découpage de programme. Il est basé sur une approche dite de vérification de signes révélateurs dans le programme analysé. La présence ou non de ces signes permet de conclure s’il y a ou non du code malicieux dans le programme. Cet outil n’est pas disponible pour être essayé. On peut consulter [23] dans le but d’en savoir plus en français sur la théorie à propos de cet outil. 82 8.1.2 Vista Vista 1 est un outil qui permet de dessiner les graphes qui représentent un programme en langage C. Il s’agit des graphes de flot de contrôle, de dépendance de contrôle et de dépendance de données. Pour chacun de ces graphes, il est possible d’accéder à sa structure de données, mais aussi d’en avoir une représentation visuelle. Malheureusement, il n’a pas été possible d’avoir une copie de l’outil pour l’essayer. 8.1.3 Unravel Unravel 2 [24] est un outil de découpage de programme pour le code en langage C tout comme CodeSurfer. Il est gratuit, mais la raison pour laquelle il n’a pas été essayé est qu’il ne semble pas offrir autant de possibilités que CodeSurfer. Par contre, il est possible d’avoir son code source, ce qui permet de modifier l’outil. 8.1.4 ITS4 ITS4 3 [25] est un outil servant à vérifier si un programme en C utilise des fonctions qui causent des vulnérabilités comme les débordements de tampon qui peuvent être causés entre autres par la fonction strcpy(). Tout ce que fait l’outil est de parcourir le code source à la recherche de ces fonctions, ce qui résulte donc en plusieurs faux positifs. C’est donc la raison pour laquelle il n’a pas été essayé. Par contre, l’outil peut être utile pour un programmeur qui ne connaı̂t pas bien ce qui peut causer des vulnérabilités, mais qui s’en soucie. Aussi, une chose intéressante est que l’outil est gratuit et son code source est disponible. 1 http://www.cigital.com/VISTA-demo/ http://hissa.nist.gov/unravel/ 3 http://www.cigital.com/its4/ 2 83 8.2 Utilité des outils pour la détection de code malicieux À part les outils qui détectent vraiment le code malicieux comme le fait Samcots, on peut séparer les outils trouvés et qui sont intéressant comme ayant trois fonctionnalités différentes : dessiner des graphes, faire du découpage de programme et détecter des erreurs pouvant survenir à l’exécution. Voici l’utilité que peuvent avoir chacune d’elle dans la détection de code malicieux. 8.2.1 Graphes Les graphes forment une représentation du programme analysée. Cette représentation peut-être utile pour détecter du code malicieux. Samcots débute d’ailleurs par construire le graphe de flot de contrôle du programme qu’il analyse. Si on considère un virus qui a copié son code à l’intérieur d’un autre programme exécutable, se code devrait nécessairement être détaché du code du programme infecté. Un graphe de flot de contrôle du fichier exécutable (ou de la version désassemblée de celui-ci) montrera une partie de code qui semble détachée du reste. Il faudrait donc extraire cette partie et l’analyser pour voir s’il s’agit de code malicieux. 8.2.2 Découpage Le découpage de programme sert à faire ressortir le code qui a une dépendance avec des instruction particulières du programme. Il peut être utile dans la détection de code malicieux, car on s’en sert pour extraire un fragment de code qui pourrait être malicieux et l’analyser. Il est plus facile d’analyser une partie d’un programme plutôt que celui-ci en entier. Malicious Code Filter utilise le découpage sur les signes révélateurs qui permettent de croire qu’un code est malicieux. Par exemple, il est possible de faire un découpage avant sur les instructions de lecture d’un fichier du disque. Ceci permet de mettre en évidence l’utilisation de son contenu. Si le résultat de ce découpage contient des instructions qui envoient des données sur le réseau, on peut croire que le code est malicieux. 84 8.2.3 Erreurs pouvant survenir à l’exécution On peut les considérer comme étant déjà un type de code malicieux. Il s’agit par contre d’un type particulier puisqu’il est inclus dans les programmes de façon non intentionnel. Les outils qui détectent ces erreurs ne peuvent pas détecter les autres types de code malicieux comme les virus, vers et autres. 8.3 Faire la recherche autrement Cette recherche a été menée de façon à trouver des outils d’analyse statique et de les essayer. Ces outils devaient de préférence faire la détection de code malicieux sinon leurs résultats devaient être utiles pour réaliser ce but. Malheureusement, il semble bien que les outils pouvant réellement détecter du code malicieux soient très rares, sauf ceux qui détectent les erreurs dans les programmes si on décide de les considérer comme un type particulier de code malicieux bien que la plupart de ceux-ci ne donnent que de nombreux résultats dont la plupart s’avèrent non fondées (faux positifs). Les outils qui analysent le code assembleur sont aussi très rares. En plus, ces rares outils ne sont pas accessibles pour être essayés comme c’est le cas de Samcots et Malicious Code Filter. Il faudra donc encore se contenter de la littérature à propos d’eux. D’ailleurs, une recherche théorique (plutôt qu’une recherche basée sur l’essai d’outils) sur les outils pouvant détecter statiquement du code malicieux ou sur l’application des techniques d’analyse statique dans ce but serait sûrement plus fructueuse et permettrait de faire un état de l’art plus complet en ce qui a trait à la détection de code malicieux utilisant l’analyse statique. 85 Bibliographie [1] J. Kurowsky, S. Ballou, S. Nitzberg, H. Whitley, R. Wood. Trusting software : malicious code analyses. Milcom (The Military Communications Symposium), Atlantic City, New Jersey, 1999. http://www.iamsam. com/papers/milcom_malicious_code_analyses/MCAart16.htm. [2] F. Nielson, H. R. Nielson et C. Hankin. Principles of program analysis. Springer, 1999. [3] Grammatech. Dependence graphs and program slicing. 2000. http:// www.codesurfer.com/research/slicing/slicingWhitePaper.pdf. [4] C. Cifuentes, A. Fraboulet. Intraprocedural static slicing of binary executables. Proc. International Conference on Software Maintenance, pp.188-195, octobre 1997. http://www.cs.uq.edu.au/~cristina/ icsm97.ps. [5] Software Methods and Tools. Testing and test management tools. Décembre 1999. http://www.methods-tools.com/tools/testing. html. [6] P. Cousot. Logiciels d’interprétation abstraite / Abstract Interpretation Software Packages. Février 2000. http://www.di.ens.fr/~cousot/ aisoftware.shtml. [7] B. Marick. Static analysis tools. 1998. http://voss.fernuni-hagen. de/import/pi3/GI/ToolList/t-static.htm. [8] T. Shepard. Incomplete list of testing tools. http://www.cs.queensu. ca/~shepard/testing.dir/under.construction/tool_list.html. [9] J. Krinke. Projects. http://www.infosun.fmi.uni-passau.de/st/ staff/krinke/slicing/node2.html. [10] X. Tao. Software evolution & program analysis links. http://www.cs. washington.edu/homes/taoxie/softevolutionlink.htm. 86 [11] J. Bergeron, M. Debbabi, J. Desharnais, M. M. Erhioui, Y. Lavoie et N. Tawbi. Static detection of malicious code in executable programs. First Symposium on Requirements Engineering for Information Security, Indianapolis, mars 2001. [12] I. Guilfanov. An advanced interactive multi-processor disaddembler. 2000. http://www.datarescue.com. [13] G. Sander. Visualization of compiler graphs. http://www.cs.uni-sb. de/RE/users/sander/html/gsvcg1.html. [14] F-Secure Corporation. Semisoft. 1998. http://www.europe.f-secure. com/v-descs/net666.shtml. [15] V. I. Shelekhov et S. V. Kuksenko. On the practical static checker of semantic run-time errors. Proc. of the 6th Asia Pacific Software Engineering Conference APSEC’99, Japon, 1999. http://www.waspsoft. com/osar_ps.zip. [16] P. Anderson et T. Teitelbaum. Software inspection using CodeSurfer. Juillet 2001. http://www.codesurfer.com/research/papers/ AndersonTeitelbaum.pdf. [17] Grammatech. CodeSurfer user guide and technical reference, Release 1.5 Patchlevel 0. 2001. www.grammatech.com/csurf-doc/manual.html. [18] PolySpace Technologies. PolySpace C Verifier : Product leaflet. http: //www.polyspace.com/docs/CLeaflet.pdf. [19] PolySpace Technologies. Rolling demo - Run-time error detection. http: //www.polyspace.com/video_RTE_download.htm. [20] PolyScace Technologies. Rolling Demo - Concurrent acesses analysis on shared data. http://www.polyspace.com/video_variables_ download.htm. [21] PolySpace Technologies. PolySpace C Verifer getting started. http:// www.polyspace.com/docs/C-Getting-Started.pdf. [22] R. W. Lo, K. N. Levitt et R. A. Olsson. MCF : A Malicious Code Filter. Computers & Security, Vol.14, No.6, pp. 541-566, 1995. http: //seclab.cs.ucdavis.edu/papers/llo95.ps. [23] B. Ktari. Détection de code malicieux. Université Laval. Janvier 1998. [24] J. R. Lyle et D. R. Wallace. Using the Unravel program slicing tool to evaluate high integrity software. Proceedings of Software Quality Week, mai 1997. http://citeseer.nj.nec.com/lyle97using.html. 87 [25] J. Viega, J.T. Bloch, T. Kohno, G. McGraw. ITS4 : A static vulnerability scanner for C and C++ code. Proceedings of ACSAC, Décembre 2000. http://www.cigital.com/papers/download/its4.ps. 88