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 ELFType C
ElfN_Addradresse programme non signée, uintN_t
ElfN_Offoffset de fichier non signé, uintN_t
ElfN_Sectionindex de section non signé, uint16_t
ElfN_Versyminformation de version non signée, uint16_t
Elf_Byteunsigned char
ElfN_Halfuint16_t
ElfN_Swordint32_t
ElfN_Worduint32_t
ElfN_Sxwordint64_t
ElfN_Xworduint64_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.