Tout exécutable Linux — et toute bibliothèque partagée — se présente au format ELF (Executable and Linkable Format), dont la structure est décrite par le header elf.h. Comprendre son anatomie est un préalable à tout travail de reverse engineering ou d’exploitation : c’est elle qui dicte où se trouve le code, où se trouvent les données, et avec quels droits chacun est chargé en mémoire.
L’ossature d’un fichier ELF
Un binaire ELF s’articule autour de trois éléments principaux : l’ELF header, qui décrit le fichier dans son ensemble ; la program header table, qui décrit comment le charger en mémoire (présente pour les exécutables et bibliothèques) ; et la section header table, qui décrit son organisation logique pour l’édition de liens. Un même fichier peut comporter l’une, l’autre, ou les deux — un exécutable final a surtout besoin de la première pour s’exécuter, un fichier objet surtout de la seconde pour être lié.
Le fichier elf.h décrit ces structures en C, ainsi que celles nécessaires aux sections dynamiques, aux entrées de réallocation et aux tables de symboles.
Les types de base
ELF définit ses propres types, dont voici la correspondance C :
| Type ELF | Type C |
|---|---|
ElfN_Addr | adresse programme non signée, uintN_t |
ElfN_Off | offset de fichier non signé, uintN_t |
ElfN_Section | index de section non signé, uint16_t |
ElfN_Versym | information de version non signée, uint16_t |
Elf_Byte | unsigned char |
ElfN_Half | uint16_t |
ElfN_Sword | int32_t |
ElfN_Word | uint32_t |
ElfN_Sxword | int64_t |
ElfN_Xword | uint64_t |
TIP
Les structures ELF incluent, au besoin, un remplissage (padding) explicite pour garantir l’alignement à 4 octets des objets de 4 octets, et pour que la taille totale de la structure soit un multiple de 4.
L’en-tête ELF
L’ELF Header (ElfX_Ehdr, où X vaut 32 ou 64 selon l’architecture) déclare les caractéristiques propres à l’environnement d’exécution et à l’interprétabilité du fichier : classe (32/64 bits), endianness, type de fichier (exécutable, objet relogeable, bibliothèque partagée), architecture cible, et point d’entrée.
Le program header : ce que le loader charge
Le Program Header (Phdr) est un tableau de structures décrivant chacune un segment, ou toute autre information nécessaire à la préparation du programme en vue de son exécution. Un segment regroupe une ou plusieurs sections ; les program headers n’ont de sens que pour les fichiers exécutables et les objets partagés — un simple fichier objet, non encore lié, n’en a pas besoin. Leur taille est donnée par les champs e_phentsize et e_phnum de l’Ehdr.
C’est ce program header, et plus précisément ses segments de type PT_LOAD, que le noyau lit lors d’un execve pour savoir quelles portions du fichier mapper en mémoire virtuelle, à quelle adresse, et avec quels droits (lecture, écriture, exécution) — point que je développe dans la note sur la mémoire et les segments.
Le section header : la carte pour l’éditeur de liens
Le Section Header (Shdr) permet de localiser toutes les sections du fichier — .text, .data, .rodata, .bss, .got, .plt, etc. C’est la vue qu’exploite l’éditeur de liens statique, et celle que présentent en priorité les désassembleurs.
Trois autres structures complètent l’édifice, plus spécialisées : les tables de chaînes et de symboles, qui associent noms et adresses (déterminantes pour distinguer un binaire strippé d’un binaire qui ne l’est pas — voir la note sur les binaires strippés) ; les entrées de réallocation (Rel et Rela), qui indiquent où et comment ajuster les adresses lors de l’édition de liens ou du chargement ; et les tags dynamiques (Dyn), qui pilotent le chargement dynamique des bibliothèques partagées.