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

Documents pareils