Portable Executable - File Format

Portable Executable - File Format

·

28 min read

1. Introduction

  • Tout au long de mon apprentissage dans le développement d'outils de sécurité Windows, je me suis rendu compte qu'il me manquait des connaissances fondamentales sur le détails des fichiers exécutables, leur structure et fonctionnement.

  • Aujourd'hui je suis chargé de développer des outils de sécurité Windows pour Pegasy et au vu de la nature de ce qu'il y a à faire je ne pourrais pas avancer sans ces importantes connaissances du format PE.

2. Vue d'ensemble

  • "PE" signifie "Portable Executable", c'est un format de fichier pour les exécutables utilisé dans les systèmes d'exploitation Windows, il est basé sur le format de fichier COFF (Common Object File Format).

  • Les fichiers .exe ne sont pas les seuls à être des fichiers PE, les bibliothèques de liens dynamiques (.dll), les modules du kernel (.srv), les applications du panneau de configuration (.cpl) et bien d'autres sont également des fichiers PE.

  • Un fichier PE est une structure de données qui contient les informations nécessaires au chargeur du système d'exploitation pour pouvoir charger cet exécutable en mémoire et l'exécuter.

  • Dans l'image ci-dessous, une vue d'ensemble de la structure d'un PE.

Portable-executable-file-format.png

  • Dans un cas concret, voici à quoi ressemble la structure de putty64.exe (version portable de putty en x64 bit) dans l'image ci-dessous. Pour reproduire l'exemple, téléchargez l'outil PE-Bear et chargez un exécutable dedans.

image.png

DOS Header

Chaque fichier PE commence par une structure de 64 octets appelée en-tête DOS. C'est ce qui fait du fichier PE un exécutable MS-DOS.

DOS Stub

Après le "DOS Header" vient le "DOS stub" qui est un petit exécutable compatible avec MS-DOS 2.0, qui affiche simplement un message d'erreur disant "Ce programme ne peut pas être exécuté en mode DOS" lorsque le programme est exécuté en mode DOS. Ci-dessous, un exemple du programme. image.png

Ci-dessous, la donnée contenue dans le portable executable. image.png

NT Headers

La partie NT Headers contient trois parties principales :

PE Signature : Une signature de 4 octets qui identifie le fichier comme un fichier PE.

File Header : Un en-tête de fichier COFF standard. Il contient quelques informations sur le fichier PE.

Optional Header : L'en-tête le plus important des en-têtes NT, son nom est l'en-tête optionnel parce que certains fichiers comme les fichiers d'objets n'en ont pas, mais il est nécessaire pour les fichiers d'images (fichiers comme les fichiers .exe). Cet en-tête fournit des informations importantes au chargeur du système d'exploitation.

Section Table

  • Le "Section Table" suit immédiatement le "Optional Header", c'est un tableau d'en-têtes de section d'image, il y a un en-tête de section pour chaque section du fichier PE.
  • Chaque en-tête contient des informations sur la section à laquelle il fait référence.

Sections

  • Les "sections" sont l'endroit où le contenu réel du fichier est stocké, ce qui inclut des choses comme les données et les ressources que le programme utilise, ainsi que le code réel du programme, il y a plusieurs sections, chacune ayant son propre objectif.

image.png

3. DOS Header & DOS Stub

DOS Header

Vue d'ensemble :

  • "DOS Header" (également appelé MS-DOS Header) est une structure de 64 octets qui se situe au début du fichier PE.

  • Il n'est pas important pour la fonctionnalité des fichiers PE sur les systèmes Windows modernes, mais il est présent pour des raisons de rétrocompatibilité.

  • Cet en-tête fait du fichier un exécutable MS-DOS, de sorte que lorsqu'il est chargé sur MS-DOS, le stub DOS est exécuté à la place du programme réel.

  • Sans cet en-tête, si vous tentez de charger l'exécutable sous MS-DOS, il ne sera pas chargé et produira simplement une erreur générique d'exécution.

Vue d'ensemble :

  • Comme mentionné précédemment, il s'agit d'une structure de 64 octets de long, nous pouvons jeter un coup d'oeil au contenu de cette structure en regardant la structure de IMAGE_DOS_HEADER qui est contenue dans winnt.h :
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[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // File address of new exe header
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
  • Cette structure est importante pour le chargeur PE de MS-DOS, mais seuls quelques membres sont importants pour le chargeur PE des systèmes Windows. Nous n'allons donc pas tout couvrir ici, mais seulement les membres importants de la structure.

  • e_magic : C'est le premier membre de l'en-tête DOS, c'est un MOT qui occupe donc 2 octets, on l'appelle généralement le numéro magique. Il a une valeur fixe de 0x5A4D ou MZ en ASCII, et il sert de signature qui marque le fichier comme un exécutable MS-DOS.

  • e_lfanew : C'est le dernier membre de la structure d'en-tête DOS, il est situé à l'offset 0x3C dans l'en-tête DOS et il contient un offset vers le début des en-têtes NT. Ce membre est important pour le chargeur PE des systèmes Windows car il indique au chargeur où chercher l'en-tête du fichier.

Ci-dessous, une image du "DOS Header" sur un programme chargé dans PE-Bear.

image.png

  • Comme vous pouvez le voir, le premier membre de l'en-tête est le nombre magique avec la valeur fixe dont nous avons parlé, à savoir 5A4D.

  • Le dernier membre de l'en-tête (à l'offset 0x3C) porte le nom "File address of new exe header", il a la valeur 100, nous pouvons suivre cet offset et nous trouverons le début des en-têtes NT comme prévu :

image.png

DOS Stub

Vue d'ensemble :

  • Le stub DOS est un programme MS-DOS qui affiche un message d'erreur indiquant que l'exécutable n'est pas compatible avec DOS, puis se termine.

  • C'est ce qui est exécuté lorsque le programme est chargé en MS-DOS, le message d'erreur par défaut est "Ce programme ne peut pas être exécuté en mode DOS", mais ce message peut être modifié par l'utilisateur pendant la compilation.

  • C'est tout ce que nous avons besoin de savoir sur le stub DOS, nous ne nous en soucions pas vraiment.

4. NT Headers

  • Dans le post précédent, nous avons examiné la structure de l'en-tête DOS et nous avons inversé le stub DOS.

  • Dans cet article, nous allons parler de la partie NT Headers de la structure du fichier PE.

  • Avant d'entrer dans le vif du sujet, nous devons parler d'un concept important que nous allons voir souvent, à savoir le concept de "Relative Virtual Address" ou RVA. Une RVA est simplement un décalage par rapport à l'endroit où l'image a été chargée en mémoire (la base de l'image).

  • Pour convertir une RVA en une adresse virtuelle absolue, il faut donc ajouter la valeur de la RVA à la valeur de l'image de base. Les fichiers PE reposent largement sur l'utilisation des RVA, comme nous le verrons plus tard.

NT Headers (IMAGE_NT_HEADERS)

  • NT headers est une structure définie dans winnt.h par "IMAGE_NT_HEADERS", en regardant sa définition nous pouvons voir qu'elle a trois membres, une signature DWORD, une structure IMAGE_FILE_HEADER appelée FileHeader et une structure IMAGE_OPTIONAL_HEADER appelée OptionalHeader.

  • Il convient de mentionner que cette structure est définie dans deux versions différentes, l'une pour les exécutables 32 bits (également appelés exécutables PE32) appelée IMAGE_NT_HEADERS et l'autre pour les exécutables 64 bits (également appelés exécutables PE32+) appelée IMAGE_NT_HEADERS64.

  • La principale différence entre les deux versions est la version utilisée de la structure IMAGE_OPTIONAL_HEADER qui a deux versions, IMAGE_OPTIONAL_HEADER32 pour les exécutables 32 bits et IMAGE_OPTIONAL_HEADER64 pour les exécutables 64 bits.

typedef struct _IMAGE_NT_HEADERS64 {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

Signature :

  • Le premier membre de la structure des en-têtes NT est la signature PE, c'est un DWORD ce qui signifie qu'il occupe 4 octets. Il a toujours une valeur fixe de 0x50450000, ce qui se traduit par "PE\0\0" en ASCII.

Voici une capture d'écran de PE-bear montrant la signature dans un PE :

image.png

File Header (IMAGE_FILE_HEADER) :

  • Aussi appelé "The COFF File Header", l'en-tête de fichier est une structure qui contient des informations sur le fichier PE.

Elle est définie comme IMAGE_FILE_HEADER dans winnt.h, la voici :

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;

C'est une structure simple avec 7 membres :

Machine :

  • Il s'agit d'un nombre qui indique le type de machine (Architecture CPU) que l'exécutable cible, ce champ peut avoir beaucoup de valeurs, mais nous ne sommes intéressés que par deux d'entre elles, 0x8864 pour AMD64 et 0x14c pour i386. Pour une liste complète des valeurs possibles, vous pouvez consulter la documentation officielle de Microsoft .

NumberOfSections :

  • Ce champ contient le nombre de sections (ou le nombre d'en-têtes de section, c'est-à-dire la taille de la table des sections).

TimeDateStamp :

  • Un horodatage unix qui indique la date de création du fichier.

PointerToSymbolTable et NumberOfSymbols :

  • Ces deux champs contiennent le décalage du fichier vers la table de symboles COFF et le nombre d'entrées dans cette table de symboles. Cependant, ils sont définis à 0, ce qui signifie qu'aucune table de symboles COFF n'est présente, car les informations de débogage COFF sont obsolètes.

SizeOfOptionalHeader :

  • Taille de l'en-tête facultatif.

Characteristics (Caractéristiques) :

  • Un drapeau qui indique les attributs du fichier, ces attributs peuvent être des choses comme le fichier étant exécutable, le fichier étant un fichier système et non un programme utilisateur, et beaucoup d'autres choses. Une liste complète de ces indicateurs peut être trouvée dans la documentation officielle de Microsoft.

Voici le contenu de l'en-tête de fichier d'un fichier PE réel :

image.png

Optional Header (IMAGE_OPTIONAL_HEADER) :

  • Le "Optional Header" est l'en-tête le plus important des en-têtes NT, le chargeur PE recherche des informations spécifiques fournies par cet en-tête pour pouvoir charger et exécuter l'exécutable.

  • On l'appelle l'en-tête optionnel parce que certains types de fichiers, comme les fichiers objets, n'en ont pas, mais cet en-tête est essentiel pour les fichiers images. Il n'a pas de taille fixe, c'est pourquoi le membre IMAGE_FILE_HEADER.SizeOfOptionalHeader existe.

  • Les 8 premiers membres de la structure Optional Header sont standard pour chaque implémentation du format de fichier COFF, le reste de l'en-tête est une extension de l'en-tête optionnel COFF standard défini par Microsoft, ces membres supplémentaires de la structure sont nécessaires pour le chargeur et l'éditeur de liens Windows PE.

  • Comme indiqué précédemment, il existe deux versions de l'en-tête facultatif, l'une pour les exécutables 32 bits et l'autre pour les exécutables 64 bits.

Les deux versions diffèrent sur deux points :

  • La taille de la structure elle-même (ou le nombre de membres définis dans la structure) : IMAGE_OPTIONAL_HEADER32 a 31 membres alors que IMAGE_OPTIONAL_HEADER64 n'en a que 30.

  • Ce membre supplémentaire dans la version 32 bits est un DWORD nommé BaseOfData qui contient un RVA du début de la section de données.

  • Le type de données de certains des membres : Les 5 membres suivants de la structure Optional Header sont définis comme DWORD dans la version 32 bits et comme ULONGLONG dans la version 64 bits :

    ImageBase
    SizeOfStackReverse
    SizeOfStackCommit
    SizeOfHeapReserve
    SizeOfHeapCommit
    

Examinons la définition de ces deux structures.

Ci-dessous la version 32 bit.

typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //

    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;

    //
    // NT additional fields.
    //

    DWORD   ImageBase;
    DWORD   SectionAlignment;
    DWORD   FileAlignment;
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;
    DWORD   SizeOfHeaders;
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;
    DWORD   SizeOfStackReserve;
    DWORD   SizeOfStackCommit;
    DWORD   SizeOfHeapReserve;
    DWORD   SizeOfHeapCommit;
    DWORD   LoaderFlags;
    DWORD   NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

Et voici la 64 bit.

typedef struct _IMAGE_OPTIONAL_HEADER64 {
    WORD        Magic;
    BYTE        MajorLinkerVersion;
    BYTE        MinorLinkerVersion;
    DWORD       SizeOfCode;
    DWORD       SizeOfInitializedData;
    DWORD       SizeOfUninitializedData;
    DWORD       AddressOfEntryPoint;
    DWORD       BaseOfCode;
    ULONGLONG   ImageBase;
    DWORD       SectionAlignment;
    DWORD       FileAlignment;
    WORD        MajorOperatingSystemVersion;
    WORD        MinorOperatingSystemVersion;
    WORD        MajorImageVersion;
    WORD        MinorImageVersion;
    WORD        MajorSubsystemVersion;
    WORD        MinorSubsystemVersion;
    DWORD       Win32VersionValue;
    DWORD       SizeOfImage;
    DWORD       SizeOfHeaders;
    DWORD       CheckSum;
    WORD        Subsystem;
    WORD        DllCharacteristics;
    ULONGLONG   SizeOfStackReserve;
    ULONGLONG   SizeOfStackCommit;
    ULONGLONG   SizeOfHeapReserve;
    ULONGLONG   SizeOfHeapCommit;
    DWORD       LoaderFlags;
    DWORD       NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

Magic :

  • La documentation de Microsoft décrit ce champ comme un nombre entier qui identifie l'état de l'image, la documentation mentionne trois valeurs communes : 0x10B : Identifie l'image comme un exécutable PE32. 0x20B : Identifie l'image comme un exécutable PE32+. 0x107 : Identifie l'image comme une image ROM.

  • La valeur de ce champ est ce qui détermine si l'exécutable est 32 bits ou 64 bits, IMAGE_FILE_HEADER.Machine est ignoré par le chargeur PE de Windows.

MajorLinkerVersion et MinorLinkerVersion :

  • Les numéros de version majeure et mineure du linker.

SizeOfCode :

  • Ce champ contient la taille de la section de code (.text), ou la somme de toutes les sections de code s'il y a plusieurs sections.

SizeOfInitializedData (Taille des données initialisées) :

  • Ce champ contient la taille de la section des données initialisées (.data), ou la somme de toutes les sections de données initialisées s'il y a plusieurs sections.

SizeOfUninitializedData (Taille des données non initialisées) :

  • Ce champ contient la taille de la section de données non initialisées (.bss), ou la somme de toutes les sections de données non initialisées s'il y a plusieurs sections.

AddressOfEntryPoint (Adresse du point d'entrée) :

  • RVA du point d'entrée lorsque le fichier est chargé en mémoire. La documentation indique que pour les images de programme, cette adresse relative pointe vers l'adresse de départ et pour les pilotes de périphériques, elle pointe vers la fonction d'initialisation.
  • Pour les DLL, un point d'entrée est facultatif et, en cas d'absence de point d'entrée, le champ AddressOfEntryPoint est défini sur 0.

BaseOfCode :

  • Un RVA du début de la section de code lorsque le fichier est chargé en mémoire.

BaseOfData (PE32 uniquement) :

  • Une RVA du début de la section de données lorsque le fichier est chargé en mémoire.

ImageBase :

  • Ce champ contient l'adresse préférée du premier octet de l'image lorsqu'elle est chargée en mémoire (l'adresse de base préférée), cette valeur doit être un multiple de 64K.
  • En raison des protections de mémoire comme ASLR, et beaucoup d'autres raisons, l'adresse spécifiée par ce champ n'est presque jamais utilisée, dans ce cas le chargeur PE choisit une plage de mémoire inutilisée pour charger l'image, après avoir chargé l'image dans cette adresse le chargeur entre dans un processus appelé la relocalisation où il fixe les adresses constantes dans l'image pour fonctionner avec la nouvelle base d'image, il y a une section spéciale qui contient des informations sur les endroits qui devront être fixés si la relocalisation est nécessaire, cette section est appelée la section de relocalisation (.reloc), plus sur cela dans les prochains messages.

SectionAlignment :

  • Ce champ contient une valeur qui est utilisée pour l'alignement de la section dans la mémoire (en octets), les sections sont alignées dans les limites de la mémoire qui sont des multiples de cette valeur.
  • La documentation indique que cette valeur correspond par défaut à la taille de page de l'architecture et qu'elle ne peut être inférieure à la valeur de FileAlignment.

FileAlignment :

  • Semblable à SectionAligment, ce champ contient une valeur qui est utilisée pour l'alignement des données brutes de la section sur le disque (en octets), si la taille des données réelles dans une section est inférieure à la valeur de FileAlignment, le reste du morceau est complété par des zéros pour conserver les limites de l'alignement.

  • La documentation indique que cette valeur doit être une puissance de 2 entre 512 et 64K, et si la valeur de SectionAlignment est inférieure à la taille de page de l'architecture, les tailles de FileAlignment et SectionAlignment doivent correspondre.

    MajorOperatingSystemVersion, MinorOperatingSystemVersion, MajorImageVersion, MinorImageVersion, MajorSubsystemVersion et MinorSubsystemVersion :

  • Ces membres de la structure spécifient respectivement le numéro de version majeur du système d'exploitation requis, le numéro de version mineur du système d'exploitation requis, le numéro de version majeur de l'image, le numéro de version mineur de l'image, le numéro de version majeur du sous-système et le numéro de version mineur du sous-système.

Win32VersionValue :

  • Un champ réservé qui, selon la documentation, doit être défini sur 0.

SizeOfImage :

  • La taille du fichier image (en octets), y compris tous les en-têtes. Elle est arrondie à un multiple de SectionAlignment car cette valeur est utilisée lors du chargement de l'image en mémoire.

SizeOfHeaders :

  • La taille combinée du stub DOS, de l'en-tête PE (NT Headers) et des en-têtes de section, arrondie à un multiple de FileAlignment.

CheckSum :

  • Une somme de contrôle du fichier image, utilisée pour valider l'image au moment du chargement.

Subsystem :

  • Ce champ spécifie le sous-système Windows (le cas échéant) requis pour exécuter l'image. Une liste complète des valeurs possibles de ce champ se trouve dans la documentation officielle de Microsoft.

DLLCharacteristics :

  • Ce champ définit certaines caractéristiques du fichier image exécutable, comme s'il est compatible NX et s'il peut être déplacé au moment de l'exécution.

SizeOfStackReserve, SizeOfStackCommit, SizeOfHeapReserve et SizeOfHeapCommit :

  • Ces champs spécifient la taille de la pile à réserver, la taille de la pile à commettre, la taille de l'espace de tas local à réserver et la taille de l'espace de tas local à commettre respectivement.

LoaderFlags :

  • Un champ réservé qui, selon la documentation, doit être mis à 0.

NumberOfRvaAndSizes :

  • Taille du tableau DataDirectory.

DataDirectory :

  • Un tableau de structures IMAGE_DATA_DIRECTORY. Nous en parlerons dans le prochain article.

Examinons le contenu de "Optional Header" de putty64.exe.

image.png

  • Nous pouvons parler de certains de ces champs, le premier étant le champ Magic au début de l'en-tête, il a la valeur 0x20B ce qui signifie qu'il s'agit d'un exécutable PE32+. image.png

  • Nous pouvons voir que le point d'entrée RVA est 0x12C4 image.png

et le début de la section de code RVA est 0x1000, il suit l'alignement défini par le champ SectionAlignment qui a la valeur 0x1000. image.png

  • L'alignement du fichier est défini à 0x200, et nous pouvons le vérifier en regardant n'importe laquelle des sections, par exemple la section de données : image.png image.png

5. Data Directories, Section Headers and Sections

Data Directories

  • Le dernier membre de la structure IMAGE_OPTIONAL_HEADER était un tableau de structures IMAGE_DATA_DIRECTORY définies comme suit :
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
  • En image ça donne ceci. image.png

  • IMAGE_NUMBEROF_DIRECTORY_ENTRIES est une constante définie avec la valeur 16, ce qui signifie que ce tableau peut avoir jusqu'à 16 entrées IMAGE_DATA_DIRECTORY :

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16
  • Une structure IMAGE_DATA_DIRETORY est définie comme suit :
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
  • Il s'agit d'une structure très simple avec seulement deux membres, le premier étant une RVA pointant vers le début du répertoire de données et le second étant la taille du répertoire de données.

  • Qu'est-ce qu'un répertoire de données ? Fondamentalement, un répertoire de données est un élément de données situé dans l'une des sections du fichier PE. Les répertoires de données contiennent des informations utiles dont le chargeur a besoin, un exemple de répertoire très important est le répertoire d'importation qui contient une liste de fonctions externes importées d'autres bibliothèques.

  • Veuillez noter que tous les répertoires de données n'ont pas la même structure, l'adresse virtuelle IMAGE_DATA_DIRECTORY.VirtualAddress pointe vers le répertoire de données, mais c'est le type de ce répertoire qui détermine comment ce morceau de données va être analysé.

  • Voici une liste des Data Directories définis dans winnt.h. (Chacune de ces valeurs représente un index dans le tableau DataDirectory) :

// Directory Entries

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // Debug Directory
//      IMAGE_DIRECTORY_ENTRY_COPYRIGHT       7   // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT            12   // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM Runtime descriptor
  • Si nous examinons le contenu de IMAGE_OPTIONAL_HEADER.DataDirectory de putty64, nous pouvons voir des entrées où les deux champs sont définis sur 0 : image.png

  • Cela signifie que ce répertoire de données spécifique n'est pas utilisé (n'existe pas) dans le fichier exécutable.

Sections and Section Headers

Sections

  • Les sections sont les conteneurs des données réelles du fichier exécutable, elles occupent le reste du fichier PE après les en-têtes, précisément après les en-têtes de section.

  • Certaines sections ont des noms spéciaux qui indiquent leur but, nous allons passer en revue certaines d'entre elles, et une liste complète de ces noms peut être trouvée dans la documentation officielle de Microsoft sous la section "Special Sections".

.text : Contient le code exécutable du programme.
.data : Contient les données initialisées. .bss : Contient les données non initialisées. .rdata : Contient des données initialisées en lecture seule. .edata : Contient les tables d'exportation. .idata : Contient les tables d'importation. .reloc : Contient les informations de relocalisation des images. .rsrc : Contient les ressources utilisées par le programme, notamment les images, les icônes ou même les binaires intégrés. .tls : (Thread Local Storage), fournit un stockage pour chaque thread en cours d'exécution du programme.

image.png

Section Headers

  • Après le "Optional Header" et avant les sections viennent les "Section Headers". Ces en-têtes contiennent des informations sur les sections du fichier PE.

  • Un "Section Headers" est une structure nommée IMAGE_SECTION_HEADER définie dans winnt.h 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;

Name :

  • Premier champ de l'en-tête de section, un tableau d'octets de la taille IMAGE_SIZEOF_SHORT_NAME qui contient le nom de la section.
  • La valeur de IMAGE_SIZEOF_SHORT_NAME est de 8, ce qui signifie que le nom d'une section ne peut pas comporter plus de 8 caractères. Pour les noms plus longs, la documentation officielle mentionne une solution de contournement en remplissant ce champ avec un décalage dans la table des chaînes de caractères.
  • Cependant, les images exécutables n'utilisent pas de table des chaînes de caractères et cette limitation de 8 caractères s'applique aux images exécutables.

PhysicalAddress ou VirtualSize :

  • Une union définit plusieurs noms pour la même chose, ce champ contient la taille totale de la section lorsqu'elle est chargée en mémoire.

VirtualAddress :

  • La documentation indique que pour les images exécutables, ce champ contient l'adresse du premier octet de la section par rapport à la base de l'image lorsqu'elle est chargée en mémoire, et pour les fichiers objets, il contient l'adresse du premier octet de la section avant que la relocalisation ne soit appliquée.

SizeOfRawData :

  • Ce champ contient la taille de la section sur le disque, elle doit être un multiple de IMAGE_OPTIONAL_HEADER.FileAlignment.
  • SizeOfRawData et VirtualSize peuvent être différents, nous en expliquerons la raison plus loin.

PointerToRawData :

  • Un pointeur vers la première page de la section dans le fichier, pour les images exécutables, il doit être un multiple de IMAGE_OPTIONAL_HEADER.FileAlignment.

PointerToRelocations :

  • Un pointeur de fichier vers le début des entrées de relocalisation pour la section. Il a la valeur 0 pour les fichiers exécutables.

PointerToLineNumbers :

  • Un pointeur de fichier vers le début des entrées de numéros de ligne COFF pour la section. Il est défini à 0 car les informations de débogage COFF sont dépréciées.

NumberOfRelocations :

  • Le nombre d'entrées de relocalisation pour la section, il est défini à 0 pour les images exécutables.

NumberOfLinenumbers :

  • Le nombre d'entrées de numéro de ligne COFF pour la section, il est fixé à 0 parce que les informations de débogage COFF sont dépréciées.

Characteristics :

  • Les flags qui décrivent les caractéristiques de la section. Ces caractéristiques sont des choses comme si la section contient du code exécutable, contient des données initialisées/non initialisées, peut être partagée en mémoire.
  • Une liste complète des flags de caractéristiques de section peut être trouvée dans la documentation officielle de Microsoft.

  • SizeOfRawData et VirtualSize peuvent être différents, et ce pour de multiples raisons.

  • SizeOfRawData doit être un multiple de IMAGE_OPTIONAL_HEADER.FileAlignment, donc si la taille de la section est inférieure à cette valeur, le reste est ajouté et SizeOfRawData est arrondi au multiple le plus proche de IMAGE_OPTIONAL_HEADER.FileAlignment.

  • Cependant, lorsque la section est chargée en mémoire, elle ne suit pas cet alignement et seule la taille réelle de la section est occupée. Dans ce cas, SizeOfRawData sera supérieur à VirtualSize.

  • L'inverse peut également se produire. Si la section contient des données non initialisées, ces données ne seront pas prises en compte sur le disque, mais lorsque la section sera mappée en mémoire, la section s'agrandira pour réserver de l'espace mémoire pour le moment où les données non initialisées seront initialisées et utilisées.

  • Cela signifie que la section sur le disque occupera moins d'espace qu'elle n'en occupera en mémoire, dans ce cas VirtualSize sera supérieur à SizeOfRawData.

Voici la vue des en-têtes de section dans PE-bear : image.png

  • Nous pouvons voir les champs Raw Addr. et Virtual Addr. qui correspondent à IMAGE_SECTION_HEADER.PointerToRawData et IMAGE_SECTION_HEADER.VirtualAddress.

  • Raw Size et Virtual Size correspondent à IMAGE_SECTION_HEADER.SizeOfRawData et IMAGE_SECTION_HEADER.VirtualSize.

  • Nous pouvons voir comment ces deux champs sont utilisés pour calculer où se termine la section, à la fois sur le disque et en mémoire.

  • Par exemple, si nous prenons la section .text, elle a une adresse brute de 0x400 et une taille brute de 0xE00, si nous les additionnons, nous obtenons 0xA6A00 qui est affiché comme la fin de la section sur le disque.

  • De même, nous pouvons faire la même chose avec la taille et l'adresse virtuelles, l'adresse virtuelle est 0x1000 et la taille virtuelle est 0xA686, si nous les additionnons, nous obtenons 0xB686.

  • Le champ Characteristics marque certaines sections comme étant en lecture seule, d'autres en lecture-écriture et d'autres encore comme étant lisibles et exécutables. image.png

  • PointerToRelocations, NumberOfRelocations et NumberOfLinenumbers sont fixés à 0 comme prévu. image.png

6. PE Imports (Import Directory Table, ILT, IAT)

  • Pour ce point nous allons parler d'un aspect très important des fichiers PE, les importations dans un PE.
  • Pour comprendre comment les fichiers PE gèrent leurs importations, nous allons passer en revue certains des répertoires de données présents dans la section Import Data (.idata), l'Import Directory Table, l'Import Lookup Table (ILT) ou également appelée Import Name Table (INT) et l'Import Address Table (IAT).

Import Directory Table

  • Le "Import Directory Table" est un "Data Directory" situé au début de la section .idata.

  • Elle consiste en un tableau de structures nommé IMAGE_IMPORT_DESCRIPTOR, chacune d'entre elles correspondant à une DLL.

  • Il n'a pas de taille fixe, aussi le dernier IMAGE_IMPORT_DESCRIPTOR du tableau est mis à zéro (NULL-Padded) pour indiquer la fin du tableau du répertoire d'importation.

IMAGE_IMPORT_DESCRIPTOR est défini comme suit :

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;
        DWORD   OriginalFirstThunk;
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;
    DWORD   ForwarderChain;
    DWORD   Name;
    DWORD   FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

OriginalFirstThunk :

  • RVA de l'ILT.

TimeDateStamp :

  • Un horodateur, qui est initialement défini à 0 s'il n'est pas lié et défini à -1 s'il est lié. Dans le cas d'une importation non liée, l'horodatage est mis à jour avec l'horodatage de la DLL une fois l'image liée.

  • Dans le cas d'une importation liée, il reste fixé à -1 et l'horodatage réel de la DLL peut être trouvé dans le tableau du répertoire d'importation liée dans le IMAGE_BOUND_IMPORT_DESCRIPTOR correspondant.

ForwarderChain :

  • L'index de la première référence de la chaîne de transfert. Il s'agit d'un élément responsable du transfert de DLL. (Le transfert de DLL est lorsqu'une DLL transfère certaines de ses fonctions exportées à une autre DLL).

Name :

  • Un RVA d'une chaîne ASCII qui contient le nom de la DLL importée.

FirstThunk :

  • RVA de l'IAT.

Bound Imports

  • Une "bound import" signifie essentiellement que la table d'importation contient des adresses fixes pour les fonctions importées. Ces adresses sont calculées et écrites pendant la compilation par l'éditeur de liens.

  • L'utilisation d'importations liées est une optimisation de la vitesse, elle réduit le temps nécessaire au chargeur pour résoudre les adresses des fonctions et remplir l'IAT. Cependant, si au moment de l'exécution, les adresses liées ne correspondent pas aux adresses réelles, le chargeur devra résoudre à nouveau ces adresses et réparer l'IAT.

  • Lors de la discussion sur IMAGE_IMPORT_DESCRIPTOR.TimeDateStamp, j'ai mentionné que dans le cas d'une importation liée, l'horodatage est fixé à -1 et l'horodatage réel de la DLL peut être trouvé dans le IMAGE_BOUND_IMPORT_DESCRIPTOR correspondant dans le répertoire de données d'importation liée.

Bound Import Data Directory

  • Le "Bound Import Data Directory" est similaire à "Import Directory Table", mais comme son nom l'indique, il contient des informations sur les importations liées.

  • Il se compose d'un tableau de structures IMAGE_BOUND_IMPORT_DESCRIPTOR et se termine par un IMAGE_BOUND_IMPORT_DESCRIPTOR mis à zéro.

IMAGE_BOUND_IMPORT_DESCRIPTOR est défini comme suit :

typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
    DWORD   TimeDateStamp;
    WORD    OffsetModuleName;
    WORD    NumberOfModuleForwarderRefs;
// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
} IMAGE_BOUND_IMPORT_DESCRIPTOR,  *PIMAGE_BOUND_IMPORT_DESCRIPTOR;

TimeDateStamp :

  • L'horodatage de la DLL importée.

OffsetModuleName :

  • Un décalage vers une chaîne contenant le nom de la DLL importée. Il s'agit d'un décalage par rapport au premier IMAGE_BOUND_IMPORT_DESCRIPTOR.

NumberOfModuleForwarderRefs :

  • Le nombre de structures IMAGE_BOUND_FORWARDER_REF qui suivent immédiatement cette structure. IMAGE_BOUND_FORWARDER_REF est une structure identique à IMAGE_BOUND_IMPORT_DESCRIPTOR, la seule différence étant que le dernier membre est réservé.

Import Lookup Table (ILT)

On l'appelle parfois "Import Name Table" (INT).

Chaque DLL importée possède une Import Lookup Table. IMAGE_IMPORT_DESCRIPTOR.OriginalFirstThunk contient le RVA de l'ILT de la DLL correspondante.

L'ILT est essentiellement un tableau de noms ou de références, il indique au chargeur quelles fonctions de la DLL importée sont nécessaires.

L'ILT consiste en un tableau de nombres de 32 bits (pour PE32) ou de 64 bits pour (PE32+), le dernier est mis à zéro pour indiquer la fin de l'ILT.

Chacune de ces entrées code l'information comme suit : Bit 31/63 (bit le plus significatif) :

  • C'est ce qu'on appelle l'indicateur Ordinal/Name, il spécifie s'il faut importer la fonction par nom ou par ordinal.

Bits 15-0 :

  • Si l'indicateur Ordinal/Name est défini à 1, ces bits sont utilisés pour contenir le numéro ordinal de 16 bits qui sera utilisé pour importer la fonction, les bits 30-15/62-15 pour PE32/PE32+ doivent être définis à 0.

Bits 30-0 :

  • Si l'indicateur Ordinal/Nom est défini sur 0, ces bits sont utilisés pour contenir un RVA d'une table Hint/Name.

Hint/Name Table

  • Une table Hint/Name est une structure définie dans winnt.h comme IMAGE_IMPORT_BY_NAME :
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    CHAR   Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

Hint :

  • Un mot qui contient un nombre, ce nombre est utilisé pour rechercher la fonction, ce nombre est d'abord utilisé comme un index dans la table des pointeurs de noms d'exportation, si cette vérification initiale échoue une recherche binaire est effectuée sur la table des pointeurs de noms d'exportation de la DLL.

Nom :

  • Une chaîne à terminaison nulle qui contient le nom de la fonction à importer.

Import Address Table (IAT)

  • Sur le disque, l'IAT est identique à l'ILT, mais lors de la délimitation, lorsque le binaire est chargé en mémoire, les entrées de l'IAT sont écrasées par les adresses des fonctions qui sont importées.

Résumé

  • Pour résumer ce que nous avons vu dans la partie sur les imports PE, pour chaque DLL dont l'exécutable charge des fonctions, il y aura un IMAGE_IMPORT_DESCRIPTOR dans l'Image Directory Table. L'IMAGE_IMPORT_DESCRIPTOR contiendra le nom de la DLL, et deux champs contenant les RVA de l'ILT et de l'IAT.

  • L'ILT contient les références de toutes les fonctions qui sont importées de la DLL. L'IAT sera identique à l'ILT jusqu'à ce que l'exécutable soit chargé en mémoire, puis le chargeur remplira l'IAT avec les adresses réelles des fonctions importées.

  • Si l'importation de la DLL est une importation liée, les informations relatives à l'importation seront contenues dans des structures IMAGE_BOUND_IMPORT_DESCRIPTOR dans un répertoire de données distinct appelé répertoire de données d'importation liée.

Voici les informations d'importation contenues dans un fichier PE tel que putty64.exe. image.png

Toutes ces entrées sont des IMAGE_IMPORT_DESCRIPTORs.

Comme vous pouvez le voir, le TimeDateStamp de tous les imports est fixé à 0, ce qui signifie qu'aucun de ces imports n'est lié, ce qui est également confirmé dans la colonne Bound ? ajoutée par PE-bear.

PE Base Relocations

  • Dans ce point, nous allons parler des relocalisations de la base PE. Nous allons discuter de ce que sont les délocalisations, puis nous examinerons le tableau des délocalisations.

Relocations

  • Lorsqu'un programme est compilé, le compilateur suppose que l'exécutable sera chargé à une certaine adresse de base, cette adresse est enregistrée dans IMAGE_OPTIONAL_HEADER.ImageBase, certaines adresses sont calculées puis codées en dur dans l'exécutable en fonction de l'adresse de base.

  • Cependant, pour diverses raisons, il est peu probable que l'exécutable obtienne l'adresse de base souhaitée, il sera chargé à une autre adresse de base, ce qui rendra invalides toutes les adresses codées en dur.

  • Une liste de toutes les valeurs codées en dur qui devront être corrigées si l'image est chargée à une autre adresse de base est enregistrée dans une table spéciale appelée Relocation Table (un répertoire de données dans la section .reloc).

  • Le processus de relocalisation (effectué par le chargeur) permet de corriger ces valeurs.

Prenons un exemple, le code suivant définit une variable int et un pointeur vers cette variable :

int test = 2;
int* testPtr = &test;
  • Pendant la compilation, le compilateur suppose une adresse de base, disons qu'il suppose une adresse de base de 0x1000, il décide que le test sera situé à un décalage de 0x100 et sur cette base il donne à testPtr une valeur de 0x1100.

  • Plus tard, un utilisateur exécute le programme et l'image est chargée en mémoire. Il obtient une adresse de base de 0x2000, ce qui signifie que la valeur codée en dur de testPtr sera invalide, le chargeur corrige cette valeur en ajoutant la différence entre l'adresse de base supposée et l'adresse de base réelle, dans ce cas c'est une différence de 0x1000 (0x2000 - 0x1000), donc la nouvelle valeur de testPtr sera 0x2100 (0x1100 + 0x1000) qui est la nouvelle adresse correcte de test.

Relocation Table

  • Comme le décrit la documentation de Microsoft, la table de relocalisation de base contient des entrées pour toutes les relocalisations de base dans l'image.

  • C'est un répertoire de données situé dans la section .reloc, il est divisé en blocs, chaque bloc représente les relocalisations de base pour une page de 4K et chaque bloc doit commencer sur une frontière de 32 bits.

  • Chaque bloc commence par une structure IMAGE_BASE_RELOCATION suivie d'un nombre quelconque d'entrées de champs de décalage.

  • La structure IMAGE_BASE_RELOCATION spécifie le RVA de la page, et la taille du bloc de relocalisation.

    typedef struct _IMAGE_BASE_RELOCATION {
      DWORD   VirtualAddress;
      DWORD   SizeOfBlock;
    } IMAGE_BASE_RELOCATION;
    typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
    
  • Chaque entrée du champ de décalage est un WORD, dont les 4 premiers bits définissent le type de relocalisation (consultez la documentation Microsoft pour obtenir une liste des types de relocalisation), les 12 derniers bits stockent un décalage par rapport à la RVA spécifiée dans la structure IMAGE_BASE_RELOCATION au début du bloc de relocalisation.

  • Chaque entrée de relocalisation est traitée en ajoutant la RVA de la page à l'adresse de base de l'image, puis en ajoutant le décalage spécifié dans l'entrée de relocalisation, une adresse absolue de l'emplacement qui doit être fixé peut être obtenue. image.png