graphELF - Karine Mordal

Transcription

graphELF - Karine Mordal
Université Paris 8
Département d’Informatique
2 rue de la liberté
93526 Saint-Denis Cedex 02
Mémoire de DEA
DEA Intelligence Artificielle et Optimisation Combinatoire
Paris 8 - Paris 13
graphELF :
un outil d’aide à la compréhension de fichiers ELF
Karine Mordal-Manet
Direction : Françoise Balmas
1 octobre 2004
2
Remerciements
Je remercie Françoise Balmas pour ses conseils avisés, sa compréhension
et sa disponibilité, Harald Wertz pour son entousiasme.
Je remercie également Philippe Manet pour les heures sacrifiées à mes
recherches et sa patiente à toute épreuve, pour sa relecture attentive et sa
longue expérience professionnelle.
Je remercie Philippe Dague pour sa compréhension.
3
Résumé
Nous présentons l’outil graphELF qui analyse des fichiers exécutables au
format ELF pour en extraire les informations qu’il contient. Ces informations
sont stockées dans un fichier de données et analysées pour fournir différents
graphes :
– un graphe des appels de fonctions
– un graphe de flot de contrôle pour chacune des fonctions du programme
– un graphe détaillé, pour chaque fonction du programme, du code désassemblé découpé en blocs représentant le flot de contrôle.
graphELF analyse les fichiers au format ELF. Ce type de fichier correspond aux fichiers au format objet définis pour les plates-formes de type
Unix. Il s’agit des fichiers exécutables, relocalisables ou encore des librairies
dynamiques.
Un fichier au format ELF est découpé en sections regroupant des informations de même type. Ces sections sont elles-même regroupées en segments qui
représentent l’organisation de la mémoire lorsque le programme est chargé
en mémoire. graphELF utilise ces sections et ces segments pour recueillir les
informations du fichier binaire.
graphELF recueille entre autres le code désassemblé du programme sous
forme d’un listing découpé fonction par fonctions. Les graphes fournis permettent ensuite de mieux comprendre ce code désassemblé ainsi que la structure du programme.
graphELF exploite ensuite ces informations dans le cadre de la détection
de failles de sécurité. Les graphes qu’il fournit ainsi que l’analyse des informations du fichier binaire permettent ainsi à notre outil de faire ressortir
notamment d’éventuels débordements de tampons ou bogues de format.
4
Table des matières
1 Introduction
15
1.1
Architecture d’un fichier binaire . . . . . . . . . . . . . . . . .
17
1.2
Fonctionnement de graphELF . . . . . . . . . . . . . . . . . .
20
1.3
Un fichier binaire ELF vu par graphELF . . . . . . . . . . . .
23
1.3.1
Le fichier de données du programme Test . . . . . . .
24
1.3.2
Les graphes du programme Test . . . . . . . . . . . . .
29
Plan de lecture . . . . . . . . . . . . . . . . . . . . . . . . . .
31
1.4
2 Le format ELF
33
2.1
Le format objet . . . . . . . . . . . . . . . . . . . . . . . . . .
33
2.2
Les liens avec les librairies . . . . . . . . . . . . . . . . . . . .
34
2.2.1
Lien statique . . . . . . . . . . . . . . . . . . . . . . .
34
2.2.2
Lien dynamique . . . . . . . . . . . . . . . . . . . . . .
35
Gestion de la mémoire . . . . . . . . . . . . . . . . . . . . . .
36
2.3.1
Le chargement en mémoire d’un programme . . . . . .
36
2.3.2
L’espace d’adressage d’un processus . . . . . . . . . .
38
2.3.3
Organisation de la mémoire . . . . . . . . . . . . . . .
38
2.3.4
La pile et l’exécution d’une fonction . . . . . . . . . .
39
2.4
Structure générale d’un fichier au format objet . . . . . . . .
41
2.5
Le format ELF . . . . . . . . . . . . . . . . . . . . . . . . . .
43
2.5.1
L’en-tête d’un fichier ELF . . . . . . . . . . . . . . . .
44
2.5.2
Deux angles de vue . . . . . . . . . . . . . . . . . . . .
45
2.5.3
Les sections et leur en-tête . . . . . . . . . . . . . . . .
46
2.5.4
Les segments et leurs en-têtes . . . . . . . . . . . . . .
50
2.5.5
La table des symboles et la table des noms . . . . . . .
52
2.3
5
6
2.6
La gestion des adresses dynamiques . . . . . . . . . . . . . . .
55
2.6.1
Le segment dynamic . . . . . . . . . . . . . . . . . . .
56
2.6.2
L’interpréteur de programme . . . . . . . . . . . . . .
57
2.6.3
La table GOT . . . . . . . . . . . . . . . . . . . . . . .
57
2.6.4
la table PLT . . . . . . . . . . . . . . . . . . . . . . .
58
2.6.5
Résolution d’un appel dynamique . . . . . . . . . . . .
59
3 Présentation de graphELF
63
3.1
Contraintes techniques . . . . . . . . . . . . . . . . . . . . . .
63
3.2
Présentation générale de graphELF . . . . . . . . . . . . . . .
64
3.2.1
Les options de graphELF . . . . . . . . . . . . . . . .
65
3.2.2
Organisation du programme . . . . . . . . . . . . . . .
65
3.3
Fonction de début de graphELF . . . . . . . . . . . . . . . . .
69
3.4
Phase de lecture du fichier ELF . . . . . . . . . . . . . . . . .
70
3.4.1
Structure de stockage des informations . . . . . . . . .
70
3.4.2
Algorithmes de lecture du fichier . . . . . . . . . . . .
73
3.4.3
Exemple d’application avec le fichier gencode : le
contenu du fichier ELF . . . . . . . . . . . . . . . . . .
75
Phase de construction des appels de fonctions . . . . . . . . .
84
3.5
3.5.1
Lecture du fichier en passant par les noms des fonctions 84
3.5.1.1
3.5.2
Lecture du fichier en passant par les adresses des fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . . .
86
3.5.2.1
Algorithme de lecture par les adresses . . . .
86
3.5.2.2
Précisions sur la recherche de l’adresse de début de la fonction main et sur celle de fin de
programme . . . . . . . . . . . . . . . . . . .
87
Algorithme de découpage par blocs . . . . . .
89
3.5.3
Algorithme de recherche des instructions call . . . . .
90
3.5.4
Graphes des appels de fonction du programme gencode 92
3.5.2.3
3.6
Algorithme de lecture de la table des symboles 84
Phase de construction du flot de contrôle des fonctions . . . .
93
3.6.1
Découpage de la fonction analysée en blocs . . . . . .
94
3.6.2
Création des liens entre les blocs . . . . . . . . . . . .
95
3.6.3
Cas des sauts vers des fonctions . . . . . . . . . . . . .
96
7
3.7
3.6.4
Cas des sauts vers des tables indexées . . . . . . . . .
97
3.6.5
Les graphes du programme gencode
98
. . . . . . . . . .
Emploi de la bibliothèque dynamique libdisasm . . . . . . . . 102
3.7.1
Les fonctions de la bibliothèque . . . . . . . . . . . . . 103
3.7.2
Algorithme de désassemblage . . . . . . . . . . . . . . 103
3.8
Création de graphes avec graphviz . . . . . . . . . . . . . . . 104
3.9
Comparaison avec l’existant : les outils dans le même domaine 107
3.9.1
readelf : un outil affichant les informations contenues
dans un fichier ELF . . . . . . . . . . . . . . . . . . . 107
3.9.2
Les désassembleurs . . . . . . . . . . . . . . . . . . . . 108
3.9.3
3.9.2.1
objdump . . . . . . . . . . . . . . . . . . . . 108
3.9.2.2
bastard . . . . . . . . . . . . . . . . . . . . . 112
3.9.2.3
Quelques autres désassembleurs . . . . . . . . 116
Les outils générateurs de graphes d’appels de fonction 116
3.9.3.1
flowgraph . . . . . . . . . . . . . . . . . . . . 117
3.9.3.2
bin2graph . . . . . . . . . . . . . . . . . . . . 120
3.9.4
elfsh : un outil complet . . . . . . . . . . . . . . . . . . 123
3.9.5
Conclusions . . . . . . . . . . . . . . . . . . . . . . . . 125
4 Discussion, évolution
4.1
127
Discussion - Evolution . . . . . . . . . . . . . . . . . . . . . . 127
4.1.1
Modifications d’un programme propriétaire . . . . . . 127
4.1.2
Recherche de failles dans un programme : l’audit de
sécurité . . . . . . . . . . . . . . . . . . . . . . . . . . 128
4.1.2.1
Les débordements de tampon . . . . . . . . . 130
Les parades existantes . . . . . . . . . . . . . . 132
Les recherches de débordements de tampon
dans un fichier binaire . . . . . . . . 133
4.1.2.2
Les bogues de format . . . . . . . . . . . . . 134
Les moyens de défense . . . . . . . . . . . . . . 137
4.2
4.1.3
Fonctionalités cachées d’un programme . . . . . . . . . 139
4.1.4
Aide au développement d’un logiciel . . . . . . . . . . 139
4.1.5
Le format ELF et les virus . . . . . . . . . . . . . . . . 140
Exemples d’application . . . . . . . . . . . . . . . . . . . . . . 143
8
4.2.1
La liste des bibliothèques dynamiques . . . . . . . . . 143
4.2.2
Les chaînes de caractères . . . . . . . . . . . . . . . . . 145
4.2.3
Une fonction jamais appelée . . . . . . . . . . . . . . . 147
4.2.4
Mise en évidence de fonctions particulières . . . . . . . 149
4.2.4.1
malloc et free . . . . . . . . . . . . . . . . . . 150
4.2.4.2
malloc et free non standards . . . . . . . . . 152
4.2.5
Optimisation du compilateur . . . . . . . . . . . . . . 154
4.2.6
Liste de fonctions suspectes . . . . . . . . . . . . . . . 157
4.2.6.1
Analyse de strcat . . . . . . . . . . . . . . . . 161
4.2.6.2
Analyse de strcpy . . . . . . . . . . . . . . . 164
4.2.6.3
Analyse de printf dans la fonction fa . . . . . 164
4.2.6.4
Analyse de printf dans la fonction main . . . 165
4.2.6.5
Analyse de printf dans la fonction fc . . . . . 166
5 Conclusion
167
Affinage des analyses dans la recherche de failles
de sécurité . . . . . . . . . . . . . . 168
Amélioration de la compréhension du code
désassemblé . . . . . . . . . . . . . . 170
Limitation de la plate-forme et du compilateur
Interface plus facilement utilisable
170
. . . . . . . 171
L’analyse dynamique du fichier ELF . . . . . . 171
Bibliographie
177
A Un affichage avec readelf
179
B Quelques graphes construits par graphELF
185
C Le code source de graphELF
191
Table des figures
1.1
Schéma général des sections d’un fichier au format ELF . . .
17
1.2
Les principaux segments d’un fichier au format objet . . . . .
18
1.3
Correspondance entre les sections et les segments d’un fichier
ELF . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
19
1.4
Les sources du programme Test. . . . . . . . . . . . . . . . . .
23
1.5
Graphe des appels de fonction de Test. . . . . . . . . . . . . .
30
1.6
Graphe du flot de contrôle de la fonction fa du programme Test. 31
1.7
Graphe des blocs de code de la fonction fa du programme Test. 32
2.1
Appel de la fonction printf située dans une librairie statique. .
2.2
Appel de la fonction printf située dans une librairie dynamique. 35
2.3
Chargement en mémoire d’un fichier au format ELF . . . . .
37
2.4
Schéma général de la mémoire d’un programme . . . . . . . .
39
2.5
Schéma général de la pile lors de l’appel d’une fonction . . . .
40
2.6
Schéma général d’un fichier au format objet. . . . . . . . . . .
42
2.7
Schéma général des en-têtes de sections d’un fichier au format
ELF. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
46
34
2.8
Correspondance entre la table des noms et la table des symboles. 53
2.9
Pointeurs entre la table PLT et la table GOT . . . . . . . . .
60
2.10 Appel suivants à une fonction externe . . . . . . . . . . . . .
62
3.1
Graphe des appels de fonctions du graphELF . . . . . . . . .
67
3.2
Algorithme général de graphELF . . . . . . . . . . . . . . . .
69
3.3
Algorithme principal de lecture du fichier ELF . . . . . . . . .
73
3.4
Algorithme de lecture des sections du fichier ELF . . . . . . .
74
3.5
Algorithme de lecture des segments du fichier ELF . . . . . .
74
9
10
3.6
Algorithme de lecture par les noms . . . . . . . . . . . . . . .
85
3.7
Algorithme de parcours de la section text à la recherche des
fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
87
3.8
Algorithme de découpage du code en blocs de fonctions . . . .
89
3.9
Algorithme de lecture des instructions call . . . . . . . . . . .
91
3.10 Le graphe des appels de fonctions par les noms de Test2 . . .
93
3.11 Le graphe des appels de fonctions par les adresses de Test2 . .
93
3.12 Algorithme de recherche des instructions de sauts . . . . . . .
95
3.13 Algorithme de parcours des blocs construits . . . . . . . . . .
96
3.14 Graphe représentant une instruction switch. . . . . . . . . . .
98
3.15 Le graphe de la fonction main du programme gencode . . . .
99
3.16 Le graphe de la fonction main du programme gencode représentée avec le code . . . . . . . . . . . . . . . . . . . . . . . . 100
3.17 Algorithme général de désassemblage du code . . . . . . . . . 104
3.18 Script GPR utilisé par graphELF . . . . . . . . . . . . . . . . 106
3.19 Le graphe des appels de fonctions de GG . . . . . . . . . . . . 118
3.20 Le graphe de flot de contrôle de GG . . . . . . . . . . . . . . 119
3.21 Le graphe de flot de contrôle de GG généré par bin2graph. . . 121
3.22 Le graphe des appels de fonctions de GG généré par graphELF.122
4.1
Exemple de débordement de buffer . . . . . . . . . . . . . . . 131
4.2
Schéma de la pile lors de l’appel de la fonction printf . . . . . 136
4.3
Le code source de l’exemple de fonction non appelée. . . . . . 147
4.4
Graphe des appels de fonction du programme exemple. . . . . 148
4.5
Graphe décrivant la fonction fa. . . . . . . . . . . . . . . . . . 148
4.6
Code source du programme Appel_malloc . . . . . . . . . . 149
4.7
Graphe des appels de fonction d’un programme . . . . . . . . 150
4.8
Graphe de la fonction 0x804839c . . . . . . . . . . . . . . . . 151
4.9
Code désassemblé du programme Appel_malloc . . . . . . 153
4.10 Graphe des appels de fonction . . . . . . . . . . . . . . . . . . 154
4.11 Graphe des appels de fonction . . . . . . . . . . . . . . . . . . 155
4.12 Graphe de la fonction main du programme Appel_malloc . 156
4.13 Graphe de la fonction main du programme Appel_malloc,
optimisée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
11
4.14 Graphe des appels de fonctions du programme fn_dout . . . 159
4.15 Graphe du flot de contrôle de la fonction fa de fn_dout . . . 159
4.16 Graphe du flot de contrôle de la fonction fb de fn_dout . . . 160
4.17 Graphe détaillé du code de la fonction fa de fn_dout . . . . 160
4.18 Graphe détaillé du code de la fonction fb de fn_dout . . . . 161
4.19 Schéma de la pile lors de l’exécution de la fonction fb . . . . . 163
4.20 Graphe détaillé du code de la fonction main de fn_dout . . 165
12
Liste des tableaux
2.1
Les principales sections ELF . . . . . . . . . . . . . . . . . . .
48
2.2
Exemple d’un segment Text sous Unix . . . . . . . . . . . . .
51
2.3
Exemple d’un segment Data sous Unix . . . . . . . . . . . . .
51
2.4
Valeurs des drapeaux du segment dynamic . . . . . . . . . .
56
13
14
Chapitre 1
Introduction
Nous présentons graphELF, un outil d’analyse de fichiers binaires ayant
pour objectif de fournir une aide dans la compréhension du code de programmes ainsi que dans la recherche de failles de sécurité. graphELF collecte
les informations contenues dans un fichier au format ELF sur une plate-forme
Intel et les mémorise dans un fichier de données. Ces informations sont ensuite exploitées et analysées pour fournir :
– Un listing contenant le code désassemblé.
– Un graphe général des appels de fonction pour l’ensemble du programme.
– Un graphe par fonction détaillant le flot de contrôle 1 de chacune des
fonctions.
– Un graphe par fonction du code désassemblé, découpé selon ce flot de
contrôle.
– Une mise en évidence des fonctions présentant un risque en terme de
sécurité.
Notre outil constitue une aide à l’analyse d’un fichier binaire. Cette analyse s’avère souvent nécessaire dans différents domaines, par exemple dans le
cadre de la sécurité informatique, de la recherche de virus ou encore du développement d’un logiciel devant communiquer avec un logiciel commercial.
Dans le domaine de la sécurité informatique, lors d’un audit de sécurité
sur un logiciel propriétaire 2 , le code exécutable constitue la seule base de travail. Les techniques d’analyse d’un fichier binaire 3 servent alors à s’assurer,
1. Nous appelons flot de contrôle les boucles et les branchements conditionnels contenus
dans une fonction.
2. Nous entendons par ce terme un logiciel pour lequel on ne dispose pas des sources.
3. Ces techniques sont regroupées sous le nom de reverse engineering, cependant ce
terme est ambigü. Dans le milieu industriel, il désigne uniquement l’analyse de fichier
binaire tandis que dans la recherche académique, il recouvre également des techniques
d’analyse de code source. C’est pourquoi nous utilisons l’expression analyse de fichier
binaire.
15
16
par exemple, que le logiciel ne possède pas de fonctionnalités cachées indésirables, qu’il ne contient pas de failles le rendant vulnérable à une attaque
de pirate ou de virus, ou encore qu’il ne possède aucune porte dérobée.
Les éditeurs d’anti-virus ont également recours en permanence à l’analyse de fichiers binaires. En effet, le seul moyen d’analyser un virus pour en
déterminer la signature et développer ensuite la parade à ce virus passe par
la compréhension de ce dernier, dont on ne dispose bien évidemment jamais
des sources. Les éditeurs travaillent donc au désassemblage du virus et à la
compréhension de ce code désassemblé, souvent obfusqué ou crypté.
De même, il peut parfois s’avérer nécessaire d’analyser des logiciels dont
on ne dispose pas des sources, afin d’en comprendre les caractéristiques techniques et d’établir une compatibilité entre son propre programme et ces
logiciels propriétaires. Dans ce cas, l’analyse de binaire a souvent comme
objectif de recueillir les différentes structures et contraintes sur les données
afin d’établir la communication entre les deux logiciels.
L’analyse d’un fichier binaire peut également fournir des indications non
négligeables telles que la liste des fonctions externes et des bibliothèques
nécessaires à l’exécution du programme, ce qui peut s’avérer utile lors de
l’utilisation d’un programme propriétaire. En effet, ceux-ci sont souvent peu
documentés quant à leurs appels de fonctions externes.
Les techniques d’analyse d’un fichier binaire ne peuvent être similaires
aux techniques d’analyse appliquées sur du code source. En effet, un fichier
binaire n’est pas constitué uniquement de la traduction du code source du
programme en langage binaire. Il contient de nombreuses informations permettant de faire fonctionner ce programme :
– Des indications sur l’emplacement du début du programme.
– Une image de la mémoire virtuelle du processus créé lors de l’exécution
du programme
– Des indications sur les fonctions externes appelées par celui-ci ainsi
que sur les librairies nécessaires
– De nombreuses fonctions d’initialisation et de fin de programme ajoutées par le compilateur.
– Eventuellement, des informations permettant le débogage du programme.
De plus, la traduction du code source en langage assembleur effectuée
par le compilateur dépend étroitement du niveau d’optimisation requis à
la compilation ainsi que du compilateur employé. Ainsi, le code assembleur
issu de la traduction par le compilateur peut alors soit refléter quasiment
exactement le code source si aucune optimisation n’a été effectuée ou être à
l’opposé extrêment différent.
C’est pour toutes ces raisons que nous avons développé graphELF, un
outil qui parcourt l’ensemble du contenu d’un fichier binaire pour collecter
17
les informations nécessaires pour la compréhension du programme. Il fournit
de surcroît une aide facilitant la lecture du code désassemblé ainsi que des
moyens pour accélérer et orienter les recherches de vulnérabilités dans un
programme.
1.1
Architecture d’un fichier binaire
Figure 1.1 – Schéma général des sections d’un fichier au format ELF
Un fichier binaire est constitué d’informations structurées [BWC01] selon une norme prédéfinie 4 . Le premier champ d’information du fichier nous
renseigne sur le type de format du fichier et sur les emplacements des autres
informations. Le fichier est ensuite découpé en sections qui regroupent les
informations de même type. La figure 1.1 présente les principales sections
d’un fichier binaire et leur contenu. On constate par exemple que le code
4. Pour le format ELF, se référer à [Com95].
18
exécutable à proprement parler est contenu dans une section nommée .text
tandis que la table des symboles est contenue dans une section nommée
.symtab. Les données globales du programme sont réparties selon leur type :
les données de type constante se situent dans la section .rodata, les données initialisées dans la section .data et les données non initialisées dans la
section .bss. Toutes les informations contenues dans un fichier au format
objet sont ainsi réparties dans des sections et pour les lire, il suffit donc de
pouvoir accéder à chacune de ces sections.
Figure 1.2 – Les principaux segments d’un fichier au format objet
Dans le cadre d’un fichier au format ELF, un autre découpage doit être
pris en compte : bien que le fichier soit organisé en sections, il est également
découpé en segments. En effet, le format ELF distingue les sections, qui
sont utiles lors de la phase d’édition des liens statiques 5 , des segments qui
sont employés lors du chargement en mémoire du programme à exécuter. Les
segments regroupent différentes informations du fichier en fonction de leur
5. Il s’agit de lier entre eux des fichiers objets pour en faire un exécutable.
19
futur chargement en mémoire et reflètent donc l’image mémoire du processus
créé à l’exécution du programme comme le montre la figure 1.2. Chaque
segment regroupe plusieurs sections du fichier selon qu’elles doivent être
chargées ou non en mémoire par exemple, ou encore selon qu’elles doivent être
accessibles en mémoire en lecture et écriture ou uniquement en lecture. Par
exemple, le segment text de la figure 1.2 est un segment chargé en mémoire et
accessible uniquement en lecture. Il contient entre autres la section .text. Le
segment data contient les sections qui doivent être accessibles en mémoire en
lecture et écriture. Le fichier représenté dans la figure 1.2 contient également
des sections qui ne sont pas incluses à l’intérieur de segments telles que les
sections liées au débogage ou encore la section .symtab qui contient la table
des symboles statique ainsi que la section .strtab qui contient la table des
noms qui lui est associée. Ces sections, qui ne participent en aucune façon
à l’exécution du programme, sont optionnelles dans un fichier exécutable et
peuvent donc être ou non présentes.
Figure 1.3 – Correspondance entre les sections et les segments d’un fichier
ELF
20
Ainsi, un fichier au format ELF contient à la fois des sections et des
segments comme le résume la figure 1.3. Il contient également deux tables
d’en-têtes, sous forme de listes de structures, qui permettent de situer les
sections et les segments à l’intérieur du fichier (cf. figures 1.1 et 1.2). La table
des en-têtes de section pointe vers les sections tandis que la table des en-têtes
de programme pointe vers les segments. Cependant lors de son exécution,
un programme n’accède qu’à ses segments. Aussi, les en-têtes donnant les
indications sur les sections sont facultatifs et peuvent avoir été supprimées
du fichier exécutable.
S’il est relativement aisé de recueillir toutes les informations contenues
dans un fichier binaire en parcourant les sections de ce dernier, il est en
revanche plus délicat d’obtenir ces mêmes informations lorsque seuls les
segments demeurent accessibles. Le code du programme, par exemple, est
contenu dans la section .text. Lorsque les en-têtes de sections sont encore
présents on peut accéder directement au code puisque l’en-tête se référant à
la section .text contient l’adresse de début et la taille de cette section. En
revanche, lorsque la table des en-têtes de section est absente, le passage par
la table des en-têtes de programme rend la tâche plus ardue. Le schéma de
la figure 1.2 indique que la section .text est contenue dans le segment text,
entourée d’autres sections. Or, dans ce cas l’en-tête du segment text ne fournit que l’adresse de début du segment et sa taille. Il faut donc avoir recours
à des calculs pour déterminer les adresses de début et de fin de cette section : l’adresse de début pour la section .text correspond en fait à l’adresse
de point d’entrée du programme fourni par l’en-tête du fichier tandis que
l’adresse de fin de cette section n’est déterminée que grâce à des calculs et
déductions dépendants du compilateur utilisé.
1.2
Fonctionnement de graphELF
Notre outil, graphELF, est basé sur l’organisation des fichiers au format
ELF. Il recueille et analyse des informations qu’il stocke dans un fichier de
données.
Il utilise soit les en-têtes de section lorsqu’elles sont présentes, soit les entêtes de programme pour accéder aux informations contenues dans un fichier
binaire. De même, graphELf utilise la table des symboles pour décoder les
informations lorsqu’elle est encore présente dans le fichier ou recourt à des
algorithmes différents pour pallier si nécessaire son absence et obtenir les
mêmes informations.
graphELF possède donc trois modes de fonctionnement selon qu’il peut :
– lire les sections et la table des symboles.
– lire les sections mais pas la table des symboles (dans ce cas il effectue
d’autres calculs pour récupérer les informations).
21
– lire les segments parce que les en-têtes de section sont absentes et effectuer des calculs pour récupérer les informations qu’il ne peut récupérer
de la table des symboles 6 .
Les premières indications recherchées par graphELF se situent dans les
différents champs de la structure d’en-tête du fichier, celle-ci étant obligatoirement stockée dans les premiers octets du fichier. Ces champs indiquent
entre autres le format du fichier, le type du fichier, la machine sur laquelle il
fonctionne, mais également les positions dans le fichier des en-têtes de segment et de section du fichier. Ces dernières informations lui permettent ainsi
de situer les en-têtes afin de les analyser.
Les sections d’un fichier ELF permettent un accès direct aux informations
du fichier. graphELF privilégie donc la collecte d’informations en parcourant
directement les sections. Par contre, lorsque les en-têtes de section sont absentes, et contrairement à d’autres outils qui lisent les fichiers ELF tels que
objdump [Fre02a], graphELF accède néanmoins aux mêmes informations en
exploitant directement les segments et leurs en-têtes qui sont eux obligatoirement toujours présents dans un fichier exécutable.
Les tables d’en-tête sont réprésentées sous forme de listes chainées de
structures (cf. 2.5.3 et 2.5.4). graphELF lit les octets du fichier binaire qui
correspondent à ces listes chaînées et les transcrit de façon à pouvoir les lire
directement : il remplace entre autres les champs numériques correspondant
à des symboles prédéfinis par les noms adéquats et livre les adresses sous
forme hexadécimale afin de pouvoir les comprendre comme des adresses sans
ambiguité. graphELF dresse ainsi la liste des segments présents dans le fichier
avec leur taille et leur position dans le fichier ainsi que la liste des sections
lorsque cela est possible.
Ensuite, graphELF utilise deux algorithmes différents pour collecter les
autres informations dont il a besoin, selon la présence ou non de la table
des en-têtes de section. Dans le cas de la présence d’en-têtes de section,
graphELF lit les structures des en-têtes pour connaître la position de début
et de fin dans le fichier des sections contenant les informations recherchées et
pour en lire le contenu. En revanche, lorsque graphELF utilise les segments,
il ne peut obtenir ces positions de manière directe. Il analyse alors d’abord
les informations concernant les segments pour effectuer ensuite des calculs
permettant de déduire ces mêmes positions de manière indirecte 7 (cf. partie 8
3.4.2).
6. Si les en-têtes de section ont été supprimés (par exemple en utilisant la commande
sstrip), la table des symboles a nécessairement été supprimée aussi.
7. Notons que si la manière d’accéder aux informations diffère, en revanche, les informations recueillies sont identiques.
8. Nous réservons le terme de section pour désigner les sections contenues dans un
fichier au format ELF. C’est pourquoi nous utiliserons le mot partie pour désigner les
différentes sections de ce mémoire.
22
A l’étape suivante, graphELF vérifie la présence de tables des symboles
pour les décoder le cas échéant. Un fichier ELF contient deux tables des symboles : l’une, statique et optionnelle, qui référence les symboles du programme
(les noms des variables globales et les noms des fonctions notamment) et la
seconde, dynamique et obligatoire, qui contient la liste des fonctions dynamiques appelées par ce dernier 9 . Ces tables se situent dans des sections
distinctes. Couplées à leurs tables des noms respectives, situées également
dans des sections différentes, elle permettent de décoder, lors de la phase
de désassemblage du programme (voir ci-dessous), les noms des fonctions
utilisées par celui-ci.
graphELF recherche ensuite les informations nécessaires à l’analyse des
appels dynamiques. Celles-ci se situent dans différentes sections et sont accédées directement lorsque la table des en-têtes de section est présente ou par
l’intermédiaire des informations contenues dans le segment dynamic, dans le
cas contraire. Ces informations permettent ensuite de déterminer la liste des
fonctions externes utilisées par le programme ainsi que les librairies nécessaires au fonctionnement de celui-ci.
Après avoir recueilli les différentes informations contenues dans les sections du progamme 10 , graphELF passe au désassemblage du code exécutable.
graphELF utilise la librairie libdisasm [lib04] pour transformer le binaire en
instructions assembleur (cf. partie 3.7). Lorsque la table des symboles statiques est présente, graphELF la parcourt tout d’abord pour décoder les
noms des fonctions et récupérer leur adresse et leur taille. Grâce à ces résultats, le désassemblage est ensuite effectué fonction par fonction. En revanche, lorsque la table des symboles est absente, le code du programme
est tout d’abord entièrement désassemblé pour ensuite être analysé afin de
déterminer les adresses de début et de fin des fonctions, les noms de celles-ci
ne pouvant être déterminés.
A l’issue de cette collecte d’informations, graphELF procède à différentes
analyses aboutissant notamment à différents graphes. Ces analyses sont présentées et détaillées dans la partie suivante, à travers l’exemple de l’analyse
du programme Test contenu dans un fichier nommé test1.c et présenté
dans la figure 1.4.
9. Ces appels concernent toutes les fonctions définies dans des librairies dynamiques
(les .so sur les systèmes Unix par exemple). Les appels de fonctions de la librairie standard
C comme, par exemple, la fonction printf en font partie.
10. Rappelons que graphELF recueille toutes ces informations, qu’il dispose ou non de
la table des en-têtes de section : c’est le type de parcours du fichier qui varie et non les
informations obtenues.
23
Programme Test
fa(int a){
#include <stdio.h>
int b;
if (a<5)
{
fb();
}
else
{
b =sqrt(a*80);
printf("%d \n",b);
}
main(){
int i,a;
for(a=0;a<10;a++)
{
printf("la valeur de a :
%d \n",a);
fa(a);
}
}
}
fb(){
char src[30];
char dst[20];
fgets(src,30,stdin);
strcpy(dst,src);
}
Figure 1.4 – Les sources du programme Test.
1.3
Un fichier binaire ELF vu par graphELF
Cette partie montre des extraits du fichier de données concernant le programme Test (cf. figure 1.4) analysé par graphELF :
–
–
–
–
les en-têtes
les tables des symboles et des noms
les chaînes de caractères contenues dans le programme
le code désassemblé des fonctions
Puis les résultats des analyses effectuées :
– le graphe des appels de fonctions
– le graphe de flot de contrôle d’une fonction
– le graphe du code désassemblé représenté sous forme de blocs correspondant à ce flot de contrôle
– la mise en valeur des appels de fonctions dynamiques dangeureuses
24
1.3.1
Le fichier de données du programme Test
Les premières informations recueillies par graphELF sont contenues dans
l’en-tête du programme dont voici un extrait :
Partie I , informations ELF
entete du fichier
type du fichier : EXEC (Executable file)
type de la machine : Intel 80386
Ces informations nous indiquent qu’il s’agit d’un fichier exécutable au
format ELF pour plate-forme Intel. Ce programme est donc analysable par
notre outil.
Viennent ensuite les en-têtes de section :
les entetes de section
nombre d’entree 35
index
section
[ 0]
[ 1]
.interp
[ 2]
.note.ABI-tag
[ 3]
.hash
[ 4]
.dynsym
[ 5]
.dynstr
[ 6]
.gnu.version
[ 7]
.gnu.version_r
[ 8]
.rel.dyn
[ 9]
.rel.plt
[10]
.init
[11]
.plt
[12]
.text
[13]
.fini
[14]
.rodata
[15]
.data
[16]
.eh_frame
[17]
.dynamic
type
NULL
PROGBITS
NOTE
HASH
DYNSYM
STRTAB
VERSYM
VERNEED
REL
REL
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
DYNAMIC
adresse
0
80480f4
8048108
8048128
8048164
8048204
8048286
804829c
80482dc
80482ec
8048314
804832c
8048390
8048624
8048640
8049664
8049670
8049674
taille
0
19
32
60
160
130
20
64
16
40
23
96
660
26
35
12
4
208
25
[18]
[19]
[20]
[21]
[22]
[23]
[24]
[25]
[26]
[27]
[28]
[29]
[30]
[31]
[32]
[33]
[34]
.ctors
.dtors
.jcr
.got
.bss
.comment
.debug_aranges
.debug_pubnames
.debug_info
.debug_abbrev
.debug_line
.debug_frame
.debug_str
.debug_ranges
.shstrtab
.symtab
.strtab
PROGBITS
PROGBITS
PROGBITS
PROGBITS
NOBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
STRTAB
SYMTAB
STRTAB
8049744
804974c
8049754
8049758
804977c
0
0
0
0
0
0
0
0
0
0
0
0
8
8
4
36
8
217
152
95
768
285
545
88
285
24
313
1856
929
Ces en-têtes sont au nombre de 35, soit le nombre de sections contenues
dans le fichier. Chaque en-tête fournit le nom de la section qu’il référence,
la taille et l’adresse de début de celle-ci. Il s’agit de l’adresse virtuelle dans
l’espace d’adressage du processus (voir partie 2.3.2) et non pas de la position
dans le fichier binaire. Parmi les sections de ce programme, nous retrouvons
la section .text contenant le code, les sections .data, .rodata et .bss contenant les données, mais aussi les sections .debug* contenant des indications
de débogage ou encore les sections .dynsym ou .symtab contenant des tables
de symboles 11 .
Les en-têtes de section étant présentes, graphELF analyse ensuite le fichier grâce à cet accès direct aux sections.
Les en-têtes de programme étant toujours accessibles, elles sont donc
aussi affichées :
les entetes de programme
nombre d’entree 6
index
type
adresse
[ 0]
PHDR
8048034
[ 1]
INTERP
80480f4
[ 2]
LOAD
8048000
[ 3]
LOAD
8049664
[ 4]
DYNAMIC
8049674
[ 5]
NOTE
8048108
t_fichier
0x00000c0
0x0000013
0x0000663
0x0000118
0x00000d0
0x0000020
t_memoire
0x00000c0
0x0000013
0x0000663
0x0000120
0x00000d0
0x0000020
11. Les sections et leur contenu sont détaillées dans la partie 2.5.3.
protection
R E
R
R E
RW
RW
R
26
les sections dans les segments
segment 0
segment 1
.interp
segment 2
.interp
.note.ABI-tag
.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_r
.rel.dyn
.rel.plt
.init
.plt
.text
.fini
.rodata
segment 3
.data
.eh_frame
.dynamic
.ctors .dtors .jcr .got
segment 4
.dynamic
segment 5
.note.ABI-tag
On remarque ici que les segments sont nettement moins nombreux que
les sections et qu’ils contiennent la plupart des sections de la liste donnée
ci-dessus. Les sections qui ne sont pas contenues dans un segment sont celles
qui ne participent pas au processus d’éxecution du programme.
Les différents champs sont :
type Certains segments sont de type LOAD à savoir chargeables en mémoire.
Un seul segment est de type DYNAMIC, il s’agit du segment contenant
les informations destinées à retrouver au sein du fichier les données
nécessaires à la résolution des appels dynamiques.
adresse Il s’agit de l’adresse virtuelle du segment.
t_fichier La taille que le segment occupe dans le fichier.
t_memoire La taille que le segment occupe en mémoire. Ces deux tailles
peuvent être différentes : par exemple la taille du segment contenant
la section des données non initialisées .bss. Ces données n’occupent
pas d’espace dans le fichier alors qu’il leur est attribué un espace en
mémoire.
protection Les segments ont des propriétés d’accès : les droits d’écriture,
de lecture et d’exécution combinés.
Viennent ensuite les tables des symboles et des noms dont voici un extrait :
27
la table des symboles statique
Num:
Valeur Taille
Type
1:
80480f4
0 SECTION
2:
8048108
0 SECTION
67:
8048420
0
FUNC
68:
0
0
FILE
69:
8049748
0
OBJECT
70:
8049750
0
OBJECT
71:
8049670
0
OBJECT
72:
8049754
0
OBJECT
73:
8048600
0
FUNC
74:
0
0
FILE
Lien
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
Ndx
1
2
12
ABS
18
19
16
20
12
ABS
75:
0
0
FILE
LOCAL
ABS
76:
77:
0
0
0
0
FILE
FILE
LOCAL
LOCAL
ABS
ABS
78:
79:
0
0
0
0
FILE
FILE
LOCAL
LOCAL
ABS
ABS
80:
81:
82:
0
0
0
0
0
0
FILE
FILE
FILE
LOCAL
LOCAL
LOCAL
ABS
ABS
ABS
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
115:
0
0
8049674
8048640
8049664
8049668
8048590
80484fa
8048314
8048390
804833c
8049664
8048530
804977c
804844c
804834c
8049664
8049664
804835c
8048624
804836c
804977c
80485f0
8049758
8049784
804977c
8049664
8048644
8049664
0
8048495
0
804837c
0
0
0
4
0
0
60
32
0
0
16a
0
58
0
49
fb
0
0
36
0
7c
0
0
0
0
4
0
4
0
0
65
0
30
FILE
FILE
OBJECT
OBJECT
NOTYPE
OBJECT
FUNC
FUNC
FUNC
FUNC
FUNC
NOTYPE
FUNC
NOTYPE
FUNC
FUNC
NOTYPE
NOTYPE
FUNC
FUNC
FUNC
NOTYPE
FUNC
OBJECT
NOTYPE
OBJECT
NOTYPE
OBJECT
NOTYPE
NOTYPE
FUNC
NOTYPE
FUNC
LOCAL
LOCAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
WEAK
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
WEAK
GLOBAL
WEAK
GLOBAL
ABS
ABS
17
14
ABS
15
12
12
10
12
UND
ABS
12
ABS
12
UND
ABS
15
UND
13
UND
ABS
12
21
ABS
22
ABS
14
15
UND
12
UND
UND
Nom
frame_dummy
crtstuff.c
__CTOR_END__
__DTOR_END__
__FRAME_END__
__JCR_END__
__do_global_ctors_aux
/usr/src/packages/BUILD/
glibc-2.3/cc/csu/crtn.S
/usr/src/packages/BUILD/
glibc-2.3/cc/csu/defs.h
initfini.c
/usr/src/packages/BUILD/
glibc-2.3/cc/csu/crtn.S
<command line>
/usr/src/packages/BUILD/
glibc-2.3/cc/config.h
<command line>
<built-in>
/usr/src/packages/BUILD/
glibc-2.3/cc/csu/crtn.S
test1.c
elf-init.c
_DYNAMIC
_fp_hw
__fini_array_end
__dso_handle
__libc_csu_fini
fb
_init
_start
fgets@@GLIBC_2.0
__fini_array_start
__libc_csu_init
__bss_start
main
__libc_start_main@@GLIBC_2.0
__init_array_end
data_start
printf@@GLIBC_2.0
_fini
sqrt@@GLIBC_2.0
_edata
__i686.get_pc_thunk.bx
_GLOBAL_OFFSET_TABLE_
_end
stdin@@GLIBC_2.0
__init_array_start
_IO_stdin_used
__data_start
_Jv_RegisterClasses
fa
__gmon_start__
strcpy@@GLIBC_2.0
28
La table des symboles statiques contient les symboles utilisés par le programme avec entre autres le nom des fonctions définies par ce dernier. L’analyse des fonctions du programme pourra donc s’effectuer en utilisant cette
table qui fournit en plus du nom des fonctions leur adresse virtuelle, leur
position dans le fichier et leur taille. Le champ Type indique s’il s’agit par
exemple d’une fonction — FUNC — ou encore d’un objet — OBJECT — c’està-dire par exemple d’une variable globale définie dans le programme.
la table des symboles dynamique
Num:
Valeur Taille
Type
0:
0
0 NOTYPE
1:
804833c
16a
FUNC
2:
804834c
fb
FUNC
3:
804835c
36
FUNC
4:
804836c
7c
FUNC
5:
804977c
4 OBJECT
6:
8048644
4 OBJECT
7:
0
0 NOTYPE
8:
0
0 NOTYPE
9:
804837c
30
FUNC
Lien
LOCAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
WEAK
WEAK
GLOBAL
Ndx
Nom
UND
0
UND
fgets 120
UND __libc_start_main 120
UND
printf 120
UND
sqrt 120
22
stdin 120
14
_IO_stdin_used 1
UND _Jv_RegisterClasses 0
UND
__gmon_start__ 0
UND
strcpy 120
La table des symboles dynamiques, pour sa part, référence les fonctions
et objets dynamiques du programme. Il s’agit des fonctions et des variables
définies dans les librairies dynamiques. Le champ librairie nous indique la
librairie dont dépend l’objet nommé.
La liste des librairies requises pour le bon déroulement du programme
est la suivante :
les librairies requises
1
libm.so.6
51
libc.so.6
graphELF fournit également la liste des chaînes de caractères contenues
dans le programme. Ces chaînes correspondent ici aux arguments des fonctions printf du programme.
chaines de caractères
la valeur de a : %d
%d
Le code desassemblé du programme est ensuite fourni, fonction par fonction. Voici le code de la fonction main :
librairie
local
GLIBC_2.0
GLIBC_2.0
GLIBC_2.0
GLIBC_2.0
GLIBC_2.0
global
local
local
GLIBC_2.0
29
fonction main
804844C 55
804844D 89 E5
804844F 83 EC
8048452 83 E4
8048455 B8 00
804845A 29 C4
804845C C7 45
8048463 83 7D
8048467 7E 02
8048469 EB 28
804846B 83 EC
804846E FF 75
8048471 68 48
8048476 E8 E1
804847B 83 C4
804847E 83 EC
8048481 FF 75
8048484 E8 0C
8048489 83 C4
804848C 8D 45
804848F FF 00
8048491 EB D0
8048493 C9
8048494 C3
adresse 804844c
08
F0
00 00 00
F8 00 00 00 00
F8 09
08
F8
86 04 08
FE FF FF
10
0C
F8
00 00 00
10
F8
push ebp
mov ebp, esp
sub esp, 0x8
and esp, 0xF0
mov eax, 0x0
sub esp, eax
mov [ebp-08], 0x0
cmp [ebp-08], 0x9
jle 0x804846b
jmp 0x8048493
sub esp, 0x8
push [ebp-08]
push 0x8048648
call printf
add esp, 0x10
sub esp, 0xC
push [ebp-08]
call fa
add esp, 0x10
lea eax, [ebp-08]
inc [eax]
jmp 0x8048463
leave
ret
Concernant les instructions call, l’adresse de destination a été remplacée
par le nom de la fonction lui correspondant. De même, les instructions jmp
affichent l’adresse de destination du saut et non plus le décalage à effectuer
par rapport à la position de l’instruction.
1.3.2
Les graphes du programme Test
Pour mieux comprendre le code désassemblé et l’architecture du programme, nous nous intéressons ensuite aux graphes fournis par graphELF.
Ces graphes permettent notamment une première recherche sur d’éventuelles
failles de sécurité dans le programme.
La figure 1.5 contient le graphe des appels de fonction du programme.
Dans ce graphe, les fonctions définies dans le programme sont écrites dans des
noeuds ovales tandis que les appels de fonction dynamique sont représentées
dans des losanges. Les différentes couleurs des losanges représentent les cas
suivants :
marron la fonction est une fonction ne présentant aucun risque en terme de
sécurité.
rouge la fonction représente un risque de débordement de tampon.
30
main
fa
printf
fb
fgets
sqrt
strcpy
Figure 1.5 – Graphe des appels de fonction de Test.
bleu la fonction représente un risque de bogue de format.
La figure 1.6 donne le flot de contrôle de la fonction fa. Les noeuds ovales
représentent les points de départ des branchements : l’adresse inscrite dans
ces noeuds est celle de début de bloc et correspond également à l’adresse de
destination du saut. L’instruction ret est marquée dans un noeud particulier
nommé return afin d’identifier les possibles points de sortie de la fonction.
Les autres losanges identifient les appels de fonction avec les mêmes codes
couleur décrits précédemment pour identifier les fonctions dangeureuses. Les
appels des fonctions définies dans le programme sont pour leur part représentés dans des losanges de couleur marron tandis que les appels dynamiques
ne représentant pas de risque identifié sont de couleur verte.
La figure 1.7 détaille la fonction fa. Le graphe représenté correspond au
code désassemblé et découpé en blocs selon le flot de contrôle de la fonction.
Chaque noeud contient donc le code correspondant au bloc. On retrouve
l’adresse de début de chaque bloc qui coïncide avec un noeud du graphe
de flot de contrôle décrit précédemment ainsi que les appels de fonction et
l’instruction return.
La figure 1.7 nous indique également que la fonction contient un branchement effectué après une comparaison. Il s’agit de l’instruction if située
à l’adresse 804849f et représentée par deux branches distinctes vers deux
blocs de code différents : le bloc contenant les instructions à effectuer si le
31
0x8048495
fb
0x80484a1
0x80484a8
0x80484f8
sqrt
printf
return
Figure 1.6 – Graphe du flot de contrôle de la fonction fa du programme
Test.
test est vrai et l’autre bloc pour le test faux. Le bloc 80484f8 contient les
instructions exécutées après le if, à savoir la sortie de la fonction fa.
La fonction printf appelée dans ce programme correspond à une fonction
potentiellement dangeureuse car susceptible de produire un bogue de format
exploitable à des fins malicieuses (voir partie 4.1). La fonction strcpy est
également dangeureuse car elle peut engendrer des débordements de tampon (voir partie 4.1). Ces deux fonctions sont donc affichées d’une couleur
différente afin de repérer les points vulnérables du programme et d’orienter
les recherches lors d’un audit de sécurité par exemple. Le graphe de flot de
contrôle nous renseigne sur les adresses de début des blocs dans lesquels se
situent ces appels, tandis que le graphe contenant le code désassemblé permet d’analyser ce code et de le situer plus facilement que dans le listing
contenant le code désassemblé.
1.4
Plan de lecture
Le chapitre 2 décrit les caractéristiques d’un fichier au format ELF avec
ses sections et ses segments.
Le chapitre 3 présente graphELF avec son fonctionnement général ainsi
32
8048495
8048496
8048498
804849b
804849f
80484a1 E8 54 00 00 00
80484a6 EB 50
55
89
83
83
7F
callfb
jmp0x80484f8
fb
pushebp
movebp, esp
subesp, 0x8
cmp[ebp+08], 0x4
jg0x80484a8
E5
EC 08
7D 08 04
07
80484a8
80484ab
80484ae
80484b0
80484b3
80484b5
80484b8
80484b9
80484bc
80484c0
80484c4
80484c7
80484cc
80484cf
80484d2
80484d6
80484d8
80484dc
80484df
80484e2
80484e5
80484e8
80484eb
80484f0
80484f5
80484f8 C9
80484f9 C3
83
8B
89
C1
01
C1
50
DB
8D
8D
DD
E8
83
D9
66
B4
66
D9
DB
D9
83
FF
68
E8
83
EC
55
D0
E0
D0
E0
08
08
04
64
64
1C
A0
C4
7D
8B
0C
89
6D
5D
6D
EC
75
5E
67
C4
24
24
24
24
FE
10
FA
45
leave
ret
subesp, 0x8
movedx, [ebp+08]
moveax, edx
shleax, 0x2
addeax, edx
shleax, 0x4
pusheax
fild[esp]
leaesp, [esp+04]
leaesp, [esp-08]
fstp[esp]
callsqrt
addesp, 0x10
fstcw[ebp-06]
movax, [ebp-06]
movah, 0xC
mov[ebp-08], ax
fldcw[ebp-08]
fistp[ebp-04]
fldcw[ebp-06]
subesp, 0x8
push[ebp-04]
push0x804865E
callprintf
addesp, 0x10
02
04
04
F8
FF FF
FA
45 F8
F8
FC
FA
08
FC
86 04 08
FE FF FF
10
sqrt
return
Figure 1.7 – Graphe des blocs de code de la fonction fa du programme Test.
que le détail des différentes phases d’analyse d’un programme au format ELF.
Ce chapitre décrit également les outils utilisés par graphELF : la librairie de
désassemblage libdisasm ainsi que dot pour la construction des graphes. La
dernière partie du chapitre compare différents outils existants actuellement
avec graphELF et explique ce qu’apporte notre outil.
Le chapitre 4 décrit le cadre dans lequel s’inscrivent nos recherches, les
différentes problématiques pour lesquelles notre outil a été conçu et discute de ses possibles applications. Il montre ensuite les résultats fournis par
graphELF dans différents exemples de programmes et décrit comment ces
résultats peuvent être interprétés.
La conclusion discute des limitations de notre outil et envisage diverses
possibilités pour étendre le domaine d’analyse de graphELF et affiner les
recherches en matière de sécurité.
printf
Chapitre 2
Le format ELF
Ce chapitre est consacré au fichiers aux format objet. La partie 2.1 définit ce qu’est un fichier au format objet. Un fichier au format objet fait
généralement appel à des fonctions externes telles que les fonctions définies
dans la librairie standard C. La partie 2.2 décrit comment un tel fichier gère
ces appels. La partie 2.3 donne les étapes de chargement en mémoire d’un
fichier au format objet et décrit en particulier la gestion de la pile lors de
l’exécution d’un programme.
Il existe différents types de formats objet dépendant essentiellement des
plates-formes pour lesquelles ces fichiers sont conçus mais qui suivent tous
le même format général décrit dans la partie 2.4. La partie 2.5 s’intéresse en
particulier aux fichiers de type ELF, sujet de notre étude actuelle. La partie
2.6 pour sa part détaille le principe de résolution des appels dynamiques
contenus dans les fichiers au format ELF, en particulier sur plate-forme Intel/Linux.
2.1
Le format objet
Lors de la compilation des fichiers source d’un programme, le compilateur
effectue un certain nombre de tâches [ASU89]. Il fournit tout d’abord autant
de fichiers objet – les fichiers d’extension .o sous Unix – que de fichiers source
qu’il transmet à l’éditeur de liens. Celui-ci se charge alors de les regrouper
pour fournir soit un fichier exécutable soit une librairie dynamique selon les
options qui lui ont été spécifiées.
Les fichiers objet, appelés également fichiers relocalisables, les exécutables
et les librairies dynamiques constituent les fichiers au format objet [SCO00].
Un fichier au format objet désigne un fichier dont la structure et la composition suivent la définition d’un type de format objet particulier [Sun02].
33
34
2.2
Les liens avec les librairies
Un programme écrit en langage C fait en général appel à des fonctions
définies dans la librairie standard [KR92] ou bien encore dans d’autres librairies. Dans le cas des fichiers de type ELF les liens avec ces librairies peuvent
s’effectuer de deux manières différentes : statique ou dynamique.
Lors de la constitution du programme exécutable, l’éditeur de liens statique 1 ne se comportera pas de la même manière selon la méthode choisie.
2.2.1
Lien statique
Figure 2.1 – Appel de la fonction printf située dans une librairie statique.
Les liens statiques font référence à des librairies statiques. Dans ce cas,
l’éditeur de liens statique copie une partie de la librairie référencée directement dans le fichier exécutable [BWC01]. Celui-ci contient alors une section
text qui regroupe à la fois l’ensemble du code exécutable défini dans le programme source et la copie des fonctions utilisées dans la librairie. Le schéma
1. Il s’agit de l’éditeur de liens utilisé lors de la compilation du programme, par opposition à l’éditeur de liens dynamique chargé de résoudre les appels dynamiques lors de
l’exécution du programme.
35
de la figure 2.1 représente deux programmes prog1.c et prog2.c qui font
appel à la fonction printf. Le code exécutable de chacun des programmes
contient une copie de la définition de la fonction printf. Tout se passe alors
comme si toutes les fonctions étaient directement définies dans le programme
les utilisant.
Le programme, durant son exécution, ne fait appel à aucun module externe puisque les modules de librairies nécessaires ont été intégrés préalablement à l’intérieur du code exécutable. Cette méthode présente cependant
les inconvénients d’obliger à recompiler entièrement le programme lors d’une
modification de la librairie [BWC01] et d’augmenter le nombre de copies de
fonctions usuelles en C de manière critique.
2.2.2
Lien dynamique
Figure 2.2 – Appel de la fonction printf située dans une librairie dynamique.
Lors de liens dynamiques, l’éditeur de liens statique 2 marque les fonctions
2. Lors de la phase de compilation d’un programme, le fichier exécutable fourni ne
doit plus posséder de référence indéfinie. Or l’éditeur de liens statique ne connaît pas
les adresses des fonctions dynamiques. Il marque donc ces appels de fonction de manière
particulière afin de fournir un exécutable dont toutes les références sont définies.
36
externes comme étant des appels dynamiques. Il leur assigne des adresses
fixes [Hew01] qui correspondent en fait à des pointeurs vers l’éditeur de
liens dynamique comme le décrit le schéma de la figure 2.2. Dans ce cas, la
résolution des appels des fonctions contenues dans les librairies est retardée :
elle n’est plus réellement effectuée par l’éditeur de liens statique mais par un
éditeur de liens dynamique, lors du chargement du programme et durant son
exécution [BWC01].
C’est pour cela que le fichier exécutable contient un certain nombre d’informations dynamiques qui sont chargées en mémoire et qui permettent la
résolution de ce type d’appel. La partie 2.6 détaille la résolution des appels
dynamiques dans un fichier au format ELF.
L’utilisation de librairies dynamiques facilite la maintenance de celles-ci.
En effet, lorsqu’une librairie est modifiée, il est inutile de recompiler les programmes lui faisant référence 3 ; il suffit simplement de changer la librairie et
les programmes, lors de leurs appels de fonctions dynamiques, la référenceront automatiquement.
En revanche, si la librairie est modifiée à des fins douteuses ou encore si
un bogue y réside, tous les programmes utilisant cette librairie sont affectés.
2.3
Gestion de la mémoire
Cette partie présente des notions de gestion et d’organisation de la mémoire lors du chargement et de l’exécution d’un programme sur une plateforme Unix.
2.3.1
Le chargement en mémoire d’un programme
Un programme, pour être exécuté, doit d’abord être chargé en mémoire.
Cette phase est effectuée par le chargeur de programmes qui crée une image
mémoire du programme à partir des informations lues dans le fichier exécutable. Parmi les tâches effectuées, le chargeur copie en mémoire différentes
parties du fichier [Lev99], appelées sections (cf. partie 2.5.3) et les répartit
dans des segments (cf. partie 2.5.4) tels que réprésentés dans la figure 2.3.
On distingue ainsi :
– Les informations accessibles en mémoire et modifiables par le processus, telles que les données. Ce segment est accessible en lecture et
écriture.
– Les informations accessibles uniquement en lecture, tel que le code du
programme lui-même. Ce segment est accessible en lecture et exécution.
37
Figure 2.3 – Chargement en mémoire d’un fichier au format ELF
Lors du chargement en mémoire d’un fichier exécutable, le système d’exploitation prend en charge l’adressage virtuel 4 et le code partagé 5 . Il procède
le plus couramment de la sorte [Lev99] pour charger un programme en mémoire :
– Lecture de l’en-tête du fichier pour obtenir la taille des différents segments.
– Vérification si le segment partageable (le segment text) est déjà chargé
en mémoire. Si tel est le cas, le système projette ce segment préalablement chargé dans l’espace virtuel du processus en cours de construction. Sinon, le système crée ce segment partageable, le projette dans
l’espace virtuel du processus et lit le segment text pour remplir le
segment partageable.
3. La recompilation est nécessaire lorsque l’interface des fonctions appelées change (par
exemple, le nombre d’arguments).
4. Le système d’exploitation et le processeur font la correspondance entre les espaces
d’adressage virtuel des processus et les adresses physiques réelles.
5. Il s’agit des segments non modifiables qui peuvent être partagés entre plusieurs
processus. Chaque processus possède son propre espace d’adressage mais un seul espace
mémoire physique est alloué à ces segments. Le système d’exploitation gère la correspondance entre les deux.
38
– Création d’un segment de données privées contenant le segment data ;
projection dans l’espace d’adressage du processus et lecture des informations contenues dans le fichier.
– Création du segment de pile et projection dans l’espace d’adressage.
– Initialisation des registres et saut vers l’adresse de début de programme
— le point d’entrée du programme.
La figure 2.3 nous montre également que toutes les parties du fichier
exécutable ne sont pas chargées en mémoire. Les informations de débogage,
figurant éventuellement dans le fichier exécutable, par exemple, sont absentes
de l’image mémoire.
2.3.2
L’espace d’adressage d’un processus
Sur un système Unix, l’éxécution d’un programme est effectué par un processus, chacun définissant son propre espace d’adressage. L’image mémoire
de chaque programme est projetée dans cet espace d’adressage.
Le programme contient des symboles localisés à des adresses virtuelles.
Ces adresses, contenues dans le fichier exécutable sont fixes [Hew01]. Elles
correspondent aux adresses dans l’image mémoire du processus. Ainsi, les
informations chargées en mémoire se situent toujours à des positions fixes
dans l’espace d’adressage du processus les exécutant [LMKQ89].
Si le programme est exécuté plusieurs fois en parallèle, chaque processus
créé possèdera son propre espace d’adressage. Cependant, les zones accessibles en lecture seule pointeront vers une unique zone mémoire physique
tandis que les zones modifiables resteront propres à chacun des processus.
2.3.3
Organisation de la mémoire
Après avoir montré comment un programme est chargé en mémoire, nous
nous intéressons à l’organisation de la mémoire selon les différents objets
utilisés lors de l’exécution du programme :
– les instructions
– les variables globales initialisées
– les variables globales non initialisées
– Les variables locales
– Les variables dynamiques 6
Le schéma de la figure 2.4 réprésente les différentes zones qui contiennent :
6. On désigne par variable dynamique une variable dont la taille est définie au cours
de l’exécution du programme : une zone référencée par un pointeur et allouée dynamiquement. Par opposition, une variable statique est une variable dont la taille est définie dès
la compilation. Les pointeurs sont des variables statiques : leur taille est connue dès la
compilation alors que la zone qu’ils pointent ne l’est pas forcément.
39
Figure 2.4 – Schéma général de la mémoire d’un programme
Text : les instructions machine
Data : les variables globales initialisées
Bss : les variables globales non initialisées
user stack frame : zone mémoire réservée à l’exécution du programme qui
se découpe ainsi :
tas : les variables dynamiques 7
pile : les variables locales, les variables d’environnement et les arguments transmis aux fonctions du programme (voir détails cidessous).
2.3.4
La pile et l’exécution d’une fonction
La pile contient les variables locales et les arguments des fonctions. Lors
d’un appel de fonction, le contexte d’appel doit également être sauvegardé
7. Les variables dynamiques sont référencées par un pointeur. Lors de la déclaration
de ce dernier, une zone mémoire lui est allouée dans la zone Data ou sur la pile selon
qu’il est de type global ou local. Ce n’est qu’au cours de l’exécution du programme, lors
de l’allocation mémoire de la zone pointée par ce dernier, qu’il reçoit comme valeur une
adresse située dans le tas.
40
de façon à ré-initialiser la pile une fois la fonction exécutée : la pile contient
donc également l’adresse de retour de la fonction et le pointeur de base de
l’appelant.
La pile se manipule essentiellement avec deux instructions, push pour
empiler un objet et pop pour dépiler.
Figure 2.5 – Schéma général de la pile lors de l’appel d’une fonction
Le schéma de la figure 2.5 illustre l’organisation générale d’une pile au
cours de l’exécution d’une fonction, avec la position dans la pile des différentes variables et arguments de celle-ci.
L’exécution d’une fonction se décompose en quatre étapes [BGR01a] :
– L’appel de la fonction avec, en préalable à l’instruction d’appel, l’em-
41
pilement de ses paramètres sur la pile. Cet empilement se fait dans
l’ordre inverse des paramètres fournis à la fonction comme schématisé
dans la figure 2.5.
– Le prologue de la fonction destiné à sauvegarder l’état de la pile en
prévision de la sortie de la fonction. Ce prologue est toujours constitué
des mêmes instructions machine (cf. partie 3.5.2.1) et place notamment
dans la pile la valeur de l’adresse de retour de la fonction ainsi que
la valeur du registre ebp de l’appelant dans la pile. Vient ensuite la
réservation de l’espace mémoire nécessaire à l’exécution de la fonction :
l’empilement des variables locales 8 .
– L’exécution de la dite fonction.
– L’épilogue de la fonction avec le retour à l’état initial : le pointeur de
pile est incrémenté pour vider la pile des variables locales, le pointeur de
base de l’appelant est chargé et l’adresse de retour est lue. Le pointeur
de pile est à nouveau incrémenté pour vider la pile des arguments de
la fonction qui vient d’être exécutée.
Le schéma de la figure 2.5 contient également :
– Un registre esp, extended stack pointer ou pointeur de pile qui est
décrémenté et incrémenté au fur et à mesure des empilements et dépilements. Il se situe toujours au sommet de la pile.
– Un registre ebp, extended base pointer ou pointeur de base qui se situe
au même endroit que la sauvegarde du pointeur de base de l’appelant
sur le schéma. Ce registre contient l’adresse de début de l’environnement de la fonction en cours d’exécution et ne varie pas au cours de
cette exécution. Il sert de référence pour les arguments et les variables :
les premiers sont référencés par un décalage positif tandis que les secondes le sont par un décalage négatif.
2.4
Structure générale d’un fichier au format objet
Quel que soit le type de fichier au format objet (exécutable, librairie ou
.o) ce dernier suit toujours la même structure générale représentée dans la
figure 2.6.
Un fichier au format objet se compose tout d’abord d’un en-tête de fichier
situé obligatoirement dans les premiers octets du fichier et qui indique le type
de format objet, donne des informations générales sur le fichier et pointe vers
les différentes zones de celui-ci (voir figure 2.6).
Cet en-tête de fichier représente le point de départ de toute analyse puisqu’il permet d’accéder aux informations contenues dans le fichier.
8. En général, elles sont empilées dans l’ordre de déclaration sauf optimisation effectuée
par le compilateur qui peut alors avoir totalement changé cet ordre et même avoir procédé
plus tôt dans le code à l’empilement de ces variables.
42
Figure 2.6 – Schéma général d’un fichier au format objet.
Le fichier au format objet de la figure 2.6 contient ensuite des en-têtes
pointant vers des sections. Les en-têtes de section sont des structures prédéfinies qui fournissent les renseignements permettant d’accéder aux différentes
sections du fichier.
Les sections représentent des parties distinctes du fichier. Chaque section
contient le même type de données et se trouve nécessairement dans une zone
contiguë à l’intérieur du fichier : les données de même type ne peuvent être
morcelées en différents endroits du fichier. On distingue les trois principales
sections suivantes :
– la section text qui contient le code exécutable du fichier
– la section data qui contient les données globales initialisées
43
– la section bss qui contient les données globales non initialisées
La partie 2.5.3 présente plus en détail les différents types de sections ainsi
que leur structure.
Le fichier de la figure 2.6 se compose également d’une table des symboles
associée à une table de noms. Celles-ci contiennent les références et les noms
des symboles utilisés par le programme. Elles sont détaillées dans la partie
2.5.5.
Quel que soit le type de format objet, les fichiers suivent tous cette même
structure et sont construits de manière similaire. En particulier, les fichiers
au format objet possèdent tous les trois sections citées ci-dessus ainsi que les
tables des symboles et de noms. Nous détaillons dans la partie suivante le
format ELF, format pour lequel notre outil, graphELF, a été conçu.
2.5
Le format ELF
Le format ELF (ou “executable and linking format”) représente le type de
format objet utilisé sur la majorité des plates-formes Unix. Ses spécificités
sont décrites dans [Com95] et les structures qui le composent sont définies
dans le fichier elf.h disponible sur chaque plate-forme Unix. Cette partie a
pour but de décrire les principales caractéristiques du format ELF 9 .
Ce format de fichier, créé par Unix System Laboratories, est désormais
utilisé sur les systèmes Unix System V release 4. Il est également devenu le
format de fichier objet des systèmes Linux et FreeBSD.
Le format ELF gère à la fois les instructions 32 et 64 bits. La différence
majeure entre ces deux formats correspond à la taille des pointeurs. Les structures qui composent les différentes parties d’un fichier ELF sont similaires
pour les deux formats, seul le préfixe des noms des strutures les différencie, à
savoir Elf32_ ou Elf64_ [VR03]. Ainsi un outil implémenté pour un format
fonctionne également pour l’autre sans modification majeure. Nous décrivons
ici le format 32 bits, étant donné que graphELF a été implémenté sur une
machine 32 bits.
Nous décrivons dans cette partie les différentes structures qui composent
un fichier au format ELF avec tout d’abord l’en-tête d’un fichier ELF (partie
2.5.1). Dans la partie 2.5.2, nous expliquons pourquoi un fichier ELF contient
des sections — détaillées dans la partie 2.5.3 — et des segments — détaillés
dans la partie 2.5.4. La partie 2.5.5 précise le fonctionnement de la table des
symboles en relation avec la table des noms.
9. Le détail des structures contenues dans un fichier au format ELF ainsi que leurs
champs ont déjà fait l’objet d’une étude détaillée [MM03].
44
2.5.1
L’en-tête d’un fichier ELF
L’en-tête ELF se situe au début du fichier et en décrit l’organisation.
L’exemple suivant reproduit un en-tête ELF, caractérisant un fichier exécutable sur une plate-forme Linux/32 bits. Le fichier pris pour exemple dans
ce chapitre est le même que celui utilisé dans la partie 1.3 (voir figure 1.4 en
page 23).
En-tête du fichier Test
ELF Header:
Magic:
7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class:
ELF32
Data:
2’s complement, little endian
Version:
1 (current)
OS/ABI:
UNIX - System V
ABI Version:
0
Type:
EXEC (Executable file)
Machine:
Intel 80386
Version:
0x1
Entry point address:
0x8048390
Start of program headers:
52 (bytes into file)
Start of section headers:
4696 (bytes into file)
Flags:
0x0
Size of this header:
52 (bytes)
Size of program headers:
32 (bytes)
Number of program headers:
6
Size of section headers:
40 (bytes)
Number of section headers:
35
Section header string table index:
32
Parmi les informations fournies, on remarque les champs suivants :
– Les premiers champs nous fournissent des indications d’ordre général
sur le fichier :
magic contient un nombre appelé nombre magique qui indique le type
de format utilisé. L’analyse de ce nombre s’effectue grâce à des
macro-fonctions fournies dans le fichier elf.h.
class indique le format, ici ELF en 32 bits.
OS indique la plate-forme.
machine indique le type de processeur.
type nous renseigne sur la nature du fichier : exécutable, librairie ou
encore relocalisable.
45
– l’en-tête fournit ensuite les offsets 10 des différentes parties du fichier
ELF :
– Deux champs contiennent pour le premier l’offset dans le fichier de
la table des en-têtes de programme 11 et pour le second, l’offset dans
le fichier de la table des en-têtes de section.
– Ces champs sont suivis d’indications sur la taille de ces tables et sur
leur nombre d’entrées, ceci afin de permettre l’accès à la totalité des
en-têtes.
– Le point d’entrée du programme est également fourni, permettant de
retrouver directement l’adresse de début d’exécution du programme.
2.5.2
Deux angles de vue
Nous expliquons maintenant la différence entre les sections et les segments
contenus dans un fichier au format ELF.
On peut parcourir le fichier selon deux angles différents. En effet, si l’on se
place du point de vue de l’éditeur de liens statique (cf. partie 2.2), ce dernier
va parcourir les en-têtes de section pour accéder ensuite aux sections et
retrouver les informations dont il a besoin pour lier les fichiers relocalisables
(les fichiers *.o) entre eux et résoudre les références non encore résolues. Il
livrera ensuite un fichier ne contenant plus aucune référence non résolue : un
exécutable ou une librairie dynamique.
Les sections servent à l’éditeur de liens statique et il n’est absolument
pas indispensable d’en connaître le contenu et leur localisation pour exécuter
le programme. C’est pourquoi la table des en-têtes de section est optionnelle
et peut très bien avoir été supprimée d’un fichier exécutable.
En revanche, un fichier exécutable, pour pouvoir être chargé en mémoire,
doit contenir une image de celle-ci. Pour y parvenir, le format ELF adopte
un découpage du fichier en segments. Ces derniers sont accédés par la table
des en-têtes de programme. Les segments reflètent donc l’image mémoire
d’un exécutable et la table des en-têtes de programme est obligatoirement
présente dans un fichier exécutable.
La figure 1.3 de la page 19 représente un fichier ELF selon ces deux angles
différents. Elle décrit un fichier soit sous forme de sections soit sous forme de
segments. La figure nous permet également de comprendre qu’un segment
englobe souvent plusieurs sections. Il s’agit de deux façons différentes d’accéder aux mêmes informations contenues dans le fichier : ces informations ne
10. Nous employons le terme offset pour désigner les octets correspondant à une position
dans le fichier.
11. Nous rappelons que les en-têtes de section — en anglais section header — pointent
vers les sections tandis que les en-têtes de programme — en anglais program header –
pointent vers les segments.
46
sont ni déplacées ni dupliquées à l’intérieur du fichier selon que l’on envisage
les sections ou les segments.
2.5.3
Les sections et leur en-tête
Comme nous venons de le voir, un fichier ELF peut contenir des en-têtes
de section qui permettent d’accéder aux sections contenues dans le fichier.
Ces en-têtes sont regroupés dans un tableau de structures, chacune de ces
structures décrivant une section comme le montre la figure 2.7. Les champs
principaux de ces structures sont les suivants :
nom un index dans la table des noms de section qui contient la chaîne de
caractères du nom de la section.
adresse si la section peut être chargée en mémoire, ce champ contient son
adresse virtuelle 12 .
offset la position dans le fichier.
taille la taille de la section.
Figure 2.7 – Schéma général des en-têtes de sections d’un fichier au format
ELF.
12. L’adresse virtuelle est l’adresse dans l’image mémoire du processus.
47
L’exemple suivant nous fournit une table des en-têtes de section caractéristique :
Table des en-tetes de section
nombre d’entree 35
index
section
[ 0]
[ 1]
.interp
[ 2]
.note.ABI-tag
[ 3]
.hash
[ 4]
.dynsym
[ 5]
.dynstr
[ 6]
.gnu.version
[ 7]
.gnu.version_r
[ 8]
.rel.dyn
[ 9]
.rel.plt
[10]
.init
[11]
.plt
[12]
.text
[13]
.fini
[14]
.rodata
[15]
.data
[16]
.eh_frame
[17]
.dynamic
[18]
.ctors
[19]
.dtors
[20]
.jcr
[21]
.got
[22]
.bss
[23]
.comment
[24]
.debug_aranges
[25]
.debug_pubnames
[26]
.debug_info
[27]
.debug_abbrev
[28]
.debug_line
[29]
.debug_frame
[30]
.debug_str
[31]
.debug_ranges
[32]
.shstrtab
[33]
.symtab
[34]
.strtab
type
NULL
PROGBITS
NOTE
HASH
DYNSYM
STRTAB
VERSYM
VERNEED
REL
REL
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
DYNAMIC
PROGBITS
PROGBITS
PROGBITS
PROGBITS
NOBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
STRTAB
SYMTAB
STRTAB
adresse
0
80480f4
8048108
8048128
8048164
8048204
8048286
804829c
80482dc
80482ec
8048314
804832c
8048390
8048624
8048640
8049664
8049670
8049674
8049744
804974c
8049754
8049758
804977c
0
0
0
0
0
0
0
0
0
0
0
0
taille
0
19
32
60
160
130
20
64
16
40
23
96
660
26
35
12
4
208
8
8
4
36
8
217
152
95
768
285
545
88
285
24
313
1856
929
Le nombre de sections d’un fichier est variable. En effet, il existe des sections prédéfinies qui figurent dans tout exécutable, mais on peut également
créer et définir ses propres sections et l’en-tête qui leur correspond. Le cas le
plus fréquent de création de sections particulières sert à définir des caratéristiques propres à une plate-forme. En dehors de ces sections système, cette
démarche reste toutefois rare et des sections non prédéfinies doivent susciter
la méfiance et une analyse minutieuse de leur contenu, dans le contexte de
48
la sécurité informatique 13 .
Certaines sections prédéfinies sont utilisées par le système d’exploitation
et possèdent donc parfois des particularités différentes selon les systèmes.
C’est le cas des sections gérant les liens dynamiques ou des sections .init
et .fini par exemple.
Une section doit satisfaire certaines conditions [Com95] :
– chaque section possède un et un seul en-tête de section dans la table
des en-têtes de section.
– chaque section occupe une séquence contiguë d’octets dans le fichier.
– aucun octet d’une section ne peut appartenir à une autre section.
– un fichier objet peut contenir un espace inactif, c’est-à-dire un espace
qui ne fait pas partie d’une section. Les données de ces portions de
fichier ne sont pas spécifiées.
Table 2.1 – Les principales sections ELF
.text
.rodata
.data
.bss
.symtab
.strtab
.dynsym
.dynstr
.init
.fini
.debug_*
.line
.shstrtab
.dynamic
.plt
.got
Comme le montre l’affichage de la table des symboles précédente, les
sections contenues dans un fichier ELF sont plus nombreuses que les trois
principales sections d’un fichier au format objet définies précédemment.
La table 2.1 dresse une liste des principales sections que nous détaillons
ci-dessous. Bien que non exhaustive, cette liste recouvre les sections les plus
importantes, notamment celles qui sont utilisées par notre outil :
13. Ces sections sont d’autant plus suspectes si elles possèdent une adresse indiquant
qu’elles peuvent être chargées en mémoire.
49
– .text : contient les instructions du programme.
– .data : contient les données initialisées. Elle est chargée en mémoire
et accessible en écriture.
– .rodata : contient les constantes du programme telles que les chaînes
de caractères. Il s’agit de données non modifiables, chargées également
en mémoire et accessibles uniquement en lecture.
– .bss : contient les données non initialisées du programme. Par définition, le système initialise ces données à zéro lors du lancement du
programme. Elle n’occupe pas de place significative dans le fichier mais
en occupe en mémoire et ces données peuvent être écrites durant l’exécution du programme.
Les tables de symboles et de noms (décrites en 2.5.5) se situent dans les
sections qui portent les noms suivants :
– .symtab : contient la table des symboles statiques.
– .strtab : correspond à la table des noms utilisée par la table des
symboles statiques.
– .shstrtab : contient la table des noms de section utilisée par la table
des en-têtes de section.
– .dynsym : table des symboles dynamiques.
– .dynstr : table des noms dynamiques. Contient les chaînes de caractères correspondant aux liens dynamiques. Cette table est utilisée par
la table des symboles dynamiques.
Les sections suivantes ne sont pas chargées en mémoire :
– .debug_* : contient des informations de débogage. Toutes les sections
dont le nom commence par .debug sont gérées par le débogueur.
Les deux sections suivantes concernent le prologue et l’épilogue de l’exécution du programme :
– .init : Cette section contient des instructions exécutables à l’initialisation du programme par l’éditeur de liens dynamiques. Le compilateur
crée cette section qui contient le code et les informations nécessaires à
l’initialisation des objets et de l’environnement d’éxécution [BWC01].
Le code contenu dans cette section est exécuté avant l’exécution du
point d’entrée [Lu95].
– .fini : cette section contient des instructions exécutables lorsque le
programme se finit normalement. Après l’instruction exit, le processus
exécute le code de cette section qui contient les instructions nécessaires
à la destruction des objets initialisés avant l’exécution du programme.
Les sections suivantes, détaillées en partie 2.6, sont destinées à la résolution des liens dynamiques :
– .dynamic : contient les informations sur les liens dynamiques.
– .interp : contient le chemin d’accès d’un interpréteur de programme.
Il s’agit de l’éditeur de liens dynamique qui charge les bibliothèques
dynamiques.
50
– .plt : contient la Procedure Linkage Table, à savoir la table des liens
de procédure ou de fonction.
– .got : contient la Global Offset Table, à savoir la table de relocalisation
des données globales.
– .gnu.version_r : contient les noms des librairies dynamiques requises
par le programme.
– .gnu.version : contient autant d’entrées que la table des symboles
dynamiques et fait le lien entre ces symboles et les librairies dont ils
dépendent. Ces deux dernières sections sont détaillées dans la partie
3.4.2 de description de l’outil graphELF.
2.5.4
Les segments et leurs en-têtes
D’un point de vue exécutable, un fichier ELF est découpé en segments.
Ces derniers sont accessibles par la table des en-têtes de programme. De la
même manière que pour les sections, chaque en-tête est une structure qui
fournit les moyens d’accès à un segment :
type ce champ permet de savoir si le segment est chargé en mémoire ou
encore s’il s’agit du segment dynamic (cf. partie 2.6.1).
adresse contient l’adresse virtuelle en mémoire du segment.
offset contient la position dans le fichier.
taille contient la taille du segment dans le fichier.
taille mémoire celle du segment en mémoire.
droits décrit les droits d’accès du segment en mémoire : lecture, écriture,
exécution ou une combinaison des ces trois types de droits.
Le découpage en segments reflète l’image mémoire du processus. Les segments sont moins nombreux que les sections : un segment contient d’ailleurs
souvent plusieurs sections comme le montrent les tables 2.2 et 2.3 qui nous
donnent deux exemples caratéristiques de segments sous Unix. On remarque
également que si les sections regroupent des informations selon leur nature,
les segments suivent un autre principe : ils regroupent les informations selon
leur type d’accès en mémoire.
L’exemple suivant nous décrit la table d’en-têtes de programme ainsi que
le contenu des segments du programme Test :
51
Table 2.2 – Exemple d’un segment Text sous Unix
.interp
.note.ABI-tag
.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_r
.rel.dyn
.rel.plt
.init
.plt
.text
.fini
.rodata
Table 2.3 – Exemple d’un segment Data sous Unix
.data
.dynamic
.got
.bss
Les entetes de programme
nombre d’entree 6
index
type
adresse t_fichier t_memoire
[ 0]
PHDR
8048034 0x00000c0 0x00000c0
[ 1]
INTERP
80480f4 0x0000013 0x0000013
[ 2]
LOAD
8048000 0x0000663 0x0000663
[ 3]
LOAD
8049664 0x0000118 0x0000120
[ 4]
DYNAMIC
8049674 0x00000d0 0x00000d0
[ 5]
NOTE
8048108 0x0000020 0x0000020
protection
R E
R
R E
RW
RW
R
Les sections dans les segments
segment 0
segment 1
segment 2
segment 3
segment 4
segment 5
.interp
.interp
.note.ABI-tag
.hash
.dynsym
.dynstr
.gnu.version
. gnu.version_r
.rel.dyn
.rel.plt
.init
.plt
.text
.fini
.rodata
.data
.eh_frame
.dynamic
.ctors
.dtors
.jcr
.dynamic
.note.ABI-tag
.got
52
On y retrouve deux segments de type LOAD qui correspondent aux deux
segments chargeables en mémoire : data et text. L’un est accessible en
lecture et écriture pour le segment data et l’autre en lecture et exécution.
Le segment de type DYNAMIC correspond au segment dynamic décrit dans
la partie 2.6.1. Le segment de type NOTE est un segment non chargeable en
mémoire et qui regroupe des informations essentiellement liées aux débogage.
Le segment de type INTERP contient la section .interp qui référence l’éditeur
de liens dynamique utilisé.
2.5.5
La table des symboles et la table des noms
Nous présentons dans cette partie le contenu des sections .symtab et
.strtab : une table des symboles et son fonctionnement.
Un fichier ELF contient plusieurs tables des noms et tables des symboles :
– La table des symboles statiques, associée à sa table des noms, contient
tous les symboles du programme 14 .
– La table des symboles dynamiques, associée à sa table des noms,
contient les objets et fonctions des librairies dynamiques.
– La table des noms contenue dans la section .shstrtab contient les
noms des sections.
Une table des symboles contient les informations nécessaires à la localisation des symboles lors de la compilation du programme. Chaque entrée dans
la table des symboles correspond à une structure prédéfinie qui contient les
informations suivantes :
nom contient un index vers la table des noms.
valeur contient la valeur du symbole associé : en général, l’adresse virtuelle
du symbole.
taille de l’objet représenté par le symbole ou zéro si ce dernier n’en a pas.
Une fonction possède en général une taille tandis qu’une variable globale non initialisée n’a pas de taille par exemple, de même qu’un nom
de fichier.
type du symbole (variable, fonction, section, fichier) et ses attributs (local,
global).
index de l’en-tête de la section qui définit ce symbole.
Le nom du symbole met en relation la table des symboles avec la table des
noms contenant la chaîne de caractères du nom du symbole comme expliqué
dans la figure 2.8.
14. Il s’agit des fonctions définies dans le programme, des fichiers du programme, des
fonctions dynamiques utilisées par le programme et des données globales du programme.
Les variables locales aux fonctions ne possèdent plus de nom et sont définies directement
sur la pile lors de l’exécution du programme.
53
Figure 2.8 – Correspondance entre la table des noms et la table des symboles.
Un fichier ELF contient deux tables des symboles, chacune en relation
avec une table des noms. La table des symboles statiques est utilisée uniquement par l’éditeur de liens statique et n’étant pas chargée en mémoire, elle
peut être retirée du fichier exécutable. En revanche, la table des symboles
dynamiques renseigne sur les symboles externes appelés par le programme
et doit nécessairement être chargée en mémoire afin que l’éditeur de liens
dynamique puisse résoudre ces appels lors de l’exécution du programme.
Voici un extrait de la table des symboles statiques du programme Test
(cf. figure 1.4 de la page 23) :
54
la table des symboles
Num:
35:
38:
39:
49:
52:
56:
57:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
76:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
115:
Valeur Taille
0
0
0
0
0
0
0
0
0
0
0
0
0
0
80483b4
0
0
0
8049744
0
804974c
0
8049754
0
804966c
0
8049780
1
80483e0
0
8048420
0
0
0
8049748
0
8049750
0
8049670
0
8049754
0
8048600
0
0
0
0
0
0
0
8049674
0
8048640
4
8049664
0
8049668
0
8048590
60
80484fa
32
8048314
0
8048390
0
804833c
16a
8049664
0
8048530
58
804977c
0
804844c
49
804834c
fb
8049664
0
8049664
0
804835c
36
8048624
0
804836c
7c
804977c
0
80485f0
0
8049758
0
8049784
0
804977c
4
8049664
0
8048644
4
8049664
0
0
0
8048495
65
0
0
804837c
30
Type
FILE
FILE
FILE
FILE
FILE
FILE
FILE
FUNC
FILE
OBJECT
OBJECT
OBJECT
OBJECT
OBJECT
FUNC
FUNC
FILE
OBJECT
OBJECT
OBJECT
OBJECT
FUNC
FILE
FILE
FILE
OBJECT
OBJECT
NOTYPE
OBJECT
FUNC
FUNC
FUNC
FUNC
FUNC
NOTYPE
FUNC
NOTYPE
FUNC
FUNC
NOTYPE
NOTYPE
FUNC
FUNC
FUNC
NOTYPE
FUNC
OBJECT
NOTYPE
OBJECT
NOTYPE
OBJECT
NOTYPE
NOTYPE
FUNC
NOTYPE
FUNC
Lien
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
WEAK
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
WEAK
GLOBAL
WEAK
GLOBAL
Ndx
ABS
ABS
ABS
ABS
ABS
ABS
ABS
12
ABS
18
19
20
15
22
12
12
ABS
18
19
16
20
12
ABS
ABS
ABS
17
14
ABS
15
12
12
10
12
UND
ABS
12
ABS
12
UND
ABS
15
UND
13
UND
ABS
12
21
ABS
22
ABS
14
15
UND
12
UND
UND
Nom
<command line>
<built-in>
abi-note.S
init.c
initfini.c
<command line>
<built-in>
call_gmon_start
crtstuff.c
__CTOR_LIST__
__DTOR_LIST__
__JCR_LIST__
p.0
completed.1
__do_global_dtors_aux
frame_dummy
crtstuff.c
__CTOR_END__
__DTOR_END__
__FRAME_END__
__JCR_END__
__do_global_ctors_aux
initfini.c
test1.c
elf-init.c
_DYNAMIC
_fp_hw
__fini_array_end
__dso_handle
__libc_csu_fini
fb
_init
_start
fgets@@GLIBC_2.0
__fini_array_start
__libc_csu_init
__bss_start
main
__libc_start_main@@GLIBC_2.0
__init_array_end
data_start
printf@@GLIBC_2.0
_fini
sqrt@@GLIBC_2.0
_edata
__i686.get_pc_thunk.bx
_GLOBAL_OFFSET_TABLE_
_end
stdin@@GLIBC_2.0
__init_array_start
_IO_stdin_used
__data_start
_Jv_RegisterClasses
fa
__gmon_start__
strcpy@@GLIBC_2.0
55
Les symboles numérotés de 0 à 34 n’ont pas été représentés ici : ils correspondent aux noms des 35 sections du fichier. On retrouve dans cette table les
noms des fonctions définies dans le fichier — main ou fa par exemple — mais
également les noms des fonctions dynamiques appelées par le programme —
printf à la ligne 101 — ainsi que les noms des fonctions et objets standards
ajoutées par le compilateur. Le nom du fichier qui contient le programme
test1.c y est également défini.
Voici la table des symboles dynamiques du programme Test :
la table des symboles dynamique
Num:
0:
1:
2:
3:
4:
5:
6:
7:
8:
9:
Valeur Taille
0
0
804833c
16a
804834c
fb
804835c
36
804836c
7c
804977c
4
8048644
4
0
0
0
0
804837c
30
Type
NOTYPE
FUNC
FUNC
FUNC
FUNC
OBJECT
OBJECT
NOTYPE
NOTYPE
FUNC
Lien
LOCAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
WEAK
WEAK
GLOBAL
Ndx
Nom
UND
UND
fgets
UND
__libc_start_main
UND
printf
UND
sqrt
22
stdin
14
_IO_stdin_used
UND _Jv_RegisterClasses
UND
__gmon_start__
UND
strcpy
librairie
local
GLIBC_2.0
GLIBC_2.0
GLIBC_2.0
GLIBC_2.0
GLIBC_2.0
global
local
local
GLIBC_2.0
qui regroupe uniquement les fonctions et objets dynamiques du programme. On y retrouve donc les fonctions printf ou encore sqrt.
Les appels de ces fonctions sont résolues lors du chargement du programme ou au cours de son exécution, comme cela est détaillé dans la partie
suivante.
2.6
La gestion des adresses dynamiques
Cette partie présente en détail le mécanisme d’appel de fonction dynamique pour les fichiers au format ELF.
Pour résoudre un appel dynamique, le fichier ELF contient différentes
informations réparties dans :
– la table des symboles dynamiques
– la table des noms dynamiques
– le segment dynamic
– une référence à l’interpréteur de programme (l’éditeur de liens dynamique)
– la table got
– la table plt
Les tables des symboles et des noms dynamiques ont été décrites dans
la partie 2.5.5. Le segment dynamic, l’interpréteur de programme, la table
56
GOT et la table PLT sont détaillés respectivement, dans les parties 2.6.1,
2.6.2, 2.6.3 et 2.6.4 avant d’expliquer comment l’éditeur de liens dynamique
résoud ces appels dans la partie 2.6.5.
2.6.1
Le segment dynamic
Le segment dynamic contient une unique section : la section .dynamic
utilisée par l’éditeur de liens dynamique pour accéder à toutes les données
qui lui sont nécessaires. Dans ce cas, section et segment sont confondus et
recouvrent les mêmes informations.
Ce segment est composé d’un tableau de structures. Chaque entrée fournit une valeur interprétable selon un champ servant de drapeau. Le tableau
2.4 dresse la liste des drapeaux ainsi que l’interprétation de la valeur qui
lui est associée. Si par exemple, dans une structure, le champ correspondant
au drapeau a pour valeur DT_SYMTAB, alors le champ valeur de la structure
contiendra l’adresse de la table des symboles dynamiques.
Table 2.4 – Valeurs des drapeaux du segment dynamic
valeur du drapeau
DT_NEEDED
DT_PLTRELSZ
DT_PLTGOT
DT_STRTAB
DT_STRSZ
DT_SYMTAB
DT_SYMENT
DT_INIT
DT_FINI
valeur associée à ce drapeau
index dans la table des noms dynamiques contenant le nom
d’une bibliothèque requise pour l’exécution du fichier
exécutable. Il y a autant de structures de ce type que
de bibliothèques requises.
taille totale en octets des entrées relocalisables
associées à la table PLT.
adresse associée à la table PLT et/ou à la table GOT
adresse de la table des noms dynamiques
taille en octets de la table des noms dynamiques
adresse de la table des symboles dynamiques
taille en octets d’une entrée dans la table
des symboles dynamiques
adresse de la fonction d’initialisation
adresse de la fonction de fin de programme
Ces structures permettent à l’éditeur de liens dynamiques d’accéder aux
différentes informations dont il a besoin pour résoudre les appels dynamiques.
graphELF utilise également cette section pour calculer les adresses de
début et de fin des sections dynamiques telles que la section .plt ou .got,
calculs qui reposent également sur l’agencement des sections dans les segments (cf. partie 3.4.2).
57
2.6.2
L’interpréteur de programme
Le fichier ELF contient un segment qui référence l’interpréteur de programme utilisé pour résoudre les appels dynamiques. Dans le cas d’un fichier
pour plate-forme Unix, le chargeur de programme et l’éditeur de liens dynamiques sont identiques : il s’agit généralement de ld.so [MD+ 00].
Celui-ci vérifie les noms des librairies requises par le programme et les
charge en mémoire au besoin. Il gère la correspondance entre les adresses des
symboles de ces librairies et leur emplacement dans l’espace d’adressage du
processus.
2.6.3
La table GOT
La table GOT (Global Offset Table) contient une entrée par donnée externe référencée dans le programme. Par exemple, un programme faisant
appel plusieurs fois à la fonction printf se verra allouer une seule entrée
dans la table GOT pour cette fonction printf. Cette table est créée par
l’éditeur de liens statique.
Les symboles externes référencés par le programme tels que les appels
de fonctions dynamiques (printf par exemple) se situent dans des espaces
mémoire partagés dont le type d’adressage est de format PIC 15 [Lev99]. En
revanche, un exécutable contient des adresses fixes dans l’espace d’adressage privé du processus. Pour permettre au processus de s’exécuter avec des
adresses fixes tout en gardant le mécanisme d’adressage de type PIC pour
référencer les symboles partagés [Lev99], une correspondance entre ces deux
adresses est établie par redirection des références PIC vers des adresses fixes
contenues dans la table GOT.
Cependant, lorsque l’éditeur de liens statique crée la table, les adresses
des symboles ne sont pas encore connues et ne le seront qu’après le chargement en mémoire. La table GOT sera donc mise à jour par l’éditeur de liens
dynamique au fur et à mesure de l’exécution du programme.
L’éditeur de liens dynamique ayant besoin d’accéder et de modifier la
table GOT, celle-ci se situe donc en mémoire dans un segment accessible en
lecture et écriture. De plus, cette table est en relation avec la table PLT dont
nous détaillons le contenu dans la partie suivante. La relation entre les deux
tables est expliquée dans la partie 2.6.5.
15. Le format PIC — Position Independant Code — est un code pouvant être exécuté
à n’importe quelle adresse.
58
2.6.4
la table PLT
La Procedure Linkage Table est une table qui contient une entrée par
appel de fonction dynamique. Cette table n’est pas modifiée au cours de
l’exécution du programme et se situe donc dans le segment non modifiable
de la mémoire. Chaque entrée est composée de trois instructions telles que
le montre l’exemple pour le programme Test :
décodage de la PLT
PLT0
804832C FF 35 5C 97
8048332 FF 25 60 97
8048338 00 00
804833A 00 00
fgets
804833C FF 25 64 97
8048342 68 00 00 00
8048347 E9 E0 FF FF
__libc_start_main
804834C FF 25 68 97
8048352 68 08 00 00
8048357 E9 D0 FF FF
printf
804835C FF 25 6C 97
8048362 68 10 00 00
8048367 E9 C0 FF FF
sqrt
804836C FF 25 70 97
8048372 68 18 00 00
8048377 E9 B0 FF FF
strcpy
804837C FF 25 74 97
8048382 68 20 00 00
8048387 E9 A0 FF FF
(null)
804838C 00 00
04 08
04 08
push 0804975C
jmp 08049760
add [eax], al
add [eax], al
04 08
00
FF
jmp 08049764
push 0x0
jmp -0x20
04 08
00
FF
jmp 08049768
push 0x8
jmp -0x30
04 08
00
FF
jmp 0804976C
push 0x10
jmp -0x40
04 08
00
FF
jmp 08049770
push 0x18
jmp -0x50
04 08
00
FF
jmp 08049774
push 0x20
jmp -0x60
add [eax], al
La première entrée de la table PLT (PLT0) correspond à une référence
particulière. Créée par l’éditeur de liens statique, elle pointe vers l’éditeur de
liens dynamique. Pour cela, elle renvoie vers la table GOT à l’index +4 puis
à l’index +8, ces index correspondant aux second et troisième mots 16 de la
table GOT.
Les trois instructions d’une entrée dans la table PLT sont les suivantes :
16. Le terme de mot est employé ici au sens de mot machine. En effet, les données
contenues dans la table sont codées en binaire et interprétables 4 octets par 4 octets.
59
– La première instruction correspond toujours à un jmp, à savoir un saut
à travers la table GOT. Il s’agit d’un saut indirect : le saut est effectué
vers l’adresse contenue dans l’entrée de la table GOT pointé par le
jump.
– La seconde instruction push empile une adresse qui sera utilisée ensuite
par l’éditeur de liens dynamique. Il s’agit d’une entrée dans la table de
relocalisation dynamique qui sera utilisée par la routine ld-resolve 17 .
– La troisième instruction jmp pointe vers la première entrée de la table
PLT. Cette instruction sert à appeler l’éditeur de liens dynamique.
Voici la table GOT du programme Test, avec en particulier les premiers
mots de cette table :
décodage de la GOT
adresse
8049758
804975C
8049760
8049764
8049768
804976C
8049770
8049774
8049778
adresse pointée
08049674
00000000
00000000
08048342
08048352
08048362
08048372
08048382
00000000
Si les mots pointent tous vers des entrées dans la table PLT, les trois premiers mots en revanche diffèrent : ils contiennent respectivement l’adresse
de la section .dynamic, l’adresse de ld-resolve et l’adresse de la structure
link_map de l’objet courant [VR03]. link_map est une table qui contient
les noms des librairies requises et les noms de tous les symboles situés dans
ces librairies. Ainsi, cette table est utilisée par l’éditeur de liens dynamique
lorsqu’il résoud un appel de fonction : il consulte cette table pour trouver
la librairie dans laquelle ce symbole est référencé et consulte ensuite cette
librairie pour déterminer l’adresse du symbole [ano02]. Les second et troisième mots de la table sont mis à jour lors du chargement en mémoire du
programme par le chargeur de programme, c’est pourquoi, dans l’exemple,
ils ont pour valeur 0, leur valeur n’étant déterminée que plus tard.
2.6.5
Résolution d’un appel dynamique
Une fois l’éditeur de liens dynamique chargé en mémoire, celui-ci recherche par l’intermédiaire de la section .dynamic le nom des librairies dy17. Cette routine de relocalisation à la volée est appelée par l’éditeur de liens dynamique
pour résoudre les appels externes
60
namiques auquel le programme fait appel.
Les références aux données externes sont résolues par redirection vers la
table GOT. Les appels vers des fonctions externes pour leur part passent
par la table PLT et la table GOT et suivent le principe du “lazy symbol
binding” : ces appels ne sont pas résolus lors du chargement du programme
mais au cours de son exécution, au moment de l’appel de la fonction. Ainsi,
les références aux fonctions qui ne sont effectivement pas appelées au cours
de l’exécution ne sont pas résolues, ceci dans un souci de gain de temps.
Figure 2.9 – Pointeurs entre la table PLT et la table GOT
Lors d’un appel de fonction dynamique, on distingue deux cas : soit la
61
fonction n’a pas encore été appelée dans le programme, soit la fonction a
déjà été appelée. La procédure de résolution suit le schéma de la figure 2.9
détaillé comme-suit :
1. Les appels vers des fonctions externes de type dynamique pointent vers
une entrée dans la table PLT.
2. Chaque entrée de la table PLT renvoie à une entrée dans la table GOT.
3. La table GOT, lors de l’appel initial à la fonction, renvoie à la position
suivante dans la table PLT.
4. Cette position contient un décalage qui correspond à une entrée dans
la section des informations relocalisables. L’instruction pushl met ce
décalage sur la pile.
5. L’instruction suivante est exécutée. Il s’agit d’un saut vers l’entrée 0
de la table PLT : PLT0.
6. Cette entrée spécifie de mettre le second mot de la table GOT
(GOT+4) sur la pile.
7. PLT0 fait ensuite un saut vers la table GOT à l’offset +8. Celui-ci
contient l’adresse de la table link_map.
8. L’éditeur de liens dynamique prend alors le contrôle de l’éxécution.
9. Il lit les instructions restées sur la pile. L’entrée dans la table des informations de relocalisation contient un index dans la table des symboles
dynamique qui va renseigner l’éditeur de liens dynamique sur le nom
du symbole et permettre entre autres à celui-ci de résoudre l’adresse
de la fonction.
10. Une fois ce calcul effectué, l’éditeur de liens met à jour la table GOT
et la table des symboles dynamiques. Ainsi, la table GOT contient à
présent l’adresse de la fonction externe.
Lors des appels suivants à cette même fonction, la résolution a déjà été
effectuée comme le montre le schéma 2.10. La table PLT pointe vers la table
GOT qui renvoie alors directement à l’adresse de la fonction appelée.
Un fichier exécutable au format ELF est donc constitué d’un en-tête,
d’une table d’en-têtes de section optionnelle et d’une table d’en-tête de programme. Ces en-têtes permettent d’accéder aux différentes parties du fichier :
les en-têtes de section permettent d’accéder aux sections et les en-têtes de
programme aux segments. Un fichier au format ELF est donc structuré de
manière à accéder à la totalité de son contenu. graphELF utilise ainsi toutes
les structures du fichier ELF pour accéder à son contenu et l’analyser ensuite
de manière adéquate.
62
Figure 2.10 – Appel suivants à une fonction externe
Chapitre 3
Présentation de graphELF
Ce chapitre décrit le programme graphELF. La partie 3.1 présente les outils utilisés par graphELF ainsi que les limites d’utilisation du programme.
La partie 3.2 décrit les différentes parties du programme et son organisation.
Puis les parties 3.3 à 3.6 détaillent les différentes phases d’analyse de graphELF et leurs algorithmes. La partie 3.7 décrit la librairie de désassemblage
utilisée par graphELF et la partie 3.8 donne des précisions sur la construction
des différents graphes.
La partie 3.9 compare graphELF aux outils travaillant sur des fichiers
binaires et explique ce qu’apporte graphELF.
3.1
Contraintes techniques
graphELF est un outil d’analyse de fichier binaire. Ecrit en langage C,
il analyse les fichiers au format ELF uniquement. Dans le cadre de nos recherches, notre outil a été implémenté sur une plate-forme Linux Suse [Sus04]
et ne prend en compte actuellement que les fichiers ELF au format 32 bits.
De plus, graphELF pour parvenir à analyser le code passe par un processus de désassemblage. Cette étape est étroitement liée au processeur exploité.
Pour notre étude, nous analysons des fichiers compilés pour des processeurs
Intel i386. Pour exploiter le code, nous utilisons une librairie dynamique de
désassemblage : libdisasm [lib04]. Cette librairie désassemble le code pour
architecture Intel.
Le compilateur utilisé pour les fichiers exécutables est également un paramètre incontournable. Actuellement, le compilateur auquel se réfère notre
outil est gcc [Fou04]. Il s’agit du compilateur le plus couramment utilisé sur
les plates-formes Linux.
Les graphes sont construits par graphviz [Res00]. graphELF analyse le
code désasssemblé et écrit les données adéquates dans un fichier .dot qui
63
64
est ensuite exploitable par tous les outils développés pour graphviz : dotty
pour une visualisation rapide par exemple.
3.2
Présentation générale de graphELF
graphELF procède en plusieurs phases pour analyser un fichier binaire.
Il vérifie tout d’abord que le fichier correspond bien à un format qu’il peut
analyser. Pour cela, il récupère les premiers octets du fichier et essaie de
lire l’en-tête de fichier. S’il y parvient il vérifie alors que le fichier est bien
un fichier exécutable au format ELF, pour architecture Intel. Dans ce cas
seulement, il peut commencer la première phase d’analyse.
Cette première phase consiste à lire dans le fichier soit les en-têtes de
section si elles sont présentes soit les en-têtes de programme. graphELF va
ensuite procéder à une lecture complète du fichier binaire afin d’accéder à
tous les élements qui le composent. Pour cela, il utilise de préférence la table
des en-têtes de section. Lorsque celle-ci est présente, il accède aux différentes sections du fichier pour récupérer les données. Dans le cas contraire,
graphELF utilise les segments pour récupérer les mêmes informations. Cependant, cela nécessite des calculs supplémentaires (voir partie 3.4.2). Une
fois toutes ces informations récupérées, graphELF construit un fichier de
données.
La phase suivante de fonctionnement de graphELF consiste à analyser le
code pour déterminer et construire le graphe des appels de fonction. Pour
cela, graphELF détermine tout d’abord si la table des symboles statiques est
encore présente dans le fichier. Si tel est le cas, l’analyse se base sur cette
table pour déterminer les fonctions, leur nom et leurs adresses de début et
de fin. Dans le cas où la table des symboles est absente, graphELF analyse
une première fois le code assembleur pour déterminer les adresses de début
et de fin de chaque fonction. Une fois ces informations recueillies, il analyse
tour à tour les fonctions trouvées pour construire le graphe des appels de
fonctions.
La phase suivante consiste à analyser le code de chaque fonction pour
en déterminer le flot de contrôle. graphELF effectue une lecture du code et
répertorie les instructions de sauts et les instructions ret. Il analyse ensuite
les adresses pointées par ces sauts et construit une liste chaînée représentant
le squelette de la fonction. Muni de ces informations, graphELF remplit alors
un fichier avec les instructions dot correspondantes qui serviront ensuite à
construire les graphes. Au cours de cette dernière phase graphELF répertorie
les fonctions suspectes (voir section 4.1.2) et écrit dans le fichier de données
le code désassemblé de chaque fonction.
65
3.2.1
Les options de graphELF
graphELF possède un menu qui permet d’effectuer un certain nombre de
choix.
-n : Lorsque cette option est active, le graphe est construit avec les noms
des fonctions. En cas d’impossibilité, lorsque la table des symboles est
absente, le graphe sera construit avec les adresses.
-a : Lorsque cette option est active, le graphe est construit avec les adresses
des fonctions uniquement, sans tenir compte de la table des symboles.
Lorsqu’aucune des deux options n’est précisée, les deux graphes sont
contruits dans la mesure du possible.
-i : Lorsque cette option est active, les appels aux fonctions externes dynamiques sont absentes du graphe. Par défaut, ces appels sont intégrés
dans le graphe.
-p : Cette option permet de faire une mise en page du graphe pour en
permettre une impression.
-d : Cette option permet d’écrire le code désassemblé de chaque fonction
dans le fichier de données.
-f : Lorsque cette option est active, les fonctions standard d’initialisation
et de fin de programme sont prises en compte.
-g : Lorsque cette option est active, les graphes de flot de contrôle de
chaque fonction sont construits mais pas les graphes contenant le code
désassemblé.
-c : Cette option est l’option contraire à la précédente. Elle permet de
construire uniquement les graphes contenant le code désassemblé de
chaque fonction.
Lorsqu’aucune de ces deux options n’est précisée, les deux type de graphes
sont contruits.
Par défaut, graphELF construit tous les graphes possibles et pratique
toutes les analyses possibles.
3.2.2
Organisation du programme
Le programme se décompose en plusieurs phases qui sont détaillées dans
les parties suivantes et dont les appels sont représentés dans le graphe général
des appels de fonction sur la page suivante :
– Lecture du fichier exécutable en passant par les sections ou par les
segments (cf. partie 3.4) ; appel de la fonction lecture_elf.
– Ecriture des données dans le fichier de données ; appel de la fonction
ecriture_fichier_infos_elf.
66
– Analyse du code en passant par la table des symboles (cf. partie 3.5.1) ;
appel de la fonction lecture_par-nom.
– Analyse du code en passant par les adresses (cf. partie 3.5.2) ; appel
de la fonction lecture_par_adresse.
– Génération du fichier dot pour la construction du graphe des appels
de fonctions (cf. partie 3.8).
– Analyse du code de chaque fonction pour le découpage en blocs (cf.
partie 3.6).
– Génération des fichiers dot pour la construction des graphes (cf. partie
3.8).
Le graphe qui suit a été généré par graphELF exécuté sur lui-même.
67
Figure 3.1 – Graphe des appels de fonctions du graphELF
call graphe format A3.
68
verso
69
3.3
Fonction de début de graphELF
main
{
1- TANTQUE (parcours de la liste des options):
2- switch(option)
positionnement_options;
3- POUR(parcours de la liste des fichiers)
4- ouvrir_fichier;
5- lecture_ELF;
6- ecriture_infos;
7- SI(option_nom == 1)
7.1- SI(option_non_valide)
option_addr = 1;
7.2- SINON
lecture_par_nom;
8- SI(option_addr == 1)
lecture_par_adresse;
9- fermer_fichier;
}
Figure 3.2 – Algorithme général de graphELF
La figure 3.2 nous décrit l’algorithme général de graphELF :
– Etape 1 et Etape 2 : Les options passées en paramètre sont analysées.
Après vérification que chacune des options est correcte, les éventuelles
options par défaut sont positionnées.
– Etape 3 : On peut passer plusieurs fichiers en arguments. Chacun
d’eux sera traité à tour de rôle, indépendemment l’un de l’autre et
génèrera ses propres graphes.
– Etape 4 : Le fichier binaire ELF est ouvert en lecture.
– Etape 5 : Les informations du fichier sont stockées dans les structures
correspondantes. Les informations sont récupérées en passant soit par
les sections soit par les segments (cf. partie 3.4). L’en-tête est tout
d’abord analysé pour vérifier que le fichier est bien un fichier exécutable
au format ELF.
– Etape 6 : Les informations recueillies précédemment sont écrites dans
le fichier des données.
– Etape 7, Etape 7.1 et Etape 7.2 : Pour générer le graphe des appels
de fonction avec les noms, la table des symboles doit être nécessairement présente. Si cela n’est pas le cas, le graphe sera généré tout de
même mais avec les adresses.
– Etape 8 : Le graphe des appels de fonction avec les adresses peut
70
toujours être généré puisqu’il suffit d’avoir accès à la section.text, ce
qui est toujours possible.
– Etape 9 : Le fichier ELF en cours de lecture est fermé et la mémoire est
libérée en vue du traitement du prochain fichier ou pour la fermeture
du programme.
3.4
Phase de lecture du fichier ELF
La lecture du fichier constitue la première étape du programme. Elle
permet de récupérer les informations nécessaires à l’analyse du programme
mais également de vérifier que le fichier passé en argument est bien de format
ELF.
3.4.1
Structure de stockage des informations
Les informations sont stockées dans une structure globale définie pour
graphELF dans le fichier declaration.h. Cette structure regroupe les structures prédéfinies du format ELF. Elle rend le contenu du fichier accessible
sans effectuer de relecture de celui-ci systématiquement et reflète la façon
dont le fichier exécutable est structuré.
Cette structure comporte les champs suivants :
typedef struct fichier_elf
{
/* infos generales sur le fichier ELF*/
int
fd;
char
*name;
/* header ELF*/
Elf32_Ehdr
*header;
Elf32_Shdr
*table_section;
Elf32_Phdr
*table_program;
/* table des noms de section*/
char
*table_shstr;
int
shstr_size;
/* table des noms */
char
*table_str;
int
str_size;
71
/* table des symboles*/
Elf32_Sym
*table_sym;
int
sym_size;
/* section text*/
unsigned char *text;
int
text_size;
int
num_section_text;
Elf32_Addr
addr_text;
/*section rodata*/
char
*rodata;
int
rodata_size;
Elf32_Addr
adr_rodata;
char
int
Elf32_Addr
*fin_segment_text;
fin_segment_size;
adr_deb_segment;
/* table des noms dynamique*/
char
*table_str_dyn;
int
str_size_dyn;
/* table des symboles dynamique*/
Elf32_Sym
*table_sym_dyn;
int
sym_size_dyn;
/*structure de la section .dynamic*/
/* contient les infos pour retrouver les elements dynamiques*/
Elf32_Dyn
*dynamic;
Elf32_Word
dynamic_size;
/* tableau de la section .gnu.version */
/* lien entre les fonctions et leur librairie */
Elf32_Half
*tab_index_lib;
Elf32_Word
tab_index_size;
Elf32_Word
*tab_offset_dyn;
/* section plt = procedure linkage table*/
unsigned char *plt;
int
plt_size;
int
num_section_plt;
Elf32_Addr
addr_plt;
72
/*section got = global offset table*/
Elf32_Addr
*got;
int
got_size;
int
num_section_got;
Elf32_Addr
addr_got;
} objet_elf ;
Voici la définition des principaux champs de cette structure :
– fd : pointeur de fichier pour parcourir celui-ci.
– name : nom du fichier.
– Elf32_Ehdr *header : pointeur sur la structure contenant l’en-tête du
fichier.
– Elf32_Shdr *table_section : pointeur sur le tableau de structures
des en-têtes de section.
– Elf32_Phdr *table_program : pointeur sur le tableau de structures
des en-têtes de programme.
– table_shstr : table des noms de section.
– table_str : table des noms.
– Elf32_Sym *table_sym : table des symboles.
– text : contenu de la section .text.
– Elf32_Addr addr_text : adresse de début de la section .text.
– char *rodata : contenu de la section .rodata. Ce champ ainsi que les
deux champs suivants ne peuvent être remplis que lorsque la table de
en-têtes de section est présente.
– Elf32_Addr adr_rodata : adresse de début de la section .rodata.
– char *fin_segment_text : buffer servant à stocker le contenu du fichier allant de la section .fini jusqu’à la fin du segment text. Ce
buffer remplace celui contenant la section .rodata lorsque la table des
en-têtes de section est absente, ainsi que les deux champs suivants.
– Elf32_Addr adr_deb_segment : adresse de début du buffer.
– table_str_dyn : table des noms dynamiques.
– Elf32_Sym *table_sym_dyn : table des symboles dynamiques.
– Elf32_Dyn *dynamic : contenu de la section .dynamic.
– Elf32_Half *tab_index_lib : tableau d’index reliant les fonctions
à leur librairire.
– plt : contenu de la section .plt. Contient la table PLT.
– Elf32_Addr addr_plt : adresse de la section .plt.
– Elf32_Addr *got : contenu de la section .got. Contient la table GOT.
– Elf32_Addr addr_got : adresse de la section .got.
73
3.4.2
Algorithmes de lecture du fichier
Les champs de la structure sont remplis successivement lors des appels des
différentes fonctions. Les algorithmes de lecture des informations du fichier
sont décrits dans les figures 3.3, 3.4 et 3.5.
lecture_ELF()
{
1- initialisation_champs_struct;
2- lecture_header;
3- test_elf;
4- lecture_section_header;
5- lecture_shstr_table;
6- lecture_program_header;
7- SI en-tête de section
lecture_infos_par_sections;
8- SINON lecture_infos_par_segments;
}
Figure 3.3 – Algorithme principal de lecture du fichier ELF
L’algorithme principal (figure 3.3) comporte 8 étapes :
– Etape 1 : Initialisation de tous les champs de la structure objet à 0
ou à NULL.
– Etape 2 : Lecture des premiers octets du fichier et remplissage de la
structure contenant l’en-tête du fichier ELF.
– Etape 3 : Test de vérification. Validation des champs e_type et
e_ident pour s’assurer qu’il s’agit d’un fichier ELF exécutable.
– Etape 4 : Remplissage de la table des en-têtes de section.
– Etape 5 : Remplissage de la table des noms de section.
– Etape 6 : Remplissage de la table des en-têtes de programme.
– Etape 7 : Si la table des en-têtes de section a été récupérée, appel de
la fonction qui va parcourir le fichier selon les sections.
– Etape 8 : Sinon, la table des en-têtes de section est absente et appel
de la fonction de lecture du fichier par les segments.
Si la table des en-têtes de section est présente, le fichier est lu section par
section et les différents champs de la structure objet_elf sont remplis au
fur et à mesure. graphELF exécute l’algorithme suivant (donné dans 3.4) :
– Etape 1 : Parcours de la table des en-têtes de section pour récupérer
les adresses de chaque section et les traiter.
– Etape 2 : Remplissage de la table des noms si elle est présente.
– Etape 3 : Remplissage de la table des symboles si elle est présente.
– Etape 4 : Remplissage du buffer qui contient la section text.
– Etape 5 : Remplissage de la table des noms dynamiques.
74
lecture_infos_par_sections()
{
1- POUR (parcours table des en-tetes de section)
2- lecture str_table;
3- lecture sym_table;
4- lecture text;
5- lecture str_dyn_table;
6- lecture sym_dyn_table;
7- lecture plt;
8- lecture got;
9- lecture dynamic;
10- lecture index_librairie;
11- lecture rodata;
}
Figure 3.4 – Algorithme de lecture des sections du fichier ELF
–
–
–
–
Etape 6 : Remplissage de la table des symboles dynamiques.
Etape 7 : Remplissage du buffer qui contient la section .plt.
Etape 8 : Remplissage du buffer qui contient la section .got.
Etape 9 : Remplissage de la structure qui contient les informations
de la section .dynamic.
– Etape 10 : Remplissage du tableau qui contient les index référençant
les librairies. Cette table met en relation les fonctions avec les librairies
qui les définissent.
– Etape 11 : Remplissage du buffer qui contient la section .rodata.
lecture_infos_par_segments()
{
1- POUR (parcours table des en-tetes de programme)
2- lecture point d’entrée;
3- lecture segment dynamic;
4- POUR (parcours segment dynamic)
5- calcul offsets tables;
6- lecture text;
7- lecture sym_dyn_table;
8- lecture str_dyn_table;
9- lecture index_librairie;
10- lecture rodata;
}
Figure 3.5 – Algorithme de lecture des segments du fichier ELF
75
Si en revanche, la table des en-têtes de section est absente alors le fichier
est lu selon les segments qu’il contient (figure 3.5) :
– Etape 1 : Parcours de la table des en-têtes de programme.
– Etape 2 : Récupération des adresses de début et fin du segment text
afin de déterminer l’emplacement dans le fichier de la section .text.
– Etape 3 : Récupération des informations situant le segment dynamic
qui contient la section .dynamic.
– Etape 4 : Parcours de la section .dynamic afin de récupérer différentes
adresses.
– Etape 5 : A partir des adresses récupérées, calcul des offsets dans le
fichier des différentes sections.
– Etape 6 : Remplissage du buffer qui contient la section .text.
L’adresse de début de cette section correspond à l’adresse du point
d’entrée du programme et l’adresse de fin à celle de début de la section
.fini 1 .
– Etape 7 : Remplissage de la table des symboles dynamiques. L’adresse
de début de cette table est donnée dans la section .dynamic. En revanche, l’adresse de fin est calculée en fonction de sa position dans le
segment data. Cette table est située avant la table des noms dynamiques lorsque l’on travaille sur plate-forme Linux avec le compilateur
gcc.
– Etape 8 : Remplissage de la table des noms dynamiques.
– Etape 9 : Remplissage du tableau d’index des librairies.
– Etape 10 : Remplissage du buffer contenant la section .rodata. Dans
ce cas, le calcul des adresses de début et fin de cette section ne sont
pas précis. L’adresse de début correspond à l’adresse de fin du buffer
text et l’adresse de fin à celle de fin du segment text. Ainsi, non seulement la section .rodata sera bien enregistrée mais le buffer contiendra
également la setion .fini.
Une fois les champs de la structure objet_elf remplis, graphELF rempli
un fichier de données qui contient toutes les informations précédemment
recueillies comme l’illustre l’exemple ci-après.
Puis, graphELF passe à la phase suivante de la construction du graphe
des appels : soit par la table des symboles (cf. partie 3.5.1) ou soit par les
adresses des fonctions (cf. partie 3.5.2).
3.4.3
Exemple d’application avec le fichier gencode : le
contenu du fichier ELF
Afin d’illustrer le fonctionnement de graphELF nous prenons pour
exemple le programme gencode dont voici le fichier source :
1. Cette adresse de fin est liée à la disposition des sections dans les segments. Cependant, ces positions peuvent varier selon les compilateurs et les plates-formes.
76
#include <time.h>
main(){
int icode,icar,car,lettre;
f1();
srandom((int) time(NULL));
for (icode = 0; icode < 3; icode++) {
lettre=0;
printf("Code %d = ", icode+1);
for (icar = 0; icar < 5; icar++) {
do {
car = random() % 12;
if ( car > 9 ) lettre++;
} while (car > 9 && lettre > 1);
printf(car == 10 ? "A" : car == 11 ? "B" : "%d", car);
}
printf("\n");
f2();
}
}
f1(){
printf("le resultat est en cours\n");
}
f2(){
printf("le resultat est trouve\n");
}
Lors de la première phase d’analyse du fichier, graphELF construit un
fichier de données dont voici le contenu pour le programme gencode :
Partie I , informations ELF
entete du fichier
type du fichier : EXEC (Executable file)
type de la machine : Intel 80386
77
les entetes de section
nombre d’entree 35
index
section
[ 0]
[ 1]
.interp
[ 2]
.note.ABI-tag
[ 3]
.hash
[ 4]
.dynsym
[ 5]
.dynstr
[ 6]
.gnu.version
[ 7]
.gnu.version_r
[ 8]
.rel.dyn
[ 9]
.rel.plt
[10]
.init
[11]
.plt
[12]
.text
[13]
.fini
[14]
.rodata
[15]
.data
[16]
.eh_frame
[17]
.dynamic
[18]
.ctors
[19]
.dtors
[20]
.jcr
[21]
.got
[22]
.bss
[23]
.comment
[24]
.debug_aranges
[25]
.debug_pubnames
[26]
.debug_info
[27]
.debug_abbrev
[28]
.debug_line
[29]
.debug_frame
[30]
.debug_str
[31]
.debug_ranges
[32]
.shstrtab
[33]
.symtab
[34]
.strtab
type
NULL
PROGBITS
NOTE
HASH
DYNSYM
STRTAB
VERSYM
VERNEED
REL
REL
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
DYNAMIC
PROGBITS
PROGBITS
PROGBITS
PROGBITS
NOBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
PROGBITS
STRTAB
SYMTAB
STRTAB
adresse
0
80480f4
8048108
8048128
804815c
80481dc
8048236
8048248
8048268
8048270
8048298
80482b0
8048310
80485f4
8048610
8049660
804966c
8049670
8049738
8049740
8049748
804974c
8049770
0
0
0
0
0
0
0
0
0
0
0
0
taille
0
19
32
52
128
89
16
32
8
40
23
96
740
26
78
12
4
200
8
8
4
36
4
217
152
95
768
285
545
88
285
24
313
1840
916
78
les entetes de programme
nombre d’entree 6
index
type
adresse
[ 0]
PHDR
8048034
[ 1]
INTERP
80480f4
[ 2]
LOAD
8048000
[ 3]
LOAD
8049660
[ 4]
DYNAMIC
8049670
[ 5]
NOTE
8048108
t_fichier
0x00000c0
0x0000013
0x000065e
0x0000110
0x00000c8
0x0000020
t_memoire
0x00000c0
0x0000013
0x000065e
0x0000114
0x00000c8
0x0000020
protection
R E
R
R E
RW
RW
R
les sections dans les segments
segment 0
segment 1
.interp
segment 2
.interp
.note.ABI-tag
.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_r
.rel.dyn
.rel.plt
.init
.plt
.text
.fini
.rodata
segment 3
.data
.eh_frame
.dynamic
.ctors
.dtors
.jcr
.got
segment 4
.dynamic
segment 5
.note.ABI-tag
éléments de la section dynamique
type
nom/valeur
NEEDED
1
INIT
134513304
FINI
134514164
HASH
134512936
STRTAB
134513116
SYMTAB
134512988
STRSZ
89
SYMENT
16
DEBUG
0
PLTGOT
134518604
PLTRELSZ
40
PLTREL
17
JMPREL
134513264
REL
134513256
RELSZ
8
RELENT
8
VERNEED
134513224
VERNEEDNUM
1
VERSYM
134513206
NULL
0
NULL
0
NULL
0
NULL
0
NULL
0
NULL
0
79
la table des noms
nom
nom <command line>
nom /usr/src/packages/BUILD/glibc-2.3/cc/config.h
nom <built-in>
nom abi-note.S
nom /usr/src/packages/BUILD/glibc-2.3/cc/csu/abi-tag.h
nom init.c
nom /usr/src/packages/BUILD/glibc-2.3/cc/csu/crti.S
nom /usr/src/packages/BUILD/glibc-2.3/cc/csu/defs.h
nom initfini.c
nom call_gmon_start
nom crtstuff.c
nom __CTOR_LIST__
nom __DTOR_LIST__
nom __JCR_LIST__
nom p.0
nom completed.1
nom __do_global_dtors_aux
nom frame_dummy
nom __CTOR_END__
nom __DTOR_END__
nom __FRAME_END__
nom __JCR_END__
nom __do_global_ctors_aux
nom /usr/src/packages/BUILD/glibc-2.3/cc/csu/crtn.S
nom gencode.c
nom elf-init.c
nom _DYNAMIC
nom _fp_hw
nom __fini_array_end
nom __dso_handle
nom __libc_csu_fini
nom random@@GLIBC_2.0
nom _init
nom f2
nom time@@GLIBC_2.0
nom _start
nom __fini_array_start
nom __libc_csu_init
nom __bss_start
nom main
nom __libc_start_main@@GLIBC_2.0
nom __init_array_end
nom data_start
nom printf@@GLIBC_2.0
nom _fini
nom f1
nom _edata
nom __i686.get_pc_thunk.bx
nom _GLOBAL_OFFSET_TABLE_
nom _end
nom __init_array_start
nom _IO_stdin_used
nom srandom@@GLIBC_2.0
nom __data_start
nom _Jv_RegisterClasses
nom __gmon_start__
80
la table des symboles
Num:
Valeur Taille
0:
0
0
1:
80480f4
0
2:
8048108
0
3:
8048128
0
4:
804815c
0
5:
80481dc
0
6:
8048236
0
7:
8048248
0
8:
8048268
0
9:
8048270
0
10:
8048298
0
11:
80482b0
0
12:
8048310
0
13:
80485f4
0
14:
8048610
0
15:
8049660
0
16:
804966c
0
17:
8049670
0
18:
8049738
0
19:
8049740
0
20:
8049748
0
21:
804974c
0
22:
8049770
0
23:
0
0
24:
0
0
25:
0
0
26:
0
0
27:
0
0
28:
0
0
29:
0
0
30:
0
0
31:
0
0
32:
0
0
33:
0
0
34:
0
0
35:
0
0
36:
0
0
Type
NOTYPE
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
SECTION
FILE
FILE
Lien
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
37:
38:
39:
40:
0
0
0
0
0
0
0
0
FILE
FILE
FILE
FILE
LOCAL
LOCAL
LOCAL
LOCAL
41:
42:
0
0
0
0
FILE
FILE
LOCAL
LOCAL
43:
44:
45:
0
0
0
0
0
0
FILE
FILE
FILE
LOCAL
LOCAL
LOCAL
46:
47:
48:
49:
50:
0
0
0
0
0
0
0
0
0
0
FILE
FILE
FILE
FILE
FILE
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
Ndx Nom
UND
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
ABS
<command line>
ABS /usr/src/packages/BUILD/glibc-2.3/
cc/config.h
ABS
<command line>
ABS
<built-in>
ABS
abi-note.S
ABS /usr/src/packages/BUILD/glibc-2.3/
cc/csu/abi-tag.h
ABS
abi-note.S
ABS /usr/src/packages/BUILD/glibc-2.3/
cc/config.h
ABS
abi-note.S
ABS
<command line>
ABS /usr/src/packages/BUILD/glibc-2.3/
cc/config.h
ABS
<command line>
ABS
<built-in>
ABS
abi-note.S
ABS
init.c
ABS /usr/src/packages/BUILD/glibc-2.3/
cc/csu/crti.S
81
51:
0
0
FILE
LOCAL
52:
53:
0
0
0
0
FILE
FILE
LOCAL
LOCAL
54:
55:
0
0
0
0
FILE
FILE
LOCAL
LOCAL
56:
57:
58:
0
0
0
0
0
0
FILE
FILE
FILE
LOCAL
LOCAL
LOCAL
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
8048334
0
8049738
8049740
8049748
8049668
8049770
8048360
80483a0
0
804973c
8049744
804966c
8049748
80485d0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
0
FUNC
FILE
OBJECT
OBJECT
OBJECT
OBJECT
OBJECT
FUNC
FUNC
FILE
OBJECT
OBJECT
OBJECT
OBJECT
FUNC
FILE
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
75:
0
0
FILE
LOCAL
76:
77:
0
0
0
0
FILE
FILE
LOCAL
LOCAL
78:
79:
0
0
0
0
FILE
FILE
LOCAL
LOCAL
80:
81:
82:
0
0
0
0
0
0
FILE
FILE
FILE
LOCAL
LOCAL
LOCAL
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
0
0
8049670
8048610
8049660
8049664
8048560
80482c0
8048298
80484df
80482d0
8048310
8049660
8048500
8049770
80483cc
80482e0
0
0
0
4
0
0
60
5e
0
18
10
0
0
58
0
fb
fb
FILE
FILE
OBJECT
OBJECT
NOTYPE
OBJECT
FUNC
FUNC
FUNC
FUNC
FUNC
FUNC
NOTYPE
FUNC
NOTYPE
FUNC
FUNC
LOCAL
LOCAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
ABS /usr/src/packages/BUILD/glibc-2.3/
cc/csu/defs.h
ABS
initfini.c
ABS /usr/src/packages/BUILD/glibc-2.3/
cc/csu/crti.S
ABS
<command line>
ABS /usr/src/packages/BUILD/glibc-2.3/
cc/config.h
ABS
<command line>
ABS
<built-in>
ABS /usr/src/packages/BUILD/glibc-2.3/
cc/csu/crti.S
12
call_gmon_start
ABS
crtstuff.c
18
__CTOR_LIST__
19
__DTOR_LIST__
20
__JCR_LIST__
15
p.0
22
completed.1
12
__do_global_dtors_aux
12
frame_dummy
ABS
crtstuff.c
18
__CTOR_END__
19
__DTOR_END__
16
__FRAME_END__
20
__JCR_END__
12
__do_global_ctors_aux
ABS /usr/src/packages/BUILD/glibc-2.3/
cc/csu/crtn.S
ABS /usr/src/packages/BUILD/glibc-2.3/
cc/csu/defs.h
ABS
initfini.c
ABS /usr/src/packages/BUILD/glibc-2.3/
cc/csu/crtn.S
ABS
<command line>
ABS /usr/src/packages/BUILD/glibc-2.3/
cc/config.h
ABS
<command line>
ABS
<built-in>
ABS /usr/src/packages/BUILD/glibc-2.3/
cc/csu/crtn.S
ABS
gencode.c
ABS
elf-init.c
17
_DYNAMIC
14
_fp_hw
ABS
__fini_array_end
15
__dso_handle
12
__libc_csu_fini
UND
random@@GLIBC_2.0
10
_init
12
f2
UND
time@@GLIBC_2.0
12
_start
ABS
__fini_array_start
12
__libc_csu_init
ABS
__bss_start
12
main
UND
__libc_start_main@@GLIBC_2.0
82
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
8049660
8049660
80482f0
80485f4
80484c7
8049770
80485c0
804974c
8049774
8049660
8048614
8048300
8049660
0
0
0
0
36
0
18
0
0
0
0
0
4
5b
0
0
0
NOTYPE
NOTYPE
FUNC
FUNC
FUNC
NOTYPE
FUNC
OBJECT
NOTYPE
NOTYPE
OBJECT
FUNC
NOTYPE
NOTYPE
NOTYPE
GLOBAL
WEAK
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
WEAK
WEAK
ABS
15
UND
13
12
ABS
12
21
ABS
ABS
14
UND
15
UND
UND
__init_array_end
data_start
printf@@GLIBC_2.0
_fini
f1
_edata
__i686.get_pc_thunk.bx
_GLOBAL_OFFSET_TABLE_
_end
__init_array_start
_IO_stdin_used
srandom@@GLIBC_2.0
__data_start
_Jv_RegisterClasses
__gmon_start__
la table des noms dynamique
nom
nom libc.so.6
nom printf
nom time
nom srandom
nom _IO_stdin_used
nom __libc_start_main
nom __gmon_start__
nom GLIBC_2.0
la table des symboles dynamique
Num:
Valeur Taille
Type
0:
0
0
NOTYPE
1:
80482c0
5e
FUNC
2:
80482d0
10
FUNC
3:
80482e0
fb
FUNC
4:
80482f0
36
FUNC
5:
8048614
4
OBJECT
6:
8048300
5b
FUNC
7:
0
0
NOTYPE
les librairies requises
1
libc.so.6
Lien
LOCAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
WEAK
Ndx
UND
UND
UND
UND
UND
14
UND
UND
Nom
random
time
__libc_start_main
printf
_IO_stdin_used
srandom
__gmon_start__
0
79
79
79
79
1
79
0
librairie
local
GLIBC_2.0
GLIBC_2.0
GLIBC_2.0
GLIBC_2.0
global
GLIBC_2.0
local
83
décodage de la PLT
80482B0 FF 35 50 97
80482B6 FF 25 54 97
80482BC 00 00
80482BE 00 00
random
80482C0 FF 25 58 97
80482C6 68 00 00 00
80482CB E9 E0 FF FF
time
80482D0 FF 25 5C 97
80482D6 68 08 00 00
80482DB E9 D0 FF FF
__libc_start_main
80482E0 FF 25 60 97
80482E6 68 10 00 00
80482EB E9 C0 FF FF
printf
80482F0 FF 25 64 97
80482F6 68 18 00 00
80482FB E9 B0 FF FF
srandom
8048300 FF 25 68 97
8048306 68 20 00 00
804830B E9 A0 FF FF
(null)
8048310 00 00
04 08
04 08
push
08049750
jmp 08049754
add [eax], al
add [eax], al
04 08
00
FF
jmp 08049758
push
0x0
jmp -0x20
04 08
00
FF
jmp 0804975C
push
0x8
jmp -0x30
04 08
00
FF
jmp 08049760
push
0x10
jmp -0x40
04 08
00
FF
jmp 08049764
push
0x18
jmp -0x50
04 08
00
FF
jmp 08049768
push
0x20
jmp -0x60
add [eax], al
décodage de la GOT
adresse adresse pointée
804974C 08049670
8049750 00000000
8049754 00000000
8049758 080482C6
804975C 080482D6
8049760 080482E6
8049764 080482F6
8049768 08048306
804976C 00000000
chaines de caractères
Code %d =
B
%d
A
le resultat est en cours
le resultat est trouve
84
3.5
Phase de construction des appels de fonctions
La deuxième phase de fonctionnement de graphELF consiste à partir du
code exécutable, à construire un graphe des appels de fonction.
grapheELF procède en deux temps. Tout d’abord il effectue une décomposition du code en fonctions puis il recherche pour chacune de ces fonctions
les instructions call exécutant les appels de fonctions.
Nous noterons ici que dans certains cas 2 , des instructions jump peuvent
pointer vers des adresses de fonction. Ces appels de fonctions ne sont pas
traités dans cette phase mais dans la suivante.
graphELF actuellement ignore également les appels indirects du type :
80499c5:
80499cb:
80499cd:
80499cf:
8b
85
74
ff
83 74 01 00 00
c0
02
d0
mov
test
je
call
0x174(%ebx),%eax
%eax,%eax
0x80499d1
*%eax
Ces appels feront l’objet d’une étude ultérieure. En effet ces appels indirects utilisent les registres et l’étude de ces derniers est envisagé comme une
des prochaines étapes de développement de graphELF (cf. partie 5).
3.5.1
Lecture du fichier en passant par les noms des fonctions
Cette option de lecture pas les noms des fonctions correspond à une lecture de la table des symboles et de la table des noms qui lui est associée.
Ces tables doivent donc être présentes dans le fichier exécutable pour permettre l’exécution de cette fonction, ce qui n’est pas obligatoire. En effet, la
commande strip, par exemple, appliquée à un fichier exécutable permet de
supprimer entre autre ces deux tables facultatives.
Lorsque graphELF peut utiliser la table des symboles, il parcourt cette
dernière à la recherche des noms et des adresses de début et de fin des
fonctions. Une fois ces trois éléments calculés, le code correspondant est
désassemblé pour y rechercher les instructions call.
3.5.1.1
Algorithme de lecture de la table des symboles
La fonction lecture_par_nom exécute ce travail. L’algorithme est décrit
dans la figure 3.6. Il se décompose en 12 étapes décrites ainsi :
– Etape 1 : création de listes de fonctions contenant :
– les fonctions standard ajoutées par le compilateur.
2. Il s’agit de cas d’optimisation effectuée par le compilateur.
85
lecture_sections
{
1- construction listes_fonctions;
2- fd_dot=ouvrir_fichier_dot;
3- POUR(parcours de la table des symboles)
4- SI (index_section_symbole == num_section_text &&
type_symbole == STT_FUNC )
5- addr_fction = valeur_symbole;
nom_fonction = nom_symbole;
6- SI (NON_affichage_fonction_standard)
6.1- SI(fonction_trouvée == fn_standard)
6.1.1- STOP;
7- SI(portee_symbole == STB_GLOBAL)
7.1- ecriture_noeud_fichier_dot;
8- SINON
8.1- ecriture_noeud_fichier_dot;
9- recherche_instructions_call;
10- SI (option_desassemble)
10.1- desassemble_fonction;
11- ecriture_fin_fichier_dot;
12- pclose(fd_dot);
}
Figure 3.6 – Algorithme de lecture par les noms
–
–
–
–
–
–
– les fonctions susceptibles de créer un débordement de tampon.
– les fonctions pouvant être à l’origine de bogues de format.
Ces listes sont utilisées par la suite pour déterminer si un appel de
fonction est dangeureux ou non.
Etape 2 : Ouverture du fichier qui va contenir les informations nécessaires à la construction du graphe des appels. La section 3.8 détaille le
contenu du fichier généré ainsi que les fonctions y afférant.
Etape 3 : Parcours de la table des symboles.
Etape 4 : Si le symbole appartient à la section .text et si le symbole
est une fonction alors celui-ci correspond à une fonction définie dans
le fichier. Il est sélectionné pour les étapes suivantes.
Etape 5 : Récupération du nom du symbole, donc du nom de la fonction, ainsi que de l’adresse de celle-ci.
Etape 6 et Etape 6.1 et Etape 6.1.1 : Si l’option de ne pas écrire
les fonctions d’initialisation et de fin de programme est activée, alors le
nom de la fonction est comparé avec la première liste établie à l’étape
1 et, s’il s’agit d’une de ces fonctions : arrêt du traitement de celle-ci.
Etape 7 et Etape 7.1 : Si la fonction est externe — globale — alors
86
elle est renseignée comme telle dans le fichier dot.
– Etape 8 et Etape 8.1 : Sinon la fonction est inscrite comme static
— locale — dans le fichier dot.
– Etape 9 : Appel de la fonction qui parcourt la section .text pour
retrouver les éventuelles instructions call au sein de cette fonction et
remplir le fichier dot en conséquence, comme indiqué dans la figure 3.9
et expliqué dans la partie 3.5.3 ci-dessous.
– Etape 10 et Etape 10.1 : Si l’option de désassemblage est active,
appel de la fonction de désassemblage pour écrire les lignes de code
désassemblé de la fonction traitée dans le fichier de données.
– Etape 11 et Etape 12 : fermeture du fichier dot.
3.5.2
Lecture du fichier en passant par les adresses des fonctions
Cette partie du programme est mise en oeuvre en partant du principe
que les parties optionnelles d’un fichier exécutable ELF peuvent ne pas être
présentes. Dans le cas où la table des symboles et la table des noms sont
absentes, il faut travailler directement à partir de la section .text pour
chercher les différentes fonctions du programme. Cette partie est appelée
par la fonction lecture_par_adresse.
3.5.2.1
Algorithme de lecture par les adresses
Pour effectuer la recherche des fonctions du programme, quelques calculs
préliminaires sont effectués :
– recherche de l’adresse de la fonction main.
– recherche de l’adresse de fin du programme afin d’éliminer les fonctions
de fin de programme ajoutées par le compilateur.
Puis, un premier passage de désassemblage de la section .text est effectué à la recheche des fonctions. Lors de ce passage, la section .text est
découpée en blocs correspondant à chacune des fonctions du programme.
Pour déterminer les adresses de coupure — c’est-à-dire les adresses de début
de fonction ou de début de bloc — graphELF recherche deux éléments : les
instructions call et les instructions push ebp.
En effet, une instruction call sert à effectuer un appel de fonction. Elle
pointe donc vers une adresse qui correspond à un début de fonction, donc à
un début de bloc.
Les instructions push ebp ne sont exécutées que lors d’un prologue à
l’exéction d’une fonction et servent à sauvegarder le pointeur de pile (voir
partie 2.3.3). Il s’agit donc obligatoirement d’une instruction marquant le
début d’une fonction et donc un début de bloc.
87
lecture_par_adresse
{
1- fd_dot=ouvrir_fichier_dot;
2- recherche_debut_main;
3- recherche_fin_prog;
4- decoupage_blocs;
5- POUR (chaque bloc)
6- recherche_instructions_call;
7- ecriture_fin_fichier_dot;
8- fermeture_fichier_dot;
9- SI (option_disasm)
9.1- POUR (chaque bloc)
9.2- desassemble_fonction;
}
Figure 3.7 – Algorithme de parcours de la section text à la recherche des
fonctions
L’algorithme principal de parcours des segments est représenté dans la
figure 3.7 et se détaille de la sorte :
– Etape 1 : Ouverture du fichier dot contenant les informations nécessaires à la construction du graphe.
– Etape 2 : Recherche de l’adresse de début de la fonction main. Les
détails de cette recherche sont donnés dans le paragraphe suivant.
– Etape 3 : Recherche de l’adresse de fin du programme. Cette recherche
permet d’éliminer les fonctions ajoutées par le compilateur à la fin du
programme.
– Etape 4 : Découpage du code en fonctions. Cette opération permet
de déterminer les adresses de début et de fin de chaque fonction et de
découper le code en blocs d’instructions pour chaque fonction.
– Etape 5 : Parcours de tous les blocs un à un.
– Etape 6 : Recherche des instructions call pour chacune des fonctions
du programme, i.e. pour chacun des blocs de code.
– Etape 7 et Etape 8 : Fermeture du fichier dot.
– Etape 9, Etape 9.1 et Etape 9.2 : Si l’option de désassemblage est
active, alors parcours de chaque bloc, donc du code de chaque fonction
pour désassembler celle-ci et l’écrire dans le fichier de données.
3.5.2.2
Précisions sur la recherche de l’adresse de début de la
fonction main et sur celle de fin de programme
La fonction recherche_debut_main calcule la valeur de l’adresse de la
fonction main tandis que la fonction recherche_fin_prog calcule la valeur
88
de l’adresse de fin du code du programme. Ces deux adresses sont calculées
afin de réduire le champ de recherche des instructions call uniquement aux
fonctions écrites dans le programme source : les fonctions standard ajoutées
lors de la compilation ne sont donc pas prises en compte.
Pour y parvenir, nous utilisons les caractéristiques propres à la version
actuelle gcc utilisée sur une plate-forme Linux/Intel. Les premières instructions de la section .text suivent toujours le même schéma. L’exemple suivant
nous livre une représentation caractéristique de ces instructions :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
80482E0
80482E2
80482E3
80482E5
80482E8
80482E9
80482EA
80482EB
80482F0
80482F5
80482F6
80482F7
80482FC
8048301
8048302
31
5E
89
83
50
54
52
68
68
51
56
68
E8
F4
90
ED
E1
E4 F0
B0 84 04 08
50 84 04 08
9C 83 04 08
AF FF FF FF
xor ebp, ebp
pop esi
mov ecx, esp
and esp, 0xF0
push
eax
push
esp
push
edx
push
0x80484B0 $<.fini>
push
0x8048450 $<.init>
push
ecx
push
esi
push
0x804839C $<main>
call
-0x51
hlt
nop
La première instruction call du code exécutable correspond toujours
à un appel de la fonction dynamique libc_start et est immédiatement
suivie de l’instruction hlt qui signifie halt et qui détruit le processus courant.
La fonction libc_start correspond à un appel standard ajouté lors de la
génération de l’exécutable. Elle exécute un certain nombre d’instructions
d’initialisation du programme. L’instruction précédant cet appel est toujours
un push suivi d’une adresse. Cela signifie que l’adresse est mise sur la pile
du processus. Cette adresse correspond à l’adresse de début de la fonction
main. Pour récupérer celle-ci, il suffit donc de :
– désassembler le début du buffer jusqu’à y lire l’instruction call.
– L’instruction précédente correspond au push suivi de l’adresse cherchée.
– Cette adresse passée en argument de l’instruction push est par définition, sur un processeur Intel, une adresse directe (et non pas un
décalage).
– On récupère l’adresse codée sur 4 octets en petit endian par un décalage et une addition de ces octets.
De même, les instructions push des lignes 8 et 9 ont toujours pour argument les adresses init et fini. Cette dernière nous renseigne sur l’adresse
de début des fonctions standard de fin de programme ajoutées lors de la
89
compilation. En récupérant cette adresse selon le même principe que pour
l’adresse main, nous pouvons réduire le champ des recherches en éliminant
ces fonctions.
3.5.2.3
Algorithme de découpage par blocs
Le code du programme est tout d’abord vu comme un seul bloc, ne contenant qu’une fonction : la fonction main. Ce bloc — autrement dit cette fonction — a pour adresse de début, l’adresse de la fonction main, et pour adresse
de fin, l’adresse de fin de programme récupérée dans l’étape précédente. Ce
bloc est alors parcouru afin de déterminer s’il contient d’autres fonctions.
Dans ce cas, si l’adresse de la nouvelle fonction est contenue dans le bloc,
celui-ci est partagé pour séparer ces fonctions. En revanche, si l’adresse se
situe en dehors du bloc, un nouveau bloc est créé qui est ensuite parcouru
selon le même principe.
decoupage-blocs
{
1- calcul_position_buffer;
2- POUR (parcours du code de la fonction)
3- disassemble_address;
4- SI instruction == 0xE8
4.1- calcul_adr_dest;
5- SI (adr_dest dans text)
5.1- POUR (parcours blocs)
6- SI (calcul_dest == UN bloc)
6.1- STOP;
7- SINON SI (calcul_dest dans UN bloc)
7.1- division_bloc;
8- SI (calcul_dest avant premier bloc)
8.1- création_nouveau_premier_bloc;
8.2- decoupage_blocs(nouveau_premier_bloc);
9- SINON SI instruction == 0x55
9.1- calcul_dest = position courante;
9.2- POUR (parcours blocs)
10- SI (calcul_dest == UN bloc)
10.1- STOP;
11- SINON SI (calcul_dest dans UN bloc)
11.1- division_bloc;
}
Figure 3.8 – Algorithme de découpage du code en blocs de fonctions
90
La fonction decoupage_blocs exécute cette tâche. Son algorithme est
reproduit dans la figure 3.8 et il se détaille ainsi :
– Etape 1 : Calcul de la position de début de bloc dans le buffer text.
– Etape 2 : Parcours du code du bloc.
– Etape 3 : Désassemblage de chaque instruction.
– Etape 4 : Si l’instruction est un call
– Etape 4.1 : Calcul de l’adresse de destination du call
– Etape 5 : Si l’adresse de destination se situe dans la section .text
alors il s’agit de l’appel d’une fonction définie dans le code et non pas
un appel de fonction dynamique.
– Etape 5.1 : Parcours de la liste des blocs déjà construits.
– Etape 6 et Etape 6.1 : Première possibilité : le bloc existe déjà et
dans ce cas arrêt du traitement de cette adresse. Le bloc peut être déjà
présent si la fonction a déjà été appelée auparavant.
– Etape 7 et Etape 7.1 : Seconde possibilité : l’adresse appartient à
un bloc existant. Dans ce cas, division du bloc pour créer un nouveau
bloc, c’est-à-dire une nouvelle fonction.
– Etape 8 : Dernière possibilité : la fonction a été définie avant le main
et l’adresse se situe avant le premier bloc (qui a pour adresse de début,
l’adresse de la fonction main).
– Etape 8.1 et Etape 8.2 : Dans ce cas, création d’un nouveau bloc qui
débute à l’adresse d’appel de la fonction et qui finit à l’adresse de début
du main ou du premier bloc. Rappel de la fonction decoupage_bloc
pour traiter ce nouveau premier bloc afin de prendre en compte les
appels de fonction situés dans celui-ci.
– Etape 9 : Sinon si l’instruction est un push ebp
– Etape 9.1 : L’adresse de début de fonction correpond à l’adresse courante.
– Etape 9.2 : Parcours de la liste des blocs déjà construits.
– Etape 10 et Etape 10.1 : Soit le bloc existe déjà et dans ce cas arrêt
du traitement de cette adresse.
– Etape 11 et Etape 11.1 : L’adresse appartient à un bloc existant.
Dans ce cas, division du bloc.
3.5.3
Algorithme de recherche des instructions call
La fonction recherche_instructions_call permet de rechercher les instructions call contenues dans chaque fonction du programme. Elle parcourt
la partie de la section .text qui contient la définition de la fonction étudiée,
désassemble cette partie et y recherche les instructions call.
L’algorithme de recherche des instructions call est décrit dans la figure
3.9. Ses étapes se décrivent ainsi :
91
recherche_instructions_call
{
1- calcul_position_text;
2- TANTQUE (parcours de la definition de la fonction )
3- disassemble_address;
4- SI(instruction == 0xE8)
4.1- calcul_dest = calcul_adr;
5- SI (calcul_dest dans section .text)
5.1- SI (table des symboles)
5.1.1- nom_fonction_dest=recherche_une_fonction;
remplir_fichier_dot;
5.2- SINON ecrire_instructions_dot;
6 -SINON SI(option_interne == 0)
6.1- nom_fonction_dest=recherche_une_fonction;
6.2- recherche_fn_suspecte;
6.3- remplir_fichier_dot;
7- construction_call_chainée;
8- recherche_boucles;
}
Figure 3.9 – Algorithme de lecture des instructions call
– Etape 1 : Calcul de la position dans le buffer text du début de la
fonction.
– Etape 2 : Parcours de la partie de la section .text qui contient la
définition de la fonction.
– Etape 3 : Désassemblage des instructions machine les unes après les
autres. Les instructions sont enregistrées dans une structure prédéfinie
nommée instr 3 .
– Etape 4 : Si le code de l’instruction a pour valeur E8 alors il s’agit
de l’instruction call. Dans ce cas, l’adresse de destination du call est
calculée dans l’étape suivante.
– Etape 4.1 : L’instruction call est composée de son code — E8 — et
de l’adresse relative de destination. La fonction calcul_adr convertit
l’adresse relative codée sur 4 octets en petit endian sur un processeur
Intel. Pour cela, il faut opérer un décalage et une addition de ces octets.
A cette adresse est ajoutée :
– la taille de l’instruction elle-même
– la position dans la section .text de l’instruction call
– l’adresse du début de la section .text elle-même.
3. La fonction qui permet de lire les instructions machine et donc de désassembler le
code est expliquée dans la section 3.7.
92
On obtient alors l’adresse de destination dans l’espace d’adressage.
– Etape 5 : Si cette adresse se situe dans la section .text, cela signifie
qu’il s’agit d’une fonction définie dans le fichier.
– Etape 5.1 et Etape 5.1.1 : Si la table des symboles est présente,
récupération du nom de la fonction dans cette table. La fonction
recherche_une_fonction est appelée pour retrouver le nom de la
fonction à partir de son adresse. Pour retrouver son nom, la fonction
recherche_une_fonction parcourt la table des symboles pour retrouver la fonction appartenant à la section .text dont la valeur st_value
correspond à l’adresse calculée. Une fois la recherche aboutie, on retrouve le nom de la fonction grâce à son index st_name dans la table
des noms. Une fois le nom de la fonction retrouvé, inscription des renseignements adéquats — nom de fonction source et nom de fonction
de destination — dans le fichier dot (cf. section 3.8).
– Etape 5.2 : Sinon, la table des symboles étant absente, l’adresse de
la fonction fait alors office de nom. Les renseignements — adresse de
fonction source et adresse de fonction de destination —sont écrits dans
le fichier dot.
– Etape 6 et Etape 6.1 : Si la fonction ne se situe pas dans la
section .text, il s’agit d’un appel de fonction dynamique. L’option
concernant l’inscription ou non des appels de fonctions dynamiques
est alors analysée. Si les fonctions dynamiques doivent être renseignées, parcours la table des symboles dynamiques avec la fonction
recherche_une_fonction selon le même procédé que précédemment.
Le nom de la fonction est renseigné dans la table des noms dynamiques.
– Etape 6.2 et Etape 6.3 : Parcours des listes de fonctions susceptibles
de créer un trou de sécurité pour les marquer comme telles dans le
fichier dot qui est ensuite rempli.
– Etape 7 : Construction de la liste chaînée des instructions call. Cette
liste servira ensuite lors de la construction des graphes de flot de
contrôle des fonctions pour y inscrire les appels de fonction.
– Etape 8 : Passage à la phase suivante : appel de la fonction de recherche des sauts dans les fonctions.
3.5.4
Graphes des appels de fonction du programme gencode
Voici les deux graphes générés par graphELF sur le programme gencode.
Le graphe 3.10 représente celui généré par la lecture de la table des symboles tandis que le graphe 3.11 correspond à celui-ci généré en analysant les
adresses.
93
main
f2
f1
time
srandom
random
printf
Figure 3.10 – Le graphe des appels de fonctions par les noms de Test2
0x80483cc
0x80484c7
time
srandom
random
0x80484df
printf
Figure 3.11 – Le graphe des appels de fonctions par les adresses de Test2
3.6
Phase de construction du flot de contrôle des
fonctions
Une fois le graphe des appels de fonctions déterminé, la phase suivante
effectue une analyse de chaque fonction pour déterminer les instructions de
94
sauts.
Le code de chaque fonction est subdivisé en blocs correspondant à des
flux d’instructions ininterrompues. Chaque instruction provoquant un saut
ou un arrêt constitue donc une rupture dans la séquence des instructions et
engendre la création d’un nouveau bloc.
Les branchements sont représentés par une liste de blocs chaînés. Chaque
bloc contient :
– Un pointeur sur le bloc suivant utilisé pour chaîner la liste des blocs
et pouvoir la parcourir une seule fois.
– Une adresse de saut qui contient l’adresse de destination du saut. Cette
adresse sera ensuite utilisée pour compléter les pointeurs suivants ou
pour construire les liens entre les sauts jmp et les tables indexées comme
cela est expliqué en 3.6.4.
– Deux pointeurs. Ces pointeurs pointent vers les blocs possibles d’instructions à exécuter :
– Pour un bloc d’instruction se terminant par un jmp, celui-ci n’a
qu’une possibilité d’exécution après ce jmp et n’aura qu’un unique
pointeur vers le bloc de destination du jmp.
– Pour un bloc d’instruction se terminant par un saut conditionnel,
celui-ci aura deux pointeurs correspondants aux deux possibilités
d’exécution du code (la possibilité “si-vrai” et la possibilté “si-faux”).
– Pour un bloc se terminant sur l’instruction ret, aucun pointeur de
destination ne sera renseigné puisqu’il s’agit d’un instruction de fin
d’exécution.
3.6.1
Découpage de la fonction analysée en blocs
L’algorithme 3.12 se détaille comme suit :
– Etape 1 : Calcul de la position du début de la fonction dans le buffer
text.
– Etape 2 et Etape 2.1 : Parcours et désassemblage des instructions
du buffer.
– Etape 3 et Etape 3.1 : Si l’instruction est un cmp alors : calcul du
nombre comparé et stockage de sa valeur dans une variable. Cette
valeur est utilisée lors de sauts vers des tables indexées et permet de
connaître le nombre d’index de cette table.
– Etape 4 et Etape 4.1 : Si l’instruction est un saut inconditionnel :
calcul de l’adresse de destination du saut et affectation de celle-ci dans
le champ adr_saut de la structure. Le bloc actuel est fermé et un
nouveau bloc est créé qui débute à l’instruction suivant celle du saut.
– Etape 5 et Etape 5.1 : Si l’instruction est un saut conditionnel, calcul
de l’adresse de destination du saut.
95
recherche_boucles
{
1- calcul_position_buffer;
2- POUR(instructions_buffer)
2.1- disassemble_address;
3- SI (instruction == cmp)
3.1- calcul_nombre_cmp;
4- SINON SI (instruction == jmp)
4.1- adresse_saut = recherche_adresse_saut;
calcul_adresse_fin_bloc;
construction_nouveau_bloc_jmp;
5- SINON SI (instruction == j*)
5.1- recherche_adresse_saut;
5.2- construction_bloc_saut_conditionnel;
6- SINON SI (instruction == return)
6.1- construction_bloc_fin;
6.2- construction_liste_chainee_return;
7- parcours_chaine;
8- creation_fichier_dot_adresse;
9- creation_fichier_dot_code;
}
Figure 3.12 – Algorithme de recherche des instructions de sauts
– Etape 5.2 : Création d’un bloc conditionnel : le bloc actuel est fermé et
l’adresse de destination du saut est renseignée. De plus un pointeur vers
le bloc suivant est créé. Ce bloc suivant débute à l’adresse d’instruction
suivant celle du saut.
– Etape 6, Etape 6.1 et Etape 6.2 : si l’intruction est un ret alors le
bloc actuel est fermé et un nouveau bloc est créé débutant à l’adresse
d’instruction suivante mais aucun lien vers un autre bloc n’est renseigné. De plus, une liste chaînée qui contient les adresses des instructions
ret est créée qui sera utilisée lors de la construction des graphes.
– Etape 7 : Appel de la fonction parcours_chaine qui permet de chaîner les blocs entre eux selon les adresses de destination des sauts.
– Etape 8 et Etape 9 : création des fichiers dot de représentation
graphique du flot de contrôle des fonctions.
3.6.2
Création des liens entre les blocs
Une fois les blocs créés et les adresses de destination renseignées, la liste
des blocs est parcourue de manière à compléter les pointeurs reliant les blocs
96
entre eux. Pour cela, l’adresse de destination du saut de chaque bloc est comparée avec tous les blocs de façon à les relier entre eux. Plusieurs possibilités
se présentent :
– L’adresse de saut correspond à un bloc déjà existant. Dans ce cas les
deux blocs sont reliés.
– L’adresse de saut se situe à l’intérieur d’un bloc déjà existant. Dans ce
cas, le bloc trouvé est découpé en deux et les liens sont créés.
– L’adresse de saut se situe en dehors des blocs. Dans ce cas, le saut se
situe en dehors de la fonction. Il est traité par la suite (voir les deux
parties suivantes).
parcours_chaine
{
1- POUR(parcours_blocs)
2- Si (adr_saut_bloc_traite existe)
3- POUR(parcours_blocs)
3.1- SI (adr_saut_bloc_traite == adr_bloc_lu)
3.1.1- création_pointeur_vers_bloc;
3.1.2- adr_saut_bloc_traite = -1;
4- SINON SI (adr_saut_bloc_traite DANS bloc_lu)
4.1- division_bloc_lu;
4.2- affectation_saut;
4.3- adr_saut_bloc_traite = -1;
}
Figure 3.13 – Algorithme de parcours des blocs construits
L’algorithme de la fonction parcours_chaine est décrit en 3.13 et se
détaille ainsi :
– Etape 1 : Parcours de la liste des blocs.
– Etape 2 : Si l’adresse de saut existe, traitement de celle-ci.
– Etape 3 : Parcours des blocs pour rechercher l’adresse de saut.
– Etape 3.1, Etape 3.1.1 et Etape 3.1.2 : Si l’adresse correspond
à une adresse de début de bloc, création d’un lien entre ces blocs et
l’adresse de saut ayant été traitée, elle est réinitialisée à -1.
– Etape 4, Etape 4.1 à Etape 4.3 : Si en revanche, l’adresse est contenue à l’intérieur d’un bloc, dans ce cas, le bloc de destination du saut
doit être créé en divisant le bloc trouvé. Les liens sont ensuite établis
entre tous ces blocs et l’adresse de saut est réinitialisée à -1.
3.6.3
Cas des sauts vers des fonctions
Certains sauts n’ont pas encore été traités. Il s’agit des instructions jmp
vers une fonction. Cet appel de fonction indirect est utilisé par le compilateur
97
en fonction des options de compilation choisies.
Lorsqu’un saut de ce type est rencontré, l’adresse de destination du saut
est comparée avec les adresses de début des fonctions :
– Soit la table des symboles est présente et celle-ci est parcourue pour
rechercher le nom correspondant à l’adresse de destination.
– Soit la table des symboles est absente et la liste chaînée des blocs de
fonctions créée lors de la phase précédente est parcourue pour rechercher l’adresse de destination.
– S’il s’agit d’un appel de fonction dynamique, la table des symboles
dynamiques est parcourue pour résoudre l’appel.
Si la fonction de destination est trouvée alors l’instruction jmp est marquée comme étant un saut de fonction. Il s’agit évidemment d’un appel particulier dans la mesure où l’adresse de retour de cet appel n’est pas la même
que lors d’un appel effectué par un call. Pour cette raison, ce type d’appel
de fonction engendre une rupture dans le bloc d’instruction, contrairement
aux instructions call qui ne provoquent pas de point de rupture de bloc. De
plus, le bloc n’est relié à aucun autre bloc suivant.
3.6.4
Cas des sauts vers des tables indexées
Un autre cas de saut se présente également. En effet, une instruction
switch du langage C est traduite par un branchement indexé vers les différents case au moyen d’une table de sauts située dans la section .rodata.
Cette table contient autant d’entrées que la valeur maximum de case. Un
test est donc effectué pour traiter les valeurs supérieures correspondant au
cas default : il s’agit des instructions cmp et ja comme le montre l’exemple
suivant.
804a692: 3d 0f 01 00 00
804a697: 77 07
804a699: ff 24 85 04 60 05 08
cmp
ja
jmp
$0x10f,%eax
0x804a6a0
*0x8056004(,%eax,4)
Pour relier le bloc contenant le saut avec les choix possibles, il faut décoder la section .rodata qui contient les adresses de destination des sauts. Le
nombre de cas est égal au nombre maximum utilisé pour la comparaison.
La représentation de ces sauts est particulière. Elle engendre des flèches
multiples entre le bloc terminé par l’instruction jmp et les blocs commençant
aux adresses contenues dans la table indexée de .rodata comme cela est
représenté en figure 3.14.
Il reste cependant un cas actuellement non traité par graphELF : les sauts
indexés indirects en fonction du contenu d’un registre. Dans ce cas, l’instruction de comparaison qui teste le nombre de cas ne compare pas un registre
98
0x804833c
0x804835f
0x8048473 0x804848d 0x80484ac 0x804836b 0x8048380 0x804839a 0x80483bc 0x80483e3 0x80483f8 0x8048415 0x804843a 0x8048461 0x80484d0
0x80484e0 printf
return
Figure 3.14 – Graphe représentant une instruction switch.
avec un nombre mais deux registres ensemble. Il nous est actuellement impossible de déterminer le contenu de ces registres et donc de déterminer le
nombre de cas contenus dans la table indexée.
3.6.5
Les graphes du programme gencode
La figure 3.15 représente le graphe de flot de contrôle de la fonction main
tandis que la figure 3.16 nous donne une représentation sous forme de blocs
d’instructions de ce même flot.
graphELF génère également le listing du code désassemblé pour chacune
des fonctions présentes dans le programme. Voici une version de ce code
lorsque le programme contient encore la table des symboles et les en-têtes
de section :
99
0x80483cc
0x80483fe
f1
0x8048404
srandom
0x8048409
0x80484c5
return
time
0x804842c
0x8048432
0x80484a6
0x8048434
random
f2
0x8048452
0x8048457
0x804845d
0x8048463
0x804846f
0x804847e
0x8048485
0x8048475
0x804848d
0x8048494
printf
Figure 3.15 – Le graphe de la fonction main du programme gencode
fonction f2 adresse 80484df
80484DF 55
80484E0 89 E5
80484E2 83 EC 08
80484E5 83 EC 0C
80484E8 68 46 86 04 08
80484ED E8 FE FD FF FF
80484F2 83 C4 10
80484F5 C9
80484F6 C3
push ebp
mov ebp, esp
sub esp, 0x8
sub esp, 0xC
push 0x8048646
call printf
add esp, 0x10
leave
ret
100
80483cc
80483cd
80483cf
80483d2
80483d5
80483da
80483dc
80483e1
80483e4
80483e6
80483eb
80483ee
80483ef
80483f4
80483f7
80483fe 83 7D FC 02
8048402 7E 05
8048404 E9 BC 00 00 00
55
89
83
83
B8
29
E8
83
6A
E8
83
50
E8
83
C7
return
00 00 00
0C
FE FF FF
04
f1
time
8048409
8048410
8048413
8048416
8048417
8048418
804841d
8048422
8048425
leave
ret
pushebp
movebp, esp
subesp, 0x18
andesp, 0xF0
moveax, 0x0
subesp, eax
callf1
subesp, 0xC
push0x0
calltime
addesp, 0x4
pusheax
callsrandom
addesp, 0x10
mov[ebp-04], 0x0
18
F0
00 00 00
0C FF FF FF
C4 10
45 FC 00 00 00 00
cmp[ebp-04], 0x2
jle0x8048409
jmp0x80484c5
80484c5 C9
80484c6 C3
E5
EC
E4
00
C4
E6
EC
00
E5
C4
C7
83
8B
40
50
68
E8
83
C7
srandom
45 F0 00 00 00 00
EC 08
45 FC
18
CE
C4
45
mov[ebp-10], 0x0
subesp, 0x8
moveax, [ebp-04]
inceax
pusheax
push0x8048618
callprintf
addesp, 0x10
mov[ebp-08], 0x0
86 04 08
FE FF FF
10
F8 00 00 00 00
804842c 83 7D F8 04
8048430 7E 02
8048432 EB 72
80484a6
80484a9
80484ae
80484b3
80484b6
80484bb
80484be
80484c0
83
68
E8
83
E8
8D
FF
E9
EC
2A
3D
C4
24
45
00
39
0C
86 04 08
FE FF FF
10
00 00 00
FC
FF FF FF
cmp[ebp-08], 0x4
jle0x8048434
8048434
8048439
804843b
8048440
8048442
8048444
8048447
8048449
804844c
8048450
jmp0x80484a6
subesp, 0xC
push0x804862A
callprintf
addesp, 0x10
callf2
leaeax, [ebp-04]
inc[eax]
jmp0x80483fe
E8
89
B8
89
89
C1
F7
89
83
7E
87
C2
0C
C1
D0
FA
F9
55
7D
05
FE FF FF
00 00 00
1F
F4
F4 09
8048452 8D 45 F0
8048455 FF 00
random
callrandom
movedx, eax
moveax, 0xC
movecx, eax
moveax, edx
saredx, 0x1F
idiveax, ecx
mov[ebp-0C], edx
cmp[ebp-0C], 0x9
jle0x8048457
leaeax, [ebp-10]
inc[eax]
8048457 83 7D F4 09
804845b 7E 06
f2
804847e C7 45 E8 25 86 04 08
mov[ebp-18], 0x8048625
8048485 8B 45 E8
8048488 89 45 EC
804848b EB 07
moveax, [ebp-18]
mov[ebp-14], eax
jmp0x8048494
8048494
8048497
804849c
804849f
80484a2
80484a4
push[ebp-14]
callprintf
addesp, 0x10
leaeax, [ebp-08]
inc[eax]
jmp0x804842c
FF
E8
83
8D
FF
EB
75
54
C4
45
00
86
EC
FE FF FF
10
F8
cmp[ebp-0C], 0x9
jle0x8048463
804845d 83 7D F0 01
8048461 7F D1
cmp[ebp-10], 0x1
jg0x8048434
8048463
8048466
8048469
804846d
EC 08
75 F4
7D F4 0A
1E
subesp, 0x8
push[ebp-0C]
cmp[ebp-0C], 0xA
jz0x804848d
804846f 83 7D F4 0B
8048473 75 09
cmp[ebp-0C], 0xB
jnz0x804847e
83
FF
83
74
8048475 C7 45 E8 23 86 04 08
804847c EB 07
804848d C7 45 EC 28 86 04 08
mov[ebp-18], 0x8048623
jmp0x8048485
mov[ebp-14], 0x8048628
printf
Figure 3.16 – Le graphe de la fonction main du programme gencode représentée avec le code
fonction main
80483CC 55
80483CD 89 E5
80483CF 83 EC
80483D2 83 E4
80483D5 B8 00
80483DA 29 C4
80483DC E8 E6
80483E1 83 EC
80483E4 6A 00
80483E6 E8 E5
80483EB 83 C4
80483EE 50
80483EF E8 0C
80483F4 83 C4
80483F7 C7 45
80483FE 83 7D
8048402 7E 05
8048404 E9 BC
8048409 C7 45
adresse 80483cc
18
F0
00 00 00
00 00 00
0C
FE FF FF
04
FF FF FF
10
FC 00 00 00 00
FC 02
00 00 00
F0 00 00 00 00
push
mov
sub
and
mov
sub
call
sub
push
call
add
push
call
add
mov
cmp
jle
jmp
mov
ebp
ebp, esp
esp, 0x18
esp, 0xF0
eax, 0x0
esp, eax
f1
esp, 0xC
0x0
time
esp, 0x4
eax
srandom
esp, 0x10
[ebp-04], 0x0
[ebp-04], 0x2
0x8048409
0x80484c5
[ebp-10], 0x0
101
8048410
8048413
8048416
8048417
8048418
804841D
8048422
8048425
804842C
8048430
8048432
8048434
8048439
804843B
8048440
8048442
8048444
8048447
8048449
804844C
8048450
8048452
8048455
8048457
804845B
804845D
8048461
8048463
8048466
8048469
804846D
804846F
8048473
8048475
804847C
804847E
8048485
8048488
804848B
804848D
8048494
8048497
83
8B
40
50
68
E8
83
C7
83
7E
EB
E8
89
B8
89
89
C1
F7
89
83
7E
8D
FF
83
7E
83
7F
83
FF
83
74
83
75
C7
EB
C7
8B
89
EB
C7
FF
E8
EC 08
45 FC
18
CE
C4
45
7D
02
72
87
C2
0C
C1
D0
FA
F9
55
7D
05
45
00
7D
06
7D
D1
EC
75
7D
1E
7D
09
45
07
45
45
45
07
45
75
54
86
FE
10
F8
F8
04 08
FF FF
00 00 00 00
04
FE FF FF
00 00 00
1F
F4
F4 09
F0
F4 09
F0 01
08
F4
F4 0A
F4 0B
E8 23 86 04 08
E8 25 86 04 08
E8
EC
EC 28 86 04 08
EC
FE FF FF
sub
mov
inc
push
push
call
add
mov
cmp
jle
jmp
call
mov
mov
mov
mov
sar
idiv
mov
cmp
jle
lea
inc
cmp
jle
cmp
jg
sub
push
cmp
jz
cmp
jnz
mov
jmp
mov
mov
mov
jmp
mov
push
call
esp, 0x8
eax, [ebp-04]
eax
eax
0x8048618
printf
esp, 0x10
[ebp-08], 0x0
[ebp-08], 0x4
0x8048434
0x80484a6
random
edx, eax
eax, 0xC
ecx, eax
eax, edx
edx, 0x1F
eax, ecx
[ebp-0C], edx
[ebp-0C], 0x9
0x8048457
eax, [ebp-10]
[eax]
[ebp-0C], 0x9
0x8048463
[ebp-10], 0x1
0x8048434
esp, 0x8
[ebp-0C]
[ebp-0C], 0xA
0x804848d
[ebp-0C], 0xB
0x804847e
[ebp-18], 0x8048623
0x8048485
[ebp-18], 0x8048625
eax, [ebp-18]
[ebp-14], eax
0x8048494
[ebp-14], 0x8048628
[ebp-14]
printf
102
fonction main
804849C 83 C4
804849F 8D 45
80484A2 FF 00
80484A4 EB 86
80484A6 83 EC
80484A9 68 2A
80484AE E8 3D
80484B3 83 C4
80484B6 E8 24
80484BB 8D 45
80484BE FF 00
80484C0 E9 39
80484C5 C9
80484C6 C3
(suite 2):
10
F8
0C
86 04 08
FE FF FF
10
00 00 00
FC
FF FF FF
fonction f1 adresse 80484c7
80484C7 55
80484C8 89 E5
80484CA 83 EC 08
80484CD 83 EC 0C
80484D0 68 2C 86 04 08
80484D5 E8 16 FE FF FF
80484DA 83 C4 10
80484DD C9
80484DE C3
3.7
add esp, 0x10
lea eax, [ebp-08]
inc [eax]
jmp 0x804842c
sub esp, 0xC
push 0x804862A
call printf
add esp, 0x10
call f2
lea eax, [ebp-04]
inc [eax]
jmp 0x80483fe
leave
ret
push ebp
mov ebp, esp
sub esp, 0x8
sub esp, 0xC
push 0x804862C
call printf
add esp, 0x10
leave
ret
Emploi de la bibliothèque dynamique libdisasm
Accéder au contenu d’un fichier ELF pour y retrouver les appels de fonction nécessite un travail de désassemblage de la section .text ou du segment
contenant cette section.
Un assembleur et a fortiori un désassembleur sont totalement dépendants
de l’architecture de la machine, ici une architecture Intel. Le code assembleur
est composé d’instructions. Chaque instruction comporte le code instruction
(opcode) et ses opérandes. Un opérande peut être codé dans l’instruction
elle-même ou situé en mémoire. Le mode de calcul de l’adresse de chaque
opérande diffère et la longueur d’une instruction Intel varie de un à quatorze
octets. C’est pourquoi il est impossible de parcourir le code sans utiliser un
désassembleur complet, les longueurs des instructions étant différentes 4 .
Les désassembleurs gnu sont nombreux et il semblait alors inutile d’en
développer un nouveau. Nous avons utilisé la bibliothèque libdisasm [lib04].
4. Pour une référence complète sur le jeu d’instructions assembleur pour architecture
Intel, se référer à la documentation Intel [Int04].
103
Le choix de cette bibliothèque libdisasm a été déterminé par le fait que cet
outil est d’une part le moteur d’un désassembleur éprouvé [Bas04], et d’autre
part accessible sous forme d’une bibliothèque dynamique intégrable dans le
programme graphELF de manière très souple et totalement transparente.
3.7.1
Les fonctions de la bibliothèque
La bibliothèque libdisasm contient le format de chaque instruction Intel
ainsi que plusieurs fonctions prédéfinies permettant de désassembler le code
passé en paramètre. Les fonctions utilisées dans graphELF se nomment :
– disassemble_init. Cette fonction sert à initialiser le désassembleur.
– disassemble_cleanup. Cette fonction libère la mémoire et ferme le
désassembleur proprement.
– disassemble_address. Cette fonction récupère la première instruction
lue dans le buffer passé en paramètre et l’enregistre dans une structure
prédéfinie nommée instr. Elle renvoie la taille de l’instruction décodée.
La structure instr est définie dans le fichier libdis.h fourni avec la bibliothèque. Elle permet de stocker l’instruction désassemblée. Cette structure
n’est utilisée que pour le cas d’un simple affichage de la section désassemblée. En effet, les champs de la structure sont de type char avec lequel il est
moins aisé d’effectuer des calculs qu’avec un type décimal, d’où les calculs
d’adresse effectuées directement par graphELF.
3.7.2
Algorithme de désassemblage
La documentation de la bibliothèque fournit un algorithme type d’utilisation de ses fonctions. Cet algorithme appliqué à notre outil est représenté
en figure 3.17.
Pour rechercher les diverses instructions nécessaires au découpage du
code, graphELF parcourt tout le code exécutable. Celui-ci est désassemblé
pour y retrouver les instructions cherchées. L’algorithme décrivant ces opérations est présenté en figure 3.17 et se détaille de la sorte :
– Etape 1 : Initialisation du désassembleur.
– Etape 2 : Parcours du buffer du début à la fin pour la lecture de
chaque instruction.
– Etape 3 : Lecture/désassemblage de la première instruction machine
du buffer.
– Etape 4 : Si la taille de l’instruction est valide on peut la traiter.
Ceci signifie que l’instruction décodée est correcte et qu’il ne s’est pas
produit d’erreur lors du désassemblage de l’instruction.
– Etape 5 : Si l’instruction correspond à l’instruction recherchée
– Etape 6 : Calcul de l’adresse de destination.
– Etape 7 : Divers traitement de l’information selon le besoin.
104
desassemble_buffer
{
1- disassemble_init;
2- TANTQUE ( position < taille_buffer)
3- size = disassemble_address();
4- SI (size)
5- SI(buffer[position] == instruction_cherchée)
6- calcul_destination;
7- divers_traitements;
8- position += size;
9- SINON
instruction_non_valide;
position++;
10- disassemble_cleanup;
}
Figure 3.17 – Algorithme général de désassemblage du code
– Etape 8 : Pour passer à l’instruction suivante, il faut avancer la position dans le buffer de la taille correspondant à l’instruction qui vient
d’être lue. Cette taille est stockée dans la variable size renseignée par
la fonction disassemble_address().
– Etape 9 : Si la taille de l’instruction n’est pas correcte alors la lecture
a échoué. On avance la position dans le buffer de 1 pour essayer de lire
l’instruction suivante. Il s’agit ici d’un simple contrôle.
– Etape 10 : Fermeture du désassembleur.
3.8
Création de graphes avec graphviz
graphELF analyse des exécutables au format ELF et fournit des graphes
représentant la structure du programme. Les graphes sont conçus à partir
des informations lues par graphELF. Ce dernier rempli des fichiers avec des
instructions dot. Ces fichiers peuvent ensuite être exploités de manière indépendante à graphELF pour fournir les graphes voulus.
Le choix du générateur de graphe s’est porté naturellement sur graphviz
[Res00] étant donné la facilité d’utilisation du langage dot et la simplicité
des instructions à lui fournir pour qu’il construise un graphe facilement déchiffrable 5 .
5. Les instructions dot utilisées pour la construction des graphes sont détaillées dans
[MM03]
105
De plus, graphviz comprend un outil d’analyse et de traitement des
instructions dot nommé gpr permettant d’affiner le graphe en fonction de
critères personnalisables. Le langage gpr se manipule de la même manière
que le langage awk 6 . La figure 3.18 donne le script utilisé par graphELF pour
affiner le graphe des appels de fonctions.
Ce script permet d’effacer les arcs multiples qui joignent deux noeuds
identiques ainsi que les noeuds du graphe n’ayant aucun arc entrant ou sortant. Ceci permet d’obtenir des graphes plus clairs en otant les informations
superflues.
Les graphes sont générés en plusieurs étapes :
– graphELF analyse le contenu du programme exécutable.
– A partir des informations recueillies, il construit un fichier contenant
des instructions en langage dot.
– Si le fichier construit correspond au graphe des appels de fonctions,
celui-ci est à son tour analysé par le script gpr pour simplifier le graphe.
– A partir de ce fichier résultant, l’outil graphviz génère une représentation graphique des graphes.
6. Les instructions sont détaillées dans [MM03].
106
//script d’optimisation du graphe
//grammaire similaire a awk
//initialisation des variables
BEGIN{edge_t n; int arc[];int flag}
//
//
//
//
E{
pour chaque arc (edge E)
verif s’il existe deja
si oui delete de l’arc
sinon enregistrement de l’arc
flag=1;
for(arc[n])
{
if(n.tail == tail &&
n.head == head)
{
flag = 0;
delete($G,$);
}
}
if (flag == 1)
{
arc[$]++;
}
}
// pour chaque noeud (N)
// si le degre =0 cad s’il n’existe aucun arc entrant ou sortant
// delete du noeud
N{
if(degree == 0)
{
delete($G,$);
}
}
// re-ecriture du graphe
END_G{write($G)}
Figure 3.18 – Script GPR utilisé par graphELF
107
3.9
Comparaison avec l’existant : les outils dans le
même domaine
Cette partie compare notre outil avec les outils existants et disponibles
et explique en quoi graphELF apporte sa contribution.
Les outils travaillant sur des fichiers binaires au format ELF peuvent être
répartis dans les trois domaines suivants :
– les outils de lecture et d’affichage des informations contenues dans un
ficher ELF (partie 3.9.1).
– les désassembleurs (partie 3.9.2).
– les outils représentent les appels de fonction et le flot de contrôle du
programme sous forme de graphes (partie 3.9.3).
Nous détaillons l’outil elfsh dans la partie 3.9.4 du fait qu’il peut être
classé à la fois dans ces trois catégories.
3.9.1
readelf : un outil affichant les informations contenues
dans un fichier ELF
Le package binutils [Fon04] se compose d’un ensemble d’utilitaires GNU
servant à lire et analyser des fichiers binaires. Les outils de cet ensemble que
nous décrivons se nomment readelf et objdump (cf. 3.9.2.1).
readelf [Fre02b] est un outil qui permet une lecture des informations
contenues dans un fichier ELF. Il affiche sous forme de tableaux le contenu
des en-têtes, les tables des symboles et des noms ainsi que les informations des
sections dynamiques. Un exemple complet d’affichage est fourni en annexe
A.
readelf est conçu pour fonctionner sur toutes les plates-formes supportant le format ELF et traduit les informations qu’il lit dans le fichier binaire
de façon claire. graphELF utilise d’ailleurs en partie certaines fonctions du
code source de readelf pour effectuer les mêmes traductions.
Voici par exemple, la table des symboles dynamiques du programme Test
telle qu’affichée par readelf :
108
Table
Num:
0:
1:
2:
3:
4:
5:
6:
7:
8:
9:
de symboles « .dynsym » contient 10 entrées:
Valeur Tail Type
Lien
Vis
Ndx Nom
00000000
0 NOTYPE LOCAL DEFAULT UND
0804833c
362 FUNC
GLOBAL DEFAULT UND fgets@GLIBC_2.0 (2)
0804834c
251 FUNC
GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.0 (2)
0804835c
54 FUNC
GLOBAL DEFAULT UND printf@GLIBC_2.0 (2)
0804836c
124 FUNC
GLOBAL DEFAULT UND sqrt@GLIBC_2.0 (3)
0804977c
4 OBJECT GLOBAL DEFAULT
22 stdin@GLIBC_2.0 (2)
08048644
4 OBJECT GLOBAL DEFAULT
14 _IO_stdin_used
00000000
0 NOTYPE WEAK
DEFAULT UND _Jv_RegisterClasses
00000000
0 NOTYPE WEAK
DEFAULT UND __gmon_start__
0804837c
48 FUNC
GLOBAL DEFAULT UND strcpy@GLIBC_2.0 (2)
Cependant, readelf est un outil destiné uniquement à lire des informations : il ne propose aucune analyse de ces dernières. Cet outil bien que très
pratique reste donc limité. Il est d’ailleurs souvent associé à objdump pour
désassembler le code exécutable.
Lorsque les en-têtes de section ont été retirés, readelf affiche les informations concernant les segments uniquement.
3.9.2
Les désassembleurs
Bien que les désassembleurs soient nombreux, peu d’entre eux peuvent
être utilisés facilement. Les deux outils étudiés dans cette partie se nomment
objdump et bastard. objdump est sans doute le désassembleur le plus connu
en ce qui concerne les fichiers ELF et bastard l’un des désassembleurs les
plus complets actuellement.
3.9.2.1
objdump
objdump [Fre02a] fait également partie des utilitaires GNU standards de
manipulation de fichiers au format ELF. Cet outil, bien que pouvant afficher
de manière très rudimentaire le contenu des en-têtes d’un fichier ELF est
essentiellement utilisé pour le désassemblage d’un programme.
Lorsque le programme contient encore une table des symboles, objdump
analyse le code et le présente fonction par fonction. Dans ce cas, le code désassemblé est clair et facile à analyser. Il effectue également un remplacement
des adresses pointées par les instructions call par le nom de la fonction leur
correspondant, seulement dans le cas des appels de fonctions définies dans le
programme. objdump ignore les appels de fonctions dynamiques tandis que
graphELF effectue ces remplacements dans les deux cas.
Cependant, la plus grande différence entre objdump et graphELF se remarque à l’analyse du code désassemblé lorsque le programme ne possède
109
plus de table de symboles statiques. Les exemples suivants nous livrent un
extrait d’un même programme désassemblé par objdump puis par graphELF :
Résultat
08048390 <.text>:
8048390:
31
8048392:
5e
8048393:
89
8048395:
83
8048398:
50
8048399:
54
804839a:
52
804839b:
68
80483a0:
68
80483a5:
51
80483a6:
56
80483a7:
68
80483ac:
e8
80483b1:
f4
80483b2:
90
80483b3:
90
80483b4:
55
80483b5:
89
80483b7:
53
80483b8:
e8
80483bd:
5b
80483be:
81
80483c4:
52
80483c5:
8b
80483cb:
85
80483cd:
74
80483cf:
ff
80483d1:
58
80483d2:
5b
80483d3:
c9
80483d4:
c3
80483d5:
90
80483d6:
90
80483d7:
90
80483d8:
90
80483d9:
90
80483da:
90
80483db:
90
80483dc:
90
80483dd:
90
80483de:
90
80483df:
90
fourni par objdump
ed
e1
e4 f0
90 85 04 08
30 85 04 08
4c 84 04 08
9b ff ff ff
e5
00 00 00 00
c3 9b 13 00 00
83 20 00 00 00
c0
02
d0
xor
pop
mov
and
push
push
push
push
push
push
push
push
call
hlt
nop
nop
push
mov
push
call
pop
add
push
mov
test
je
call
pop
pop
leave
ret
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
%ebp,%ebp
%esi
%esp,%ecx
$0xfffffff0,%esp
%eax
%esp
%edx
$0x8048590
$0x8048530
%ecx
%esi
$0x804844c
0x804834c
%ebp
%esp,%ebp
%ebx
0x80483bd
%ebx
$0x139b,%ebx
%edx
0x20(%ebx),%eax
%eax,%eax
0x80483d1
*%eax
%eax
%ebx
110
80483e0:
80483e1:
80483e3:
80483e4:
80483e5:
80483ec:
80483ee:
80483f3:
80483f5:
80483f7:
80483f9:
8048400:
8048403:
8048408:
804840a:
804840f:
8048411:
8048413:
8048415:
804841c:
804841d:
804841e:
8048420:
8048421:
8048423:
8048424:
8048425:
804842b:
804842d:
804842f:
8048434:
8048436:
8048438:
804843b:
8048440:
8048445:
8048448:
8048449:
804844a:
804844b:
804844c:
804844d:
804844f:
8048452:
8048455:
804845a:
804845c:
8048463:
8048467:
8048469:
804846b:
55
89
50
50
80
75
a1
8b
85
74
8d
83
a3
ff
a1
8b
85
75
c6
c9
c3
89
55
89
51
51
8b
85
74
b8
85
74
83
68
e8
83
c9
c3
90
90
55
89
83
83
b8
29
c7
83
7e
eb
83
e5
3d
2e
6c
10
d2
1c
b4
c0
6c
d2
6c
10
d2
eb
05
80 97 04 08 00
96 04 08
26 00 00 00 00
04
96 04 08
96 04 08
80 97 04 08 01
f6
e5
15
d2
19
00
c0
10
ec
54
bb
c4
e5
ec
e4
00
c4
45
7d
02
28
ec
54 97 04 08
00 00 00
0c
97 04 08
7b fb f7
10
08
f0
00 00 00
f8 00 00 00 00
f8 09
08
push
mov
push
push
cmpb
jne
mov
mov
test
je
lea
add
mov
call
mov
mov
test
jne
movb
leave
ret
mov
push
mov
push
push
mov
test
je
mov
test
je
sub
push
call
add
leave
ret
nop
nop
push
mov
sub
and
mov
sub
movl
cmpl
jle
jmp
sub
%ebp
%esp,%ebp
%eax
%eax
$0x0,0x8049780
0x804841c
0x804966c,%eax
(%eax),%edx
%edx,%edx
0x8048415
0x0(%esi,1),%esi
$0x4,%eax
%eax,0x804966c
*%edx
0x804966c,%eax
(%eax),%edx
%edx,%edx
0x8048400
$0x1,0x8049780
%esi,%esi
%ebp
%esp,%ebp
%ecx
%ecx
0x8049754,%edx
%edx,%edx
0x8048448
$0x0,%eax
%eax,%eax
0x8048448
$0xc,%esp
$0x8049754
0x0
$0x10,%esp
%ebp
%esp,%ebp
$0x8,%esp
$0xfffffff0,%esp
$0x0,%eax
%eax,%esp
$0x0,0xfffffff8(%ebp)
$0x9,0xfffffff8(%ebp)
0x804846b
0x8048493
$0x8,%esp
111
804846e:
8048471:
8048476:
804847b:
804847e:
8048481:
8048484:
8048489:
804848c:
804848f:
8048491:
8048493:
8048494:
8048495:
8048496:
8048498:
804849b:
804849f:
80484a1:
80484a6:
....
....
ff
68
e8
83
83
ff
e8
83
8d
ff
eb
c9
c3
55
89
83
83
7f
e8
eb
75
48
e1
c4
ec
75
0c
c4
45
00
d0
f8
86 04 08
fe ff ff
10
0c
f8
00 00 00
10
f8
e5
ec 08
7d 08 04
07
54 00 00 00
50
pushl
push
call
add
sub
pushl
call
add
lea
incl
jmp
leave
ret
push
mov
sub
cmpl
jg
call
jmp
0xfffffff8(%ebp)
$0x8048648
0x804835c
$0x10,%esp
$0xc,%esp
0xfffffff8(%ebp)
0x8048495
$0x10,%esp
0xfffffff8(%ebp),%eax
(%eax)
0x8048463
%ebp
%esp,%ebp
$0x8,%esp
$0x4,0x8(%ebp)
0x80484a8
0x80484fa
0x80484f8
Résultat fourni par graphELF
fonction 0x804844c adresse 804844c
804844C 55
push ebp
804844D 89 E5
mov ebp, esp
804844F 83 EC 08
sub esp, 0x8
8048452 83 E4 F0
and esp, 0xF0
8048455 B8 00 00 00 00
mov eax, 0x0
804845A 29 C4
sub esp, eax
804845C C7 45 F8 00 00 00 00
mov [ebp-08], 0x0
8048463 83 7D F8 09
cmp [ebp-08], 0x9
8048467 7E 02
jle 0x804846b
8048469 EB 28
jmp 0x8048493
804846B 83 EC 08
sub esp, 0x8
804846E FF 75 F8
push [ebp-08]
8048471 68 48 86 04 08
push 0x8048648
8048476 E8 E1 FE FF FF
call printf
804847B 83 C4 10
add esp, 0x10
804847E 83 EC 0C
sub esp, 0xC
8048481 FF 75 F8
push [ebp-08]
8048484 E8 0C 00 00 00
call 0x8048495
8048489 83 C4 10
add esp, 0x10
804848C 8D 45 F8
lea eax, [ebp-08]
804848F FF 00
inc [eax]
8048491 EB D0
jmp 0x8048463
8048493 C9
leave
8048494 C3
ret
112
Dans cet exemple, objdump désassemble toute la section .text sans distinguer les différentes fonctions : il faut alors analyser le code désassemblé
pour distinguer les début et fin des fonctions et rechercher par exemple la
fonction 0x804844c. De même, les fonctions standard ajoutées par le compilateur sont affichées avec les fonctions définies dans le programme sans aucun
moyen de distinction aisée.
graphELF, contrairement à objdump, analyse le code afin de construire
des blocs constituant ces fonctions. De plus, graphELF continue à afficher les
noms des fonctions appelées dans les librairies dynamiques tel que l’appel à
la fonction printf à l’adresse 8048476. Rechercher la fonction 0x804844c
devient très simple puisqu’elle est écrite distinctement comme le montre
l’exemple précédent.
De plus, objdump ne peut lire de fichier dont les en-têtes de section ont été
supprimés, contrairement à graphELF qui analyse alors les segments pour
retrouver les informations nécessaires au désassemblage.
Notons que readelf et objdump peuvent être utilisés de manière complémentaire. Cependant, ces outils ont pour vocation un simple affichage des
données tandis que graphELF permet une analyse des informations et fournit
des graphes complémentaires.
Par ailleurs, les outils LDasm [LDa02] et reap [gru04] sont en fait des interfaces utilisant objdump pour le désassemblage lui-même. Ces outils proposent
une interface graphique permettant d’accéder plus facilement aux informations livrées par objdump. Cependant, ceux-ci n’en gardent pas moins les
mêmes limitations que objdump en ce qui concerne leur incapacité à lire un
fichier dont la table des en-têtes de section est absente. graphELF pour sa
part se verra doté prochainement d’une interface graphique permettant les
mêmes types d’affichage ainsi qu’une navigation entre les différents blocs de
code désassemblé (cf. partie 5).
3.9.2.2
bastard
bastard [Bas04] est un désassembleur qui livre en plus du code désassemblé un certain nombre d’informations sur le fichier ELF. Il repose, tout
comme graphELF, sur la librairie libdisasm (cf. partie 3.7) pour le désassemblage proprement dit.
Il affiche les informations des en-têtes du fichier ELF et donne également la liste des chaînes de caractères trouvées dans les différentes sections
du fichier lu. Cependant, bastard est avant tout un désassembleur et ses
fonctions d’affichage restent donc très limitées.
De même que graphELF, il livre un code désassemblé dont il analyse les
appels comme le montre l’exemple suivant :
113
main:
08048340
08048341
08048343
08048344
08048345
08048348
55
89 E5
52
52
83 E4 F0
E8 43 00 00 00
push
mov
push
push
and
call
0804834D E8 5E 00 00 00
call
08048352
08048353
08048354
08048356
0804835B
50
50
6A 05
68 D8 84 04 08
E8 08 FF FF FF
push
push
push
push
call
08048360
08048361
08048362
08048369
fa:
08048370
08048371
08048373
08048374
08048375
08048378
C9
C3
8D B4 26 00 00 00 00
8D BC 27 00 00 00 00
leave
ret
lea
lea
55
89 E5
53
53
8B 5D 08
E8 33 00 00 00
push
mov
push
push
mov
call
0804837D
08048380
08048381
08048383
08048384
08048385
08048386
08048389
83
59
89
5B
5D
C3
8D
8D
add
pop
mov
pop
pop
ret
lea
lea
C3 07
D8
76 00
BC 27 00 00 00 00
ebp
ebp , esp
edx
edx
esp , 0xF0
fb ;(0x8048390 was +67) ;
xrefs: >08048390[x]
fc ;(0x80483B0 was +94) ;
xrefs: >080483B0[x]
eax
eax
0x5
0x80484D8
printf ;(0x8048268 was -248) ;
xrefs: >08048268[x]
esi , [esi]
edi , [edi]
ebp
ebp , esp
ebx
ebx
ebx , [ebp+0x08]
fc ;(0x80483B0 was +51) ;
xrefs: >080483B0[x]
ebx , 0x7
ecx
eax , ebx
ebx
ebp
esi , [esi]
edi , [edi]
Cet exemple est fourni à partir d’un programme contenant encore sa
table des symboles. Dans ce cas, bastard découpe le code désassemblé en
fonctions. Il effectue également un remplacement des adresses de destination
des instructions call par le nom de la fonction correspondante comme par
exemple pour l’instruction call de l’adresse 0804835B.
Lorsque la table des en-têtes de section est absente, bastard analyse
tout de même le fichier et est capable de rechercher dans le code les blocs
correspondant aux fonctions. Cependant, l’algorithme de recherche de blocs
apparaît nettement moins efficace que celui de graphELF comme le montre
l’exemple suivant :
114
0804833D 90
0804833E 90
0804833F 90
nop
nop
nop
08048340 55
push
ebp
08048341
08048343
08048344
08048345
08048348
89 E5
52
52
83 E4 F0
E8 43 00 00 00
mov
push
push
and
call
0804834D E8 5E 00 00 00
call
08048352
08048353
08048354
08048356
0804835B
50
50
6A 05
68 D8 84 04 08
E8 08 FF FF FF
push
push
push
push
call
ebp , esp
edx
edx
esp , 0xF0
sub_08048390 ;(0x8048390 was +67)
xrefs: >08048390[x]
sub_080483B0 ;(0x80483B0 was +94)
xrefs: >080483B0[x]
eax
eax
0x5
0x80484D8
printf ;(0x8048268 was -248)
xrefs: >08048268[x]
08048360
08048361
08048362
08048369
08048370
08048371
08048373
08048374
08048375
08048378
C9
C3
8D
8D
55
89
53
53
8B
E8
leave
ret
lea
lea
push
mov
push
push
mov
call
0804837D
08048380
08048381
08048383
08048384
08048385
08048386
08048389
83
59
89
5B
5D
C3
8D
8D
B4 26 00 00 00 00
BC 27 00 00 00 00
E5
5D 08
33 00 00 00
C3 07
D8
76 00
BC 27 00 00 00 00
add
pop
mov
pop
pop
ret
lea
lea
esi , [esi]
edi , [edi]
ebp
ebp , esp
ebx
ebx
ebx , [ebp+0x08]
sub_080483B0 ;(0x80483B0 was +51)
xrefs: >080483B0[x]
ebx , 0x7
ecx
eax , ebx
ebx
ebp
esi , [esi]
edi , [edi]
; -------------------------- Subroutine sub_08048390
sub_08048390:
08048390 7F
push
ebp
; xrefs: <08048348[x]
08048391 89 E5
mov
ebp , esp
08048393 83 EC 14
sub
esp , 0x14
08048396 68 E7 84 04 08
push
0x80484E7
0804839B E8 C8 FE FF FF
call
printf ;(0x8048268 was -312)
xrefs: >08048268[x]
080483A0 C9
leave
080483A1 C3
ret
080483A2 8D B4 26 00 00 00 00
lea
esi , [esi]
080483A9 8D BC 27 00 00 00 00
lea
edi , [edi]
115
Dans cet exemple, la fonction main débute à l’adresse 08048340 mais
n’est pas identifiée comme un début de bloc (marquée par l’indication
Subroutine). La fonction fa qui débute à l’adresse 8048370 n’est pas non
plus identifiée comme début de bloc. Ceci est dû au fait que dans l’exemple
pris en considération, la fonction fa qui suit la fonction main n’est en réalité
pas appelée dans le programme et ne fait donc pas l’objet d’une cible de
destination pour une instruction call.
graphELF pour sa part détecte tout de même cette fonction puisqu’il
analyse non seulement la destination des instructions call mais également
les lignes de code caractéristiques d’un début de fonction (voir partie 3.5.2)
comme le montre le même programme désassemblé par graphELF :
fonction 0x8048340 adresse 8048340
8048340 55
8048341 89 E5
8048343 52
8048344 52
8048345 83 E4 F0
8048348 E8 43 00 00 00
804834D E8 5E 00 00 00
8048352 50
8048353 50
8048354 6A 05
8048356 68 D8 84 04 08
804835B E8 08 FF FF FF
8048360 C9
8048361 C3
8048362 8D B4 26 00 00 00 00
8048369 8D BC 27 00 00 00 00
push ebp
mov ebp, esp
push edx
push edx
and esp, 0xF0
call 0x8048390
call 0x80483b0
push eax
push eax
push 0x5
push 0x80484D8
call printf
leave
ret
lea esi, [esi]
lea edi, [edi]
fonction 0x8048370 adresse 8048370
8048370 55
8048371 89 E5
8048373 53
8048374 53
8048375 8B 5D 08
8048378 E8 33 00 00 00
804837D 83 C3 07
8048380 59
8048381 89 D8
8048383 5B
8048384 5D
8048385 C3
8048386 8D 76 00
8048389 8D BC 27 00 00 00 00
push
mov
push
push
mov
call
add
pop
mov
pop
pop
ret
lea
lea
ebp
ebp, esp
ebx
ebx
ebx, [ebp+08]
0x80483b0
ebx, 0x7
ecx
eax, ebx
ebx
ebp
esi, [esi]
edi, [edi]
L’exemple de désassemblage effectué par bastard montre ainsi que lors-
116
qu’une fonction n’est pas appelée, ce dernier n’est pas en mesure de fournir
un découpage correct du code en fonctions.
bastard est un logiciel encore en cours de développement, qui a pour
vocation avant tout de désassembler le code et de fournir une aide pour la
compréhension de celui-ci. Cependant, l’aide fournie par bastard est encore
très limitée : l’analyse du code afin de déterminer les différentes fonctions
contenues dans celui-ci reste inachevée. graphELF pour sa part effectue ce
découpage de manière correcte et offre un niveau d’abstraction supplémentaire : il fournit le code désassemblé fonction par fonction et découpe chaque
fonction elle-même en blocs rendant compte du flot de contrôle de chaque
fonction.
3.9.2.3
Quelques autres désassembleurs
En plus des deux outils décrits précédemment, il existe d’autres désassembleurs conçus pour les fichiers ELF :
– le désassembleur biew [GBK04] . Il livre le code sans aucune indication
supplémentaire. Il lit le code dont la table des en-têtes de section est
absente mais il ne livre aucune information sur le fichier, les fonctions
ou les librairies appelées. Il ne livre que du code désassemblé.
– le désassembleur fenris [RAZ02]. Il utilise la même librairie de désassemblage que graphELF, libdisasm et fournit un code désassemblé.
Cependant, il ne fonctionne pas avec toutes les distributions Linux et
reste une fois encore un simple désassembleur.
– le désassembleur Boomerang [MEW04]. Il s’agit d’un désassembleur qui
en est encore à ses toutes premières versions. Il s’installe difficilement
et reste encore limité.
– le décompilateur rec [REC00]. C’est un outil ayant pour but de traduire le code exécutable en langage de haut niveau. Cependant, rec
atteint très rapidement ses limites et la majorité des instructions restent affichées en langage assembleur. Il peut donc être classé dans la
catégorie des désassembleurs. De plus, les sources de rec n’étant pas
disponibles, il est difficile de comprendre le fonctionnement de l’outil
et les moyens de vérification de l’exactitude des résultats sont trop peu
nombreux pour utiliser cet outil ou se baser sur ses résultats.
3.9.3
Les outils générateurs de graphes d’appels de fonction
Nous nous intéressons ici aux outils flowgraph et bin2graph.
117
3.9.3.1
flowgraph
flowgraph [Ces02] est un outil d’analyse de fichier binaire au format
ELF qui genère un graphe des appels de fonction ainsi qu’un graphe représentant le flot de contrôle du programme. Les exemples [Ces02] 3.19 et 3.20 7
nous fournissent deux graphes analysant le programme gg dont voici le code
source :
Le programme GG
struct A {
int a;
char b[300];
int c;
char d;
};
static int v;
int f1(int a)
{
printf("Hi %i\n", a);
}
int f0()
{
printf("hi there\n");
}
int f4(int a, int b, int c, int d)
{
int i;
for (i = 0; i < 10; i++)
printf("hi %i %i %i %i\n", a, b, c, d);
}
int g(struct A *a)
{
char b[300];
strcpy(b, "hi");
f0(1,2,3);
f1(1);
}
7. Nous regrettons de ne pas avoir pu tester cet outil, des bogues importants résidant
dans celui-ci en empêchant le fonctionnement. Bien qu’en cours de développement, nous
remarquons également que cela fait plusieurs années que cet outil n’a pas évolué.
118
int main(int argc, char *argv[])
{
struct A a;
a.a = 12;
memset(a.b, 0, sizeof(a.b));
a.c = 5;
a.d = 10;
g(&a);
}
Figure 3.19 – Le graphe des appels de fonctions de GG
Tout comme graphELF, cet outil se base sur graphviz pour construire
le graphe. Il utilise le désassembleur objdump pour lire le fichier. Ainsi,
flowgraph possède les mêmes limitations que objdump et ne peut fonctionner
lorsque les en-têtes de section sont absents.
Le graphe des appels de fonctions fourni par flowgraph correspond à
celui fourni par graphELF (voir figure 3.22) à l’exception des appels auxfonctions f1 et strcpy qui ne sont pas représentés dans la figure 3.19. On
peut toutefois remarquer que les fonctions standards ajoutées par le compilateur sont systématiquement présentes dans le graphe tandis que notre outil
0x080482a3
0x080482a9
0x080482af
0x080482b1
je
0x080482b3
0x080482b5
0x080482b8
0x080482bd
libc.so.6
0x080482c2
Figure 3.20 – Le graphe de flot de contrôle de GG
PROGRAM
0x080482c5
_start
0x080482c6
0x08048320
_fini
0x08048322
0x08048500
0x08048488
0x08048298
0x08048323
0x08048501
0x08048489
0x08048299
0x08048325
0x08048503
0x0804848b
0x08048504
0x08048491
0x0804829c
0x08048505
0x08048492
0x0804829d
0x0804832a
0x0804850a
0x08048499
0x0804832b
0x0804850b
0x08048330
0x08048511
0x08048335
0x08048514
0x08048328
0x08048329
PROCEDURE_0x0804850a
main
_init
__do_global_ctors_aux
frame_dummy
0x080484d0
0x0804829b
0x080483a8
0x080484d1
0x080483a9
0x080484d3
PROCEDURE_0x080482a2
0x080483ab
0x080484d6
0x080483ae
0x080484d7
0x080483b3
0x0804849c
0x080484dc
0x080483b5
0x080484a2
0x080484e3
0x080484a4
0x080484e5
0x080482a2
0x080483b7
0x080483ba
je
0x08048336
__do_global_dtors_aux
0x08048519
0x080484a5
0x08048337
0x08048350
0x0804851c
0x080484aa
0x0804833c
0x08048351
0x08048341
0x0804851d
0x080484e7
je
0x080484ac
jne
0x080484e9
0x080484ec
__libc_start_main
0x08048353
0x080484b3
0x080484ef
0x080482f8
0x08048356
0x080484b7
0x080484f1
0x0804835d
0x080484ba
0x0804835f
0x080483c9
0x080483ca
__register_frame_info
0x080482d8
0x080484f2
0x080484c0
0x08048373
0x080483bf
0x080483c4
0x080484f3
0x080484c1
0x08048378
g
0x0804837b
0x080484c6
0x0804844c
0x080484cc
jne
0x08048361
0x0804837d
0x08048366
0x08048382
0x0804844d
0x080484cd
0x0804844f
jne
0x08048369
0x0804836f
0x08048371
0x08048384
0x08048456
0x08048386
0x08048389
0x0804845c
je
0x0804838e
0x08048463
0x08048468
__deregister_frame_info
0x08048393
0x0804846e
0x080482e8
0x0804839a
0x08048471
0x0804839d
0x0804839e
0x08048473
0x08048475
0x08048477
0x0804847c
0x0804847f
0x08048481
0x08048486
0x08048487
f1
f0
0x080483e0
0x080483e1
0x080483f9
0x080483e3
0x080483fb
0x080483e6
0x080483fe
0x080483e9
0x08048401
0x080483ec
0x08048406
0x0804840b
0x0804840c
0x080483f1
printf
0x08048308
0x080483f6
0x080483f7
119
0x080483f8
120
propose cet affichage en option.
Le graphe du flot de contrôle de la figure 3.20 représente le flot de contrôle
du programme dans son intégralité : il contient toutes les fonctions du programme, y compris les fonctions standard d’initialisation et de fin de programme. En revanche, la fonction f4 n’y est pas représentée, celle-ci n’étant
appelée par aucune autre fonction. Chaque fonction est contenue dans un
rectangle. Les noeuds représentés à l’intérieur correspondent à chaque instruction assembleur — à chaque ligne de code — de la fonction. Ainsi, toutes
les lignes sont représentées par leur adresse mais pas par leur contenu, ce qui
est inutile pour la compréhension du programme et alourdit considérablement la taille du graphe. Seuls les noeuds correspondant à un branchement
ou une interruption dans le flot d’exécution du programme sont utiles.
Cet exemple nous montre que flowgraph construit un graphe déjà conséquent lorsqu’il s’agit d’un petit programme et donc inexploitable dans le cas
d’un programme de taille importante. graphELF ne retient que les noeuds
correspondant aux points de départ des branchements et fournit un graphe
par fonction, ce qui repousse considérablement la limitation due à la taille
du graphe. De plus, il peut fournir également un graphe contenant directement le code découpé en blocs permettant d’analyser directement le code
désassemblé en fonction du flot de contrôle de la fonction.
3.9.3.2
bin2graph
Tout comme flowgraph, bin2graph [Bio03] est un outil qui construit un
graphe des appels de fonction à partir d’un fichier au format ELF. bin2graph
constitue un outil léger et rapide. Ecrit en python, il se compose d’un seul
fichier qui s’exécute rapidement. Il se base sur les outils readelf et objdump
pour lire le fichier, ce qui lui impose cependant les mêmes limitations que
l’outil précédent quant à la lecture des programmes ne contenant plus d’entêtes de section.
L’exemple de la figure 3.21 représente le graphe du flot de contrôle du
programme gg. Tout comme pour flowgraph, chaque fonction du programme
est représentée dans un rectangle. Les noeuds contenus dans ceux-ci correspondent aux points de départ des branchements et interruptions de flot de
la fonction. Dans l’exemple, la fonction f4 n’apparaît pas car elle n’est pas
appelée dans le programme. bin2graph ne prend donc pas en considération
ce type de fonction contrairement à graphELF qui les détecte et les analyse
tout de même.
La principale limitation de l’outil réside dans la lisibilité du graphe fourni.
Des informations telles que les fonctions d’initialisation et de fin ajoutées par
le compilateur se mêlent au programme lui-même alors qu’une distinction
call_gmon_start 9999999 0x8048328L 9999999 __do_global_dtors_aux 9999999 frame_dummy 9999999
g 9999999
_start
call_gmon_start
0x8048328L
__do_global_dtors_aux
frame_dummy
g
call __libc_start_main
call 0x804830DL
0x804832CL
0x804833CL
0x804837DL
call strcpy
f4 9999999
0x80484A4L 9999999
call printf
f4
0x8048405L
__libc_csu_fini 9999999
__libc_csu_init 9999999
0x80484A4L
__libc_csu_fini
__libc_csu_init
0x80484ACL
call __i686.get_pc_thunk.bx
call __i686.get_pc_thunk.bx
0x8048554L 9999999
0x804830DL
0x8048347L
0x8048386L
call f0
0x80483DCL
main 9999999
0x804831DL
0x8048350L
call __gmon_start__
main
0x8048508L 9999999 0x8048574L 9999999 __do_global_ctors_aux 9999999
0x8048508L
0x8048574L
__do_global_ctors_aux
0x804857CL
0x8048592L
__i686.get_pc_thunk.bx 9999999
0x804855CL
0x8048540L
__i686.get_pc_thunk.bx
call _init
0x8048594L
0x8048554L
call _fini
0x8048573L
0x80484E4L
0x804859EL
f0 9999999
call f1
f0
0x80483E0L
call _fini
f1
call printf
0x80483E2L
0x804856FL
0x80483CEL
f1 9999999
0x8048321L
0x8048324L
0x8048363L
0x8048365L
0x804836CL
0x804836DL
0x8048398L
call memset
0x8048448L
0x8048399L
call g
call printf
0x80484A0L
0x80483B6L
0x80483E4L
0x8048407L
0x8048408L
0x8048553L
0x80484F0L
0x80485A0L
0x80484F9L
0x80485A3L
0x80484FBL
0x8048507L
121
Figure 3.21 – Le graphe de flot de contrôle de GG généré par bin2graph.
_start 9999999
122
main
g
f4
strcpy
f0
memset
f1
printf
Figure 3.22 – Le graphe des appels de fonctions de GG généré par graphELF.
aurait sans doute apporté plus de clarté. graphELF, pour sa part, donne la
possibilité d’adjoindre ou non ces fonctions dans le graphe.
De plus, bin2graph ne fournit pas de graphe représentant uniquement
les appels de fonction. grapheELF distingue le graphe des appels de fonction,
des graphes décomposant chaque fonction. Cela fournit un graphe des appels
de fonction plus simple et plus lisible comme le montre la figure 3.22.
Ces deux outils, flowgraph et bin2graph, reposent sur une utilisation
d’outils déjà existants et sont donc soumis aux mêmes limitations que ces
derniers. graphELF pour sa part possède ses propres algorithmes de lecture
du fichier et de désassemblage ce qui lui permet d’analyser un plus grand
nombre de programmes. De plus, graphELF fournit des graphes supplémentaires reproduisant les blocs de code désassemblé selon le flot de contrôle
et non plus simplement les noeuds correspondant aux points de départ des
branchements.
123
3.9.4
elfsh : un outil complet
elfsh [Elf04] est actuellement l’outil le plus performant disponible sur
plate-forme Linux, bien que toujours en cours de développement 8 . Le but
de ce programme est de créer un véritable environnement interactif dédié
au format ELF et à l’analyse du code désassemblé d’un programme pour
un usage orienté sécurité informatique. Pour ce faire, il intègre en plus des
fonctions permettant une modification directe du code exécutable et offre la
possibilité d’y adjoindre ses propres modules.
De plus, les affichages fournis par elfsh sont mis en page de manière
lisible et une analyse préalable est effectuée pour relier entre elles différentes
informations, tel que le montre l’exemple suivant :
[076] (nil)
[085] 0x80494fc
[086] 0x80484d0
[090] 0x8048390
[091] 0x8048230
[092] 0x8048280
[096] 0x8048340
[097] 0x80483b0
[101] 0x8048268
FILENAME initfini.c size:0000000000 foffset:000000
scope:Local sctndx:65521 => (NULL)
VARIABLE _DYNAMIC size:4294963156 foffset:001276
scope:Global sctndx:17 => .dynamic
VARIABLE _fp_hw
size:0000000004 foffset:001232
scope:Global sctndx:14 => .rodata
FUNCTION fb
size:0000000018 foffset:000912
scope:Global sctndx:12 => .text + 272
FUNCTION _init size:0000000080 foffset:000560
scope:Global sctndx:10 => .init
FUNCTION _start size:0000000000 foffset:000640
scope:Global sctndx:12 => .text
FUNCTION main
size:0000000034 foffset:000832
scope:Global sctndx:12 => .text + 192
FUNCTION fc
size:0000000005 foffset:000944
scope:Global sctndx:12 => .text + 304
FUNCTION printf@@GLIBC_2.0 size:0000000054 foffset:000616
scope:Global sctndx:00 => .plt + 32
Cet exemple affiche un extrait de la table des symboles analysé par elfsh.
On y retrouve le nom des symboles avec leur adresse, leur type, leur taille,
leur offset dans le fichier ELF mais également la section dans laquelle se situe
le symbole. La fonction fb par exemple a pour adresse virtuelle 0x8048390,
elle est de type FUNCTION, a pour taille 0018 et se situe à l’offset 000912
dans le fichier. De plus, elle est définie dans la section .text en position
.text + 272.
La partie désassemblage de elfsh repose tout comme graphELF sur l’utilisation d’une librairie lui traduisant le code machine en instructions assembleur. Voici un extrait de code désassemblé par elfsh :
8. La version disponible à ce jour est la version 0.51b3.
124
080483C4
080483C5
080483C7
080483CA
080483CD
080483D2
080483D4
080483D7
080483D9
080483DE
080483E1
080483E4
080483E6
080483EB
080483EE
080483F3
080483F7
080483F9
080483FE
08048401
08048403
08048408
0804840B
0804840C
[foff:
[foff:
[foff:
[foff:
[foff:
[foff:
[foff:
[foff:
[foff:
[foff:
[foff:
[foff:
[foff:
[foff:
[foff:
[foff:
[foff:
[foff:
[foff:
[foff:
[foff:
[foff:
[foff:
[foff:
964] main + 0
965] main + 1
967] main + 3
970] main + 6
973] main + 9
978] main + 14
980] main + 16
983] main + 19
985] main + 21
990] main + 26
993] main + 29
996] main + 32
998] main + 34
1003] main + 39
1006] main + 42
1011] main + 47
1015] main + 51
1017] main + 53
1022] main + 58
1025] main + 61
1027] main + 63
1032] main + 68
1035] main + 71
1036] main + 72
08048398 [foff: 920] fonctionC + 0
..................
080483C2 [foff: 962] fonctionC + 42
push
mov
sub
and
mov
sub
sub
push
call
add
sub
push
call
add
mov
cmp
js
mov
mov
jmp
call
mov
leave
ret
%ebp
%esp,%ebp
$8,%esp
$FFFFFFF0,%esp
$0,%eax
%eax,%esp
$C,%esp
$0
<fonctionA>
$10,%esp
$C,%esp
$0
<fonctionB>
$10,%esp
%eax,<globalI>
$0,FFFFFFFC(%ebp)
<main + 63>
<globalI>,%eax
%eax,FFFFFFF8(%ebp)
<main + 68>
<fonctionC>
FFFFFFF8(%ebp),%eax
55
89
83
83
B8
29
83
6A
E8
83
83
6A
E8
83
A3
83
78
A1
89
EB
E8
8B
C9
C3
push
%ebp
55
ret
E5
EC
E4
00
C4
EC
00
5E
C4
EC
00
73
C4
78
7D
0A
78
45
05
90
45
C3
Les adresses de destination des appels de fonction ont été remplacées par
les noms des fonctions. Des informations ont été ajoutées afin de situer les
lignes de code : chaque ligne commence par l’adresse de l’instruction et est
suivie par sa position dans le fichier ainsi que sa position dans la fonction.
Ces renseignements ne sont toutefois pas indispensables à la compréhension
du code et alourdissent parfois la lecture d’une ligne d’instruction.
La modification du code, pour sa part, en est encore au stade du développement. Les fonctionnalités implémentées, détaillées dans [May03] ne sont
pas toujours fiables. Les fonctionnalités de suppression et de reconstruction
de la table des en-têtes de section par exemple ne fonctionneent pas dans
tous les cas de figure.
elfsh possède une interface et offre la possibilité d’être étendu en lui
ajoutant des modules ce dont ne dispose pas encore notre outil, graphELF.
De plus, il intègre la possibilité de modifier le code désassemblé de manière à
ce que le programme puisse s’exécuter une fois les modifications effectuées :
il s’agit du point fort de cet outil qui est centré sur la modification de code et
sur l’accessibilité du code désassemblé grâce à son interface. Il fournit ainsi
une aide à un expert capable de lire des lignes de code désassemblé mais ne
08
F0
00 00 00
0C
FF FF FF
10
0C
FF FF FF
10
95 04 08
FC 00
95 04 08
F8
FF FF FF
F8
125
lui propose pas directement d’outils de recherche automatique de vulnérabilités dans un programme ni même d’indications lui permettant d’orienter ses
investigations. Il laisse cette tâche aux soin de l’utilisateur en lui donnant la
possibilité de développer ses propres modules.
Ainsi, en terme de sécurité informatique, graphELF a d’ores et déjà pris
une orientation différente, se centrant plus sur l’analyse des résultats et sur la
possibilité de détecter de manière automatique les fragilités d’un programme
ou du moins d’orienter les recherches. De plus, graphELF fournit des graphes
que elfsh ne fournit pas encore même s’il donne la possibilité de développer
un module pouvant génerer ces graphes.
3.9.5
Conclusions
Les outils capables de manipuler des fichiers au format ELF sont pour
la plupart en cours de développement et gardent encore de nombreuses faiblesses. Nous constatons également que si de nombreux projets dans ce domaine sont annoncés, très peu sont développés de manière continue et finalement, il n’existe que quelques outils suffisamment développés pour être
utilisables aisément.
Certains outils tels que readelf ont pour vocation un simple affichage du
contenu du fichier, tandis que d’autres tels que bastard s’orientent nettement
plus vers le désassemblage avec des options plus ou moins conséquentes selon
l’état d’avancement du projet. Les outils tels que bin2graph construisent des
graphes.
graphELF pour sa part, est un outil complet : il se veut à la fois un
outil d’affichage des données d’un fichier au format ELF, d’affichage du code
désassemblé et d’aide à sa compréhension par l’intermédiaire des graphes
qu’il fournit. Seul elfsh semble avoir pris cette même direction, avec de surcroit des modules intégrables à la demande et des fonctions de modification
du code.
Cependant, graphELF apporte sa contribution dans un domaine supplémentaire : il a pour but principal d’aider à la recherche de faiblesses dans
un exécutable et fournit une analyse et une interprétation des données qui
font défaut aux autres outils. Le but de notre recherche est avant tout de
parvenir à automatiser un certain nombre de tâches dans la recherche de
failles de sécurité.
126
Chapitre 4
Discussion, évolution
Dans ce chapitre nous discutons dans la partie 4.1 des possibilités d’apport de notre outil dans différents domaines de recherche. La partie 4.2 se
compose d’exemples concrets d’application de notre outil qui viennent étayer
nos propos.
4.1
Discussion - Evolution
Cette partie explique comment notre outil, graphELF, apporte une aide
dans différents domaines détaillés dans cette partie :
– la modification de programmes propriétaires (partie 4.1.1)
– la recherche de failles de sécurité dans un programme (partie 4.1.2) :
les débordements de tampon (partie 4.1.2.1) et les bogues de format
(partie 4.1.2.2) en particulier.
– la détection de fonctionnalités cachées dans un programme (partie
4.1.3)
– le développement de logiciels ayant besoin de communiquer avec un
logiciel propriétaire (partie 4.1.4)
– la détection de virus (partie 4.1.5).
4.1.1
Modifications d’un programme propriétaire
Lorsque l’on exploite un logiciel propriétaire 1 , on ne dispose jamais de
ses sources. Comment savoir alors s’il ne représente pas une menace pour
la sécurité du système informatique ? En effet, régulièrement des failles de
sécurité sont découvertes, quel que soit le système d’exploitation, et il est
1. Nous rappelons que ce terme est utilisé pour désigner un logiciel dont on ne dispose
pas des sources.
127
128
nécessaire d’effectuer les mises à jour adéquates pour éviter toute attaque
exploitant celles-ci.
Parfois, le seul moyen de sécuriser son système consiste alors à modifier le code du programme utilisant des fonctionnalités non sécurisées. Or,
lorsqu’il s’agit d’un logiciel propriétaire, seul le propriétaire du programme
peut le modifier en conséquence. Et, lorsque celui-ci n’existe plus ou encore
lorsqu’il ne maintient plus ce logiciel, il ne reste qu’une possibilité : modifier
directement le programme binaire puisque l’on ne dispose pas de ses sources.
graphELF apporte alors une aide précieuse dans la mesure où il fournit
des renseignements permettant de comprendre le programme et plus encore,
de situer précisément les lignes d’assembleur à modifier au moyen des graphes
et des découpages du code désassemblé du programme. Nous envisageons
d’ailleurs un développement de cette aide en implémentant une interface
graphique et en intégrant un module d’analyse de flux de données.
D’autres fois, pour remédier aux failles de sécurité d’un programme, il
faut par contre remplacer le code d’une fonction par une nouvelle définition de celle-ci enregistrée dans un fichier externe. On dispose alors de deux
approches [May03] : lier le programme avec un fichier objet relocalisable
ou appeler des fonctions définies dans une librairie dynamique. La première
technique consiste à écrire les modifications dans un fichier relocalisable (un
.o) et à modifier ensuite les sections et les segments du programme ainsi que
leurs en-têtes pour intégrer ces modifications : on lie manuellement le fichier
et le programme de la même manière que l’éditeur de liens statique l’aurait
fait. La seconde technique consiste à écrire les modifications sous forme de
fonctions définies dans une librairie dynamique : la table PLT du programme
doit ensuite être modifiée pour exécuter les appels de fonction dynamique
adéquats.
graphELF, en analysant la structure d’un fichier exécutable et en affichant son contenu, permet de situer les endroits où un ajout de code est
possible ou encore de cibler les modifications à apporter à la table PLT.
4.1.2
Recherche de failles dans un programme : l’audit de
sécurité
Les programmes propriétaires n’échappant pas aux failles de sécurité,
il est souvent indispensable, lorsque l’on utilise un tel programme, d’effectuer un audit sur ce programme afin de garantir la sécurité de son système
informatique.
A la manière d’un audit classique, l’auditeur examine le listing du code
désassemblé du programme, à la recherche d’appels de fonctions réputées
pour leurs problèmes de sécurité [Bru04]. L’auditeur concentre ses recherches
129
vers les parties du code traitant les données dynamiques 2 ou encore les
points sensibles susceptibles de provoquer des débordements de tampon et
des bogues de format. Ces vulnérabilités sont, en effet, les principales cibles
des attaques de programmes et constituent la moitié des failles recensées
actuellement [WK02].
Pour effectuer un audit de sécurité sur un programme propriétaire, l’auditeur se voit donc contraint de lire le code assembleur comme il pourrait le
faire avec un code source. Ceci pose un problème majeur : cet audit demande
à être mené par un auditeur expert en langage assembleur et même effectuée
par une telle personne, cette tâche s’avère souvent extrêmement longue. En
revanche, elle permet de détecter les problèmes les plus complexes.
Pour assister un auditeur dans ce type de tâche, il est donc souhaitable
de développer des outils favorisant la compréhension du code assembleur et
automatisant certaines recherches afin d’obtenir un gain de temps. L’objectif
est de proposer une aide fiable qui permettra alors à l’auditeur de concentrer
ses recherches [CWE02] uniquement sur des points particuliers et non sur
l’intégralité du code. Les désassembleurs — qui fournissent le code désassemblé du programme — intègrent, dans ce but, de plus en plus de fonctionnalités visant à aider à la compréhension du listing fourni. Le désassembleur
le plus fiable et le plus utilisé actuellement est IDApro [IDA04], un logiciel
commercial qui intègre des options permettant de comprendre plus facilement le code désassemblé et qui permet également d’automatiser un certain
nombre de recherches dans les lignes de code. L’auditeur dispose également
de débogueurs pour l’aider dans ses recherches. Ceux-ci lui permettent de
tracer l’exécution du programme 3 .
graphELF fournit également une aide dans ce contexte d’audit de sécurité, puisqu’il construit des graphes facilitant la lecture et la compréhension
du code. De plus, il fournit d’ores et déjà des informations plus ciblées telles
que la liste des fonctions peu sûres utilisées par le programme. Ces fonctions
sont mises en évidence sur les graphes, ce qui permet de déterminer l’endroit
où des appels à ces fonctions sont effectués, sans avoir à parcourir tout le
code désassemblé. A terme, graphELF se verra doté de nouvelles fonctionnalités visant à augmenter l’aide fournie en affinant, notamment, la recherche
des débordements de tampon ou des bogues de format selon les principes
expliqués ci-après.
2. A savoir les données pouvant être entrées par l’utilisateur [Bru04]
3. On distingue ici les débogueurs binaires qui fonctionnent sans aucune information
supplémentaire des débogueurs symboliques qui nécessitent des informations supplémentaires pour être utilisés correctement. Ces informations sont contenues dans les sections
.debug et sont ajoutées dans le programme en utilisant l’option -g du compilateur gcc.
IDApro est d’ailleurs également un débogueuer binaire.
130
4.1.2.1
Les débordements de tampon
Le principe d’un débordement de tampon est d’écrire une donnée dans
un tampon trop petit, la donnée dépassant alors la zone mémoire qui lui
était allouée et écrasant des zones mémoires non autorisées. L’exploitation
de ces débordements à des fins douteuses peut engendrer des dégâts au sein
d’un système informatique et constitue la vulnérabilité la plus fréquente et
la plus dangeureuse [CWP+ 99].
Une exploitation de débordement de tampon est caractérisée par deux
points essentiels [WK03, BST00] :
– l’injection, généralement dans la pile, d’un code malicieux dans le programme vulnérable : le shellcode 4 .
– un détournement du flot de contrôle du programme qui permet à l’attaquant d’exécuter ce code malicieux (voir ci-dessous).
Un exemple fréquent d’attaque revient à détourner le programme pour
exécuter un shell et permettre à l’attaquant d’exécuter ensuite ses propres
commandes à distance. Le shell possède alors les mêmes droits d’accès que le
programme détourné : si ce dernier possède des droits administrateur alors
l’attaquant a accès à l’intégralité de la machine [WK03, CWP+ 99].
Le détournement du flot de contrôle du programme est effectué soit en
modifiant le contenu de la mémoire — plus exactement de la pile — par
l’intermédiaire des données fournies et enregistrées dans les tampons, soit en
trompant l’exécution d’une fonction vulnérable qui écrit elle-même dans la
mémoire et altère les données de contrôle d’exécution du programme.
L’adresse de retour d’une fonction constitue la cible la plus visée : l’attaque de cette adresse nommée stack smashing attack est la plus commune
de toutes les attaques [BST00].
L’exemple suivant montre comment peut être altérée cette adresse de retour si l’on remplit un tampon et que l’on déborde de ce dernier suffisamment
pour écraser cette adresse :
#include <stdio.h>
int main(int argc, char ** argv)
{
char buffer[8];
strcpy(buffer,argv[1]);
}
4. Un shellcode est une suite d’instructions injectées dans le programme cible et exécutées par ce dernier. Ce code manipule directement les registres et est écrit obligatoirement
en instructions machine codées en hexadécimal. Il ne s’agit pas d’exécuter un shell au sens
Unix du terme : le shellcode constitue une enveloppe permettant la prise de contrôle à
distance du processus et l’exécution de commandes à distance.
131
Figure 4.1 – Exemple de débordement de buffer
Le schéma de la figure 4.1 illustre cet exemple. Si le programme est appelé
avec l’argument AAAAAAAABBBB1234 alors la fonction main recevra ce même
argument. Celui-ci est stocké sur la pile lors de l’appel de la fonction. Lors
de l’exécution de la fonction, l’argument est copié dans le buffer. Cependant,
celui-ci n’a qu’une taille de 8 octets et il se produit donc un débordement de
tampon : les 8 premiers octets de l’argument sont copiés dans le buffer et le
reste écrase les données suivantes :
– l’adresse du pointeur de base de l’appelant est remplacée par les caractères BBBB
– l’adresse de retour est remplacée par les caractères 1234 5
5. 42 correspond à B en hexadécimal et l’adresse 0x34333231 correspond à 1234 codé
132
– le premier caractère de l’argument ne contient plus le caractère A mais
le symbole \0.
Ainsi, après l’exécution de la fonction appelée, le programme ne continuera pas à l’endroit prévu mais interprétera le contenu de l’adresse de retour comme étant une adresse et essaiera de se brancher à cet endroit, ce
qui, vraissemblablement, dans notre cas, provoquera un arrêt anormal du
programme.
Pour exploiter une telle vunérabilité, l’attaquant doit tout d’abord copier
sur la pile un shellcode tel que celui-ci :
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
et remplacer l’adresse de retour de la fonction par l’adresse qui référence
ce shellcode sur la pile. Le tableau shellcode correspond à l’instruction C
suivante : exec("/bin/sh"). Cette instruction, écrite en hexadécimal, prendra le contrôle du programme après l’exécution de la fonction appelée et
exécutera un shell.
Les parades existantes
De nombreux programmes tentent de pallier ce problème de différentes
manières, statiques [DW01] ou dynamiques :
– StackGuard [CPM+ 98, BG03], par exemple, est un outil qui injecte
dans le code des drapeaux de contrôle d’écriture dans la mémoire et
qui stoppe le programme en cas de tentative d’écrasement de l’adresse
de retour du call. Cependant, le programme doit être compilé avec
StackGuard pour fonctionner. Ainsi, cet outil ne peut être utilisé
lorsque l’on ne dispose pas des sources du programme vulnérable.
– La librairie Libsafe [BST00] intercepte les fonctions dites sensibles et
analyse leurs arguments de manière à empêcher les risques d’écriture
en dehors du tampon. Cette méthode implémentée sous forme d’une
librairie dynamique ne demande pas la recompilation du programme
mais n’intercepte qu’un ensemble prédéfini de fonctions.
– Une technique [BST00] consiste à copier l’adresse de retour de la fonction avant son appel et de comparer les valeurs de retour avec celles
copiées. Si les adresses sont différentes, l’exécution du programme est
interrompue. La aussi, il faut disposer des sources du programme pour
exploiter cette méthode et recompiler le programme.
en little endian.
133
– Une autre méthode s’intéresse non pas aux fichiers mais à la pile en
rendant cette dernière non exécutable [RD03]. Ainsi, puisque l’attaquant écrit son shellcode sur la pile, celle-ci n’étant plus exécutable,
une erreur sera générée 6 lorsque ce shellcode tentera de s’exécuter. Cependant, comme le montrent Baratloo et al. [BST00] et Raynal et al.
[RD03] une variante de l’exploitation de débordement de tampon peut
aisément contourner cette protection.
Les méthodes statiques essaient de prévoir les attaques et de corriger les
failles tandis que les méthodes dynamiques détectent l’intrusion quand elle a
lieu et stoppent l’exécution du programme [LE01]. Haugh [HB03] montre
que les techniques dynamiques fournissent de meilleurs résultats que les
techniques statiques basées sur l’analyse du code source. Prasad et Chiueh
[PcC03] pour leur part, montrent que les outils les plus efficaces contre les
débordements de tampon sont des extensions du compilateur. Ces derniers
nécessitant de recompiler le programme avec ces outils, il faut donc posséder
les sources du programme pour les utiliser.
Ainsi, lorsque le seul matériel utilisable reste le programme binaire luimême, ces outils, ainsi que les techniques d’analyse statique ne sont pas applicables. De plus, les différents systèmes proposés ne suppriment pas tous les
risques et restent contournables comme le montrent Wilander et al. [WK03],
Cowan et al.[CWP+ 99] et Bailleux et al. [BG03]. Ceci rend donc nécessaire,
lors d’un audit de sécurité la recherche de débordements de tampon de manière à les supprimer avant même l’exécution du programme.
Les recherches de débordements de tampon dans un fichier binaire
Quelques méthodes s’appliquant directement sur les fichiers binaires
existent. RAD [PcC03], par exemple, est un outil qui modifie le programme
exécutable en insérant, à la manière de StackGuard (cf. partie 4.1.2.1), des
séquences de code avant et après l’appel d’une fonction afin de vérifier si
des valeurs ont été modifiées. La technique est implémentée avec des fichiers
au format PE 7 . Dans ce cas il s’agit de tester tous les appels de fonction
sans distinction du contenu de ces fonctions. Cette méthode a pour objectif de stopper l’exécution du programme lorsqu’un débordement se produit
et non pas de détecter et corriger les lignes de code pouvant provoquer des
débordements.
Cifuentes propose une approche basée sur une analyse dynamique d’un
fichier binaire et l’implémentation d’un débogueur qui désassemble le code
6. Cette technique empêche cependant un certain nombre de programmes de fonctionner : les programmes s’auto-modifiant, ainsi que les fonctions de traitement de signaux
des systèmes Unix.
7. Il s’agit du format de fichier objet Windows
134
exécutable pour reconstruire le source à partir du binaire et retrouver ainsi
des informations supprimées du programme [CWE02]. Il traduit le langage
assembleur en langage C chaque fois que cela lui est possible afin d’optimiser la compréhension du programme et suggère une interface graphique qui
permettrait d’avoir accès à toutes les informations. Cette approche fournit
une aide à la compréhension du programme mais ne permet pas de recherche
automatique des débordements de tampon.
Nous envisageons pour notre part une autre approche pour détecter ces
débordements. Celle-ci consiste à analyser les appels de fonction afin de
déterminer si cet appel correspond ou non à une faille avérée.
graphELF recense d’ores et déjà les fonctions susceptibles de produire
un débordement de tampon et fournit un ensemble de graphes, supports à
la navigation dans le code désassemblé qui aident à une analyse plus fine de
celui-ci.
Cette liste de fonctions dangeureuses doit encore être affinée avec les
critères d’analyse suivants [Bru04] :
– La fonction fait-elle référence à un buffer local de taille fixe, c’est-à-dire
une variable située sur la pile et donc référencée par l’intermédiaire des
registres esp ou ebp ?
– Si tel est le cas, des contrôles sur la taille des données écrites dans ce
tampon sont-ils effectués ? Peut-on écrire au delà du tampon ? L’analyse devra rechercher des instructions de type cmp par exemple.
– L’utilisateur a-t-il accès à ce tampon ? Les données écrites dans le
tampon sont-elles fournies par l’utilisateur et donc par un éventuel
attaquant ?
Nos recherches s’orientent ainsi vers un moyen de détecter automatiquement
un certain nombre de cas de débordements de tampon, à travers la construction d’une représentation du flux de données du programme et son analyse.
4.1.2.2
Les bogues de format
Lors d’un audit de sécurité, la seconde vulnérabilité recherchée concerne
les failles de type chaîne de format appelées également bogues de format.
Ces bogues engendrent des vulnérabilités tout aussi dangeureuses que les
débordements de tampon. Ils sont liés aux fonctions de lecture et écriture du
langage C qui utilisent un mécanisme de formatage des données [KLA+ 04].
Le type de format souhaité est transmis à ces fonctions par l’intermédiaire
de chaînes de format qui contiennent un signe % suivi du type de conversion
voulu comme par exemple %2X. Voici un exemple d’appel de fonction correct
contenant une chaîne de format :
printf("voici le résultat : %d \n", res);
135
Voici maintenant un exemple d’appel mal construit créant un bogue de
format exploitable :
main(int argc, char *argv[])
{
printf(argv[1]);
printf("\n");
return 0;
}
Ce programme affiche le contenu du premier argument qui lui
est transmis. Cependant, si on appelle le programme avec l’argument
"%x %x %x %x", la première fonction printf est alors appelée de cette manière : printf("%x %x %x %x"). Or dans cet appel, on fournit à printf
quatre types de format mais aucun argument à leur substituer pour l’affichage. Cependant, plutôt que de provoquer une erreur, cet appel fournit un
résultat ressemblant à :
bffff6e4 bffff698 40157fd8 40018420
printf affiche en effet le contenu des quatre mots (4*4 octets) présents
sur la pile, à l’endroit même où devraient se situer les quatre arguments
à afficher comme le montre le schéma de la figure 4.2. Ce type d’appel qui
affiche le contenu de la pile permet d’explorer celle-ci à la recherche d’adresses
intéressantes telle qu’une adresse de retour par exemple [BGR01b].
L’exploitation d’un bogue de format ne consiste pas uniquement à afficher
le contenu de la pile mais également, une fois ces adresses repérées, à y
écrire des données. Pour y parvenir, l’attaquant utilise une chaîne de format
particulière : %n. Cette chaîne sert non plus à afficher un argument selon
un format défini mais à écrire à l’adresse fournie en argument le nombre de
caractères précédant cette chaîne. Voici un exemple tiré de [BGR01b] qui
illustre le fonctionnement de la chaîne de format %n :
#include <stdio.h>
main(){
char *buf = "0123456789";
int n;
printf("%s%n\n",buf, &n);
printf("n = %d\n",n);
}
La première fonction printf affiche le contenu de buf (son second argument), à savoir 0123456789. Cette chaîne comporte dix caractères. Ce
136
Figure 4.2 – Schéma de la pile lors de l’appel de la fonction printf
nombre 10 est donc mémorisé à l’adresse fournie en troisième argument à
printf, à savoir &n. Le second printf affiche le contenu de la variable n :
10.
Si au lieu de mettre la chaîne %n en relation avec un argument, comme
c’est le cas dans l’exemple précédent, la fonction printf est appelée ainsi :
printf("25%n");
alors, printf dépile 4 octets de la pile (la taille d’un pointeur sur un entier)
et utilise cette valeur comme pointeur : la valeur 2 sera donc enregistrée à
137
l’adresse récupérée sur la pile. On peut donc, de cette manière, écrire des
données dans la mémoire du processus.
Une exploitation de ce bogue peut ainsi se dérouler en deux phases : la
première consiste à afficher les différentes adresses écrites sur la pile juqu’à
récupérer une adresse cible tandis que la seconde phase consiste à écraser
cette adresse avec, par exemple, une nouvelle adresse pointant sur un shellcode préalablement chargé en mémoire [BGR01b].
Les moyens de défense
De nombreux outils tels que FormatGuard [CBBKH01] ont été implémentés afin de rechercher ces bogues, mais il s’agit d’outils nécessitant de
recompiler le programme, donc de disposer ici aussi du code source.
Lorsqu’il s’agit de rechercher ces bogues sur un logiciel propriétaire par
exemple, il faut analyser le code désassemblé. Pour parvenir à détecter ces
bogues, Brulez décrit la méthodologie suivante [Bru04] :
– Recenser les fonctions faisant appel à ce type de format.
– Obtenir le nombre de paramètres de la fonction. En effet, lorsqu’un
programmeur néglige le paramètre de format, le nombre de paramètres
passés à la fonction est alors inférieur au nombre attendu.
– Définir si l’erreur est exploitable : un utilisateur peut-il saisir les données passées en argument ?
Nous étudions actuellement une méthode basée sur ces trois points, applicable à graphELF. Notre outil dresse déjà la liste des fonctions faisant
appel à des chaînes de format. Il nous faut donc ensuite affiner cette liste en
ne retenant que les fonctions ayant un nombre d’arguments incorrect.
Prenons, par exemple, le cas de la fonction printf. Lorsqu’il n’y a qu’un
paramètre transmis à la fonction, il faut vérifier s’il s’agit ou non d’une chaîne
de caractères simple, donc d’une donnée située dans la section .rodata. Dans
ce cas, il n’y a pas de risque et la fonction peut être écartée. En revanche, si
l’argument transmis à la fonction est une variable, cela signifie qu’il manque
obligatoirement la chaîne de format et que cette fonction représente une
faille avérée. D’autres fonctions, telles que la fonction sprintf admettent
trois paramètres au minimum. Or si le nombre de paramètres ne correspond
pas, là encore la seule omission possible est la chaîne de format.
Pour déterminer le nombre d’arguments transmis à la fonction, il faut rechercher dans le code désassemblé les arguments mis sur la pile avant l’appel
de la fonction. Voici le code assembleur de l’exemple suivant :
138
main(int argc, char *argv[])
{
printf(argv[1]);
printf("voici le resultat %x\n",argc );
return 0;
}
804833c:
804833d:
804833f:
8048342:
8048345:
804834a:
804834c:
804834f:
8048352:
8048355:
8048357:
804835c:
804835f:
8048362:
8048365:
804836a:
804836f:
8048372:
8048377:
8048378:
55
89
83
83
b8
29
83
8b
83
ff
e8
83
83
ff
68
e8
83
b8
c9
c3
e5
ec
e4
00
c4
ec
45
c0
30
0c
c4
ec
75
98
f9
c4
00
08
f0
00 00 00
0c
0c
04
ff
10
08
08
84
fe
10
00
ff ff
04 08
ff ff
00 00
push
mov
sub
and
mov
sub
sub
mov
add
pushl
call
add
sub
pushl
push
call
add
mov
leave
ret
%ebp
%esp,%ebp
$0x8,%esp
$0xfffffff0,%esp
$0x0,%eax
%eax,%esp
$0xc,%esp
0xc(%ebp),%eax
$0x4,%eax
(%eax)
printf
$0x10,%esp
$0x8,%esp
0x8(%ebp)
$0x8048498
printf
$0x10,%esp
$0x0,%eax
Cet exemple contient deux appels à la fonction printf. Le premier appel
ne possède qu’un argument tandis que le second en contient deux. Le code
assembleur contient une instruction pushl avant le premier appel à printf
et deux instructions push avant le second appel. Ces instructions servent à
placer sur la pile les arguments des fonctions appelées. Ainsi, en comptabilisant ces dernières, nous pouvons déterminer le nombre d’arguments d’une
fonction.
Cependant, il nous faut également tenir compte des options de compilation utilisées. En effet, dans certains cas, en plus des arguments transmis à
la fonction et placés sur la pile, une instruction supplémentaire d’ajustement
de pile peut fausser l’analyse. De plus, les instructions servant à mettre les
arguments sur la pile ne sont pas toujours aussi clairement détectables et
peuvent être plus éloignées dans le code ou écrites sous d’autres formes.
Le troisième point de l’analyse consiste à rechercher si le bogue est exploitable. Pour le déterminer, les analyses à effectuer sont identiques à celles
des débordements de tampon.
139
4.1.3
Fonctionalités cachées d’un programme
Déterminer si le programme étudié fonctionne tel qu’on le désire, lors d’un
audit de sécurité par exemple, devient de plus en plus indispensable. Exécutet-il uniquement ce pour quoi il est prévu ou possède-t-il des fonctionnalités
cachées qui permettraient d’accéder à des informations confidentielles par
exemple ?
Ces méthodes de détournement d’informations sont de plus en plus fréquentes. Des applications comme Word ou encore Acrobat sont d’ailleurs
à l’origine de fuites d’informations [FCD03], simplement par le fait que les
documents qu’ils engendrent possèdent parfois des champs cachés contenant
des informations confidentielles mais accessibles en quelques clics de souris
dans les menus d’édition de ces documents. Un autre type de détournement
est effectué par des logiciels nommés spywares. Ces derniers sont installés
par l’intermédiaire d’un programme hôte sur une machine, à l’insu de l’utilisateur et profitent des connexions internet pour envoyer des données qu’ils
récupèrent sur la machine.
L’analyse du fichier binaire constitue souvent un des moyens à envisager pour répondre à cette question. graphELF peut là encore aider à cette
analyse de par la liste des fonctions externes appelées par le programme
entre autres. En effet, un logiciel de gestion par exemple qui fait appel à des
fonctions système de transfert de données peut légitimement succiter des interrogations : à quoi servent ces transferts, quelles données sont affectées et
vers où le transfert aboutit ?
4.1.4
Aide au développement d’un logiciel
graphELF peut également fournir de l’aide dans le développement et la
modification de logiciels. En effet, il peut arriver qu’une société qui utilise
du code propriétaire sous forme de modules ou de librairies par exemple et
qui souhaihaite faire évoluer son programme doive également faire évoluer ce
code propriétaire. Or, il n’est pas toujours possible de faire modifier ce code
par le propriétaire. Dans ce cas, la société dispose d’un moyen : analyser le
code exécutable pour comprendre comment fonctionne le module et en réimplémenter une partie afin d’y adjoindre les modifications souhaitées. Elle
peut également décider de se séparer de la contrainte liée à l’utilisation de
code propriétaire, bien que certaines contraintes légales doivent être prise
en compte. Il s’agit ici de fournir une aide à l’analyse de l’algorithme du
programme qui, pour sa part, est libre de droits et non pas de copier le code
qui, lui, est protégé.
Il existe également un autre cas dans lequel l’analyse du code exécutable est parfois incontournable. En effet, les équipes de développement d’un
140
logiciel ont parfois besoin de connaître comment fonctionne un logiciel propriétaire pour permettre une compatibilité entre le logiciel qu’ils développent
et ce dernier.
Un exemple connu de ce phénomène est le développement du logiciel
Samba 8 [Dra04]. Les concepteurs de Samba ne disposaient pas des spécifications nécessaires à la communication avec Windows. Pour pallier ce manque,
ces derniers ont dû analyser le code exécutable de Windows. On notera que
bien que la société Microsoft ait intenté un procès aux concepteurs de Samba,
le procès a été perdu 9 .
Notre outil peut fournir là encore une aide en facilitant la compréhension
du programme exécutable à l’aide de ses graphes par exemple. graphELF ne
remplace pas une analyse du code désassemblé mais fournit les informations
permettant de comprendre comment est articulé le programme ou encore
quelles sont les fonctions standard utilisées par celui-ci. De plus, nous envisageons de développer une interface permettant d’accéder plus facilement
aux informations recueillies par notre outil ou encore de naviguer interactivement dans le code désassemblé en fonction des appels et des sauts du
programme. De plus, une possibilité d’annotation du code est également nécéssaire à terme, permettant par exemple d’écrire des commentaires dans le
code désassemblé ou de donner des noms significatifs aux variables et aux
fonctions utilisées (ces noms devront ensuite être propagés de manière automatique dans le code).
4.1.5
Le format ELF et les virus
Contrairement à une idée fausse, il existe des virus sur les plates-formes
Unix bien que le format de fichier ELF soit un format extrêmement structuré
qui laisse peu de place à l’injection de code malicieux et qui rend difficile son
exploitation par des virus.
L’analyse de virus constitue également un domaine dans lequel un outil
comme graphELF peut apporter une aide incontestable. En effet, lorsqu’un
virus est détecté, le seul moyen d’en implémenter la parade passe par une
analyse de ce virus, pour lequel on ne dispose jamais des sources. Il faut donc
désassembler le virus et analyser le code assembleur pour en comprendre le
fonctionnement. Les analyses fournies par graphELF permettent de comprendre la structure du virus, repèrent les appels de fonctions dynamiques et
aide à la compréhension du code désassemblé. Malheureusement, graphELF
8. Ce logiciel est un logiciel Open Source capable de faire communiquer des machines
sous Linux et Windows de manière transparente. Basé sur les protocoles de communication
SMB et CIFS qu’il émule, il propose les mêmes services que les serveurs et clients Windows
propriétaires.
9. Le but strict de compatibilité entre les deux logiciels pour lequel les techniques
d’analyse de code binaire ont été employées n’a donc pas été déclaré illégal.
141
n’est pas en mesure actuellement d’analyser un code crypté ou encore un
code qui aurait subi des procédures d’obfuscation comme cela est souvent
le cas lorsque l’auteur du virus veut empêcher un décodage trop rapide du
code du virus.
Les virus s’attaquant aux fichiers ELF disposent de plusieurs méthodes
pour parvenir à leur fin d’après les analyses de Van Oers [Oer00], Cesare
[Ces00] et Bartolich [Bar02] :
1. Le virus est injecté dans le fichier ELF juste après l’en-tête de ce dernier. Le fichier infecté possède alors deux en-têtes de fichier, l’original
et celui du virus. Le reste des données du fichier se trouve alors décalé.
2. Le point d’entrée du programme est modifié. Il ne pointe plus vers le
début du programme, qui se situe toujours dans un même intervalle
d’adresses, mais vers un autre endroit du fichier qui contient le code
du virus.
3. Le point d’entrée du programme n’est pas modifié mais l’instruction
machine qu’il contient est une redirection vers le code du virus et non
plus directement le programme à exécuter.
4. Le virus est injecté dans le programme par l’intermédiaire d’un ajout
de fichier relocalisable contenant le code du virus. Le fichier infesté
possèdera alors une section supplémentaire et la taille d’un segment
aura considérablement augmenté afin de permettre au fichier .o d’être
chargé dans l’espace mémoire du processus.
5. Le code du virus est ajouté dans une section existante, telle que la
section .fini. Dans ce cas la taille de la section augmente.
6. Le segment text qui contient entre autre le code à exécuter est modifié :
le code du virus y est inséré et la taille du segment est modifiée pour
prendre en compte cette insertion [Ces00].
7. Le segment data est modifié de façon à contenir le code du virus. Dans
ce cas, le point d’entrée du programme est redirigé vers le code du
virus.
8. Le code du virus est exécuté en détournant un appel de fonction dynamique par l’intermédiaire des tables GOT et PLT [Ces99]. Pour y
parvenir, il existe plusieurs méthodes :
– La table PLT est modifiée de façon à rediriger un appel de fonction
dynamique vers le virus.
– L’exécution du programme est simulée pour recueillir l’adresse mémoire dans laquelle se situe la fonction appelée de manière dynamique. Cette adresse est ensuite insérée dans le virus [gru01].
– La librairie requise est remplacée par une copie contenant le code du
virus. Cette copie est ajoutée directement dans le fichier exécutable.
Dans ce cas la taille du fichier augment de manière considérable
[gru01].
142
graphELF fournit des éléments susceptibles de nous renseigner sur l’éventuelle infection d’un programme :
– Si une section a été ajoutée il nous en donnera le nom lors de l’édition
de la table des en-têtes de section.
– Si le point d’entrée a été modifié, celui-ci nous apparaîtra suspect s’il
se situe dans un segment différent du segment text ou encore à une
adresse située à la fin du segment. En effet, les segments chargés en
mémoire sont situés à des adresse fixes dans le code, adresses déterminées lors de la compilation par l’éditeur de liens statique. Pour une
architecture i386, cette adresse est généralement 0x8048000 [gru01].
– Si le point d’entrée a été redirigé, le désassemblage du fichier ne pourra
s’effectuer correctement.
Voici autant de points qui peuvent alarmer lors de l’analyse d’un fichier
binaire et orienter dès lors la recherche du code injecté dans le programme
afin de le nettoyer et d’analyser ce code. On notera cependant que seule une
analyse dynamique d’un programme peut déterminer si ce dernier est infecté
à travers ses appels dynamiques, ce que ne fait pas actuellement notre outil.
143
4.2
Exemples d’application
Dans cette section, nous donnons des exemples d’application de graphELF et nous expliquons comment utiliser les informations livrées par ce
dernier.
4.2.1
La liste des bibliothèques dynamiques
Parmi les informations fournies par graphELF se trouve la liste des librairies dynamiques appelées par le programme. Cette liste peut avoir plusieurs
utilités. Tout d’abord, elle permet de s’assurer que le système possède les
librairies adaptées à ce programme puisqu’en général, les programmes propriétaires sont rarement documentés correctement quant à leurs appels de
fonctions externes. Cette liste permet également un premier contrôle de sécurité concernant par exemple les fonctionnalités cachées d’un programme
(cf. partie 4.1.3).
De plus, graphELF fournit non seulement la liste des librairies mais également la liste des fonctions appelées appartenant à ces librairies. Ainsi,
comme le montre l’exemple suivant, graphELF fournit plus d’informations
que ne le fait un simple appel à la fonction /usr/bin/ldd :
appel de ldd :
libdisasm.so => /usr/lib/libdisasm.so (0x40028000)
libc.so.6 => /lib/i686/libc.so.6 (0x4003e000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
Cet affichage informe que le programme nécessite trois librairies pour
fonctionner. La dernière ligne fait référence au chargeur de programme ld.so.
graphELF pour sa part ne référence pas cette librairie particulière à cet
endroit. Voici l’affichage de graphELF :
resultats de graphELF :
les librairies requises
1
libdisasm.so
152
libc.so.6
144
la table des symboles dynamique
Num:
Valeur Taille
Type
0:
0
0
NOTYPE
1:
80489b8
11b
FUNC
2:
80489c8
3a
FUNC
3:
8052828
0
OBJECT
4:
80489d8
57
FUNC
5:
80489e8
25
FUNC
6:
80489f8
71
FUNC
7:
8048a08
d4
FUNC
8:
8048a18
21
FUNC
9:
8048a28
21
FUNC
10:
8048a38
cc
FUNC
11:
8048a48
34
FUNC
12:
8048a58
3a
FUNC
13:
8048a68
7e
FUNC
14:
8048990
0
FUNC
15:
8048a78
a9
FUNC
16:
8048a88
1c1
FUNC
17:
80529c0
4
OBJECT
18:
8048a98
14e
FUNC
19:
8048aa8
49
FUNC
20:
8048ab8
36
FUNC
21:
8048ac8
af
FUNC
22:
8048ad8
c2
FUNC
23:
80529ac
0
NOTYPE
24:
8048ae8
d4
FUNC
25:
8048af8
fb
FUNC
26:
8048b08
1aa
FUNC
27:
8048b18
36
FUNC
28:
804f524
0
FUNC
29:
8048b28
3c
FUNC
30:
8048b38
186
FUNC
31:
8052b00
60
OBJECT
32:
8048b48
38
FUNC
33:
8048b58
7c
FUNC
34:
8048b68
c4
FUNC
35:
80529ac
0
NOTYPE
36:
805290c
0
OBJECT
37:
8048b78
c2
FUNC
38:
8052b60
0
NOTYPE
39:
8048b88
32
FUNC
40:
804f544
4
OBJECT
41:
8048b98
31
FUNC
42:
8048ba8
164
FUNC
43:
8048bb8
474
FUNC
44:
0
0
NOTYPE
45:
8048bc8
1f
FUNC
46:
80529c4
4
OBJECT
47:
8048bd8
7c
FUNC
48:
80529c8
4
OBJECT
49:
0
0
NOTYPE
50:
8048be8
30
FUNC
Lien
LOCAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
WEAK
GLOBAL
GLOBAL
GLOBAL
GLOBAL
WEAK
GLOBAL
Ndx
Nom
UND
UND
getchar
UND
mkdir
ABS
_DYNAMIC
UND
strdup
UND
strcmp
UND
close
UND
perror
UND
fprintf
UND
pclose
UND
dirname
UND
__errno_location
UND
disassemble_init
UND
system
10
_init
UND
popen
UND
malloc
22
stderr
UND
__xstat
UND
getopt
UND
chdir
UND
strlen
UND
__xpg_basename
ABS
__bss_start
UND disassemble_address
UND
__libc_start_main
UND
strcat
UND
printf
13
_fini
UND
lseek
UND
fclose
22
ext_arch
UND
snprintf
UND
open
UND
exit
ABS
_edata
ABS _GLOBAL_OFFSET_TABLE_
UND
free
ABS
_end
UND
fopen
14
_IO_stdin_used
UND
sprintf
UND
fwrite
UND
realpath
UND _Jv_RegisterClasses
UND disassemble_cleanup
22
optind
UND
read
22
optopt
UND
__gmon_start__
UND
strcpy
librairie
local
GLIBC_2.0
GLIBC_2.0
global
GLIBC_2.0
GLIBC_2.0
GLIBC_2.0
GLIBC_2.0
GLIBC_2.0
GLIBC_2.1
GLIBC_2.0
GLIBC_2.0
local
GLIBC_2.0
global
GLIBC_2.1
GLIBC_2.0
GLIBC_2.0
GLIBC_2.0
GLIBC_2.0
GLIBC_2.0
GLIBC_2.0
GLIBC_2.0
global
local
GLIBC_2.0
GLIBC_2.0
GLIBC_2.0
global
GLIBC_2.0
GLIBC_2.1
local
GLIBC_2.0
GLIBC_2.0
GLIBC_2.0
global
global
GLIBC_2.0
global
GLIBC_2.1
global
GLIBC_2.0
GLIBC_2.0
GLIBC_2.3
local
local
GLIBC_2.0
GLIBC_2.0
GLIBC_2.0
local
GLIBC_2.0
145
Dans la liste des librairies requises, on retrouve les mêmes librairies que
celles mentionnées par ldd. graphELF fournit également la table des symboles dynamiques qui contient la liste des fonctions dynamiques avec pour
chacune d’entre-elles dans le champ librairie le nom de la librairie dont
elle dépend.
Dans cet exemple, le fichier analysé utilise les fonctions de la librairie
GLIBC, située dans le fichier libc.so.6 ce qui paraît vraisemblable. Il utilise
également une librairie nommée libdisasm, librairie de désassemblage. Ceci
pourrait être douteux mais se révèle normal puisqu’il s’agit de l’analyse du
programme graphELF lui-même.
La liste des appels des fonctions dynamiques ne comporte également aucune fonction semblant inopportune. On y trouve de nombreuses fonctions
ayant trait aux fichiers : ouverture, fermeture, écriture et lecture. Ce programme a en effet besoin d’accéder à des fichiers en lecture comme en écriture.
4.2.2
Les chaînes de caractères
La liste de toutes les chaînes de caractères présentes dans le fichier exécutable constitue également une source d’indications importante. En effet, cela
peut donner une première idée de ce que fait le programme, mais également
de sa vulnérabilité potentielle. En effet, toutes les fois où un utilisateur est
amené à dialoguer avec le programme peut être source de failles dues à des
débordements de tampon ou à des bogues de format comme expliqué dans
la partie 4.1.2. Or, lorsque le programme fait appel à l’utilisateur, il affiche
pratiquement toujours un message.
L’exemple ci-dessous nous montre un extrait de la liste des chaînes de
caractères contenues dans le programme ls :
Liste des chaînes de caractères
full-iso
none
classify
file-type
lc
rc
ec
no
fi
di
ln
pi
bd
cd
mi
ex
do
m
01;34
01;36
01;35
01;33
01;32
%b %e
%Y
146
%b %e %H:%M
escape
directory
dired
full-time
human-readable
inode
kilobytes
numeric-uid-gid
no-group
hide-control-chars
reverse
width
almost-all
ignore-backups
si
dereference-command-line
ignore
dereference
literal
quote-name
recursive
show-control-chars
tabsize
time-style
block-size
author
help
version
verbose
long
commas
horizontal
across
vertical
single-column
extension
atime
access
use
ctime
status
yes
force
never
auto
if-tty
dev_ino_pop
ls.c
%lu
/usr/share/locale
coreutils
//DIRED//
//SUBDIRED//
main
found
QUOTING_STYLE
LS_BLOCK_SIZE
COLUMNS
POSIXLY_CORRECT
TABSIZE
--time
--sort
--quoting-style
--indicator-style
--format
invalid tab size: %s
.*~
5.0
vdir
*=@|
invalid time style format %s
invalid line width: %s
--color
time style
%Y-%m-%d %H:%M:%S.%N %z
TIME_STYLE
posix-long-iso
%Y-%m-%d %H:%M
%Y-%m-%d
LS_COLORS
target
unrecognized prefix: %s
reading directory %s
:
total
cannot read symbolic link %s
%-8s
%-8lu
%s %3lu
%3lu, %3lu
%8s
%*s
->
Report bugs to <%s>.
[email protected]
147
#include <stdio.h>
main(){
int i,a;
a=0;
if (a<10)
{
a+=2;
if(a>=0){
fb();
a=a+3;
}
fc();
a=+a;
}
else{
a=0;
}
fa(int e){
e=e+7;
fc();
return e;
}
fb(){
printf("ici\n");
}
fc(){
int a=0;
a=a+1;
}
if(a==0)
a=50;
printf("le resultat %d",a);
}
Figure 4.3 – Le code source de l’exemple de fonction non appelée.
Cette liste correspond au contenu du la section rodata qui contient entre
autres toutes les variables définies en lecture seule, donc toutes les chaînes de
caractères fournies en argument aux différentes fonctions telles que printf.
Comme on pourrait s’y attendre, on ne retrouve pas de chaînes de caractères demandant la saisie de données, ce programme ne comportant pas de
messages interactifs. En revanche, on retrouve des messages d’affichage tels
que :invalid tab size: %s.
4.2.3
Une fonction jamais appelée
Il est parfois nécessaire de désassembler un programme lorsque l’on souhaite le modifier mais que les sources ont disparu. Le plus souvent, il s’agit
de progammes ayant déjà un certain temps d’existence et ayant subi bon
nombre de modifications. Non seulement graphELF aide à la compréhension globale du programme grâce aux graphes qu’il produit, mais il peut
également identifier des portions de code inutiles : des fonctions ayant été
implémentées mais n’étant plus appelées.
La figure 4.3 contient le code source de l’exemple utilisé ici. Ce pro-
148
0x804833c
0x80483b4
0x80483a0
0x80483cc
printf
Figure 4.4 – Graphe des appels de fonction du programme exemple.
80483a0 55
80483a1 89 E5
80483a3 83 EC 08
80483a6 83 45 08 07
80483aa E8 1D 00 00 00
80483af 8B 45 08
80483b2 C9
80483b3 C3
0x80483cc
pushebp
movebp, esp
subesp, 0x8
add[ebp+08], 0x7
callfc
moveax, [ebp+08]
leave
ret
return
Figure 4.5 – Graphe décrivant la fonction fa.
gramme contient la définition de la fonction fa qui n’est pourtant jamais
appelée.
Le graphe des appels de fonction du programme est représenté dans la
figure 4.4. On y retrouve la fonction fa à l’adresse 0x80483a0 qui fait appel
à la fonction fb (adresse 0x80483cc) mais qui n’est pas appelée. La fonction
main quant à elle se situe à l’adresse 0x804833c.
149
graphELF est donc en mesure de reconnaître des fonctions non appelées
lorsqu’il effectue le découpage du code désassemblé en fonctions, contrairement à bastard par exemple qui avait intégré fa dans la fonction main
sans aucune distinction (voir partie 3.9.2.2). De plus, graphELF fournit les
graphes inhérents à cette fonction tel que celui correspondant au code désassemblé découpé suivant le flot de contrôle de la fonction (représenté en figure
4.5).
Notre outil traite et distingue toutes les fonctions définies dans le code,
qu’elles soient donc appelées ou non.
4.2.4
Mise en évidence de fonctions particulières
Le programme Appel_malloc utilisé pour cette série d’exemples est
donné dans la figure 4.6.
#include <stdio.h>
main()
{
int i,a;
char **buf;
a=0;
if (a<10)
{
a+=2;
if(a>=0){
for(i=0;i<10;i++)
{
buf[i] = (char *)malloc(strlen("bonjour"));
}
a=a+3;
}
a=+a;
}
else{
a=0;
}
for(i=0;i<10;i++)
{
free(*buf);
buf++;
}
printf("le resultat %d",a);
}
Figure 4.6 – Code source du programme Appel_malloc
150
4.2.4.1
malloc et free
grapheELF analyse le code exécutable d’un fichier et fournit entre autres
un graphe des appels de fonction. L’exemple 4.7 nous montre le graphe
des appels de fonction du programme Appel_malloc utilisant les fonctions
malloc et free.
0x804839c
malloc
free
printf
Figure 4.7 – Graphe des appels de fonction d’un programme
Ces deux fonctions sont délicates à utiliser et doivent faire l’objet d’un
suivi particulier. En effet, si un programme ne libère pas la mémoire allouée
par les appels à la fonction malloc, ce programme fait alors l’objet de fuite de
mémoire qui peuvent parfois s’avérer problématiques. De plus, les libérations
de mémoire ne sont pas toujours faites correctement et il est important de
vérifier que les paramètres passés à la fonction free correspondent bien aux
valeurs retournées par la fonction malloc.
Le graphe 4.7 nous permet également de situer ces fonctions à l’intérieur
du programme : la fonction référencée 0x804839c fait appel aux fonctions
malloc et free. Le graphe de cette fonction représenté en 4.8 nous permet
de mieux diriger nos recherches quant à la partie du code à examiner. Lors
de la lecture de ce graphe on comprend que les fonctions malloc et free sont
appelées chacune à l’intérieur d’une boucle. Les boucles sont repérables dans
cet exemple par le fait qu’elles possèdent deux arcs qui relient le bloc test
au bloc corps de la boucle : le premier descend au bloc corps et le second en
remonte. La première boucle, située au milieu du graphe fait appel à malloc
tandis que la seconde située en bas du graphe fait appel à free. Les tests de
boucle sont identiques quant au nombre avec lequel la variable est comparée :
cmp [ebp-0C] 0x9
Le même nombre de boucles est effectué pour l’allocation comme pour la
151
804839c
804839d
804839f
80483a0
80483a1
80483a4
80483a7
80483ac
80483ae
80483b5
80483b9
80483bb
80483be
80483c1
80483c5
80483c7 C7 45 F4 00 00 00 00
80483ce 83 7D F4 09
80483d2 7E 02
80483d4 EB 24
80483d6
80483d9
80483e0
80483e3
80483e6
80483e8
80483ed
80483f0
80483f3
80483f6
80483f8
jmp0x80483fa
80483fa 8D 45 F0
80483fd 83 00 03
8048400 EB 07
8B
8D
8B
83
6A
E8
83
89
8D
FF
EB
45
1C
75
EC
07
B3
C4
04
45
00
D4
E5
EC
E4
00
C4
45
7D
47
10
F0
00 00 00
F0 00 00 00 00
F0 09
45 F0
00 02
7D F0 00
42
leaeax, [ebp-10]
add[eax], 0x2
cmp[ebp-10], 0x0
js0x8048409
mov[ebp-0C], 0x0
8048402 C7 45 F0 00 00 00 00
8048433
8048436
8048439
804843e
8048443
8048446
8048449
804844a
804844b
804844c
83
FF
68
E8
83
8D
5B
5E
5D
C3
EC
75
68
7D
C4
65
FE FF FF
10
1E
F4
malloc
jmp0x8048433
08
F0
85 04 08
FE FF FF
10
F8
printf
mov[ebp-10], 0x0
moveax, [ebp-0C]
leaebx, (04*eax)+
movesi, [ebp-14]
subesp, 0xC
push0x7
callmalloc
addesp, 0x10
mov[esi+ebx], eax
leaeax, [ebp-0C]
inc[eax]
jmp0x80483ce
8048409 C7 45 F4 00 00 00 00
8048416 EB 1B
pushebp
movebp, esp
pushesi
pushebx
subesp, 0x10
andesp, 0xF0
moveax, 0x0
subesp, eax
mov[ebp-10], 0x0
cmp[ebp-10], 0x9
jg0x8048402
cmp[ebp-0C], 0x9
jle0x80483d6
F4
85 00 00 00 00
EC
0C
leaeax, [ebp-10]
add[eax], 0x3
jmp0x8048409
8D
83
83
78
55
89
56
53
83
83
B8
29
C7
83
7F
subesp, 0x8
push[ebp-10]
push0x8048568
callprintf
addesp, 0x10
leaesp, [ebp-08]
popebx
popesi
popebp
ret
mov[ebp-0C], 0x0
8048410 83 7D F4 09
8048414 7E 02
cmp[ebp-0C], 0x9
jle0x8048418
8048418
804841b
804841e
8048423
8048426
8048429
804842c
804842f
8048431
subesp, 0xC
push[ebp-14]
callfree
addesp, 0x10
leaeax, [ebp-14]
add[eax], 0x4
leaeax, [ebp-0C]
inc[eax]
jmp0x8048410
83
FF
E8
83
8D
83
8D
FF
EB
EC
75
AD
C4
45
00
45
00
DD
0C
EC
FE FF FF
10
EC
04
F4
free
return
Figure 4.8 – Graphe de la fonction 0x804839c
libération de la mémoire. D’un point de vue analyse statique du programme,
il semble donc que le code soit correct.
Cependant, en examinant le reste du code de la fonction, on peut émettre
une réserve. En effet, la fonction se compose d’un certain nombre de branchements conditionnels représentés par deux arcs partant du même bloc pour
se diriger vers deux blocs différents : le bloc si-vrai et le bloc si-faux. Or
en suivant les différents chemins possibles, on s’aperçoit que l’on peut soit
entrer dans la boucle qui fait appel à la fonction malloc soit la contourner.
Dans les deux cas, le code est ensuite dirigé vers la boucle qui fait appel à la
fonction free. Or si le programme n’est pas entré dans la première boucle,
il ne devrait pas entrer dans la seconde pour ne pas libérer de la mémoire
152
reservée à autre chose ! Il faut donc analyser le test de la seconde boucle pour
savoir si elle est effectuée systématiquement ou non.
Le test cmp [ebp-0C] 0x9 précise que l’on compare le contenu du registre [ebp-0C] avec le nombre 0x9, c’est-à-dire la variable de test avec
la valeur 9. Or le bloc situé juste avant contient l’instruction suivante :
mov [ebp-0C] 0x0. On initialise donc ici le registre avec la valeur 0 avant
de pénétrer dans la boucle. Donc, le programme entre nécessairement dans la
boucle, qu’il soit passé ou non avant dans la boucle faisant appel au malloc.
Il s’agit donc d’une erreur de programmation qu’il faut corriger.
La figure 4.9 fournit le code désassemblé de la fonction étudiée, compilé
sans aucune option particulière. Analyser ce code présenté sous forme d’un
simple listing, s’avère plus délicat que lorsqu’il est découpé en différents blocs
représentant le flot de contrôle de la fonction. Les graphes nous ont permis
entre autres de détecter les boucles, les branchements et les chemins possibles
au cours de l’exécution du programme, beaucoup plus aisément que lors de
la lecture linéaire du code désassemblé. graphELF facilite donc la détection
d’éventuelles anomalies d’utilisation des fonctions malloc et free.
4.2.4.2
malloc et free non standards
Le graphe 4.10 nous fournit un autre exemple intéressant. Il s’agit du
même programme Appel_malloc (cf. figure 4.6) qui ne fait plus appel aux
fonctions malloc et free de la librairie standard mais à des fonctions malloc
et free définies dans un fichier lié au moment de la compilation. Contrairement à l’exemple précédent, les fonctions malloc et free n’apparaissent plus
dans des losanges signifiant que ce sont des fonctions dynamiques mais dans
des ovales bleus signifiant qu’il s’agit de fonctions externes définies dans le
programme. Le graphe 4.10 nous permet de conclure que les fonctions malloc
et free employées ici ne sont pas celles de la librairie standard 10 .
Le graphe de la figure 4.11 a été effectué sur le même exemple, mais
une fois la table des symboles retirée. Dans ce cas, les noms des fonctions
malloc et free n’apparaissent plus et ont été remplacés par leur adresse
respective,0x8048a4b et 0x8048c4d. Dans ce cas, il faut donc une analyse
minutieuse du code désassemblé pour parvenir à déterminer ce que font ces
fonctions. Le graphe 4.11 nous fournit une première indication avec les appels
dynamiques memset, memcpy, getpagesize et sbrk. Il s’agit d’appels système
mettant en jeu la mémoire et permettant par exemple l’allocation de pages
10. Nous serions parvenus à la même conclusion en étudiant les lignes de code desassemblé mais beaucoup moins rapidement. En effet, l’adresse pointée par l’instruction call
pour appeler ces fonctions ne reférence plus une entrée dans la table PLT mais un endroit
situé dans la section .text. Rappelons que l’instruction call a pour argument un décalage
et non une adresse : il faut donc effectuer un calcul pour connaître l’adresse.
153
fonction main adresse 804839c
804839C
804839D
804839F
80483A0
80483A1
80483A4
80483A7
80483AC
80483AE
80483B5
80483B9
80483BB
80483BE
80483C1
80483C5
80483C7
80483CE
80483D2
80483D4
80483D6
80483D9
80483E0
80483E3
80483E6
80483E8
80483ED
80483F0
80483F3
80483F6
80483F8
80483FA
80483FD
8048400
8048402
8048409
8048410
8048414
8048416
8048418
804841B
804841E
8048423
8048426
8048429
804842C
804842F
8048431
8048433
8048436
8048439
804843E
8048443
8048446
8048449
804844A
804844B
804844C
55
89
56
53
83
83
B8
29
C7
83
7F
8D
83
83
78
C7
83
7E
EB
8B
8D
8B
83
6A
E8
83
89
8D
FF
EB
8D
83
EB
C7
C7
83
7E
EB
83
FF
E8
83
8D
83
8D
FF
EB
83
FF
68
E8
83
8D
5B
5E
5D
C3
E5
EC
E4
00
C4
45
7D
47
45
00
7D
42
45
7D
02
24
45
1C
75
EC
07
B3
C4
04
45
00
D4
45
00
07
45
45
7D
02
1B
EC
75
AD
C4
45
00
45
00
DD
EC
75
68
7D
C4
65
10
F0
00 00 00
F0 00 00 00 00
F0 09
F0
02
F0 00
F4 00 00 00 00
F4 09
F4
85 00 00 00 00
EC
0C
FE FF FF
10
1E
F4
F0
03
F0 00 00 00 00
F4 00 00 00 00
F4 09
0C
EC
FE FF FF
10
EC
04
F4
08
F0
85 04 08
FE FF FF
10
F8
push
mov
push
push
sub
and
mov
sub
mov
cmp
jg
lea
add
cmp
js
mov
cmp
jle
jmp
mov
lea
mov
sub
push
call
add
mov
lea
inc
jmp
lea
add
jmp
mov
mov
cmp
jle
jmp
sub
push
call
add
lea
add
lea
inc
jmp
sub
push
push
call
add
lea
pop
pop
pop
ret
ebp
ebp, esp
esi
ebx
esp, 0x10
esp, 0xF0
eax, 0x0
esp, eax
[ebp-10], 0x0
[ebp-10], 0x9
0x8048402
eax, [ebp-10]
[eax], 0x2
[ebp-10], 0x0
0x8048409
[ebp-0C], 0x0
[ebp-0C], 0x9
0x80483d6
0x80483fa
eax, [ebp-0C]
ebx, (04*eax)+
esi, [ebp-14]
esp, 0xC
0x7
malloc
esp, 0x10
[esi+ebx], eax
eax, [ebp-0C]
[eax]
0x80483ce
eax, [ebp-10]
[eax], 0x3
0x8048409
[ebp-10], 0x0
[ebp-0C], 0x0
[ebp-0C], 0x9
0x8048418
0x8048433
esp, 0xC
[ebp-14]
free
esp, 0x10
eax, [ebp-14]
[eax], 0x4
eax, [ebp-0C]
[eax]
0x8048410
esp, 0x8
[ebp-10]
0x8048568
printf
esp, 0x10
esp, [ebp-08]
ebx
esi
ebp
Figure 4.9 – Code désassemblé du programme Appel_malloc
154
calloc
malloc
fragmenter
memset
realloc
main
memcpy
free
initialise
recherche_page
getpagesize
sbrk
printf
page_libre
liberer_frag
Figure 4.10 – Graphe des appels de fonction
mémoire. graphELF facilite ensuite la lecture du code désassemblé en fournissant un listing contenant ce code réparti fonction par fonction et non pas
d’un seul bloc. Nous pouvons donc analyser le code fonction après fonction.
Le graphe 4.11 nous permet de plus de délaisser les fonctions 0x8048eb1 et
0x8048eff qui sont définies mais se sont pas appelées comme le souligne le
graphe. Les graphes du flot de contrôle et du code réparti en blocs aident
ensuite à une meilleure compréhension : les boucles et les branchements sont
facilement repérés par exemple.
Déterminer que les fonctions 0x8048a4b et 0x8048c4d sont des fonctions
d’allocation et de libération de mémoire similaires à malloc et free reste
difficile et long. Cependant, graphELF offre tout de même un gain de temps
considérable : il permet de situer les fonctions, facilite la compréhension du
code désassemblé et évite d’avoir à lire entièrement le code pour identifier
les fonctions utiles.
4.2.5
Optimisation du compilateur
Lors de la compilation d’un fichier, le compilateur effectue des tâches
telles que l’ajout des fonctions d’initialisation et de fin ou encore différentes
155
0x8048eb1
0x80487c4
memset
0x8048eff
0x8049036
0x8048a4b
memcpy
0x8048c4d
0x804847c
0x8048849
0x8048637
sbrk
0x80485b1
getpagesize
printf
Figure 4.11 – Graphe des appels de fonction
optimisations selon les options reçues en ligne de commandes.
Reprenons le programme {Appel\_malloc} (cf. figure 4.6) faisant appel
aux fonctions malloc et free non standard, pour mettre en évidence différentes optimisations du code. Dans cet exemple, nous avions remarqué que
le programme pouvait ou non passer dans la boucle faisant appel à malloc.
Le graphe 4.12 fournit le code désassemblé de la fonction main non optimisée tandis que le graphe 4.13 nous montre la même fonction optimisée.
Les deux graphes contiennent bien une première boucle qui fait appel à la
fonction malloc. Cependant, dans le graphe 4.12 le passage par cette boucle
est précédé d’une condition tandis que le graphe 4.13 nous montre que le
passage par cette boucle est obligatoire, une fois le code optimisé. En effet,
la condition permettant de passer ou non par la boucle est une condition
qui est toujours vraie. Le compilateur en optimisant, a simplifié le code en
supprimant la condition. Ainsi, ce qui était à l’origine une erreur de programmation se trouve effacée du code exécutable. Ceci montre que le code
exécutable est parfois très différent du code source, ce que graphELF met
en évidence avec les graphes des fonctions qu’il fournit : on remarque plus
facilement que les branchements changent et que les boucles se transforment
parfois en code linéaire.
156
8049036
8049037
8049039
804903a
804903b
804903e
8049041
8049046
8049048
804904f
8049053
8049055
8049058
804905b
804905f
8049061 C7 45 F4 00 00 00 00
8049068 83 7D F4 09
804906c 7E 02
804906e EB 24
8049070
8049073
804907a
804907d
8049080
8049082
8049087
804908a
804908d
8049090
8049092
jmp0x8049094
8049094 8D 45 F0
8049097 83 00 03
804909a EB 07
8B
8D
8B
83
6A
E8
83
89
8D
FF
EB
45
1C
75
EC
07
C4
C4
04
45
00
D4
E5
EC
E4
00
C4
45
7D
47
10
F0
00 00 00
F0 00 00 00 00
F0 09
45 F0
00 02
7D F0 00
42
leaeax, [ebp-10]
add[eax], 0x2
cmp[ebp-10], 0x0
js0x80490a3
mov[ebp-0C], 0x0
804909c C7 45 F0 00 00 00 00
F9 FF FF
10
1E
F4
malloc
80490aa 83 7D F4 09
80490ae 7E 02
80490cf
80490d2
80490d5
80490da
80490df
80490e2
80490e5
80490e6
80490e7
80490e8
83
FF
68
E8
83
8D
5B
5E
5D
C3
EC
75
08
B1
C4
65
jmp0x80490cf
08
F0
92 04 08
F2 FF FF
10
F8
printf
mov[ebp-10], 0x0
moveax, [ebp-0C]
leaebx, (04*eax)+
movesi, [ebp-14]
subesp, 0xC
push0x7
callmalloc
addesp, 0x10
mov[esi+ebx], eax
leaeax, [ebp-0C]
inc[eax]
jmp0x8049068
80490a3 C7 45 F4 00 00 00 00
80490b0 EB 1D
pushebp
movebp, esp
pushesi
pushebx
subesp, 0x10
andesp, 0xF0
moveax, 0x0
subesp, eax
mov[ebp-10], 0x0
cmp[ebp-10], 0x9
jg0x804909c
cmp[ebp-0C], 0x9
jle0x8049070
F4
85 00 00 00 00
EC
0C
leaeax, [ebp-10]
add[eax], 0x3
jmp0x80490a3
8D
83
83
78
55
89
56
53
83
83
B8
29
C7
83
7F
subesp, 0x8
push[ebp-10]
push0x8049208
callprintf
addesp, 0x10
leaesp, [ebp-08]
popebx
popesi
popebp
ret
80490b2
80490b5
80490b8
80490ba
80490bf
80490c2
80490c5
80490c8
80490cb
80490cd
83
8B
FF
E8
83
8D
83
8D
FF
EB
EC
45
30
8E
C4
45
00
45
00
DB
mov[ebp-0C], 0x0
cmp[ebp-0C], 0x9
jle0x80490b2
0C
EC
subesp, 0xC
moveax, [ebp-14]
push[eax]
callfree
addesp, 0x10
leaeax, [ebp-14]
add[eax], 0x4
leaeax, [ebp-0C]
inc[eax]
jmp0x80490aa
FB FF FF
10
EC
04
F4
free
return
Figure 4.12 – Graphe de la fonction main du programme Appel_malloc
De même, l’exemple précédent nous montre aussi que si le programme
source ainsi que le code compilé sans optimisation faisaient appel à la fonction
free, le code optimisé en revanche ne fait plus appel à cette fonction. En
effet, lors de la phase d’optimisation, le compilateur a intégré le code de la
fonction free directement dans la fonction main : il a inliné la fonction free.
graphELF, bien que n’ayant pas été conçu pour comprendre le fonctionnement d’un compilateur, peut tout de même fournir des indications précises
sur les façons dont le compilateur optimise le code.
157
8048a00
8048a01
8048a03
8048a04
8048a05
8048a06
8048a09
8048a0b
8048a0e
8048a11
8048a13
8048a18
8048a1b
8048a1e
8048a1f
8048a22
8048a24
8048a2b
8048a2e
8048a34
8048a3a
8048a41
8048a43
8048a45
8B
8B
66
C7
8B
85
0F
C7
55
1D
C7
45
32
DB
84
45
D4
00
83
6A
E8
83
89
43
83
7E
00
D0
C4
45
EC
A1
F0
FF
04
00
FF
08
00
FF
99
00
00
00
55
89
57
56
53
83
31
83
FF
EC
DB
E4
EC
07
68
C4
04
0C
FB
EA
09
00
pushebp
movebp,
pushedi
pushesi
pushebx
subesp,
xorebx,
andesp,
E5
FA
10
9E
00
2C
F0
FF
esp
0x2C
ebx
0xF0
subesp, 0xC
push0x7
callmalloc
addesp, 0x10
mov[esi+(04*ebx)],
incebx
cmpebx, 0x9
jle0x8048a0e
FF
mov[ebp-2C],
0x0
eax
malloc
movedx, [ebp-30]
movebx, 0804A1C4
mov[ebp-10], 0x0
mov[ebp-14], 0xFFFFFFFF
movesi, [edx]
testebx, ebx
jz0x8048ae4
8048a4b
8048a4c
90
8D
74
26
00
8048a50
8048a53
8048a58
8048a5d
8048a5f
8B
BA
BF
39
7D
4B
01
01
CA
2B
04
00
00
00
00
8048a61
8048a66
8048a69
8048a6e
8048a6f
8048a72
8048a75
8048a77
A1
89
A1
40
89
8B
39
7C
C0
45
D0
A1
E8
A1
45
E4
04
C6
0B
nop
leaesi,
04
08
04
08
moveax, 0x0804A1C0
mov[ebp-18], eax
moveax, 0x0804A1D0
inceax
mov[ebp-1C], eax
D3
moveax, [ebx+(08*edx)]
cmpesi, eax
jl0x8048a84
8048a79
8048a7c
8048a7e
8048a84
8048a85
8048a88
8048a8a
47
0F
39
7C
BF
CA
E6
[esi]
movecx, [ebx+04]
movedx, 0x1
movedi, 0x1
cmpedx, ecx
jge0x8048a8c
00
00
03
39
0F
45
C6
8C
E8
90
00
00
8048b14
8048b19
8048b1d
8048b21
8048b24
incedi
movsxedx, di
cmpedx, ecx
jl0x8048a72
D7
addeax, [ebp-18]
cmpesi, eax
jl0x8048b14
00
8048b26
8048b29
8048b2b
8048b2e
8048b33
8048b35
8048b37
8048b39
8048b3b
8048b3e
8048a8c
8048a90
83
75
7D
06
EC
FF
8D
89
C1
B8
D3
89
89
F7
89
E9
48
F2
FA
01
E0
C1
F0
F9
55
49
8B
85
75
8048a9f
8048aa4
45
C0
45
66
0F
8048aaa
8048aae
8048ab4
8048ab7
8048aba
8048abc
8048b43
8048b49
8048b50
8048b53
8048b57
8048b5d
8048b60
8048b61
8048b68
8048b6c
8048b71
8048b74
8048b77
8048b7a
8048b7c
8048b7f
8048b82
8048b85
8048b88
8048b8b
8048b8f
8048b92
0F
47
66
66
66
8B
8B
8B
89
8D
8B
89
8B
8B
66
89
75
8048b94
8048ae4
8048ae7
8048aea
8048aeb
8048aee
8048af1
8048af4
8048af8
8048afe
8048aff
8048b00
8048b02
8048b07
8048b0c
8048b0f
8048b10
8048b11
8048b12
8048b13
8B
8B
42
83
89
89
83
0F
50
50
6A
68
E8
8D
5B
5E
5F
5D
C3
8B
66
89
66
8B
35
8B
75
89
35
BF
D7
C7
8B
89
0C
45
04
01
04
0C
41
4D
04
FF
04
C9
44
4D
4C
D3
D8
86
E9
D3
D3
04
D8
D3
4D
8E
4B
55
4D
D4
D0
C1
55
4D
7D
8E
04
D4
D0
D4
2D
05
98
84
65
90
F8
F4
D0
15
D8
55
C8
D3
DE
D3
A1
D0
04
A1
08
04
DE
A1
04
08
04
00
06
F0
FF
FF
09
FF
FF
04
FF
FF
FF
08
FF
printf
00
08
8B
89
BF
45
73
4C
4D
45
E4
EC
83
85
0F
8B
89
8B
85
74
D3
F2
F2
06
movcx, [ebx+(08*edx)+06]
mov[ebp-0E], cx
movsxeax, [ebp-0E]
cmpeax, [ebp-1C]
jz0x8048b99
03
1F
00
00
00
EC
FF
FF
FF
cmp[ebp-14],
jnz0x8048a98
8048a92
8048a94
8048a96
8048a98
8048a9b
8048a9d
66
66
0F
3B
74
8B
85
75
leaecx, [eax+03]
movedx, esi
saredx, 0x1F
moveax, 0x1
shleax, cl
movecx, eax
moveax, esi
idiveax, ecx
mov[ebp-14], edx
jmp0x8048a8c
-0x1
1B
DB
B8
movebx, [ebx]
testebx, ebx
jnz0x8048a50
8048b99
8048b9e
8048ba2
8048ba4
moveax, [ebp-14]
testeax, eax
jnz0x8048ae4
7D
99
F0
00
BF
0D
55
04
C0
0A
55
C8
E0
91
00
00
F2
A1
cmp[ebp-10],
jnz0x8048b43
00
04
movesi, 0804A1D0
movdx, 0804A1D0
mov[ebp-28], esi
mov[ebp-22], dx
movesi, 0804A1C8
8048abe
8048ac0
movsxedx, di
incedi
mov[ebx+(08*edx)+04], 0x0
movcx, [ebp-22]
mov[ebx+(08*edx)+06], cx
movecx, [ebx+(08*edx)]
moveax, [ebp-28]
moveax, [esi+(04*eax)]
mov[ecx], eax
leaeax, [ebx+(08*edx)]
movecx, [ebx+(08*edx)]
mov[ecx+04], eax
movecx, [ebp-28]
moveax, [ebx+(08*edx)]
dec[ebp-10]
mov[esi+(04*ecx)], eax
jnz0x8048b5d
jmp0x8048ae4
8048ac8
8048acb
8048ace
8048ad0
8048ad3
8048ad6
8048ad9
8048adc
8048adf
8048ac2
8048ac4
8048ac6
8B
8B
89
0F
8D
89
8B
89
66
55
04
06
BF
3C
7E
7D
34
FF
E0
91
C7
C3
04
E0
B9
4C
C3
04
8B
89
41
85
4C
4D
D3
F0
04
F5
FE
FF
movcx, [ebx+(08*edx)+04]
mov[ebp-10], cx
incecx
jnz0x8048a9f
FF
0x0
movsxedx, [ebp-0E]
movecx, 0804A1C8
mov[ebp-20], edx
moveax, [ecx+(04*edx)]
testeax, eax
jz0x8048ac8
08
66
66
66
0F
39
74
8B
85
75
F0
22
00
C0
F6
8048baa
E9
35
FF
FF
FF
jmp0x8048ae4
cmpeax, esi
jz0x8048ae4
moveax, [eax]
testeax, eax
jnz0x8048abe
movedx, [ebp-20]
moveax, [ecx+(04*edx)]
mov[esi], eax
movsxeax, di
leaedi, [ebx+(08*eax)]
mov[esi+04], edi
movedi, [ebp-20]
mov[ecx+(04*edi)], esi
dec[ebx+(08*eax)+04]
movedx, [ebp-2C]
movecx, [ebp-30]
incedx
addecx, 0x4
mov[ebp-2C], edx
mov[ebp-30], ecx
cmp[ebp-2C], 0x9
jle0x8048a2b
pusheax
pusheax
push0x5
push0x8049098
callprintf
leaesp, [ebp-0C]
popebx
popesi
popedi
popebp
ret
return
Figure 4.13 – Graphe de la fonction main du programme Appel_malloc,
optimisée
4.2.6
Liste de fonctions suspectes
Parmi les fonctions de la librairie C, il en existe un certain nombre à utiliser avec beaucoup de précautions. En effet, ces fonctions peuvent créer des
failles et être sources de trous de sécurité exploitables facilement. graphELF
dispose de deux listes de fonctions pouvant poser problème. Ces listes ont été
établies d’après les listes de fonctions, fournies dans [KLA+ 04, WK02]. La
première liste référence les fonctions susceptibles d’être à l’origine de débordements de tampon, tandis que la seconde référence celles pouvant amener
des bogues de format. Ces deux failles de sécurité ont été détaillées dans
158
4.1.2.
Voici le code source de l’exemple fn_dout que nous allons maintenant
décrire :
#include <stdio.h>
#define BUFSIZE 8
fb(){
int i;
char dest[150];
char buf[BUFSIZE];
main(int argc, char **argv)
{
int n=0;
fa(argv[1]);
do{
printf(argv[n]);
n++;
}while(n<10);
fc(n);
}
fa(char *arg)
{
for(i=0;i<50;i++)
{
fgets(buf,5,stdin);
strcat(dest,buf);
}
}
fc(int n)
{
printf("c’est fini\n");
}
char buffer[BUFSIZE];
int n;
n=random();
if(n>10)
strcpy(buffer,arg);
else
printf(arg);
fb();
}
Dans cet exemple, les appels aux fonctions strcpy, strcat et les deux
premiers appels à la fonction printf constituent des failles de sécurité avec
un débordement de tampon pour les premiers et un bogue de format pour
les appels à printf. En revanche, dans la fonction fc, printf possède un
paramètre qui ne représente aucun danger pour le programme.
Nous allons déterminer ces résultats à partir du code désassemblé et des
analyses fournies par graphELF.
Lorsque graphELF rencontre un appel vers une telle fonction, il met cet
appel en avant de façon à attirer l’attention. Dans le graphe de la figure 4.14
les fonctions strcpy et strcat font partie de la liste des fonctions susceptibles
159
main
fc
fa
printf
fb
fgets
random
strcpy
strcat
Figure 4.14 – Graphe des appels de fonctions du programme fn_dout
0x804848c
0x80484a0
strcpy
0x80484b4
0x80484c2
fb
random
printf
return
Figure 4.15 – Graphe du flot de contrôle de la fonction fa de fn_dout
de provoquer des débordements de tampon. Elle apparaissent donc dans le
graphe avec une couleur différente, à savoir en rouge. De même, la fonction
printf fait pour sa part partie des fonctions pouvant provoquer des bogues
de format, elle figure alors dans un noeud de couleur bleue.
160
0x80484c9
0x80484d9
0x80484df
0x80484e1
0x804851b
fgets
strcat
return
Figure 4.16 – Graphe du flot de contrôle de la fonction fb de fn_dout
804848c
804848d
804848f
8048492
8048497
804849a
804849e
80484a0
80484a3
80484a6
80484a9
80484aa
80484af
80484b2
83
FF
8D
50
E8
83
EB
EC 08
75 08
45 F8
B1 FE FF FF
C4 10
0E
strcpy
subesp, 0x8
push[ebp+08]
leaeax, [ebp-08]
pusheax
callstrcpy
addesp, 0x10
jmp0x80484c2
55
89
83
E8
89
83
7E
80484b4
80484b7
80484ba
80484bf
80484c2 E8 02 00 00 00
80484c7 C9
80484c8 C3
fb
E5
EC
79
45
7D
14
83
FF
E8
83
18
FE FF FF
F4
F4 0A
EC
75
91
C4
pushebp
movebp, esp
subesp, 0x18
callrandom
mov[ebp-0C], eax
cmp[ebp-0C], 0xA
jle0x80484b4
0C
08
FE FF FF
10
callfb
leave
ret
subesp, 0xC
push[ebp+08]
callprintf
addesp, 0x10
printf
return
Figure 4.17 – Graphe détaillé du code de la fonction fa de fn_dout
L’étape suivante de l’analyse consiste à vérifier l’environnement d’appel
de ces fonctions afin de définir si les fonctions suspectes ont été appelées
random
161
80484c9
80484ca
80484cc
80484d2
55
89 E5
81 EC C8 00 00 00
C7 45 F4 00 00 00 00
80484d9 83 7D F4 31
80484dd 7E 02
80484df EB 3A
jmp0x804851b
804851b C9
804851c C3
leave
ret
80484e1
80484e4
80484ea
80484ec
80484f2
80484f3
80484f8
80484fb
80484fe
8048504
8048505
804850b
804850c
8048511
8048514
8048517
8048519
83
FF
6A
8D
50
E8
83
83
8D
50
8D
50
E8
83
8D
FF
EB
cmp[ebp-0C], 0x31
jle0x80484e1
EC 04
35 78 97 04 08
05
85 40 FF FF FF
28
C4
EC
85
pushebp
movebp, esp
subesp, 0xC8
mov[ebp-0C], 0x0
FE FF FF
10
08
40 FF FF FF
85 48 FF FF FF
2F FE FF FF
C4 10
45 F4
00
BE
fgets
subesp, 0x4
push08049778
push0x5
leaeax, [ebp-000000C0]
pusheax
callfgets
addesp, 0x10
subesp, 0x8
leaeax, [ebp-000000C0]
pusheax
leaeax, [ebp-000000B8]
pusheax
callstrcat
addesp, 0x10
leaeax, [ebp-0C]
inc[eax]
jmp0x80484d9
strcat
return
Figure 4.18 – Graphe détaillé du code de la fonction fb de fn_dout
correctement ou si leur appel engendre une faille de sécurité dans le programme. Il nous faut donc analyser le code désassemblé. Pour situer ces
appels de fonction dans le code, graphELF fournit les graphes de flot de
contrôle des fonctions les appelant. Ces graphes sont reproduits dans les
figures 4.15 pour la fonction fa et 4.16 pour la fonction fb. Les noeuds correspondent aux adresses de début de bloc dans les fonctions. Le graphe 4.16
nous permet par exemple de déterminer que la fonction strcat est appelée
dans le bloc 0x80484e1.
Pour retrouver ce bloc de code, nous disposons soit du listing désassemblé de chaque fonction, soit des graphes 4.17 pour la fonction fa et 4.18
pour la fonction fb. Ceux-ci présentent le code découpé en blocs selon le
flot de contrôle de chaque fonction et permettent donc de situer ces blocs
directement.
4.2.6.1
Analyse de strcat
Prenons la fonction strcat appelée dans fb. Nous remarquons qu’elle
se situe dans une boucle (le bloc possède un arc descendant et un arc montant) de 50 tours. Le nombre de tours est déterminé en analysant les deux
162
instructions suivantes :
mov [ebp -0c], 0x0
cmp [ebp -0c], 0x31
Ces instructions sont situées avant l’entrée dans la boucle. La première signifie que la variable [ebp -0c] est initialisée à 0. La seconde instruction
est une instruction de comparaison entre la variable [ebp -0c] et la valeur
49. On peut donc conclure que la boucle comprend 50 tours.
Avant l’appel de la fonction strcat, le programme effectue un appel à
la fonction fgets. Analysons tout d’abord cette fonction et les arguments
qui lui sont transmis. Elle reçoit trois arguments, placés sur la pile par les
instructions :
80484E4
80484EA
80484EC
80484F2
FF 35 78 97 04 08
6A 05
8D 85 40 FF FF FF
50
push
push
lea
push
08049778
0x5
eax, [ebp-000000C0]
eax
La première instruction place une adresse sur la pile, la seconde place
le chiffre 5. La troisième instruction place l’adresse de la variable locale
[ebp-000000C0] dans le registre eax et le dernier push place cette adresse
sur la pile. La fonction fgets a donc pour premier argument une variable
locale de fb, pour second argument le chiffre 5 et pour troisième argument
une adresse. Déterminons maintenant à quoi correspond cette adresse. La
table des en-têtes de section du programme nous permet de situer l’adresse
dans la section .got :
les entetes de section (extrait)
index
[21]
section
.got
type
PROGBITS
adresse
8049750
taille
40
Il s’agit donc d’une variable globale définie dans une librairie dynamique.
Nous pouvons donc rechercher son nom dans la table des symboles dynamiques :
la table des symboles dynamiques (extrait)
Num:
Valeur Taille
Type
Lien
Ndx
6:
8049778
4
OBJECT GLOBAL
22
Nom
stdin
librairie
GLIBC_2.0
L’appel de la fonction fgets est donc : fgets(variable_locale,5,stdin).
Les 5 caractères saisis au clavier sont donc recopiés dans un tampon défini
localement dans la fonction fb.
Revenons à la fonction strcat appelée par la seconde instruction call.
Celle-ci reçoit deux arguments :
163
80484FE
8048504
8048505
804850B
8D 85 40 FF FF FF
50
8D 85 48 FF FF FF
50
lea
push
lea
push
eax, [ebp-000000C0]
eax
eax, [ebp-000000B8]
eax
Le premier est identique à la variable locale utilisée par fgets. Le second
est également une variable locale à la fonction fb. strcat est donc appelée
ainsi : strcat(var_locale_2,variable_locale). Nous pouvons conclure
que ce qui est saisi au clavier et stocké dans variable_locale est ensuite
enregistré dans var_locale_2, le tout pendant 50 tours.
Figure 4.19 – Schéma de la pile lors de l’exécution de la fonction fb
La taille des données saisies est de 5 octets. Donc la fonction copie (50 * 5)
octets — soit 250 octets — dans var_locale_2. Il nous reste à déterminer la
taille de cette variable pour affirmer qu’il se produit ou non un débordement
de tampon. D’après le référencement des variables locales :
[ebp-0C]
[ebp-000000C0]
[ebp-000000B8]
et le schéma de la figure 4.19, var_locale_2 est de taille inférieure à 250
octets. Nous avons donc ici tous les éléments pour affirmer que cet appel
constitue un débordement de tampon exploitable :
164
–
–
–
–
Une fonction peu sûre, strcat.
Un tampon de taille fixe.
Pas de contrôle entre la taille de ce qui est copié et la taille du tampon.
Une saisie de données effectuée directement par l’utilisateur.
4.2.6.2
Analyse de strcpy
La fonction fa fait appel à la fonction strcpy. Le graphe de la figure 4.15
en page 159 nous indique que cet appel est effectué dans le bloc 0x80484a0.
Le graphe de la figure 4.17 en page 160 nous fournit le code désassemblé que
nous devons analyser.
Les arguments transmis à strcpy sont les suivants :
80484A3 FF 75 08
80484A6 8D 45 F8
80484A9 50
push [ebp+08]
lea eax, [ebp-08]
push eax
Le premier argument empilé correspond à un argument de la fonction fa
tandis que le second correspond à une variable locale à fa. L’appel de la
fonction strcpy est donc le suivant : strcpy(var_loc, arg_fa). L’argument transmis à fa est donc copié dans un tampon de taille fixe de 8 octets.
Pour déterminer si cet appel peut être dangeureux, il nous faut analyser
l’argument transmis à fa et donc son appel.
D’après la figure 4.14 de la page 159, fa est appelée une seule fois dans
la fonction main. Le graphe détaillé (cf. figure 4.20) de cette fonction nous
indique que la fonction fa reçoit un seul argument :
804843F 8B 45 0C
8048442 83 C0 04
8048445 FF 30
mov eax, [ebp+0C]
add eax, 0x4
push [eax]
Cet argument — [ebp+0C] — est un paramètre passé à la fonction main.
Il s’agit donc d’un argument fourni par l’utilisateur lors de l’exécution du
programme. De plus, la taille de cet argument n’est pas vérifiée.
Nous pouvons donc affirmer que l’appel de la fonction strcpy constitue
une faille de sécurité.
4.2.6.3
Analyse de printf dans la fonction fa
La fonction fa fait appel à la fonction printf. En procédant de la même
manière que lors de l’analyse des fonctions strcat et strcpy au moyen des
graphes des figures 4.15 en page 159 et 4.17 en page 160, nous constatons
que printf reçoit un seul argument :
165
804842c
804842d
804842f
8048432
8048435
804843a
804843c
804843f
8048442
8048445
8048447
804844c
804844f
8048456
8048459
804845c
8048463
8048466
8048469
804846e
8048471
8048474
8048476
804847a
804847c
804847f
8048482
8048487
804848a
804848b
83
FF
E8
83
C9
C3
EC
75
96
C4
0C
FC
00 00 00
10
fc
83
8B
8D
8B
FF
E8
83
8D
FF
83
7E
EC
45
14
45
34
E2
C4
45
00
7D
DA
55
89
83
83
B8
29
83
8B
83
FF
E8
83
C7
E5
EC
E4
00
C4
EC
45
C0
30
40
C4
45
0C
FC
85 00 00 00 00
0C
10
FE FF FF
10
FC
08
F0
00 00 00
0C
0C
04
00 00 00
10
FC 00 00 00 00
subesp, 0xC
moveax, [ebp-04]
leaedx, (04*eax)+
moveax, [ebp+0C]
push[eax+edx]
callprintf
addesp, 0x10
leaeax, [ebp-04]
inc[eax]
cmp[ebp-04], 0x9
jle0x8048456
FC 09
subesp, 0xC
push[ebp-04]
callfc
addesp, 0x10
leave
ret
pushebp
movebp, esp
subesp, 0x8
andesp, 0xF0
moveax, 0x0
subesp, eax
subesp, 0xC
moveax, [ebp+0C]
addeax, 0x4
push[eax]
callfa
addesp, 0x10
mov[ebp-04], 0x0
fa
printf
return
Figure 4.20 – Graphe détaillé du code de la fonction main de fn_dout
80484B7 FF 75 08
push [ebp+08]
Cet argument est le même que celui passé à strcpy : l’argument fourni par
l’utilisateur lors de l’exécution du programme. Nous remarquons, de plus, que
printf ne reçoit qu’un unique argument. Nous pouvons donc conclure qu’il
s’agit d’un bogue de format exploitable puisque rien n’empêche l’utilisateur
de fournir comme argument une chaîne similaire à %x %n, par exemple.
4.2.6.4
Analyse de printf dans la fonction main
La fonction main fait appel à la fonction printf. Le graphe de la figure
4.20 nous fournit les indications nécessaires à l’analyse de cet appel :
– La fonction est appelée dans une boucle.
– Le test d’arrêt de la boucle est écrit en fin de bloc, il s’agit donc d’une
boucle de type do{...}while. Le programme entre au moins une fois
dans cette boucle.
– printf reçoit un seul argument qui varie à chaque tour comme nous
le détaillons ci- dessous.
Le code suivant :
166
804845C 8D 14 85 00 00 00 00
8048463 8B 45 0C
8048466 FF 34 10
lea edx, (04*eax)
mov eax, [ebp+0C]
push [eax+edx]
nous indique que printf reçoit un argument qui correspond une fois encore
à ce que l’utilisateur a saisi lors du lancement du programme. La boucle
permet de balayer tous les arguments saisis pour les afficher les uns après les
autres. Cet appel de fonction est donc une faille de sécurité.
4.2.6.5
Analyse de printf dans la fonction fc
Voici le listing désassemblé de la fonction fc :
fonction fc adresse 804851d
804851D 55
804851E 89 E5
8048520 83 EC 08
8048523 83 EC 0C
8048526 68 58 86 04 08
804852B E8 20 FE FF FF
8048530 83 C4 10
8048533 C9
8048534 C3
push ebp
mov ebp, esp
sub esp, 0x8
sub esp, 0xC
push 0x8048658
call printf
add esp, 0x10
leave
ret
L’argument mis sur la pile avant l’appel de la fonction printf correspond à une adresse située dans la section .rodata. Cette section contient les
données globales en lecture seule, notamment les chaînes de caractères. Nous
pouvons, grâce à la lecture de la section .rodata effectuée par graphELF,
connaître la chaine de caractères affichée : c’est fini. On peut donc écarter cet appel à printf puisqu’il ne pose aucun problème et n’affiche pas de
données saisies par l’utilisateur.
L’analyse du code désassemblé nous a permis de parvenir aux mêmes
conclusions que si l’on avait effectué une lecture du code source du programme. Cette analyse a été facilitée par les graphes et les informations (la
table des symboles dynamiques, les adresses des sections en particulier) fournis par notre outil. Ceci montre que, bien qu’une grande partie de l’analyse
soit encore manuelle, l’analyse de fichiers binaires est largement facilitée par
graphELF.
Chapitre 5
Conclusion
Nous avons présenté graphELF, un outil d’aide à la compréhension de
fichiers binaires au format ELF. Cet outil fonctionne sur une plate-forme
Intel avec système Linux. Il analyse un fichier ELF et fournit les informations
contenues dans celui-ci :
– des informations générales sur le fichier (le type du fichier —exécutable,
relocalisable ou librairie —, la plate-forme, le processeur, etc.)
– les tables des symboles dynamiques et statiques
– la répartition des sections dans les segments
– les tables GOT et PLT
– la liste des librairies dynamiques utilisées par le programme
– la liste des chaînes de caractères contenues dans le programme
– le listing assembleur du code découpé par fonctions
– le graphe des appels de fonctions
– un graphe du flot de contrôle par fonction représentant l’organisation
de chaque fonction
– un graphe par fonction représentant le code désassemblé découpé blocs
par blocs en fonction du flot de contrôle
– une analyse des fonctions externes appelées.
Toutes ces informations permettent ensuite une meilleure compréhension du
programme.
Les tables des symboles nous renseignent sur le nom des fonctions du
programme. La liste des librairies requises pour le fonctionnement du programme couplée à la liste des appels de fonctions dynamiques nous fournit
des indications sur les communications entre le programme et le système
(via les fonctions réseaux par exemple) ainsi que celles entre l’utilisateur et
le programme (via les fonctions d’affichage ou de saisie de données entre
autres).
Le code désassemblé découpé en fonctions est plus facilement interprétable que le listing complet sans aucune séparation. De plus, les graphes
167
168
contribuent de manière significative à une plus grande compréhension du
code assembleur et permettent donc de conclure plus facilement sur ce que
fait chacune des fonctions implémentées et sur la façon dont est construit
le programme. Ces graphes permettent une meilleure localisation des éventuelles modifications à apporter au programme.
Les algorithmes implémentés dans graphELF, qui analysent le code désassemblé afin de le subdiviser en différents blocs tiennent compte du niveau
d’optimisation du programme. En effet, plus le code a été optimisé plus il
est difficile de retrouver les différents blocs de code qui constituent les fonctions : les appels de fonction peuvent être effectués avec une instruction
call comme avec une instruction jmp, une instruction ret ne constitue pas
forcément une fin de définition de fonction. De plus, graphELF est capable
d’effectuer toutes les opérations d’analyse quels que soient les éléments optionnels supprimés dans le fichier ELF : il peut fournir les mêmes résultats
avec ou sans en-tête de section, avec ou sans table des symboles.
Bien que graphELF effectue un travail sur le code désassemblé, cet outil
n’a pas pour objectif le désassemblage du code uniquement mais celui de
fournir à terme une première analyse automatique d’un fichier au format ELF
afin d’orienter les recherches et les modifications éventuelles à apporter à un
programme, notamment dans le cadre de la sécurité informatique. C’est pour
cela que graphELF ne s’arrète pas à la compréhension du code désassemblé
mais fournit dès à présent des indications quant à la recherche d’éventuelles
failles dans le programme.
Ainsi, l’analyse de ces informations nous fournit des renseignements permettant d’orienter et d’accelérer les recherches de failles de sécurité. Les
chaînes de caractères nous donnent une première estimation des messages
contenus dans le programme ainsi que des éventuels bogues de format présents dans le fichier. Les débordements de tampons qui pourraient se produire
par l’intermédiaire d’appels de fonctions réputées dangeureuses ou du moins
à risque sont facilement repérés grâce à la mise en évidence de ces appels de
fonction. graphELF permet également une première estimation quant à la
possibilité de virus contenu dans le programme en situant le point d’entrée du
programme ou en examinant les noms des différentes sections. En matière de
sécurité, l’analyse et la compréhension du programme permettent également
de détecter d’éventuelles fonctionnalités cachées et souvent indésirables.
graphELF est donc un outil qui analyse un fichier exécutable au format
ELF, affiche les informations contenues dans ce fichier, exécute un travail
de désassemblage et d’analyse des lignes de code désassemblé et livre des
graphes qui permettent d’aider à la compréhension de ce code.
Affinage des analyses dans la recherche de failles de sécurité
169
Etant donné notre but principal en matière de sécurité, les analyses effectuées par graphELF doivent être plus précises. Les listes des fonctions
dangeureuses notamment doivent être affinées car ces listes ne tiennent actuellement pas compte du contexte d’utilisation de ces fonctions. Les débordements de tampons doivent être analysés afin de déterminer s’il y a ou non
risque avéré de débordement tandis que les bogues de format peuvent être
envisagés de manière plus sélective en éliminant les fonctions correctement
construites, à savoir celles qui possèdent un nombre d’argument correct.
Pour affiner ces recherches de vulnérabilités, nous devons déterminer la
taille des tampons utilisés par les fonctions ou encore le nombre d’arguments
transmis. De même, pour savoir si les débordements de tampons et les bogues
de format peuvent être exploitables, il faut déterminer si les arguments fournis aux fonctions sont accessibles à l’utilisateur. graphELF ne peut actuellement fournir ce type d’indications de manière automatique. C’est pourquoi
un nouvel axe de recherche doit être envisagé : l’examen du flux des données.
Pour y parvenir, nous distinguerons les variables globales des variables locales
et arguments transmis aux fonctions. En effet, les premières sont accessibles
directement par leur adresse tandis que les autres sont transmises par l’intermédiaire de la pile. Pour ces dernières, il faudra prendre en compte les
options de compilation du programme : selon l’option choisie, ces variables
sont accessibles par le registre EPB ou par le registre ESP. Il nous faudra donc
envisager deux algorithmes différents ou analyser les registres à la manière
de Kiss et al. qui analysent chaque instruction et déterminent quel registre
est lu ou écrit pour ensuite répercuter l’éventuelle modification dans le flux
de données [KJLG03].
L’analyse du flux de données du programme pourra également nous permettre de résoudre des cas posant actuellement problème dans la détection
des branchements : la résolution des jmp indexés se référant au contenu d’un
registre pour déterminer le branchement (cf. partie 3.6.4) ou encore la résolution des appels de fonction indirects (cf. partie 3.5). L’analyse du flux de
donnée peut en effet nous permettre de déterminer le contenu d’un registre
à un moment de l’exécution du programme [CF97].
Les différentes options de compilation du programme feront également
l’objet d’une étude afin de déterminer le nombre d’arguments transmis à
une fonction, par exemple. En effet, selon le compilateur utilisé et le niveau
d’optimisation requis, les instructions assembleur d’accession à la pile et de
passage de paramètres peuvent être totalement différents. Chaque option
d’optimisation doit donc être étudiée afin de pouvoir définir des modèles
qui serviront de base pour l’analyse. Les arguments des fonctions appelées
peuvent, par exemple, être transmis dans les lignes assembleur situées juste
avant l’appel de fonction, ou au contraire, beaucoup plus tôt dans le code avec
d’autres arguments concernant d’autres appels de fonction. Les instructions
d’ajustement de la pile sont également différentes et interviennent dans le
170
calcul du nombre d’arguments transmis à la fonction.
Amélioration de la compréhension du code désassemblé
Le code désassemblé fourni actuellement par graphELF est découpé en
blocs permettant une meilleure lisibilité du code. Cependant, il peut être
utile d’annoter ce code au fur et à mesure de la lecture, de la même manière
que l’on peut adjoindre des commentaires à un code source.
De plus, actuellement, les fonctions n’ont pas obligatoirement de nom
(en cas d’absence de la table des symboles, les fonctions ont pour “nom” leur
adresse de début) et les variables ne sont pas clairement identifiées. La prochaine étape consiste à implémenter la possibilité de nommer explicitement
ces objets et de transmettre ce nom de manière automatique à travers tout
le code désassemblé. Ceci permettra alors de mieux identifier les variables et
d’accélérer la compréhension du programme.
Limitation de la plate-forme et du compilateur
Le format 64 bits n’est actuellement pas pris en compte est fait partie
de la prochaine étape de développement de l’outil. Pour ce faire, il suffit
d’ajouter une définition du type d’objet utilisé avant de commencer l’analyse
du fichier.
Par ailleurs, graphELF repose sur des caractéristiques propres au compilateur et à la plate-forme pour augmenter la précision de ses découpages et
de ses extractions d’informations (pour rechercher notamment l’adresse de
début de la fonction main, ou encore pour connaître précisement les adresses
des sections dans les segments) dans un fichier ELF. Ceci restreint l’application de notre outil uniquement à des fichiers compilés par gcc. Pour
augmenter les capacités de notre outil, il nous faudra analyser des fichiers
compilés par d’autres compilateurs et déterminer comment retrouver, par
exemple, l’adresse de la fonction main ou encore déterminer si les sections
apparaissent dans le même ordre. Les différences relevées en fonction des
compilateurs devront donc être intégrées à notre outil sous forme de modules pour que l’analyse s’effectue ensuite de manière transparente, quel que
soit le compilateur utilisé.
Le format ELF est répandu sur de nombreuses plates-formes Unix. Or
graphELF n’analyse que des fichiers ELF pour plate-forme Intel. Nous devons maintenant étendre les possibilités d’analyse de graphELF à d’autres
plates-formes. La phase d’extraction des informations dans un fichier ELF
se subira pas de modifications majeures puisqu’elle repose sur l’organisation
171
d’un fichier au format ELF qui est quasiment la même pour toutes les platesformes à l’exception de certaines sections dépendantes de cette dernière. En
revanche, la phase de désassemblage de graphELF repose actuellement uniquement sur l’utilisation de la librairie libdisasm concue pour désassembler
du code pour processeur Intel. Il nous faudra donc exploiter ou créer d’autres
librairies capables d’analyser les instructions machine pour des processeurs
différents. Les phases d’analyse du code désassemblé devront pour leur part
subir des modifications mineures, les algorithmes développés ne reposant pas
uniquement sur les caractéristiques du code assembleur pour plate-forme Intel.
Interface plus facilement utilisable
graphELF fournit ses résultats dans un fichier qu’il nous faut ensuite parcourir pour rechercher les informations adéquates. Il serait plus aisé de disposer d’une interface permettant un accès direct aux informations recherchées.
Une telle interface donnera également la possibilité de naviguer de manière
interactive dans le listing du code desassemblé : un appel de fonction pourra
par exemple mener directement à la fonction appelée, tout comme un saut
pourra renvoyer directement sur l’adresse de destination du saut.
Il faudra également prendre en compte la taille des graphes fournis par
notre outil et donner la possibilité de rendre la lecture de ces derniers plus
aisée encore : ouvrir ou fermer des noeuds du graphe pour afficher ou non
certains détails par exemple, ou encore n’afficher que les parties étudiées et
faire évoluer cet affichage à la demande [Bal04].
Cette interface pourra également donner la possibilité d’écrire et de charger des scripts de recherche dans le code assembleur, à la façon du logiciel
IDApro. Un script permettant le décodage de code obfusqué ou crypté par
exemple peut s’évérer indispensable pour accéder au code de certains programmes, notamment des virus. En effet, certaines techniques mises en place
pour augmenter les difficultés de lecture du code désassemblé peuvent être
contournés tels que le décalage du code [Bru03].
L’analyse dynamique du fichier ELF
graphELF a pour objectif l’analyse d’un programme et la détection de
problèmes liés à la sécurité logicielle. Dans ce but, une simple analyse statique
telle que le fait graphELF est souvent insuffisante. Elle doit être mise en
relation avec une analyse dynamique du programme : la simulation de son
exécution à la manière de ce qui est proposé par Cifuentes [CWE02].
172
Les résultats fournis par graphELF devront donc être mis en relation
avec une analyse de l’image mémoire du programme entre autre. Ainsi, les
détournements d’appels de fonctions dynamiques effectuées par l’intermédiaire de changements d’adresse dans la table GOT pourraient être mis en
évidence. L’analyse statique nous fournissant le nom de la fonction supposée
être appelée et l’analyse dynamique celle de la fonction réellement appelée.
Notre objectif est de faire évoluer notre outil d’analyse de fichiers binaires
au format ELF vers un système capable de détecter de manière automatique
différentes failles de sécurité, de faciliter au maximum la compréhension du
code désassemblé ainsi que de fournir un environnement de programmation
spécialisé pour le développement, la maintenance et la vérification des programmes exécutables.
Bibliographie
[ano02]
anonymous. Runtime process infection. Phrack Magazine, 59,
juillet 2002.
[ASU89]
Alfred Aho, Ravi Sethi, and Jeffrey Ullman. Compilateurs principes, techniques et outils. InterEditions, Paris, 1989. ISBN
2-7296-0295-X.
[Bal04]
Françoise Balmas. Displaying dependence graphs : a hierarchical approach. Journal of Software Maintenance and Evolution :
Reseach and Practice, 16(3) :151–185, May-June 2004.
[Bar02]
Alexander Bartolich. The elf virus writing howto. Technical
report, 2002.
[Bas04]
Bastard the bastard disassembly environment (home page).
http://bastard.sourceforge.net/, 2004.
[BG03]
Christophe Bailleux and Christophe Grenier. Protections
contre l’exploitation des débordements de buffer - bibliothèques
et compilateurs. MISC Mag, 2003. http://www.miscmag.com/
articles.
[BGR01a]
Christophe Blaess, Christophe Grenier, and Frédéric Raynal.
Eviter les failles de sécurité dès le développement d’une application 2ème partie. Linux Magazine France, 1(24), janvier
2001.
[BGR01b]
Christophe Blaess, Christophe Grenier, and Frédéric Raynal.
Eviter les failles de sécurité dès le développement d’une application 4ème partie. Linux Magazine France, 1(26), mars 2001.
[Bio03]
Philippe Biondi. bin2graph. http://www.cartel-securite.
fr/pbiondi/, 2003.
[Bru03]
Nicolas Brulez.
Analyse d’un vers par désassemblage.
MISC Mag, janvier-février 2003. http://www.miscmag.com/
articles.
[Bru04]
Nicolas Brulez. Recherche de vulnérabilité par désassemblage. MISC Mag, Mars-Avril 2004. http://www.miscmag.
com/articles.
173
174
[BST00]
Arash Baratloo, Navjot Singh, and Timothy Tsai. Transparent
run-time defense against stack smashing attacks. In Proceedings
of 2000 USENIX Annual Technical Conference. USENIX, June
2000.
[BWC01]
David M. Beazley, Brian D. Ward, and Ian R. Cooke. The inside
story on shared libraries and dynamic loading. Computing in
Science and Engineering, September-October 2001.
[CBBKH01] Crispin Cowan, MAtt Barringer, Steve Beattie, and Greg
Kroah-Hartman. Formatguard : Automatic protection from
printf format string vulnerabilities. In Proceedings of the 10th
USENIX Security Symposium. USENIX, August 2001.
[Ces99]
Silvio Cesare. Shared library call redirection using elf plt infection. November 1999.
[Ces00]
Silvio Cesare. Unix viruses. 2000.
[Ces02]
Silvio Cesare. flowgraph. http://www.big.net.au/~silvio/
coding/graphing/flowgraph/, 2002.
[CF97]
Cristina Cifuentes and Antoine Fraboulet. Intraprocedural static slicing of binary executables. In Proceedings of the 1997 International Conference on Software Maintenance. IEEE, 1997.
[Com95]
TIS Committee. Executable and linking format specification.
Technical report, Tool Interface Standard committee, may 1995.
edition 1.2.
[CPM+ 98]
Crispan Cowan, Calton Pu, Dave Maier, Jonathan Walpole,
Peat Bakke, Steve Beattie, Aaron Grier, Perry Wagle, and Qian
Zhang. Stackguard : Automatic adaptive detection and prevention of buffer_overflow attacks. In Proceedings of the 7th
USENIX Security Symposium. USENIX, January 1998.
[CWE02]
Cristina Cifuentes, Trent Waddington, and Mike Van Emmerik.
Computer security analysis through decompilation and highlevel debugging. In Proceedings of the Eighth Working Conference on Reverse Engineering 2001. IEEE, 2002.
[CWP+ 99]
Crispan Cowan, Perry Wagle, Calton Pu, Steve Beattie, and
Jonathan Walpole. Buffer overflows : Attacks and defenses for
the vulnerability of the decade. In Proceedings of DARPA Information Survivability Conference and Expo. IEEE, 1999.
[Dra04]
Samuel Dralet. Le reverse engineering et ses raisons. MISC
Mag, juillet-août 2004. http://www.miscmag.com/articles.
[DW01]
Drew Dean David Wagner. Intrusion detection via static analysis. In Proceedings of the IEEE Symposium on Security and
Privacy. IEEE, 2001.
175
[Elf04]
[FCD03]
[Fon04]
[Fou04]
[Fre02a]
[Fre02b]
[GBK04]
[gru01]
[gru04]
[HB03]
[Hew01]
[IDA04]
[Int04]
[KJLG03]
[KLA+ 04]
[KR92]
Elfsh. http://www.devhell.org/~mayhem/projects/elfsh/,
2004.
Eric Fifiol, Patrick Chambet, and Eric Detoisien. La fuite d’informations dans les documents propriétaires. MISC Mag, MaiJuin 2003. http://www.miscmag.com/articles.
Free Software Fondation. binutils. http://www.gnu.org/
software/binutils/, 2004.
Free Software Foundation. the gcc home page. http://gcc.
gnu.org/, 2004.
Free Software Fondation, Inc. objdump - display information
from object files, 2002. pages man objdump.
Free Software Fondation, Inc. readelf - Displays information
about ELF files, 2002. pages man readelf.
Andrew Golovnia, Konstantin Boldyshev, and Nick Kurshev.
Biew is binary view project (home page). http://biew.
sourceforge.net/en/biew.html, 2004.
grugq. Cheating the elf. subversive dynamic linking to libraries.
Technical report, 2001.
grugq. the reverse engineer’s assembly producer (home page).
http://grugq.tripod.com/reap/, 2004.
Eric Haugh and Matt Bishop. Testing c programms for buffer
overflow vulnerabilities. Technical report, University of Cafifornia at Davis, 2003.
Hewlett-Packard Compagny. HP-UX Linker and Libraries
User’s Guide, March 2001. Part Number : B2355-90721.
Idapro disassembler and degugger (home page). http://www.
datarescue.com/idabase/ida.htm, 2004.
Intel. IA-32 Intel Architecture Software Developer’s Manuals - Vol 1,2,3. http://www.intel.com/design/pentium4/
manuals/index_new.htm, 2004.
Akos Kiss, Judit Jasz, Gabor Lehotai, and Tibor Gyimothy.
Interprocedural static slicing of binary executables. In Proceedings of the Third IEEE International Workshop on Source
Code Analysis and Manipulation (SCAM’03). IEEE, September
2003.
Jack Koziol, David Litchfield, Dave Aitel, Chris Anley, Sinan
Eren, Neel Mehta, and Riley Hassell. The Shellcoder’s Handbook. Discovering and Exploiting Security Holes. Wiley Publishing, Inc., 2004. ISBN 0-7645-4468-3.
Brian W. Kernighan and Dennis M. Ritchie. Le langage C.
Masson et Prentice Hall, 1992.
176
[LDa02]
Ldasm - site de référence.
projects/ldasm.html, 2002.
[LE01]
David Larochelle and David Evans. Statically detecting likely
buffer overflow vulnerabilities. In Proceedings of the 10th USENIX Security Symposium. USENIX, August 2001.
[Lev99]
John R. Levine. Linkers and Loaders. Morgan-Kauffman, october 1999. ISBN 1-55860-496-0.
[lib04]
libdisasm - basic disasm engine.
sourceforge.net/libdisasm.html, 2004.
[LMKQ89]
Samuel J. Leffer, Marshall Kirk McKusick, Michael J. Karels,
and John S. Quaterman. The design and implementation of the
4.3 BSD Unix Operating System. Addison-Wesley Publishing
Compagny, 1989. ISBN 0-201-06169-1.
[Lu95]
H. Lu. Elf : From the programmer’s perspective. http://
citeseer.nj.nec.com/lu95elf.html, 1995.
[May03]
Mayhem. The cerberus elf interface. Devhell Labs and Phrack
Magazine, 1(61), 2003.
[MD+ 00]
Roland McGrath, Ulrich Drepper, et al. ld.so, ld-linux.so* Chargeur et éditeur de liens dynamique, october 2000. Manuel
de l’administrateur Linux.
[MEW04]
Arkadiusz Miskiewicz, Mike Van Emmerik, and Trent Waddington. Boomerang - an attempt at a general, open source, retargetable decompiler of native executable programs (home page).
http://boomerang.sourceforge.net/, 2004.
[MM03]
Karine Mordal-Manet. mémoire de maitrise informatique : les
fichiers objets et le format elf. Technical report, Université Paris
8, Département informatique, juin 2003.
[Oer00]
Marieus Van Oers. Linux viruses - elf file format. Virus Bulletin
Conference, 2000.
[PcC03]
Manish Prasad and Tzi cker Chiueh. A binary rewriting defense
against stack based buffer overflow attacks. In Proceedings of
the General Strack : USENIX Annual Technical Conference.
USENIX, June 2003.
[RAZ02]
RAZOR. Fenris (home page). http://www.bindview.com/
Support/RAZOR/Utilities/Unix_Linux/fenris_index.cfm,
2002.
[RD03]
Frédéric Raynal and Samuel Dralet. Protections contre l’exploitation des débordements de buffer - les patches. MISC Mag,
2003. http://www.miscmag.com/articles.
[REC00]
Rec - reverse engineering compiler (home page). http://www.
backerstreet.com/rec/rec.htm, 2000.
http://www.feedface.com/
http://bastard.
177
[Res00]
AT&T Labs Research. Graphviz - open source graph drawing software. http://www.research.att.com/sw/tools/
graphviz/, 2000.
[SCO00]
SCO. Developer’s Topics, august 2000. manuel de référence
concernant SCO OpenServer Release 5.0.6.
[Sun02]
Sun Microsystems, Inc. Linker and Libraries Guide, may 2002.
Part NO : 816-1386-10.
[Sus04]
Suse - home page.
2004.
[VR03]
Julien Vanegue and Sébastien Roy. Reverse engineering des systèmes elf/intel. Actes du Symposium sur la sécurité des Technologies de l’Information et des Communications, juin 2003.
[WK02]
John Wilander and Mariam Kambar. A comparison of publicly available tools for static intrusion prevention. In Published
at the 7th Nordic Workshop on Secure IT Systems, Karlstad,
Sweden, 2002.
[WK03]
John Wilander and Mariam Kambar. A comparaison of publicly available tools for dynamic buffer overflow prevention. In
Published at the 10th Network and Distributed System Security
Symposium, 2003.
http://www.suse.com/us/index.html,
178
Annexe A
Un affichage avec readelf
En-tête ELF:
Magique:
7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Classe:
ELF32
Données:
complément à 2,
système à octets de poids faible d’abord
(little endian)
Version:
1 (current)
OS/ABI:
UNIX - System V
Version ABI:
0
Type:
EXEC (fichier exécutable)
Machine:
Intel 80386
Version:
0x1
Adresse du point d’entrée:
0x8048390
Début des en-têtes de programme:
52 (octets dans le fichier)
Début des en-têtes de section:
4696 (octets dans le fichier)
Fanions:
0x0
Taille de cet en-tête:
52 (bytes)
Taille de l’en-tête du programme: 32 (bytes)
Nombre d’en-tête du programme:
6
Taille des en-têtes de section:
40 (bytes)
Nombre d’en-têtes de section:
35
Table d’indexes des chaînes d’en-tête de section: 32
En-têtes de section:
[Nr] Nom
[ 0]
[ 1] .interp
[ 2] .note.ABI-tag
[ 3] .hash
[ 4] .dynsym
[ 5] .dynstr
[ 6] .gnu.version
[ 7] .gnu.version_r
[ 8] .rel.dyn
Type
NULL
PROGBITS
NOTE
HASH
DYNSYM
STRTAB
VERSYM
VERNEED
REL
179
Adr
00000000
080480f4
08048108
08048128
08048164
08048204
08048286
0804829c
080482dc
Décala.Taille
000000 000000
0000f4 000013
000108 000020
000128 00003c
000164 0000a0
000204 000082
000286 000014
00029c 000040
0002dc 000010
ES Fan LN Inf Al
00
0
0 0
00
A 0
0 1
00
A 0
0 4
04
A 4
0 4
10
A 5
1 4
00
A 0
0 1
02
A 4
0 2
00
A 5
2 4
08
A 4
0 4
180
[ 9] .rel.plt
REL
080482ec 0002ec 000028 08
A 4
[10] .init
PROGBITS
08048314 000314 000017 00 AX 0
[11] .plt
PROGBITS
0804832c 00032c 000060 04 AX 0
[12] .text
PROGBITS
08048390 000390 000294 00 AX 0
[13] .fini
PROGBITS
08048624 000624 00001a 00 AX 0
[14] .rodata
PROGBITS
08048640 000640 000023 00
A 0
[15] .data
PROGBITS
08049664 000664 00000c 00 WA 0
[16] .eh_frame
PROGBITS
08049670 000670 000004 00
A 0
[17] .dynamic
DYNAMIC
08049674 000674 0000d0 08 WA 5
[18] .ctors
PROGBITS
08049744 000744 000008 00 WA 0
[19] .dtors
PROGBITS
0804974c 00074c 000008 00 WA 0
[20] .jcr
PROGBITS
08049754 000754 000004 00 WA 0
[21] .got
PROGBITS
08049758 000758 000024 04 WA 0
[22] .bss
NOBITS
0804977c 00077c 000008 00 WA 0
[23] .comment
PROGBITS
00000000 00077c 0000d9 00
0
[24] .debug_aranges
PROGBITS
00000000 000858 000098 00
0
[25] .debug_pubnames
PROGBITS
00000000 0008f0 00005f 00
0
[26] .debug_info
PROGBITS
00000000 00094f 000300 00
0
[27] .debug_abbrev
PROGBITS
00000000 000c4f 00011d 00
0
[28] .debug_line
PROGBITS
00000000 000d6c 000221 00
0
[29] .debug_frame
PROGBITS
00000000 000f90 000058 00
0
[30] .debug_str
PROGBITS
00000000 000fe8 00011d 01 MS 0
[31] .debug_ranges
PROGBITS
00000000 001105 000018 00
0
[32] .shstrtab
STRTAB
00000000 00111d 000139 00
0
[33] .symtab
SYMTAB
00000000 0017d0 000740 10
34
[34] .strtab
STRTAB
00000000 001f10 0003a1 00
0
Clé des fanions:
W (écriture), A (allocation), X (exécution), M (fusion), S (chaînes)
I (info), L (ordre des liens), G (groupe), x (inconnu)
O (traiterment additionnel requis pour l’OS) o (spécifique à l’OS),
p (spécifique au processeur)
En-têtes de programme:
Type
Décalage Adr. vir.
PHDR
0x000034 0x08048034
INTERP
0x0000f4 0x080480f4
[Réquisition de l’interpréteur
LOAD
0x000000 0x08048000
LOAD
0x000664 0x08049664
DYNAMIC
0x000674 0x08049674
NOTE
0x000108 0x08048108
b 4
0 4
0 4
0 16
0 4
0 4
0 4
0 4
0 4
0 4
0 4
0 4
0 4
0 4
0 1
0 8
0 1
0 1
0 1
0 1
0 4
0 1
0 1
0 1
55 4
0 1
Adr.phys. T.Fich. T.Mém. Fan Alignement
0x08048034 0x000c0 0x000c0 R E 0x4
0x080480f4 0x00013 0x00013 R
0x1
de programme: /lib/ld-linux.so.2]
0x08048000 0x00663 0x00663 R E 0x1000
0x08049664 0x00118 0x00120 RW 0x1000
0x08049674 0x000d0 0x000d0 RW 0x4
0x08048108 0x00020 0x00020 R
0x4
Section à la projection de segement:
Sections de segment...
00
01
.interp
02
.interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r
.rel.dyn .rel.plt .init .plt .text .fini .rodata
03
.data .eh_frame .dynamic .ctors .dtors .jcr .got .bss
181
04
05
.dynamic
.note.ABI-tag
Dynamic segment at offset 0x674 contains 21 entries:
Étiquettes Type
Nom/Valeur
0x00000001 (NEEDED)
Librairie partagées: [libm.so.6]
0x00000001 (NEEDED)
Librairie partagées: [libc.so.6]
0x0000000c (INIT)
0x8048314
0x0000000d (FINI)
0x8048624
0x00000004 (HASH)
0x8048128
0x00000005 (STRTAB)
0x8048204
0x00000006 (SYMTAB)
0x8048164
0x0000000a (STRSZ)
130 (bytes)
0x0000000b (SYMENT)
16 (bytes)
0x00000015 (DEBUG)
0x0
0x00000003 (PLTGOT)
0x8049758
0x00000002 (PLTRELSZ)
40 (bytes)
0x00000014 (PLTREL)
REL
0x00000017 (JMPREL)
0x80482ec
0x00000011 (REL)
0x80482dc
0x00000012 (RELSZ)
16 (bytes)
0x00000013 (RELENT)
8 (bytes)
0x6ffffffe (VERNEED)
0x804829c
0x6fffffff (VERNEEDNUM)
2
0x6ffffff0 (VERSYM)
0x8048286
0x00000000 (NULL)
0x0
Section de relocalisation « .rel.dyn » à l’adresse de décalage 0x2dc
contient 2 entrées:
Décalage
Info
Type
Val.-sym
Noms-symboles
08049778 00000806 R_386_GLOB_DAT
00000000
__gmon_start__
0804977c 00000505 R_386_COPY
0804977c
stdin
Section de relocalisation « .rel.plt » à l’adresse de décalage 0x2ec
contient 5 entrées:
Décalage
Info
Type
Val.-sym
Noms-symboles
08049764 00000107 R_386_JUMP_SLOT
0804833c
fgets
08049768 00000207 R_386_JUMP_SLOT
0804834c
__libc_start_main
0804976c 00000307 R_386_JUMP_SLOT
0804835c
printf
08049770 00000407 R_386_JUMP_SLOT
0804836c
sqrt
08049774 00000907 R_386_JUMP_SLOT
0804837c
strcpy
Il n’y a pas de section unwind dans ce fichier.
Table de symboles « .dynsym » contient 10 entrées:
Num:
Valeur Tail Type
Lien
Vis
Ndx Nom
0: 00000000
0 NOTYPE LOCAL DEFAULT UND
1: 0804833c
362 FUNC
GLOBAL DEFAULT UND fgets@GLIBC_2.0 (2)
2: 0804834c
251 FUNC
GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.0(2)
182
3:
4:
5:
6:
7:
8:
9:
0804835c
0804836c
0804977c
08048644
00000000
00000000
0804837c
54
124
4
4
0
0
48
FUNC
FUNC
OBJECT
OBJECT
NOTYPE
NOTYPE
FUNC
GLOBAL
GLOBAL
GLOBAL
GLOBAL
WEAK
WEAK
GLOBAL
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
UND
UND
22
14
UND
UND
UND
printf@GLIBC_2.0 (2)
sqrt@GLIBC_2.0 (3)
stdin@GLIBC_2.0 (2)
_IO_stdin_used
_Jv_RegisterClasses
__gmon_start__
strcpy@GLIBC_2.0 (2)
Table de symboles « .symtab » contient 116 entrées:
Num:
Valeur Tail Type
Lien
Vis
Ndx Nom
0: 00000000
0 NOTYPE LOCAL DEFAULT UND
1: 080480f4
0 SECTION LOCAL DEFAULT
1
2: 08048108
0 SECTION LOCAL DEFAULT
2
3: 08048128
0 SECTION LOCAL DEFAULT
3
4: 08048164
0 SECTION LOCAL DEFAULT
4
5: 08048204
0 SECTION LOCAL DEFAULT
5
6: 08048286
0 SECTION LOCAL DEFAULT
6
7: 0804829c
0 SECTION LOCAL DEFAULT
7
8: 080482dc
0 SECTION LOCAL DEFAULT
8
9: 080482ec
0 SECTION LOCAL DEFAULT
9
10: 08048314
0 SECTION LOCAL DEFAULT
10
11: 0804832c
0 SECTION LOCAL DEFAULT
11
12: 08048390
0 SECTION LOCAL DEFAULT
12
13: 08048624
0 SECTION LOCAL DEFAULT
13
14: 08048640
0 SECTION LOCAL DEFAULT
14
15: 08049664
0 SECTION LOCAL DEFAULT
15
16: 08049670
0 SECTION LOCAL DEFAULT
16
17: 08049674
0 SECTION LOCAL DEFAULT
17
18: 08049744
0 SECTION LOCAL DEFAULT
18
19: 0804974c
0 SECTION LOCAL DEFAULT
19
20: 08049754
0 SECTION LOCAL DEFAULT
20
21: 08049758
0 SECTION LOCAL DEFAULT
21
22: 0804977c
0 SECTION LOCAL DEFAULT
22
23: 00000000
0 SECTION LOCAL DEFAULT
23
24: 00000000
0 SECTION LOCAL DEFAULT
24
25: 00000000
0 SECTION LOCAL DEFAULT
25
26: 00000000
0 SECTION LOCAL DEFAULT
26
27: 00000000
0 SECTION LOCAL DEFAULT
27
28: 00000000
0 SECTION LOCAL DEFAULT
28
29: 00000000
0 SECTION LOCAL DEFAULT
29
30: 00000000
0 SECTION LOCAL DEFAULT
30
31: 00000000
0 SECTION LOCAL DEFAULT
31
32: 00000000
0 SECTION LOCAL DEFAULT
32
33: 00000000
0 SECTION LOCAL DEFAULT
33
34: 00000000
0 SECTION LOCAL DEFAULT
34
35: 00000000
0 FILE
LOCAL DEFAULT ABS <command line>
36: 00000000
0 FILE
LOCAL DEFAULT ABS /usr/src/packages/BUILD/g
37: 00000000
0 FILE
LOCAL DEFAULT ABS <command line>
38: 00000000
0 FILE
LOCAL DEFAULT ABS <built-in>
183
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
080483b4
00000000
08049744
0804974c
08049754
0804966c
08049780
080483e0
08048420
00000000
08049748
08049750
08049670
08049754
08048600
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
08049674
08048640
08049664
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
4
0
FILE
FILE
FILE
FILE
FILE
FILE
FILE
FILE
FILE
FILE
FILE
FILE
FILE
FILE
FILE
FILE
FILE
FILE
FILE
FILE
FUNC
FILE
OBJECT
OBJECT
OBJECT
OBJECT
OBJECT
FUNC
FUNC
FILE
OBJECT
OBJECT
OBJECT
OBJECT
FUNC
FILE
FILE
FILE
FILE
FILE
FILE
FILE
FILE
FILE
FILE
FILE
OBJECT
OBJECT
NOTYPE
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
LOCAL
GLOBAL
GLOBAL
GLOBAL
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
ABS
ABS
ABS
ABS
ABS
ABS
ABS
ABS
ABS
ABS
ABS
ABS
ABS
ABS
ABS
ABS
ABS
ABS
ABS
ABS
12
ABS
18
19
20
15
22
12
12
ABS
18
19
16
20
12
ABS
ABS
ABS
ABS
ABS
ABS
ABS
ABS
ABS
ABS
ABS
17
14
ABS
abi-note.S
/usr/src/packages/BUILD/g
abi-note.S
/usr/src/packages/BUILD/g
abi-note.S
<command line>
/usr/src/packages/BUILD/g
<command line>
<built-in>
abi-note.S
init.c
/usr/src/packages/BUILD/g
/usr/src/packages/BUILD/g
initfini.c
/usr/src/packages/BUILD/g
<command line>
/usr/src/packages/BUILD/g
<command line>
<built-in>
/usr/src/packages/BUILD/g
call_gmon_start
crtstuff.c
__CTOR_LIST__
__DTOR_LIST__
__JCR_LIST__
p.0
completed.1
__do_global_dtors_aux
frame_dummy
crtstuff.c
__CTOR_END__
__DTOR_END__
__FRAME_END__
__JCR_END__
__do_global_ctors_aux
/usr/src/packages/BUILD/g
/usr/src/packages/BUILD/g
initfini.c
/usr/src/packages/BUILD/g
<command line>
/usr/src/packages/BUILD/g
<command line>
<built-in>
/usr/src/packages/BUILD/g
test1.c
elf-init.c
_DYNAMIC
_fp_hw
__fini_array_end
184
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
115:
08049668
08048590
080484fa
08048314
08048390
0804833c
08049664
08048530
0804977c
0804844c
0804834c
08049664
08049664
0804835c
08048624
0804836c
0804977c
080485f0
08049758
08049784
0804977c
08049664
08048644
08049664
00000000
08048495
00000000
0804837c
0
96
50
0
0
362
0
88
0
73
251
0
0
54
0
124
0
0
0
0
4
0
4
0
0
101
0
48
OBJECT
FUNC
FUNC
FUNC
FUNC
FUNC
NOTYPE
FUNC
NOTYPE
FUNC
FUNC
NOTYPE
NOTYPE
FUNC
FUNC
FUNC
NOTYPE
FUNC
OBJECT
NOTYPE
OBJECT
NOTYPE
OBJECT
NOTYPE
NOTYPE
FUNC
NOTYPE
FUNC
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
WEAK
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
GLOBAL
WEAK
GLOBAL
WEAK
GLOBAL
HIDDEN
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
HIDDEN
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
DEFAULT
15 __dso_handle
12 __libc_csu_fini
12 fb
10 _init
12 _start
UND fgets@@GLIBC_2.0
ABS __fini_array_start
12 __libc_csu_init
ABS __bss_start
12 main
UND __libc_start_main@@GLIBC_
ABS __init_array_end
15 data_start
UND printf@@GLIBC_2.0
13 _fini
UND sqrt@@GLIBC_2.0
ABS _edata
12 __i686.get_pc_thunk.bx
21 _GLOBAL_OFFSET_TABLE_
ABS _end
22 stdin@@GLIBC_2.0
ABS __init_array_start
14 _IO_stdin_used
15 __data_start
UND _Jv_RegisterClasses
12 fa
UND __gmon_start__
UND strcpy@@GLIBC_2.0
Histogramme de la longeur de la liste des baquets (total de 3 baquets):
Long.
Nombre
% de couverture totale
0 0
( 0.0%)
1 1
( 33.3%)
11.1%
2 0
( 0.0%)
11.1%
3 0
( 0.0%)
11.1%
4 2
( 66.7%)
100.0%
La version de section « .gnu.version » des symboles contient 10 entrée:
Adr: 0000000008048286 Décalage: 0x000286 Lien: 4 (.dynsym)
000:
0 (*local*)
2 (GLIBC_2.0)
2 (GLIBC_2.0)
2 (GLIBC_2.0)
004:
3 (GLIBC_2.0)
2 (GLIBC_2.0)
1 (*global*)
0 (*local*)
008:
0 (*local*)
2 (GLIBC_2.0)
Version nécessitant la section « .gnu.version_r » contenant 2 entrées:
Adr: 0x000000000804829c Décalage: 0x00029c Lien vers la section: 5 (.dynstr)
000000: Version: 1 Fichier: libm.so.6 Compteur: 1
0x0010: Nom: GLIBC_2.0 Fanions: aucun Version: 3
0x0020: Version: 1 Fichier: libc.so.6 Compteur: 1
0x0030: Nom: GLIBC_2.0 Fanions: aucun Version: 2
Annexe B
Quelques graphes construits
par graphELF
Le premier graphe représente le graphe des appels de fonction de la commande
ls.
Les deux graphes suivants représentent le graphe de flot de contrôle d’une fonction de ls et le code désassemblé pour cette même fonction.
Les deux derniers graphes représentent le graphe de flot de contrôle d’une fonction du programme objdump et le code désassemblé pour cette même fonction.
185
190
Annexe C
Le code source de graphELF
Cette annexe contient le code source du programme graphELF.
191

Documents pareils