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