(en français "exécutable portable") est un format de fichiers dérivé du COFF regroupant les exécutables et les bibliothèques sur les systèmes d'exploitation Windows 32 bits et 64 bits.
Les extensions couvertes par ce format sont : l'extension .exe, l'extension .ocx (OLE et ActiveX), l'extension .dll et l'extension .cpl.
C'est également le format utilisé pour les exécutables de l'UEFI (.efi).
Portable Executable est souvent abrégé par PE.
Historique
Microsoft a adopté le format PE avec l'introduction de Windows NT 3.1. Toutes les versions suivantes de Windows, incluant Windows 10/11, supportent ce format. Auparavant les fichiers «exécutables» étaient au format NE — New Executable File Format, « New » faisant référence à CP/M, et aux fichiers *.com —.
La création du format PE a été induite par la volonté de Microsoft de concevoir une structure de fichier qui puisse s'adapter aux différentes machines utilisant Windows NT, lequel était initialement capable de supporter d'autres architectures que le x86 d'Intel (Power PC et Motorola 68000 par exemple). L'idée fut donc de créer une structure comme ces architectures.
Schéma du format PE
Un fichier exécutable PE est structuré de la façon suivante :Structure du Format PE|377x377px
Les deux premiers octets du fichier représentent les caractères MZ.
En-tête MZ sous MS-DOS
L'en-tête MZ permet au système d'exploitation de reconnaître le fichier comme étant un exécutable valide dans le cas où celui-ci serait lancé depuis MS-DOS, afin de pouvoir exécuter son segment DOS.
Voici la structure de l'en-tête, en langage C :
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // Magic number WORD e_cblp; // Bytes on last page of file WORD e_cp; // Pages in file WORD e_crlc; // Relocations WORD e_cparhdr; // Size of header in paragraphs WORD e_minalloc; // Minimum extra paragraphs needed WORD e_maxalloc; // Maximum extra paragraphs needed WORD e_ss; // Initial (relative) SS value WORD e_sp; // Initial SP value WORD e_csum; // Checksum WORD e_ip; // Initial IP value WORD e_cs; // Initial (relative) CS value WORD e_lfarlc; // File address of relocation table WORD e_ovno; // Overlay number WORD e_res; // Reserved words WORD e_oemid; // OEM identifier (for e_oeminfo) WORD e_oeminfo; // OEM information; e_oemid specific WORD e_res2; // Reserved words LONG e_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
On reconnaît par exemple : e_magic qui doit valoir "MZ" e_lfanew qui contient l'adresse du début de l'en-tête PE
Segment DOS Le segment DOS est exécuté lorsque Windows ne reconnaît pas le fichier comme étant au format PE, ou s'il est exécuté sous MS-DOS. Il affiche en général un message comme This program cannot be run in DOS mode, littéralement traduit, Ce programme ne peut pas être exécuté en mode DOS.
En-tête PE L'en-tête PE est un ensemble de structures, regroupées dans une même et unique structure nommée IMAGE_NT_HEADER. Voici son prototype en langage C :
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER OptionalHeader; } IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
Signature permettant d'identifier le fichier, qui doit être égale à 0x00004550, soit "PE\0\0" (\0 étant un octet nul).
FileHeader est une structure nommée IMAGE_FILE_HEADER contenant des informations sur la structuration du fichier, et prototypée comme suit :
typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Voir http://msdn.microsoft.com/en-us/library/windows/desktop/ms680313%28v=vs.85%29.aspx
OptionalHeader est une structure nommée IMAGE_OPTIONAL_HEADER Voir http://msdn.microsoft.com/en-us/library/windows/desktop/ms680339%28v=vs.85%29.aspx pour plus de détails
Adresses physique, virtuelle et mémoire La première chose à savoir est que l'exécutable est chargé en mémoire à l'adresse ImageBase (présent dans OptionalHeader) si cette adresse est disponible. Sinon il est chargé à une autre adresse qui sera la nouvelle valeur ImageBase.
Dans l'en-tête et le corps d'un fichier PE, on trouve trois adressages différents : Les adresses physiques représentent une position dans le fichier. Dans l'en-tête, leur nom commence par PointerTo Les adresses virtuelles indiquent une position en mémoire relative à ImageBase. Dans l'en-tête, leur nom commence par Virtual Les adresses mémoire sont aussi une position en mémoire mais cette fois absolue, ces adresses sont le plus souvent présentes dans le code de l'application et les données mais pas dans les en-têtes. Dans le cas où ImageBase a été modifié, la table des relocalisations indique la position des adresses mémoire à rectifier.
Répertoires Les répertoires sont des parties du fichier utilisées lors de son chargement.
La position et la taille des données de ces répertoires sont indiquées dans le champ DataDirectory de OptionalHeader qui est un tableau de structures IMAGE_DATA_DIRECTORY décrit comme suit : typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;
VirtualAddress étant l'adresse du début du contenu du répertoire une fois les sections chargées en mémoire, relative à ImageBase (présent dans OptionalHeader).
Et Size sa taille.
Bien que le champ NumberOfRvaAndSizes de OptionalHeader indique leur nombre, il y en a habituellement 16 : Position Nom Description 0 IMAGE_DIRECTORY_ENTRY_EXPORT Table des exports 1 IMAGE_DIRECTORY_ENTRY_IMPORT Table des imports 2 IMAGE_DIRECTORY_ENTRY_RESOURCE Table des ressources 3 IMAGE_DIRECTORY_ENTRY_EXCEPTION Table des exceptions 4 IMAGE_DIRECTORY_ENTRY_SECURITY Table des certificats 5 IMAGE_DIRECTORY_ENTRY_BASERELOC Table des relocalisations 6 IMAGE_DIRECTORY_ENTRY_DEBUG Informations de débogage 7 IMAGE_DIRECTORY_ENTRY_COPYRIGHT / IMAGE_DIRECTORY_ENTRY_ARCHITECTURE Données spécifiques aux droits de copies ou à l'architecture 8 IMAGE_DIRECTORY_ENTRY_GLOBALPTR pointeurs globaux 9 IMAGE_DIRECTORY_ENTRY_TLS Table de stockage des threads locaux (TLS (Thread local storage) table en anglais) 10 IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG Load configuration table 11 IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT Table des imports liés 12 IMAGE_DIRECTORY_ENTRY_IAT Table des adresses des imports 13 IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT Descripteur des imports en différé 14 IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR en-tête runtime COM+ 15 - réservé: doit être vide.
Table des sections
vignette|Les sections d'un programme vu par PEiD
La Table des Sections est située juste derrière l'en-tête PE. Il s'agit d'un tableau contenant plusieurs structures IMAGE_SECTION_HEADER.
Ces structures contiennent les informations sur les sections du binaire devant être chargé en mémoire.
Le champ NumberOfSections de la structure IMAGE_FILE_HEADER indique combien d'entrées il y a dans cette table. Le maximum supporté par Windows est de 96 sections.
La Table des Sections est prototypé comme suit : typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Chaque section a un nom de 8 caractères, ce nom n'est pas significatif mais on peut généralement trouver les suivants :
Nom Description .text Le code (instructions) du programme .bss Les variables non-initialisées .reloc La table des relocalisations (le sixième répertoire) .data Les variables initialisées .rsrc Les ressources du fichier (le troisième répertoire: Curseurs, Sons, Menus…) .rdata Les données en lectures seule .idata La table d'import (le second répertoire) .upx Signe d'une compression UPX, propre au logiciel UPX .aspack Signe d'un package ASPACK, propre au logiciel ASPACK .adata Signe d'un package ASPACK, propre au logiciel ASPACK
Table des relocalisations
La table des relocalisations est présente dans le sixième répertoire 'Base relocation table'. Elle indique les adresses virtuelles des valeurs représentant une adresse mémoire. Elle permet, dans le cas où l'exécutable n'a pas été chargé en mémoire à l'adresse ImageBase, de replacer toutes les références mémoire pour correspondre à la nouvelle valeur ImageBase.
La table des relocalisations est une suite de structures de tailles variables. Chacune est composée d'un en-tête de type IMAGE_BASE_RELOCATION puis d'un tableau de valeurs de type WORD (de taille 16 bits). Il arrive parfois que la dernière valeur soit 0, dans ce cas elle ne sert que pour l'alignement sur .
typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; } IMAGE_BASE_RELOCATION,*PIMAGE_BASE_RELOCATION;
SizeOfBlock est la taille en octets de la structure, en-tête compris. VirtualAddress est l'adresse virtuelle (relative à ImageBase) de base pour les valeurs
On ne connait pas le nombre de structures présentes dans la table des relocalisations, mais on sait sa taille et donc on s'arrête lorsque l'on a atteint la fin. De même on ne sait pas combien de valeurs suivent chaque en-tête, mais on le déduit de la taille.
Chaque valeur est composée de 4 bits d'informations et de 12 bits de données.
La partie information peut être l'une de ces valeurs : Valeur Description IMAGE_REL_BASED_ABSOLUTE IMAGE_REL_BASED_HIGH IMAGE_REL_BASED_LOW IMAGE_REL_BASED_HIGHLOW La donnée est une adresse relative à la base (adresse virtuelle = VirtualAddress + donnée) IMAGE_REL_BASED_HIGHADJ IMAGE_REL_BASED_MIPS_JMPADDR
Le plus souvent seul IMAGE_REL_BASED_HIGHLOW est utilisé.
La valeur de l'adresse mémoire ainsi calculée sera décalée selon la différence entre l'ImageBase d'origine et l'adresse du début de la mémoire allouée au programme.
Table d'import - IAT vignette|gauche|Exemple d'IAT sur un programme Windows
L'IAT, qui signifie import address table, contenue dans le second répertoire import table (généralement dans la section .idata ou .rdata), indique les adresses des API importées par un logiciel, ainsi que les noms des DLL important ces fonctions. Les API, contenues dans ces DLL, permettent aux logiciels de fonctionner correctement.
Son existence est due au fait que les API sont adressées différemment en fonction des OS.
Il faut savoir en premier lieu qu'une structure nommée IMAGE_IMPORT_DESCRIPTOR est utilisée pour chaque DLL appelée; plus une dernière de 5 DWORD mis à zéro qui définit une terminaison.
Pour chaque DLL importée, une structure nommée IMAGE_THUNK_DATA sera utilisée pour chaque API de cette DLL ; il y aura donc autant de IMAGE_THUNK_DATA que de fonctions exportées par une DLL, plus un dernier DWORD spécifiant la terminaison de cette DLL.
Une troisième structure, IMAGE_IMPORT_BY_NAME, définit le nom des API ainsi que leur numéro ORDINAL (nombre de 16 bits identifiant une fonction au sein d'une DLL). Il en existe autant qu'il y a d'API importées par DLL. Par exemple, sur l'image d'exemple ci-dessus, il y a trois IMAGE_IMPORT_BY_NAME définies pour advapi32.dll, car seulement trois de ses API sont utilisées par le programme.
typedef struct _IMAGE_IMPORT_DESCRIPTOR { _ANONYMOUS_UNION union { DWORD Characteristics; // 0 for terminating null import descriptor DWORD OriginalFirstThunk; // It points to the first thunk IMAGE_THUNK_DATA } DUMMYUNIONNAME; DWORD TimeDateStamp; // 0 if not bound DWORD ForwarderChain; // -1 if no forwarders DWORD Name; // RVA of DLL Name. DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) } IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR; typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; //Ordinal Number BYTE Name; //Name of function