Skip to content

Niveau 4 · Chapitre 4.1

Fichiers exécutables : ELF, PE et Mach-O

Ce que contient un fichier programme et comment l’OS le lit : en-têtes, point d’entrée, segments et sections, permissions, informations de liaison dynamique — disséqués sur de vrais fichiers ELF, PE et Mach-O, avec les protections qu’ils déclarent.

Un exécutable n’est pas que du code machine. C’est un format de fichier : un contrat entre l’éditeur de liens qui l’écrit et le système d’exploitation qui le charge. Le fichier indique à l’OS pour quel CPU il est fait, quels octets placer où en mémoire et avec quelles permissions, de quelles bibliothèques il a besoin, et où se trouve la première instruction. Le chapitre sur l’assembleur, l’éditeur de liens et le chargeur suivait un programme à travers cette chaîne d’outils ; celui-ci ouvre le fichier lui-même.

Trois formats couvrent presque tous les ordinateurs d’aujourd’hui :

FormatUtilisé parOctets magiques
ELF (Executable and Linkable Format)Linux, les BSD, Android, la plupart des systèmes embarqués7f 45 4c 46 — \x7fELF
PE (Portable Executable)Windows, les firmwares UEFI4d 5a — MZ
Mach-OmacOS, iOScf fa ed fe (64 bits)

La commande file les reconnaît à ces octets magiques. Les trois partagent la même anatomie : un en-tête, une description de la façon de placer le fichier en mémoire, le code et les données, et des tables supplémentaires pour les symboles, les relocations et la liaison dynamique.

ELF, octet par octet

Voici un minuscule programme Linux x86-64 — il écrit hello et se termine — lié en un exécutable statique de 1 288 octets. Il fonctionne vraiment : hello, code de sortie 0. Voici ses 64 premiers octets, l’en-tête ELF :

00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............
00000010: 0200 3e00 0100 0000 6011 2000 0000 0000  ..>.....`. .....
00000020: 4000 0000 0000 0000 c802 0000 0000 0000  @...............
00000030: 0000 0000 4000 3800 0500 4000 0900 0700  ....@.8...@.....
OctetsChampValeur
7f 45 4c 46magique\x7fELF
02classe64 bits
01donnéespetit-boutiste
02 00typeET_EXEC, un exécutable à adresse fixe
3e 00machine0x3e, x86-64
60 11 20 00 …point d’entrée0x201160, l’adresse de _start
40 00 …position des en-têtes de programmejuste après l’en-tête
05 00nombre d’en-têtes de programme5
09 00nombre d’en-têtes de section9

Chaque champ de plusieurs octets est en petit-boutiste. Le point d’entrée est l’endroit où le noyau envoie le CPU une fois le programme chargé.

Les segments : comment le charger

Les en-têtes de programme décrivent des segments : des plages du fichier à placer en mémoire, avec des permissions. C’est ce que lit le noyau :

Type   Offset   VirtAddr  FileSiz  MemSiz   Flags
PHDR   0x000040 0x200040  0x000118 0x000118 r--
LOAD   0x000000 0x200000  0x00015e 0x00015e r--     en-têtes + .rodata
LOAD   0x000160 0x201160  0x000021 0x000021 r-x     .text
LOAD   0x000181 0x202181  0x000004 0x001004 rw-     .data + .bss
STACK  0        0         0        0        rw-
  • Le segment de code est lisible et exécutable, mais pas modifiable. Le segment de données est modifiable, mais pas exécutable. Cette séparation, appelée W^X (écriture ou exécution, jamais les deux), empêche d’exécuter simplement des données injectées comme du code.
  • Le dernier LOAD fait 4 octets dans le fichier (.data) mais 0x1004 en mémoire. Les 4 Ko supplémentaires sont .bss : le noyau les fournit sous forme de mémoire à zéro, et ils n’occupent aucune place dans le fichier.
  • STACK avec les drapeaux rw- demande une pile non exécutable.

Les sections : la vue de l’éditeur de liens

Les sections — .text, .rodata, .data, .bss, .symtab… — sont la vue plus fine qu’utilisent éditeurs de liens, débogueurs et désassembleurs. Plusieurs sections sont regroupées dans chaque segment selon leurs permissions. Le noyau n’a pas du tout besoin de la table des sections : strip peut retirer les symboles et le programme fonctionne toujours. C’est pourquoi les binaires « strippés » sont plus difficiles à analyser — les noms ont disparu — mais se chargent exactement de la même façon.

ELF dynamique : PIE, interpréteur, bibliothèques

La plupart des programmes Linux ne sont pas statiques. Voici un programme C compilé avec les réglages par défaut de gcc (sur une machine Linux ARM64 ; la structure est identique sur x86-64). Son en-tête indique Type: DYN (Position-Independent Executable file), et ses en-têtes de programme comptent plus d’entrées :

Type         Offset   VirtAddr  FileSiz  MemSiz   Flg
PHDR         0x000040 0x000040  0x0001f8 0x0001f8 R
INTERP       0x000238 0x000238  0x00001b 0x00001b R
    [Requesting program interpreter: /lib/ld-linux-aarch64.so.1]
LOAD         0x000000 0x000000  0x00088c 0x00088c R E
LOAD         0x00fdc8 0x01fdc8  0x000274 0x000278 RW
DYNAMIC      0x00fdd8 0x01fdd8  0x0001e0 0x0001e0 RW
GNU_STACK    0        0         0        0        RW
GNU_RELRO    0x00fdc8 0x01fdc8  0x000238 0x000238 R
  • PIE : les adresses commencent à 0. Tout le programme est chargé à une base aléatoire choisie à l’exécution (ASLR) : son point d’entrée, 0x640, n’est qu’un décalage.
  • INTERP nomme l’éditeur de liens dynamique à lancer en premier. Sous Linux x86-64, c’est /lib64/ld-linux-x86-64.so.2.
  • DYNAMIC pointe vers la section dynamique, dont la première entrée est ici NEEDED libc.so.6 : les bibliothèques à charger. Elle contient aussi les tables de relocation et FLAGS_1: PIE.
  • GNU_RELRO marque les données qui doivent passer en lecture seule une fois les relocations appliquées par l’éditeur de liens dynamique, comme la GOT en full RELRO.

Pour voir le résultat en mémoire, lisez /proc/<pid>/maps. Pour un cat en cours d’exécution, on voit chaque segment du programme, de la libc et de l’éditeur de liens dynamique à leurs adresses aléatoires, avec les permissions demandées par les en-têtes de programme :

aaaae4b10000-aaaae4b19000 r-xp  /usr/bin/cat
aaaae4b2f000-aaaae4b30000 r--p  /usr/bin/cat
aaaae4b30000-aaaae4b31000 rw-p  /usr/bin/cat
aaaaf18be000-aaaaf18df000 rw-p  [heap]
ffffbbb90000-ffffbbd1b000 r-xp  …/libc.so.6
ffffbbd47000-ffffbbd6e000 r-xp  …/ld-linux-aarch64.so.1
ffffbbd83000-ffffbbd85000 r-xp  [vdso]
fffff8b3a000-fffff8b5b000 rw-p  [stack]

Le [vdso] est une petite bibliothèque que le noyau place dans chaque processus, pour que des appels comme gettimeofday n’aient pas besoin d’un vrai appel système.

PE : le format de Windows

Un fichier PE commence par une relique des années 1980. Voici les 96 premiers octets d’un exécutable Windows 64 bits minimal :

00000000: 4d5a 7800 0100 0000 0400 0000 0000 0000  MZx.............
00000010: 0000 0000 0000 0000 4000 0000 0000 0000  ........@.......
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000030: 0000 0000 0000 0000 0000 0000 7800 0000  ............x...
00000040: 0e1f ba0e 00b4 09cd 21b8 014c cd21 5468  ........!..L.!Th
00000050: 6973 2070 726f 6772 616d 2063 616e 6e6f  is program canno
  • MZ : tout fichier PE commence par un en-tête MS-DOS et un minuscule programme DOS, le stub, qui affiche This program cannot be run in DOS mode.
  • À l’offset 0x3C, e_lfanew (ici 0x78) pointe vers le vrai en-tête : la signature PE\0\0, puis l’en-tête COFF (machine 0x8664 = x86-64, nombre de sections…) et l’en-tête optionnel, qui n’a rien d’optionnel.

Quelques champs de l’en-tête optionnel de ce fichier :

ChampValeurSignification
Magic0x20BPE32+, une image 64 bits
ImageBase0x140000000adresse de chargement préférée (la valeur par défaut des programmes 64 bits)
AddressOfEntryPoint0x1000point d’entrée, en RVA
SectionAlignment / FileAlignment0x1000 / 0x200sections espacées de 4 Ko en mémoire, de 512 o dans le fichier
Subsystem3un programme console
DllCharacteristics0x8160ASLR (DYNAMIC_BASE), ASLR 64 bits (HIGH_ENTROPY_VA), NX_COMPAT, compatible terminal server

Les adresses PE sont surtout des RVA — adresses virtuelles relatives, des décalages par rapport à l’endroit où l’image est chargée. L’en-tête optionnel se termine par les répertoires de données : des pointeurs vers la table d’importation (les DLL et fonctions à résoudre dans l’IAT), la table d’exportation (pour les DLL), les relocations de base (.reloc, nécessaires quand l’ASLR déplace l’image), les ressources, le TLS et les informations de débogage.

Mach-O : le format d’Apple

Un fichier Mach-O est un en-tête suivi d’une liste de commandes de chargement (load commands). Un petit programme C compilé sur un Mac :

magic        cputype  filetype  ncmds  flags
MH_MAGIC_64  ARM64    EXECUTE   17     NOUNDEFS DYLDLINK TWOLEVEL PIE
  • Les commandes LC_SEGMENT_64 définissent les segments : __PAGEZERO, __TEXT (code et constantes), __DATA et __LINKEDIT (symboles et informations de liaison). __PAGEZERO couvre les 4 premiers Go sans aucun accès en 64 bits : tout pointeur nul ou tronqué à 32 bits provoque immédiatement un plantage.
  • LC_MAIN donne le point d’entrée, LC_LOAD_DYLINKER nomme l’éditeur de liens dynamique (/usr/lib/dyld), et LC_LOAD_DYLIB liste les bibliothèques — tout programme charge libSystem.B.dylib.
  • LC_CODE_SIGNATURE : sur Apple silicon, tout exécutable doit porter une signature, même ad hoc, sinon le noyau refuse de le lancer.

Mach-O a aussi des binaires universels (ou fat) : plusieurs fichiers Mach-O complets, un par architecture, derrière un petit en-tête. Sur ce Mac, /usr/bin/ssh contient une tranche x86_64 et une tranche arm64e.

Côte à côte

ELFPEMach-O
Point d’entréee_entry dans l’en-têteAddressOfEntryPoint (RVA)LC_MAIN
Description du chargementen-têtes de programme (segments)table des sections + alignementcommandes LC_SEGMENT_64
Bibliothèques nécessairesDT_NEEDEDrépertoire d’importationLC_LOAD_DYLIB
Pointeurs vers les fonctions importéesGOT (+ stubs PLT)IATpointeurs de symboles, paresseux ou non
Éditeur de liens dynamiquePT_INTERP (ld-linux…)intégré au chargeur de l’OSLC_LOAD_DYLINKER (dyld)
Indépendance de la positionPIE (ET_DYN)DYNAMIC_BASE + .relocdrapeau PIE

En lire un soi-même

TâcheLinux / ELFWindows / PEmacOS / Mach-O
quel est ce fichier ?filefile, PE-bearfile, lipo -archs
en-têtereadelf -hdumpbin /headersotool -hv
segments / sectionsreadelf -lW, readelf -SWdumpbin /headersotool -l
bibliothèques nécessairesreadelf -ddumpbin /importsotool -L
désassemblerobjdump -d -M inteldumpbin /disasmotool -tV, objdump -d

Un avertissement : ldd liste les bibliothèques d’un programme, mais sur certains systèmes il le fait en exécutant l’éditeur de liens dynamique du programme. Ne l’utilisez pas sur un fichier dont vous doutez ; readelf -d ou objdump -p lisent la même information sans rien exécuter.

À retenir

  • Un exécutable est un format partagé par l’éditeur de liens et le chargeur de l’OS : ELF sous Linux, PE sous Windows, Mach-O sur les systèmes Apple.
  • L’en-tête donne l’architecture et le point d’entrée. Les segments (en-têtes de programme ELF, sections PE, LC_SEGMENT_64 Mach-O) disent quoi placer où, avec quelles permissions.
  • Les sections sont la vue plus fine de l’éditeur de liens et du débogueur. Le chargeur n’en a pas besoin, d’où le fonctionnement de strip.
  • Les programmes dynamiques nomment un interpréteur ou éditeur de liens dynamique et leurs bibliothèques nécessaires, et portent les tables que remplit le chargeur : GOT, IAT, pointeurs de symboles.
  • Les en-têtes déclarent aussi des propriétés de sécurité — PIE/ASLR, pile non exécutable, RELRO, NX_COMPAT, signatures de code — c’est pourquoi on les lit en premier quand on analyse un binaire.

Dans ce niveau

  1. 4.1Fichiers exécutables : ELF, PE et Mach-O
  2. 4.2Processus et espace d’adressagePrévu
  3. 4.3Appels système et niveaux de privilègePrévu
  4. 4.4Mémoire virtuelle et paginationPrévu
  5. 4.5Fichiers, périphériques et E/SPrévu
  6. 4.6Threads et synchronisationPrévu
  7. 4.7Virtualisation matérielle et hyperviseursPrévu
  8. 4.8Au cœur d’UNIX et de WindowsPrévu