Reverse-engineering: le format PE
Introduction
Il existe de multiples formats de fichiers exécutables. Le plus ancien est le format COM, qui consiste en un "dump" de la mémoire. Bien que ce format ne soit plus utilisé, il est utile de le connaître, afin de mieux comprendre les difficultés liées à l'adressage mémoire. Puis nous nous pencherons sur le format exécutable MS-DOS, car tous les exécutables modernes maintiennent une compatibilité avec ce format antédiluvien. Enfin, nous regarderons la structure du format courant supporté par les Windows 32 bits (95, 98, Me, NT, 2000, XP, ...)
Format COM MS-DOS
Ce format existe depuis les origines du DOS et est conçu pour le mode réel. Les fichiers portent l'extension .com (par exemple command.com). Le format de ces exécutables est simplissime : il n'existe pas. Le fichier contient une copie conforme du code binaire tel qu'il doit être chargé en mémoire. L'inconvénient majeur de ce format est que le fichier DOIT être chargé à une adresse précise. En effet, il contient des références absolues à des zones mémoire. Par exemple, regardons le code assembleur qui lit le contenu d'une variable var1 et le place dans une variable var2 :
mov ax, var1 mov var2, ax
En décompilant le binaire obtenu, nous lirons quelque chose comme :
mov ax, [1A3Eh] mov [1A40h], ax
Les adresses 0x1A3E et 0x1A40 sont absolues. L'instruction mov va lire le contenu de cette zone mémoire, que la variable attendue y soit présente ou non. Si jamais le fichier est chargé dans une zone mémoire différente, toutes les références deviennent erronées. Il y aurait bien la possibilité d'analyser une à une chaque instruction et de modifier à la volée les adresses, mais ça réclamerait un temps considérable.
L'autre inconvénient du format COM est qu'il n'y a aucune séparation entre les données, la pile et le code. Tout est placé pêle-mêle dans la même zone mémoire. Un binaire de ce type ne peut donc pas profiter des avantages du mode protégé, qui définit des droits d'accès aux zones mémoire (par exemple, le code et les données constantes en lecture seule, la pile et les données variables en lecture / écriture).
À l'heure actuelle, ce format n'est plus utilisé. Même la majorité des fichiers portant l'extension .com dans Windows sont en réalité au format exécutable MS-DOS ou Win32. Ce format est néanmoins encore utilisé par la Demo Scene et pour des petits programmes où la minimisation de la taille de l'empreinte mémoire est capitale.
Format exécutable MS-DOS
Ce format existe depuis MS-DOS 2.0. Le fichier est divisé en segments et est précédé d'un header. Cet en-tête contient beaucoup d'informations utiles, dont :
- Élément de la liste à puces
- un nombre magique, qui vaut toujours 0x4D5A. En notation little-endian, cela donne la chaîne ASCII "MZ"
- le nombre de segments
- la taille du header
- l'adresse d'une table de relocalisation
- ...
Les adresses sont toujours absolues, mais le fichier comprend désormais une table de "relocalisation". Ce tableau contient des pointeurs vers toutes les adresses mémoire à modifier si jamais le fichier exécutable n'était pas chargé à l'endroit attendu.
Avec l'apparition de Windows, de nouveaux formats exécutables ont été définis ; cependant Microsoft a tenu à conserver une compatibilité minimale avec le format initial MS-DOS. C'est pour cela que n'importe quel programme Win16 et Win32 contient - dans son en-tête - un véritable programme MS-DOS. Celui-ci affiche un message généralement d'avertissement :
This program cannot be run in DOS mode.
Cependant, ce système a été conçu pour permettre la cohabitation - dans un même fichier - de deux versions d'un même programme, l'une MS-DOS, l'autre Windows. C'est le cas de ScanDisk pour Windows 95/98.
Pour permettre la cohabitation des deux formats (exécutables DOS et Windows), le header MS-DOS a été incrémenté de quelques nouveaux champs, dont l'un contient un pointeur vers une seconde structure propre au nouveau format. La structure complète du header Ms-Dos, IMAGE_DOS_HEADER, est définie dans le fichier winnt.h :
typedef struct _IMAGE_DOS_HEADER // DOS .EXE header
{
0h WORD e_magic; // Magic number
2h WORD e_cblp; // Bytes on last page of file
4h WORD e_cp; // Pages in file
6h WORD e_crlc; // Relocations
8h WORD e_cparhdr; // Size of header in paragraphs
Ah WORD e_minalloc; // Minimum extra paragraphs needed
Ch WORD e_maxalloc; // Maximum extra paragraphs needed
Eh WORD e_ss; // Initial (relative) SS value
10h WORD e_sp; // Initial SP value
12h WORD e_csum; // Checksum
14h WORD e_ip; // Initial IP value
16h WORD e_cs; // Initial (relative) CS value
18h WORD e_lfarlc; // File address of relocation table
1Ah WORD e_ovno; // Overlay number
1Ch WORD e_res[4]; // Reserved words
24h WORD e_oemid; // OEM identifier (for e_oeminfo)
26h WORD e_oeminfo; // OEM information; e_oemid specific
28h WORD e_res2[10]; // Reserved words
3Ch LONG e_lfanew; // File address of new exe header
}
IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
Format exécutable Windows
Ce format se compose d'un en-tête MS-DOS, d'un programme MS-DOS, d'un second en-tête propre au format, suivi d'une ou plusieurs sections. Une section contient soit des données, soit du code. Elle peut se voir attribuer des droits d'exécution, de lecture et d'écriture.
A l'offset pointé par le champ e_lfanew de l'en-tête MS-DOS, se trouve une seconde structure propre aux formats Windows. Il existe deux versions majeures de celle-ci :
- le format New Executable (NE), apparu avec Windows 3.x
- le format Portable Executable (PE), apparu avec Windows NT et utilisé par toutes les versions 32-bits de Windows.
Parce que le format NE n'est plus utilisé aujourd'hui, nous ne décrirons pas dans ce document la structure de son en-tête. La signature de ce format est 0x4E45, nombre qui forme la chaîne ascii NE.
L'en-tête PE est également définie dans winnt.h par la structure IMAGE_NT_HEADERS.
typedef struct _IMAGE_NT_HEADERS
{
0h DWORD Signature;
4h IMAGE_FILE_HEADER FileHeader;
18h IMAGE_OPTIONAL_HEADER OptionalHeader;
}
IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
Pour les exécutables PE, la signature est 0x00004550, ce qui en notation little endian donne la chaîne ascii "PE\0\0". Les deux sous-structures IMAGE_FILE_HEADER et IMAGE_OPTIONAL_HEADER sont définies comme suit :
typedef struct _IMAGE_FILE_HEADER
{
0h WORD Machine;
2h WORD NumberOfSections;
4h DWORD TimeDateStamp;
8h DWORD PointerToSymbolTable;
Ch DWORD NumberOfSymbols;
10h WORD SizeOfOptionalHeader;
12h WORD Characteristics;
}
IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Malgré son nom, la structure IMAGE_OPTIONAL_HEADER n'est absolument pas optionnelle. Elle contient des informations très importantes.
typedef struct _IMAGE_OPTIONAL_HEADER
{
0h WORD Magic;
2h BYTE MajorLinkerVersion;
3h BYTE MinorLinkerVersion;
4h DWORD SizeOfCode;
8h DWORD SizeOfInitializedData;
Ch DWORD SizeOfUninitializedData;
10h DWORD AddressOfEntryPoint;
14h DWORD BaseOfCode;
18h DWORD BaseOfData;
1Ch DWORD ImageBase;
20h DWORD SectionAlignment;
24h DWORD FileAlignment;
28h WORD MajorOperatingSystemVersion;
2Ah WORD MinorOperatingSystemVersion;
2Ch WORD MajorImageVersion;
2Eh WORD MinorImageVersion;
30h WORD MajorSubsystemVersion;
32h WORD MinorSubsystemVersion;
34h DWORD Win32VersionValue;
38h DWORD SizeOfImage;
3Ch DWORD SizeOfHeaders;
40h DWORD CheckSum;
44h WORD Subsystem;
46h WORD DllCharacteristics;
48h DWORD SizeOfStackReserve;
4Ch DWORD SizeOfStackCommit;
50h DWORD SizeOfHeapReserve;
54h DWORD SizeOfHeapCommit;
58h DWORD LoaderFlags;
5Ch DWORD NumberOfRvaAndSizes;
60h IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
}
IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;
Parmi les champs qui méritent d'être examinés de plus près, il y a ImageBase, qui contient l'adresse mémoire à laquelle le programme souhaite être chargé. Windows essaie de toujours charger un programme à l'adresse demandée : s'il est effectivement chargé à cette adresse, il n'y aura pas besoin de recalculer les adresses absolues à l'aide des tables de relocalisation. ImageBase est une adresse virtuelle. Nous verrons dans le chapitre suivant à quoi elle correspond réellement.
La plupart des autres références mémoire sont des adresses relatives à ImageBase. AddressOfEntryPoint, l'adresse du point d'entrée dans l'image, ou BaseOfCode, pointeur vers le début de la section de code, ces adresses expriment un offset relatif à ImageBase. Ces adresses sont appelées RVA (Relative Virtual Address).
Le tableau DataDirectory définit une liste de "répertoires" et leur offset dans le fichier. Le nombre d'entrées dans le tableau est donné par le champ NumberOfRvaAndSizes. Ces "répertoires" réunissent des données telles que :
- les ressources (icônes, bitmaps, curseurs, ...)
- la table de "relocalisation"
- les tables d'importation et d'exportation
- les symboles de débogage
- les exceptions
- ...
Une description complète de la structure IMAGE_DATA_DIRECTORY se trouve sur MSDN.
Directement suivant le header IMAGE_NT_HEADERS, on trouve une suite de structures IMAGE_SECTION_HEADER. Le nombre de ces structures est donné par le champ NumberOfSections de IMAGE_FILE_HEADER. L'offset de la première structure se calcule donc par :
DWORD offset = dos_header.e_lfanew + sizeof( IMAGE_NT_HEADERS );
Une structure décrit la taille et l'adresse de la section dans le fichier, la taille et l'adresse de la section en mémoire, les permissions de la section (exécution, lecture, écriture).
typedef struct _IMAGE_SECTION_HEADER
{
0h BYTE Name[ IMAGE_SIZEOF_SHORT_NAME ];
8h union
{
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
Ch DWORD VirtualAddress;
10h DWORD SizeOfRawData;
14h DWORD PointerToRawData;
18h DWORD PointerToRelocations;
1Ch DWORD PointerToLinenumbers;
1Eh WORD NumberOfRelocations;
20h WORD NumberOfLinenumbers;
24h DWORD Characteristics;
}
IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Toutes les références mémoire sont relatives, soit au début du fichier, soit à l'adresse de chargement ImageBase. Il est assez fréquent que les adresses et les tailles sur le disque et en mémoire soient différentes.
Windows et la mémoire virtuelle
Bien que le format exécutable Windows prévoit un espace pour un tableau de relocalisation (relocation table), les dernières versions des compilateurs (à partir de Visual Studio 6) ne jugent plus utile de les ajouter. En effet, dans les Windows 32-bits, tout programme s'exécute dans un espace d'adresses virtuelles. Cela signifie qu'un programme a virtuellement accès à 4 Go de RAM auquel il est seul à accéder (il n'y trouvera aucun autre programme).
Cet espace d'adresses virtuelles est créé au moment du chargement du programme, par conséquent l'espace est vierge, et l'adresse absolue à laquelle le programme a prévu de se charger est toujours disponible. C'est pour cela que désormais, seules les DLLs, qui peuvent être chargées n'importe où n'importe quand continuent d'inclure une relocation table.
Extensions
Il existe de nombreuses extensions différentes pour désigner des fichiers contenant du code exécutable. Les plus courantes sont :
- EXE : les programmes
- DLL : les librairies dynamiques
- CPL : les items du panneau de configuration
- OCX : les contrôles ActiveX
- SCR : les écrans de veille
- SYS : les drivers
Tous ces fichiers ont exactement le même format. Par exemple, la seule réelle différence entre un EXE et une DLL est un flag positionné ou non dans le champ Characteristics de la structure IMAGE_FILE_HEADER.
Cependant, seuls les EXE doivent définir obligatoirement un point d'entrée (qui correspond à la procédure main). Les DLL n'en définissent généralement pas.
Informations complémentaires
- MSDN: La documentation officielle de Microsoft. Vous y trouverez (entre autres) la spécification complète du format Portable Executable PE.
- winnt.h: Fichier include C (normalement fourni avec votre compilateur Windows) qui contient la définition des structures de données présentées.
Note: Les informations sur cette page proviennent du site La Caverne Informatique.