Avant propos
À consulter (au moins pour les infos dans l'intro) :
Reversing tutorials - level 1 - Quelques outils avant de démarrer
Il faut également noter qu'il ne s'agit pas d’une retranscription de la page de manuel ELF donc certaines infos ne seront pas citées et d'autres seront approfondies.
Nous utiliserons souvent readelf (man readelf) à titre d'exemple, il ne faut pas hésiter à lire le manuel de cette commande (un simple readlf sans options vous aidera déjà).
Introduction
Le format ELF (Executable and Linking Format) est un format de fichier binaire exécutable principalement utilise sur Linux mais aussi sur d'autres plateformes. Etant donné qu'il s'agit d'un format ouvert, d’autres systèmes l'on adopté (BSD, Solaris ...).
Sous Windows le format utilise est le format PE (Portable Executable).
Tous les deux sont gérés par leur système respectif qui se chargent de leur mise en mémoire et de retrouver et exécuter les données selon l'organisation des fichiers.
Un binaire ELF est compose de divers informations et données que le compilateur met en place directement pour vous sans que vous ayez à vous en soucier. Ainsi on va retrouver dans un fichier binaire ELF toute les informations dont il a besoin pour pouvoir fonctionner et être exécuté si besoin.
Tel que :
Connaitre le format ELF est nécessaire pour faire du reversing (sous Linux). Le contraire serait comme demander de développer un programme en C# a quelqu'un qui ne connait pas ce langage.
Un format pour différents types de fichiers
Le format ELF est utilisé pour plusieurs types de fichiers sous Linux permettant en un seul format de garantir plusieurs utilisations. Ainsi le processus de mise en mémoire ... reste le même pour le système (ou presque).
Les fichiers exécutables
Ce sont les fichiers exécutables classiques tel que les programmes se trouvant dans /bin /usr/bin. Un fichier exécutable contient tout le nécessaire pour permettre son exécution (liaisons avec les librairies partagées ...). Il peut être lance directement depuis la ligne de commande.
Vous pouvez les différencier facilement avec la commande file (man file).
Un exemple de sortie avec la commande ls :
Les fichiers objets repositionnables
Ce sont des fichiers objet souvent lies avec d'autres fichiers de ce type pour former ensuite un exécutable dynamique. L'option -c de gcc permet de créer ce genre de fichier.
Un exemple de sortie de la commande file :
Les fichiers objets partagés
Comme son nom l'indique ce sont des fichiers objet partages. On les retrouve principalement comme librairies dynamiques (.so) qui seront liées dynamiquement a un exécutable lors de son exécution.
Exemple de sortie de la commande file :
Les fichiers cores
Souvent générés par gdb, ils représentent un état du binaire a un moment précis. On ne les utilise quasiment jamais.
Chaîne de production d'un binaire
gcc est magique gcc fait tout !
En effet, gcc se charge d'appeler tous les programmes qui feront d'un fichier source un binaire exécutable ELF. Cependant, lorsque vous lancez gcc avec votre fichier source, que se passe-t-il ?
Etape 1 : le préprocesseur.
Lors de cette phase, le préprocesseur va résoudre et faire l'inclusion des header utilisés dans votre programme. Autrement dit, les structures et fonctions externes ... seront prototypées.
Pour cela on utilisera la commande cpp (C Pre-Processor) (man cpp).
Exemple :
Etape 2 : la compilation
Il s'agit dans cette étape de transformer des fichiers source en code assembleur. Lors de cette étape, gcc fait des optimisations sur le code, c'est pourquoi faire de l’ASM aujourd'hui sert principalement à le comprendre mais pas pour de réels projets ni pour des optimisations (gcc est suffisamment performant). C'est ici que l'on utilise gcc.
Exemple :
Etape 3 : l'assemblage
Nous avons maintenant nos fichiers assembleur, il faut ensuite les transformer en fichiers binaires, dans notre cas en fichiers ELF repositionnables. Pour ce faire, l'assembleur GNU as fera l'affaire (man as).
Exemple :
Etape 4: la liaison (linking)
La dernière étape du processus, il s'agit de rassembler tous les fichiers objets repositionnables de votre programme pour en faire un binaire. C'est aussi lors de cette étape que le le programme appelle "le linker" va stocker des informations dans le fichier final pour lui permettre ensuite de retrouver les différentes librairies dynamiques utilisées. La commande est ld (man ld).
Warning, ld ne fonctionnera pas, du moins pas de façon classique. Pour ne pas vous embrouiller, nous verrons plus tard pourquoi.
Il nous faudra utiliser gcc qui va faire la liaison comme il se doit (même si lui utilise ld en interne finalement).
Exemple :
Le header ELF
Le header ELF est la première chose que l'on trouve dans un fichier binaire ELF. Celui-ci contient les informations primaires sur le reste du fichier et sa structure. En effet, les données sont stockées dans des endroits différents du fichier et donc contrairement à un fichier texte ascii, une lecture ligne par ligne n'est pas possible (ne servirait à rien en tout cas).
Ce header principale est positionne à l'offset 0 (le tout début du fichier). La structure ElfN_Ehdr définie dans <elf.h> nous permettra d'accéder à ces informations. Le N correspond a l'architecture qui peut varier en fonction du système et de l'architecture du binaire. Etant donné que ce binaire sera un binaire ELF 64bit, on va directement utiliser Elf64_Ehdr. (Elf32_Ehdr pour du 32bit par exemple).
Soit le code suivant :
Si vous n'êtes pas familier avec mmap (man 2 mmap), open (man 2 open) ou fstat (man 2 fstat) je vous conseille de lire les pages de manuel associées a ces syscall.
Dans notre fonction principale (main) nous allons "mapper", mettre en mémoire notre fichier en prenant le soin de faire les vérifications nécessaires d'usage (erreurs ...).
Le syscall mmap nous renvoi un void*, un pointeur générique. Etant donné que notre header se trouve à l'offset 0, on peut directement transformer ce pointeur générique en ElfN_Ehdr* pour accéder aux informations.
La structure Elf64_Ehdr est représentée comme cela :
Le premier champ e_ident va nous permettre d'identifier le binaire et vérifier qu'il s'agisse bien d'un binaire ELF 64bit.
Le manuel ELF nous informe que ce champ est un tableau de bytes et qu’on y accède par les macros suivantes :
Voilà l'intérêt de la fonction elf_check_header, on vérifie que ces valeurs sont bien celles attendues, que le fichier est valide et qu'il s'agisse bien d’un binaire ELF 64bit.
On peut ensuite extraire du header les informations plus utiles qui suivent.
Nous avons donc notre fonction elf_print_header qui se charge de nous afficher ces informations. Un petit test s'impose :
Les headers de programme (program header)
Les segments
Un segment est une portion de mémoire qui est réservé pour accueil des données. Il est défini par deux valeurs : son adresse et sa taille. Votre système utilise un mécanisme appelé segmentation permettant de découper la mémoire et gérée au plus bas niveau.
Ainsi quand vous utiliser une adresse dans un programme C, celle-ci est dite virtuelle, le système se chargera à partir de celle-ci de retrouver l'adresse physique par le mécanisme de segmentation (en bref). Je ne vais pas vous faire un cours sur la segmentation, ca nécessiterai d'appréhender pas mal de concepts et ce n’est pas le but ici (pas pour ce tutoriel).
Le principale est de savoir que votre binaire sera reparti généralement sur plusieurs segments et que chaque segment a ses propres propriétés. Chaque segment a des droits et des propriétés qui lui sont propre. Ainsi on peut retrouver le système de droits sur les fichiers (read, write, execute) sur les segments. Un segment avec le droit d'exécution accueillera la plus part du temps du code interprétable par le processeur...
Les program headers
Dans un fichier ELF, on y trouve une table de "program header" représentés par la structure suivante :
Un program header contient des informations sur le programme en fonction de son type définie par p_type. La plus part du temps il s'agit d'informations sur les segments. Voici les différents types (utiles) représentés par des macros et les informations associées :
D'autres existent qui ne nous serviront probablement jamais, jetez un œil sur le man ELF pour une liste plus exhaustive. Si vous ne devez en retenir qu'un, alors ce sera PT_LOAD, c'est dans celui-ci ou l'on retrouvera la plus part des donnes du binaire (code exécutable, données de votre programme ...).
Exemple:
Ici ce sont les deux segments LOAD qui vont nous intéresser (et le DYNAMIC ensuite) car on constate que c’est dans ces segments que ce trouvent les principales sections de notre programme (.text, .init, .data, .bss ...) que l'on verra juste maintenant.
Les sections
Comme son nom l'indique il s'agit d'une section du binaire dans laquelle sont regroupées différentes données. Comme déjà énoncé, un binaire contient plusieurs types de données, du code exécutable, des informations diverses pour la liaison de librairies ... C'est au travers ces différentes sections que l'on va pouvoir retrouver toute ces informations.
Le format ELF contient une table de headers de section nous donnant les informations sur chaque section, son emplacement dans le fichier, sa taille ... Un header est représenté avec la structure suivante :
Dans un binaire il existe plusieurs types de section, pour pouvoir les différencier, sh_type nous indiquera le type de la section (section contenant des chaines, des informations de liaison dynamique, ...).
sh_offset nous donnera la position de la section à partir du début du fichier et sh_size la taille de la section pour pouvoir retrouver ces données.
Ce qu'il faut également savoir, c'est que dans le format ELF on retrouve des sections spécifiques pour le stockage des noms de sections, le nom des symboles ... Ce sont les strings tables (tables de chaines de caractères).
Ainsi, dans le header principale, on dispose du champ e_shstrndx qui correspond à l'index dans la table des headers de section correspondant à la section contenant les noms des différentes sections. On va pouvoir ensuite retrouver chaque nom à l'aide du champ sh_name (de la structure précédente) qui définit un index dans cette section.
Dans ces sections les chaines sont représentées sous la forme suivante :
"Une chaine\0une seconde chaine\0une troisième chaine\0"
Ce n'est pas un tableau de chaines de caractères mais une simple chaine dont chaque sous-chaine est terminée par un null byte (\0).
Lorsque sh_name nous donne l'index 11, on se retrouve donc sur "une seconde ...\0une troi...\0". Lorsque l'on va utiliser des fonctions tel que printf, strlen ... on va donc automatiquement s'arrêter au null byte.
Voyons comment récupérer les noms des différentes sections grâce à cette section :
Pas si complique si ? Si vous avez du mal avec ce code je vous conseille de lire : Arithmetique des pointeurs qui vous aidera certainement !
La section .text
La section .text, probablement la plus importante est une section qui va contenir le code exécutable du binaire. La suite de bytecode interprétable par le processeur se trouve ici.
Ci-dessous on désassemble la section .text avec objdump (man objdump) :
Ce que va contenir cette section sera les bytecodes que l'on peut voir ici par exemple :
Essayons de désassembler tout ça avec notre propre code (et une lib). Pour la suite j'utiliserai la lib udis86 : http://udis86.sourceforge.net/ . Il s'agit d'une librairie qui nous permettra de convertir du bytecode en code assembleur. Cela évite de réinventer la roue quand on sait le nombre de bytecode immense existants. Si vous voulez un aperçu :
Soit le code suivant :
Je vous laisse le soin d'essayer. Voilà les instructions de votre code sous forme d'assembleur. (objdump fait la même chose en ajoutant des informations en plus).
La section .data .rodata
Dans la section .data on va retrouver les variables globales et statiques initialisées explicitement. La section .rodata est identique mais pour des valeurs constantes. L'écriture en section .rodata n'est pas permise.
Exemple :
On retrouve bien nos variables dans les sections adequates.
La section .bss
La section .bss contient elle aussi vos variables mais non-initialisées cette fois.
Exemple :
On retrouve la également nos variables cette fois avec la valeur 0 étant donne quelle ne sont pas initialisées.
La section .plt .got
Etant donné que ces sections nécessiteraient un tutoriel complet, je vous renvoie vers un article déjà rédigé : http://www.segmentationfault.fr/linu...plt-got-ld-so/
Les autres sections
Parmi les autres sections, on retrouve la section .dynamic contenant des informations sur les liaisons dynamiques. On retrouvera également d'autres sections contenant des chaines de caractères pour les symboles ...
Si vous voulez approfondir ces autres sections -> man elf.
Essayez d'utiliser la commande ldd (man ldd), les informations viennent de quelle section à votre avis ?
Les symboles
Un symbole est une étiquette qui identifie certains éléments du programme. Les symboles vont permettre de référencer les noms de vos fonctions, des fonctions des librairies dynamiques et statiques, des fichiers, des sections ...
Les symboles sont principalement utilisés pour le debug et permettent (entre autres) d'utiliser ces noms comme référence dans gdb.
Ex: lorsque l'on pose un break point sur main, main est un symbole, si l'on supprime les symboles du binaire, alors on ne pourra plus poser un break point sur main, il faudra spécifier une adresse à la place.
En effet, les symboles gonflent la taille du binaire, c'est pourquoi en production on supprime souvent ceux-là pour gagner de la place. Cela n'empêche pas le debug mais ça le complique légèrement (man strip). En version de développement, on gardera les symboles pour pouvoir faire du debug plus simplement.
Une commande utile pour lister les symboles d'un binaire ELF est nm (man nm) :
Les symboles ont différents types selon a quoi ils font référence. Plus de détails ici : http://www.thegeekstuff.com/2012/03/linux-nm-command/
Si vous voulez approfondir certains points faites le moi savoir, ici le but est de faire du reversing ne l'oubliez pas et pas de faire un interpréteur de binaire ELF ...
La prochaine étape ... surprise.
À consulter (au moins pour les infos dans l'intro) :
Reversing tutorials - level 1 - Quelques outils avant de démarrer
Il faut également noter qu'il ne s'agit pas d’une retranscription de la page de manuel ELF donc certaines infos ne seront pas citées et d'autres seront approfondies.
Nous utiliserons souvent readelf (man readelf) à titre d'exemple, il ne faut pas hésiter à lire le manuel de cette commande (un simple readlf sans options vous aidera déjà).
Introduction
Le format ELF (Executable and Linking Format) est un format de fichier binaire exécutable principalement utilise sur Linux mais aussi sur d'autres plateformes. Etant donné qu'il s'agit d'un format ouvert, d’autres systèmes l'on adopté (BSD, Solaris ...).
Sous Windows le format utilise est le format PE (Portable Executable).
Tous les deux sont gérés par leur système respectif qui se chargent de leur mise en mémoire et de retrouver et exécuter les données selon l'organisation des fichiers.
Un binaire ELF est compose de divers informations et données que le compilateur met en place directement pour vous sans que vous ayez à vous en soucier. Ainsi on va retrouver dans un fichier binaire ELF toute les informations dont il a besoin pour pouvoir fonctionner et être exécuté si besoin.
Tel que :
- Des espaces réservés au stockage de données (chaines de caractères, valeurs constantes ...)
- Des espaces réservés au stockage du code interprétable par le processeur
- Des espaces d'informations sur les librairies utilisées pour pouvoir y accéder
- Des espaces d'information de debug
- ...
Connaitre le format ELF est nécessaire pour faire du reversing (sous Linux). Le contraire serait comme demander de développer un programme en C# a quelqu'un qui ne connait pas ce langage.
Un format pour différents types de fichiers
Le format ELF est utilisé pour plusieurs types de fichiers sous Linux permettant en un seul format de garantir plusieurs utilisations. Ainsi le processus de mise en mémoire ... reste le même pour le système (ou presque).
Les fichiers exécutables
Ce sont les fichiers exécutables classiques tel que les programmes se trouvant dans /bin /usr/bin. Un fichier exécutable contient tout le nécessaire pour permettre son exécution (liaisons avec les librairies partagées ...). Il peut être lance directement depuis la ligne de commande.
Vous pouvez les différencier facilement avec la commande file (man file).
Un exemple de sortie avec la commande ls :
Code:
$ file /bin/ls /bin/ls: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, BuildID[sha1]=05e0f155082725dfdc56c4d4cedcc7c2536500a1, stripped $
Ce sont des fichiers objet souvent lies avec d'autres fichiers de ce type pour former ensuite un exécutable dynamique. L'option -c de gcc permet de créer ce genre de fichier.
Un exemple de sortie de la commande file :
Code:
$ gcc -o main.o -c main.c $ file main.o main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped $
Comme son nom l'indique ce sont des fichiers objet partages. On les retrouve principalement comme librairies dynamiques (.so) qui seront liées dynamiquement a un exécutable lors de son exécution.
Exemple de sortie de la commande file :
Code:
$ file /usr/lib/liba52-0.7.4.so /usr/lib/liba52-0.7.4.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=f6ec1f8e50e403dc0d8861b753fdbc5c867bd899, stripped $
Souvent générés par gdb, ils représentent un état du binaire a un moment précis. On ne les utilise quasiment jamais.
Chaîne de production d'un binaire
gcc est magique gcc fait tout !
En effet, gcc se charge d'appeler tous les programmes qui feront d'un fichier source un binaire exécutable ELF. Cependant, lorsque vous lancez gcc avec votre fichier source, que se passe-t-il ?
Etape 1 : le préprocesseur.
Lors de cette phase, le préprocesseur va résoudre et faire l'inclusion des header utilisés dans votre programme. Autrement dit, les structures et fonctions externes ... seront prototypées.
Pour cela on utilisera la commande cpp (C Pre-Processor) (man cpp).
Exemple :
Code:
$ cat main.c #include <stdio.h> int main() { printf("Hello World !\n"); return (0); } $ cpp main.c -o main_cpp.c $ cat main_cpp.c ...
Il s'agit dans cette étape de transformer des fichiers source en code assembleur. Lors de cette étape, gcc fait des optimisations sur le code, c'est pourquoi faire de l’ASM aujourd'hui sert principalement à le comprendre mais pas pour de réels projets ni pour des optimisations (gcc est suffisamment performant). C'est ici que l'on utilise gcc.
Exemple :
Code:
$ gcc -S main_cpp.c -o main.S $ cat main.S .file "main_cpp.c" .section .rodata .LC0: .string "Hello World !" .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushq %rbp ... $
Nous avons maintenant nos fichiers assembleur, il faut ensuite les transformer en fichiers binaires, dans notre cas en fichiers ELF repositionnables. Pour ce faire, l'assembleur GNU as fera l'affaire (man as).
Exemple :
Code:
$ as main.S -o main.o $ file main.o main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped $
La dernière étape du processus, il s'agit de rassembler tous les fichiers objets repositionnables de votre programme pour en faire un binaire. C'est aussi lors de cette étape que le le programme appelle "le linker" va stocker des informations dans le fichier final pour lui permettre ensuite de retrouver les différentes librairies dynamiques utilisées. La commande est ld (man ld).
Warning, ld ne fonctionnera pas, du moins pas de façon classique. Pour ne pas vous embrouiller, nous verrons plus tard pourquoi.
Il nous faudra utiliser gcc qui va faire la liaison comme il se doit (même si lui utilise ld en interne finalement).
Exemple :
Code:
$ gcc -o main main.o $ ./main Hello World ! $ file main main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=ba3de236fa8a892bc6124a5e70c6b9b4ee1f449b, not stripped $
Le header ELF
Le header ELF est la première chose que l'on trouve dans un fichier binaire ELF. Celui-ci contient les informations primaires sur le reste du fichier et sa structure. En effet, les données sont stockées dans des endroits différents du fichier et donc contrairement à un fichier texte ascii, une lecture ligne par ligne n'est pas possible (ne servirait à rien en tout cas).
Ce header principale est positionne à l'offset 0 (le tout début du fichier). La structure ElfN_Ehdr définie dans <elf.h> nous permettra d'accéder à ces informations. Le N correspond a l'architecture qui peut varier en fonction du système et de l'architecture du binaire. Etant donné que ce binaire sera un binaire ELF 64bit, on va directement utiliser Elf64_Ehdr. (Elf32_Ehdr pour du 32bit par exemple).
Soit le code suivant :
Code:
#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/mman.h> #include <elf.h> int elf_check_header(Elf64_Ehdr * data) { // Si les premiers bytes sont differents de ceux du format ELF if (data->e_ident[EI_MAG0] != ELFMAG0 || data->e_ident[EI_MAG1] != ELFMAG1 || data->e_ident[EI_MAG2] != ELFMAG2 || data->e_ident[EI_MAG3] != ELFMAG3) return (-1); // Si l'architecture du binaire est bien 64bits if (data->e_ident[EI_CLASS] != ELFCLASS64) return (-1); return (0); } void elf_print_header(Elf64_Ehdr * data) { printf("Point d'entree : %p\n", data->e_entry); printf("Adresse de la table des segments (relative) : + %4d bytes\n", data->e_phoff); printf("Nombre d'elements dans la table des segments : %d\n", data->e_phnum); printf("Adresse de la table des segments (relative) : + %4d bytes\n", data->e_shoff); printf("Nombre d'elements dans la table des sections : %d\n", data->e_shnum); } int main(int argc, char *argv[]) { int fd; void *data; struct stat file_infos; if (argc == 2) { if ((fd = open(argv[1], O_RDONLY)) > 0) { if (fstat(fd, &file_infos)) fprintf(stderr, "Recuperation des informations du fichier impossible\n"); else if ((data = mmap(0, file_infos.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED) fprintf(stderr, "Chargement du fichier en memoire impossible\n"); else { if (!elf_check_header((Elf64_Ehdr*)data)) { elf_print_header((Elf64_Ehdr*)data); } munmap(data, file_infos.st_size); } close(fd); } else fprintf(stderr, "Ouverture du fichier impossible\n"); } else fprintf(stderr, "USAGE: %s <binary_file>\n", argv[0]); return (0); }
Dans notre fonction principale (main) nous allons "mapper", mettre en mémoire notre fichier en prenant le soin de faire les vérifications nécessaires d'usage (erreurs ...).
Le syscall mmap nous renvoi un void*, un pointeur générique. Etant donné que notre header se trouve à l'offset 0, on peut directement transformer ce pointeur générique en ElfN_Ehdr* pour accéder aux informations.
La structure Elf64_Ehdr est représentée comme cela :
Code:
typedef struct { unsigned char e_ident[EI_NIDENT]; uint16_t e_type; uint16_t e_machine; uint32_t e_version; Elf64_Addr e_entry; Elf64_Off e_phoff; Elf64_Off e_shoff; uint32_t e_flags; uint16_t e_ehsize; uint16_t e_phentsize; uint16_t e_phnum; uint16_t e_shentsize; uint16_t e_shnum; uint16_t e_shstrndx; } Elf64_Ehdr;
Le manuel ELF nous informe que ce champ est un tableau de bytes et qu’on y accède par les macros suivantes :
- EI_MAG0 : le premier byte du nombre magique (servant à identifier le fichier comme un fichier ELF). Sa valeur doit être ELFMAG0.
- EI_MAG1 : le second byte du nombre magique (servant à identifier le fichier comme un fichier ELF). Sa valeur doit être ELFMAG1.
- EI_MAG2 : le troisième byte du nombre magique (servant à identifier le fichier comme un fichier ELF). Sa valeur doit être ELFMAG2.
- EI_MAG3 : le quatrième byte du nombre magique (servant à identifier le fichier comme un fichier ELF). Sa valeur doit être ELFMAG3.
- EI_CLASS : le cinquième byte, permettant d'identifier l'architecture du binaire. On nous propose les valeurs suivantes :
- ELFCLASS32 : le binaire est un binaire ELF 32bit
- ELFCLASS64 : le binaire est un binaire ELF 64bit
Voilà l'intérêt de la fonction elf_check_header, on vérifie que ces valeurs sont bien celles attendues, que le fichier est valide et qu'il s'agisse bien d’un binaire ELF 64bit.
On peut ensuite extraire du header les informations plus utiles qui suivent.
- Le point d'entrée (entry point) :
Le point d'entrée d'un programme est un adresse à l'laquelle le processus va commencer son exécution. A cette adresse on y trouve généralement du bytecode (code binaire directement compréhensible par le processeur). Si vous avez fait un peu d'assembleur ou que vous vous y êtres intéressé un minimum, vous devez savoir qu'il s'agit d'un langage impératif, il est exécuté ligne par ligne et comprenant des sauts pour utiliser d'autres portions de code. Le processeur (à quelques différences faites par le système qui gère un tas d'autres choses en plus) va faire de même, il va interpréter chaque instruction à la suite dans la mémoire selon leur emplacement et sera conditionne par des instructions de saut.
Le point d'entre sert à savoir ou commencer l'exécution de ce code. - Les informations sur les segments (on y reviendra)
- Les informations sur les sections (on y reviendra)
- D'autres informations plus ou moins utiles
Nous avons donc notre fonction elf_print_header qui se charge de nous afficher ces informations. Un petit test s'impose :
Code:
$ gcc -o main main.c $ cp main main2 `main' -> `main2' $ ./main main2 Point d'entree : 0x400640 Adresse de la table des segments (relative) : + 64 bytes Nombre d'elements dans la table des segments : 8 Adresse de la table des segments (relative) : + 4312 bytes Nombre d'elements dans la table des sections : 30 $ readelf -h main2 ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x400640 Start of program headers: 64 (bytes into file) Start of section headers: 4312 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 8 Size of section headers: 64 (bytes) Number of section headers: 30 Section header string table index: 27 $
Les segments
Un segment est une portion de mémoire qui est réservé pour accueil des données. Il est défini par deux valeurs : son adresse et sa taille. Votre système utilise un mécanisme appelé segmentation permettant de découper la mémoire et gérée au plus bas niveau.
Ainsi quand vous utiliser une adresse dans un programme C, celle-ci est dite virtuelle, le système se chargera à partir de celle-ci de retrouver l'adresse physique par le mécanisme de segmentation (en bref). Je ne vais pas vous faire un cours sur la segmentation, ca nécessiterai d'appréhender pas mal de concepts et ce n’est pas le but ici (pas pour ce tutoriel).
Le principale est de savoir que votre binaire sera reparti généralement sur plusieurs segments et que chaque segment a ses propres propriétés. Chaque segment a des droits et des propriétés qui lui sont propre. Ainsi on peut retrouver le système de droits sur les fichiers (read, write, execute) sur les segments. Un segment avec le droit d'exécution accueillera la plus part du temps du code interprétable par le processeur...
Les program headers
Dans un fichier ELF, on y trouve une table de "program header" représentés par la structure suivante :
Code:
typedef struct { uint32_t p_type; uint32_t p_flags; Elf64_Off p_offset; Elf64_Addr p_vaddr; Elf64_Addr p_paddr; uint64_t p_filesz; uint64_t p_memsz; uint64_t p_align; } Elf64_Phdr;
- PT_NULL : le program header n'est pas défini (pas très utile dans ce cas)
- PT_LOAD : le program header recense des informations sur un segment chargeable (le plus intéressant)
- PT_DYNAMIC : le program header contient des informations de liaison dynamique (pour la gestion des librairies ...)
- PT_INTERP : le program header nous donne des informations sur l'interpréteur, notamment son chemin pour pouvoir l'invoquer
- PT_PHDR : si présent, le header contient la taille et l'entête de programme elle-même dans le fichier et dans l'image mémoire du binaire (moins important)
D'autres existent qui ne nous serviront probablement jamais, jetez un œil sur le man ELF pour une liste plus exhaustive. Si vous ne devez en retenir qu'un, alors ce sera PT_LOAD, c'est dans celui-ci ou l'on retrouvera la plus part des donnes du binaire (code exécutable, données de votre programme ...).
Exemple:
Code:
$ readelf -l main2 Elf file type is EXEC (Executable file) Entry point 0x400640 There are 8 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x00000000000001c0 0x00000000000001c0 R E 8 INTERP 0x0000000000000200 0x0000000000400200 0x0000000000400200 0x000000000000001c 0x000000000000001c R 1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x0000000000000d2c 0x0000000000000d2c R E 200000 LOAD 0x0000000000000d30 0x0000000000600d30 0x0000000000600d30 0x0000000000000268 0x0000000000000278 RW 200000 DYNAMIC 0x0000000000000d48 0x0000000000600d48 0x0000000000600d48 0x00000000000001d0 0x00000000000001d0 RW 8 NOTE 0x000000000000021c 0x000000000040021c 0x000000000040021c 0x0000000000000044 0x0000000000000044 R 4 GNU_EH_FRAME 0x0000000000000be4 0x0000000000400be4 0x0000000000400be4 0x0000000000000044 0x0000000000000044 R 4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 8 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag .note.gnu.build-id 06 .eh_frame_hdr 07 $
Les sections
Comme son nom l'indique il s'agit d'une section du binaire dans laquelle sont regroupées différentes données. Comme déjà énoncé, un binaire contient plusieurs types de données, du code exécutable, des informations diverses pour la liaison de librairies ... C'est au travers ces différentes sections que l'on va pouvoir retrouver toute ces informations.
Le format ELF contient une table de headers de section nous donnant les informations sur chaque section, son emplacement dans le fichier, sa taille ... Un header est représenté avec la structure suivante :
Code:
typedef struct { uint32_t sh_name; uint32_t sh_type; uint64_t sh_flags; Elf64_Addr sh_addr; Elf64_Off sh_offset; uint64_t sh_size; uint32_t sh_link; uint32_t sh_info; uint64_t sh_addralign; uint64_t sh_entsize; } Elf64_Shdr;
sh_offset nous donnera la position de la section à partir du début du fichier et sh_size la taille de la section pour pouvoir retrouver ces données.
Ce qu'il faut également savoir, c'est que dans le format ELF on retrouve des sections spécifiques pour le stockage des noms de sections, le nom des symboles ... Ce sont les strings tables (tables de chaines de caractères).
Ainsi, dans le header principale, on dispose du champ e_shstrndx qui correspond à l'index dans la table des headers de section correspondant à la section contenant les noms des différentes sections. On va pouvoir ensuite retrouver chaque nom à l'aide du champ sh_name (de la structure précédente) qui définit un index dans cette section.
Dans ces sections les chaines sont représentées sous la forme suivante :
"Une chaine\0une seconde chaine\0une troisième chaine\0"
Ce n'est pas un tableau de chaines de caractères mais une simple chaine dont chaque sous-chaine est terminée par un null byte (\0).
Lorsque sh_name nous donne l'index 11, on se retrouve donc sur "une seconde ...\0une troi...\0". Lorsque l'on va utiliser des fonctions tel que printf, strlen ... on va donc automatiquement s'arrêter au null byte.
Voyons comment récupérer les noms des différentes sections grâce à cette section :
Code:
#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/mman.h> #include <elf.h> int elf_check_header(Elf64_Ehdr * data) { // Si les premiers bytes sont differents de ceux du format ELF if (data->e_ident[EI_MAG0] != ELFMAG0 || data->e_ident[EI_MAG1] != ELFMAG1 || data->e_ident[EI_MAG2] != ELFMAG2 || data->e_ident[EI_MAG3] != ELFMAG3) return (-1); // Si l'architecture du binaire est bien 64bits if (data->e_ident[EI_CLASS] != ELFCLASS64) return (-1); return (0); } void elf_print_sections_name(Elf64_Ehdr * data) { int i; // on recupere un pointeur sur le premier header de section // ne as oublier de caster en void*, autrement l'arithmetique entre les pointeurs ne sera pas bon Elf64_Shdr *section_header_start = (Elf64_Shdr*)((void*)data + data->e_shoff); // on retrouve la string table avec le nom des sections .. Elf64_Shdr sections_string = section_header_start[data->e_shstrndx]; // on recupere un pointeur sur le debut de la section et donc sur la premiere chaine char *strings = (char*)((void*)data + sections_string.sh_offset); // pour stoquer chaque section Elf64_Shdr section_header; for (i = 0; i < data->e_shnum; i++) { // on recupere le header de section courrant section_header = section_header_start[i]; // on affiche son nom a partir de la section contenant les strings et son index printf("%s\n", strings + section_header.sh_name); } } int main(int argc, char *argv[]) { int fd; void *data; struct stat file_infos; if (argc == 2) { if ((fd = open(argv[1], O_RDONLY)) > 0) { if (fstat(fd, &file_infos)) fprintf(stderr, "Recuperation des informations du fichier impossible\n"); else if ((data = mmap(0, file_infos.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED) fprintf(stderr, "Chargement du fichier en memoire impossible\n"); else { if (!elf_check_header((Elf64_Ehdr*)data)) { elf_print_sections_name((Elf64_Ehdr*)data); } munmap(data, file_infos.st_size); } close(fd); } else fprintf(stderr, "Ouverture du fichier impossible\n"); } else fprintf(stderr, "USAGE: %s <binary_file>\n", argv[0]); return (0); }
La section .text
La section .text, probablement la plus importante est une section qui va contenir le code exécutable du binaire. La suite de bytecode interprétable par le processeur se trouve ici.
Ci-dessous on désassemble la section .text avec objdump (man objdump) :
Code:
$ objdump -j.text -d main2 main2: file format elf64-x86-64 Disassembly of section .text: 0000000000400640 <_start>: 400640: 31 ed xor %ebp,%ebp 400642: 49 89 d1 mov %rdx,%r9 400645: 5e pop %rsi 400646: 48 89 e2 mov %rsp,%rdx 400649: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp 40064d: 50 push %rax 40064e: 54 push %rsp 40064f: 49 c7 c0 20 0a 40 00 mov $0x400a20,%r8 400656: 48 c7 c1 90 09 40 00 mov $0x400990,%rcx 40065d: 48 c7 c7 18 08 40 00 mov $0x400818,%rdi 400664: e8 67 ff ff ff callq 4005d0 <[email protected]> 400669: f4 hlt 40066a: 90 nop 40066b: 90 nop 40066c: 90 nop 40066d: 90 nop 40066e: 90 nop 40066f: 90 nop ... 0000000000400818 <main>: 400818: 55 push %rbp 400819: 48 89 e5 mov %rsp,%rbp 40081c: 48 81 ec b0 00 00 00 sub $0xb0,%rsp 400823: 89 bd 5c ff ff ff mov %edi,-0xa4(%rbp) 400829: 48 89 b5 50 ff ff ff mov %rsi,-0xb0(%rbp) 400830: 83 bd 5c ff ff ff 02 cmpl $0x2,-0xa4(%rbp) 400837: 0f 85 1d 01 00 00 jne 40095a <main+0x142> 40083d: 48 8b 85 50 ff ff ff mov -0xb0(%rbp),%rax 400844: 48 83 c0 08 add $0x8,%rax 400848: 48 8b 00 mov (%rax),%rax 40084b: be 00 00 00 00 mov $0x0,%esi 400850: 48 89 c7 mov %rax,%rdi 400853: b8 00 00 00 00 mov $0x0,%eax 400858: e8 c3 fd ff ff callq 400620 <[email protected]> 40085d: 89 45 fc mov %eax,-0x4(%rbp) 400860: 83 7d fc 00 cmpl $0x0,-0x4(%rbp) 400864: 0f 8e d0 00 00 00 jle 40093a <main+0x122> 40086a: 48 8d 95 60 ff ff ff lea -0xa0(%rbp),%rdx 400871: 8b 45 fc mov -0x4(%rbp),%eax 400874: 48 89 d6 mov %rdx,%rsi 400877: 89 c7 mov %eax,%edi 400879: b8 00 00 00 00 mov $0x0,%eax ... 400964: 48 8b 05 2d 06 20 00 mov 0x20062d(%rip),%rax # 600f98 <__bss_start> 40096b: be c9 0b 40 00 mov $0x400bc9,%esi 400970: 48 89 c7 mov %rax,%rdi 400973: b8 00 00 00 00 mov $0x0,%eax 400978: e8 63 fc ff ff callq 4005e0 <[email protected]> 40097d: b8 00 00 00 00 mov $0x0,%eax 400982: c9 leaveq 400983: c3 retq 400984: 90 nop 40098f: 90 nop ...
Code:
55 48 89 e5 48 81 ec b0 00 00 00 89 bd 5c ff ff ff
- http://www.intel.com/content/www/us/...2a-manual.html
- http://www.intel.com/content/www/us/...2b-manual.html
Soit le code suivant :
Code:
#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/mman.h> #include <elf.h> #include <udis86.h> int elf_check_header(Elf64_Ehdr * data) { // Si les premiers bytes sont differents de ceux du format ELF if (data->e_ident[EI_MAG0] != ELFMAG0 || data->e_ident[EI_MAG1] != ELFMAG1 || data->e_ident[EI_MAG2] != ELFMAG2 || data->e_ident[EI_MAG3] != ELFMAG3) return (-1); // Si l'architecture du binaire est bien 64bits if (data->e_ident[EI_CLASS] != ELFCLASS64) return (-1); return (0); } void elf_disass_text(Elf64_Ehdr * data, Elf64_Shdr * section_header) { // on recupere le contenu de la section text sous forme d'un tableau de unsigned char(representation d'un bytecode) unsigned char *buffer = (unsigned char*)((void*)data + section_header->sh_offset); ud_t ud_obj; // initialisation de udis86 ud_init(&ud_obj); ud_set_input_buffer(&ud_obj, buffer, section_header->sh_size); ud_set_mode(&ud_obj, 64); ud_set_syntax(&ud_obj, UD_SYN_ATT); // affichage de chaque instruction (le pointeur est incremente automatquement pour passer a la suivante) while (ud_disassemble(&ud_obj)) printf("\t%s\n", ud_insn_asm(&ud_obj)); } void elf_print_sections_name(Elf64_Ehdr * data) { int i; // on recupere un pointeur sur le premier header de section // ne as oublier de caster en void*, autrement l'arithmetique entre les pointeurs ne sera pas bon Elf64_Shdr *section_header_start = (Elf64_Shdr*)((void*)data + data->e_shoff); // on retrouve la string table avec le nom des sections .. Elf64_Shdr sections_string = section_header_start[data->e_shstrndx]; // on recupere un pointeur sur le debut de la section et donc sur la premiere chaine char *strings = (char*)((void*)data + sections_string.sh_offset); // pour stoquer chaque section Elf64_Shdr section_header; for (i = 0; i < data->e_shnum; i++) { // on recupere le header de section courrant section_header = section_header_start[i]; if (!strcmp(".text", strings + section_header.sh_name)) elf_disass_text(data, §ion_header); } } int main(int argc, char *argv[]) { int fd; void *data; struct stat file_infos; if (argc == 2) { if ((fd = open(argv[1], O_RDONLY)) > 0) { if (fstat(fd, &file_infos)) fprintf(stderr, "Recuperation des informations du fichier impossible\n"); else if ((data = mmap(0, file_infos.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED) fprintf(stderr, "Chargement du fichier en memoire impossible\n"); else { if (!elf_check_header((Elf64_Ehdr*)data)) { elf_print_sections_name((Elf64_Ehdr*)data); } munmap(data, file_infos.st_size); } close(fd); } else fprintf(stderr, "Ouverture du fichier impossible\n"); } else fprintf(stderr, "USAGE: %s <binary_file>\n", argv[0]); return (0); }
La section .data .rodata
Dans la section .data on va retrouver les variables globales et statiques initialisées explicitement. La section .rodata est identique mais pour des valeurs constantes. L'écriture en section .rodata n'est pas permise.
Exemple :
Code:
$ cat test.c int toto = 42; const int tata = 8; int main() { static char maman[8] = "coucou!"; return (0); } $ gcc -o test test.c $ objdump -d -j.data test test: file format elf64-x86-64 Disassembly of section .data: 0000000000600860 <__data_start>: ... 0000000000600868 <__dso_handle>: ... 0000000000600870 <toto>: 600870: 2a 00 00 00 *... 0000000000600874 <maman.1707>: 600874: 63 6f 75 63 6f 75 21 00 coucou!. $ objdump -d -j.rodata test test: file format elf64-x86-64 Disassembly of section .rodata: 0000000000400560 <_IO_stdin_used>: 400560: 01 00 02 00 .... 0000000000400564 <tata>: 400564: 08 00 00 00 .... $
La section .bss
La section .bss contient elle aussi vos variables mais non-initialisées cette fois.
Exemple :
Code:
$ cat test.c int toto; int main() { static char maman[8]; return (0); } $ objdump -d -j.bss test test: file format elf64-x86-64 Disassembly of section .bss: 0000000000600868 <completed.6109>: 600868: 00 00 00 00 .... 000000000060086c <maman.1706>: ... 0000000000600874 <toto>: 600874: 00 00 00 00 .... $
La section .plt .got
Etant donné que ces sections nécessiteraient un tutoriel complet, je vous renvoie vers un article déjà rédigé : http://www.segmentationfault.fr/linu...plt-got-ld-so/
Les autres sections
Parmi les autres sections, on retrouve la section .dynamic contenant des informations sur les liaisons dynamiques. On retrouvera également d'autres sections contenant des chaines de caractères pour les symboles ...
Si vous voulez approfondir ces autres sections -> man elf.
Essayez d'utiliser la commande ldd (man ldd), les informations viennent de quelle section à votre avis ?
Les symboles
Un symbole est une étiquette qui identifie certains éléments du programme. Les symboles vont permettre de référencer les noms de vos fonctions, des fonctions des librairies dynamiques et statiques, des fichiers, des sections ...
Les symboles sont principalement utilisés pour le debug et permettent (entre autres) d'utiliser ces noms comme référence dans gdb.
Ex: lorsque l'on pose un break point sur main, main est un symbole, si l'on supprime les symboles du binaire, alors on ne pourra plus poser un break point sur main, il faudra spécifier une adresse à la place.
En effet, les symboles gonflent la taille du binaire, c'est pourquoi en production on supprime souvent ceux-là pour gagner de la place. Cela n'empêche pas le debug mais ça le complique légèrement (man strip). En version de développement, on gardera les symboles pour pouvoir faire du debug plus simplement.
Une commande utile pour lister les symboles d'un binaire ELF est nm (man nm) :
Code:
$ nm main2 0000000000600d48 d _DYNAMIC 0000000000600f20 d _GLOBAL_OFFSET_TABLE_ 0000000000400a50 R _IO_stdin_used w _ITM_deregisterTMCloneTable w _ITM_registerTMCloneTable w _Jv_RegisterClasses 0000000000400d28 r __FRAME_END__ 0000000000600d40 d __JCR_END__ 0000000000600d40 d __JCR_LIST__ 0000000000600f98 D __TMC_END__ 0000000000600f98 A __bss_start 0000000000600f88 D __data_start 00000000004006e0 t __do_global_dtors_aux 0000000000600d38 t __do_global_dtors_aux_fini_array_entry 0000000000600f90 D __dso_handle 0000000000600d30 t __frame_dummy_init_array_entry 0000000000400a30 T __fstat U [email protected]@GLIBC_2.2.5 w __gmon_start__ 0000000000600d38 t __init_array_end 0000000000600d30 t __init_array_start 0000000000400a20 T __libc_csu_fini 0000000000400990 T __libc_csu_init U [email protected]@GLIBC_2.2.5 0000000000600f98 A _edata 0000000000600fa8 A _end 0000000000400a40 T _fini 0000000000400570 T _init 0000000000400640 T _start U [email protected]@GLIBC_2.2.5 0000000000600fa0 b completed.6109 0000000000600f88 W data_start 0000000000400670 t deregister_tm_clones 000000000040072c T elf_check_header 0000000000400784 T elf_print_header U [email protected]@GLIBC_2.2.5 0000000000400700 t frame_dummy 0000000000400a30 W fstat U [email protected]@GLIBC_2.2.5 0000000000400818 T main U [email protected]@GLIBC_2.2.5 U [email protected]@GLIBC_2.2.5 U [email protected]@GLIBC_2.2.5 U [email protected]@GLIBC_2.2.5 00000000004006a0 t register_tm_clones 0000000000600f98 B [email protected]@GLIBC_2.2.5 $
Si vous voulez approfondir certains points faites le moi savoir, ici le but est de faire du reversing ne l'oubliez pas et pas de faire un interpréteur de binaire ELF ...
La prochaine étape ... surprise.