Skip to content

Le format de fichier PE expliqué : Portable Executable

Un tour du format PE pour le reverse-engineer : en-tête DOS, en-têtes NT, data directories, sections, l'IAT, les RVA, et pourquoi les packers en abusent.

Publié le 7 min de lecture

Le format Portable Executable (PE) est le conteneur que Windows utilise pour presque tout le code natif qu'il exécute : les fichiers .exe, .dll, .sys et .ocx partagent tous la même structure. Si vous faites du reverse engineering sous Windows, le format PE est la carte que vous lisez avant tout le reste. Ce guide parcourt chaque partie majeure de cette carte et explique pourquoi les auteurs de malware et les packers déploient tant d'efforts à la déformer.

Vue d'ensemble

Un fichier PE est une suite d'en-têtes suivie d'une table de sections. Chaque en-tête pointe vers le suivant, et la table des sections indique au loader où se trouvent le code et les données réels. Voici la disposition globale :

text
+-----------------------------+  file offset 0
|  DOS header (MZ) + e_lfanew |
+-----------------------------+
|  DOS stub ("This program...")|
+-----------------------------+  <- e_lfanew points here
|  PE signature  "PE\0\0"     |
+-----------------------------+
|  File header (COFF)         |
+-----------------------------+
|  Optional header            |
|   + data directories[16]    |
+-----------------------------+
|  Section table              |
|   .text  .rdata  .data ...  |
+-----------------------------+
|  Section bodies (raw data)  |
|   .text  | .rdata | .data   |
|   .rsrc  | .reloc | overlay |
+-----------------------------+

Le génie du format, c'est qu'il est conçu pour être mappé, et non lu séquentiellement. Le loader copie les sections en mémoire à des adresses alignées et corrige quelques tables. Comprendre ce mapping fait la différence entre deviner et savoir.

L'en-tête DOS et le stub

Tout fichier PE commence encore par un en-tête MS-DOS de 64 octets dont les deux premiers octets sont MZ (les initiales de Mark Zbikowski). Il n'existe que pour la rétrocompatibilité, mais deux champs comptent :

  • e_magic — la signature MZ qui permet aux outils de reconnaître le fichier d'un coup d'œil.
  • e_lfanew — un offset de 4 octets (à la fin de l'en-tête DOS) pointant vers les vrais en-têtes PE.

Après l'en-tête DOS vient le DOS stub, un minuscule programme 16 bits qui affiche « This program cannot be run in DOS mode » si quelqu'un exécute le fichier sous un vrai DOS. Les loaders modernes l'ignorent et sautent directement à e_lfanew. Les malwares y cachent souvent des données, dans ou après le stub, parce que rien ne le valide.

Les en-têtes NT : signature, File header, Optional header

À e_lfanew se trouve la structure IMAGE_NT_HEADERS, qui comporte trois parties.

La signature PE

Quatre octets : PE\0\0. S'ils manquent, le loader rejette le fichier.

Le File header (en-tête COFF)

Une structure compacte de 20 octets décrivant la machine et la forme générale :

  • Machine — le CPU cible (par exemple 0x8664 pour x64).
  • NumberOfSections — le nombre d'entrées dans la table des sections.
  • Characteristics — des drapeaux comme « est une DLL » ou « image exécutable ».
  • SizeOfOptionalHeader — la taille de l'en-tête suivant.

L'Optional header

Malgré son nom, l'Optional header est obligatoire pour les exécutables. Il porte les champs dont le loader a réellement besoin :

  • Magic0x10B pour le PE32, 0x20B pour le PE32+ (64 bits).
  • AddressOfEntryPoint — la RVA où commence l'exécution.
  • ImageBase — l'adresse de chargement préférée.
  • SectionAlignment et FileAlignment — les deux granularités qui font diverger RVA et offsets de fichier.
  • DataDirectory[16] — un tableau de paires (RVA, taille) pointant vers des tables spéciales.

Les data directories

Les 16 entrées du data directory sont des raccourcis vers des tables importantes disséminées dans les sections. Celles qu'un reverse engineer consulte le plus sont :

  • Import Directory — les DLL et fonctions que le binaire importe.
  • Export Directory — les fonctions qu'une DLL met à disposition des autres.
  • Resource Directory — icônes, dialogues, manifests et blobs arbitraires (voir charges utiles dans les ressources PE).
  • Base Relocation Directory — les corrections appliquées quand l'image ne peut pas se charger à sa base préférée.
  • TLS Directory — le stockage local au thread et, surtout, les callbacks TLS qui s'exécutent avant le point d'entrée.

La table des sections et les sections courantes

Après l'Optional header vient la table des sections : un IMAGE_SECTION_HEADER de 40 octets par section. Chaque entrée mappe un morceau du fichier vers une région mémoire et fixe ses permissions. Les noms sont des conventions, pas des règles :

  • .text — code exécutable. Lecture + exécution.
  • .rdata — données en lecture seule : constantes, tables d'imports, infos de debug.
  • .data — variables globales initialisées et inscriptibles.
  • .rsrc — ressources décrites par le resource directory.
  • .reloc — corrections de relocalisation de base.

Chaque en-tête porte VirtualAddress et VirtualSize (où la section atterrit en mémoire) ainsi que PointerToRawData et SizeOfRawData (où elle vit sur le disque). Ces deux paires ne sont presque jamais égales, ce qui nous amène au concept le plus important de l'analyse PE.

RVA versus offset de fichier

Un offset de fichier est une position en octets sur le disque. Une RVA (Relative Virtual Address) est un offset par rapport à ImageBase une fois l'image mappée par le loader. Comme les sections sont alignées sur FileAlignment (souvent 512 octets) sur le disque mais sur SectionAlignment (souvent 4096 octets) en mémoire, le même octet possède deux adresses différentes.

Pour traduire une RVA en offset de fichier, vous trouvez la section qui contient la RVA, puis vous appliquez :

text
file_offset = RVA - section.VirtualAddress + section.PointerToRawData

Chaque outil qui vous permet de cliquer sur une adresse virtuelle et de voir les octets sur le disque fait exactement cela. Trompez-vous et vous lirez du charabia — ce qui est précisément la confusion que certains packers fabriquent à dessein.

Les imports, l'IAT et les exports

Quand un programme appelle CreateFileW, il ne connaît pas l'adresse de cette fonction à la compilation. L'éditeur de liens enregistre plutôt, dans l'import directory, qu'il a besoin de CreateFileW depuis kernel32.dll. Deux tableaux parallèles décrivent cela : l'Import Name Table (noms/ordinaux) et l'Import Address Table (IAT).

Au chargement, le loader Windows parcourt les imports, charge chaque DLL, résout chaque fonction et écrit les adresses réelles dans l'IAT. Le code appelle ensuite indirectement à travers l'IAT. Pour un analyste, cette table est un aveu : elle énumère presque tout ce que le binaire peut faire. C'est exactement pourquoi les attaquants la ciblent avec l'obfuscation de la table d'imports, résolvant les API dynamiquement pour que l'import directory semble vide.

L'export directory en est l'image miroir. Une DLL publie des fonctions nommées ou numérotées par ordinal pour que d'autres modules puissent les importer, en associant chaque nom à une RVA dans le module.

Le point d'entrée et l'OEP

AddressOfEntryPoint est la RVA vers laquelle le loader saute une fois le mapping et la résolution des imports terminés (et après d'éventuels callbacks TLS). Dans un programme normal, il pointe dans .text, au niveau du code de démarrage du runtime C.

Dans un programme packé, il pointe plutôt vers un petit stub de dépaquetage. Ce stub décompresse ou déchiffre le vrai code, reconstruit l'IAT et finit par sauter vers le point d'entrée d'origine (OEP) — l'adresse à laquelle le programme aurait démarré avant le packing. Trouver l'OEP est l'énigme centrale du dépaquetage. La famille plus large de ces astuces est traitée sous techniques de packing.

Pourquoi les malwares et les packers manipulent tout cela

Presque chaque déformation défensive que vous rencontrerez est une modification délibérée de l'une des structures ci-dessus :

  • Réécrire le point d'entrée pour exécuter un stub d'abord, puis sauter vers l'OEP.
  • Réduire ou falsifier l'IAT pour que les outils statiques ne voient pas quelles API sont utilisées.
  • Abuser des callbacks TLS pour exécuter du code avant que le moindre breakpoint d'un débogueur au point d'entrée ne se déclenche.
  • Cacher des charges utiles dans les ressources ou ajouter un overlay — des données accolées après la dernière section, que le loader ne mappe jamais mais que le programme lit lui-même (voir charges utiles en overlay PE).
  • Mentir sur la taille et l'alignement des sections pour casser le calcul naïf RVA-vers-offset.

Une fois que vous savez lire les en-têtes proprement, ces astuces cessent d'être de la magie et deviennent une checklist.

Et après

Le format PE récompense la pratique. Ouvrez un petit exécutable dans un visualiseur PE et tracez chaque champ depuis l'en-tête DOS jusqu'à l'IAT. Regardez ensuite comment ces mêmes champs sont weaponisés dans la bibliothèque de techniques complète, et gardez le glossaire du reverse engineering ouvert pour les acronymes.


Prêt à aller plus loin ? Choisissez un binaire, mappez ses en-têtes à la main, puis plongez dans notre bibliothèque de techniques pour voir comment chaque structure est tordue, cachée ou détournée dans la nature.

Articles liés

Un guide défensif et orienté labo pour reconnaître et dépaquer les exécutables packés : entropie, récupération de l'OEP, dumps mémoire et reconstruction de l'IAT.
Un guide pour débutants sur l'analyse de malware : les quatre types d'analyse, monter un labo sûr, le triage statique et dynamique, un parcours d'apprentissage.
Une sélection des meilleurs outils de reverse engineering en 2026 : désassembleurs, débogueurs, sandboxes, triage et analyse statique.