logo

Le format Portable Executable (PE)

La première chose à aborder est le format PE. La connaissance et la compréhension de ce format sont des prérequis pour développer des moteurs antivirus pour la plateforme Windows (historiquement, la grande majorité des virus dans le monde ciblent Windows).

Le format Portable Executable (PE) est un format de fichier utilisé par le système d'exploitation Windows pour stocker des fichiers exécutables, tels que les fichiers .EXE et .DLL. Il a été introduit avec la sortie de Windows NT en 1993, et est depuis devenu le format standard pour les exécutables sur les systèmes Windows.

Avant l'introduction du format PE, Windows utilisait une variété de formats différents pour les fichiers exécutables, incluant le format New Executable (NE) pour les programmes 16 bits et le format Compact Executable (CE) pour les programmes 32 bits. Ces formats avaient leur propre ensemble de règles et conventions, ce qui rendait difficile pour le système d'exploitation de charger et d'exécuter les programmes de manière fiable.

Afin de standardiser la mise en page et la structure des fichiers exécutables, Microsoft a introduit le format PE avec le lancement de Windows NT. Le format PE a été conçu pour être un format commun pour les programmes 32 bits et 64 bits.

Une des caractéristiques principales du format PE est son utilisation d'un en-tête standardisé, qui se trouve au début du fichier et contient plusieurs champs fournissant au système d'exploitation des informations importantes sur le fichier exécutable. Cet en-tête comprend les structures IMAGE_DOS_HEADER et IMAGE_NT_HEADER , qui sont divisées en deux sections principales : le IMAGE_FILE_HEADER et le IMAGE_OPTIONAL_HEADER.

La plupart des en-têtes du format PE sont déclarés dans le fichier d'en-tête WinNT.h

 

IMAGE_DOS_HEADER

La structure IMAGE_DOS_HEADER est un en-tête hérité qui est utilisé pour supporter la compatibilité arrière avec MS-DOS. Elle est utilisée pour stocker des informations sur le fichier qui sont requises par MS-DOS, telles que l'emplacement du code et des données du programme dans le fichier, et le point d'entrée du programme. Cela permettait aux programmes qui étaient écrits pour MS-DOS d'être exécutés sur Windows NT, à condition qu'ils aient été compilés en tant que fichiers PE.

typedef struct _IMAGE_DOS_HEADER
{

    WORD e_magic;
    WORD e_cblp;
    WORD e_cp;
    WORD e_crlc;
    WORD e_cparhdr;
    WORD e_minalloc;
    WORD e_maxalloc;
    WORD e_ss;
    WORD e_sp;
    WORD e_csum;
    WORD e_ip;
    WORD e_cs;
    WORD e_lfarlc;
    WORD e_ovno;
    WORD e_res[4];
    WORD e_oemid;
    WORD e_oeminfo;
    WORD e_res2[10];
    DWORD e_lfanew; // offset of IMAGE_NT_HEADER

} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

Voici les prochains domaines qui nous intéressent :

  • e_magic champ est utilisé pour identifier le fichier comme un fichier PE valide. Comme vous pouvez le voir, le champ e_magic est un entier non signé de 16 bits qui spécifie le "nombre magique" du fichier. Le nombre magique est une valeur spéciale qui identifie le fichier comme un fichier PE valide. Il est défini à la valeur 0x5A4D (hexadécimal), qui est la représentation ASCII des caractères "MZ" (IMAGE_DOS_SIGNATURE).

  • e_lfanew champ est utilisé pour spécifier l'emplacement de la structure IMAGE_NT_HEADERS , qui contient des informations sur la disposition et les caractéristiques du fichier PE. Comme vous pouvez le voir, le champ e_lfanew est un entier signé de 32 bits qui spécifie l'emplacement de la structure IMAGE_NT_HEADERS dans le fichier. Il est généralement défini sur le décalage de la structure par rapport au début du fichier.

 

 

Histoire

Au début des années 1980, Microsoft travaillait sur un nouveau système d'exploitation appelé MS-DOS, qui était conçu pour être un système d'exploitation simple et léger pour les ordinateurs personnels. Une des caractéristiques clés de MS-DOS était sa capacité à exécuter des exécutables, qui sont des programmes qui peuvent être exécutés sur un ordinateur.

Pour faciliter l'identification des fichiers exécutables, les développeurs de MS-DOS ont décidé d'utiliser un "nombre magique" spécial au début de chaque fichier exécutable. Ce nombre magique serait utilisé pour distinguer les exécutables des autres types de fichiers, tels que les fichiers de données ou les fichiers de configuration.

Mark Zbikowski, qui était développeur dans l'équipe MS-DOS, a eu l'idée d'utiliser les caractères « MZ » comme nombre magique. En code ASCII, la lettre « M » est représentée par la valeur hexadécimale 0x4D, et la lettre « Z » est représentée par la valeur hexadécimale 0x5A. Lorsque ces valeurs sont combinées, elles forment le nombre magique 0x5A4D, qui est la représentation ASCII des caractères « MZ ».

Aujourd'hui, la signature "MZ" est toujours utilisée pour identifier les fichiers PE, qui sont le principal format de fichier exécutable utilisé sur le système d'exploitation Windows. Elle est stockée dans le champ e_magic de la structure IMAGE_DOS_HEADER , qui est la première structure dans un fichier PE.

 

IMAGE_NT_HEADER

Le IMAGE_NT_HEADER est une structure de données qui a été introduite avec le système d'exploitation Windows NT, sorti en 1993. Elle a été conçue pour fournir au système d'exploitation une méthode standard de lecture et d'interprétation du contenu des fichiers exécutables (fichiers PE).

Avec la sortie de Windows NT, Microsoft a introduit le IMAGE_NT_HEADER comme moyen de standardiser la disposition et la structure des fichiers exécutables. Cela a facilité la charge et l'exécution des programmes par le système d'exploitation, car il ne devait supporter qu'un seul format.

https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_nt_headers32

https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_nt_headers64

typedef struct _IMAGE_NT_HEADERS32
{

    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;

} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;


typedef struct _IMAGE_NT_HEADERS64
{

    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;

} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

Le IMAGE_NT_HEADER est une structure qui apparaît au début de chaque fichier exécutable portable (PE) dans le système d'exploitation Windows. Il contient un nombre de champs qui fournissent au système d'exploitation des informations importantes sur le fichier exécutable, telles que sa taille, sa disposition et son objectif prévu.

La structure IMAGE_NT_HEADER est divisée en deux sections principales : le IMAGE_FILE_HEADER et le IMAGE_OPTIONAL_HEADER.
 

 

IMAGE_FILE_HEADER

Le IMAGE_FILE_HEADER contient des informations sur le fichier exécutable dans son ensemble, incluant son type de machine (par exemple, x86, x64), le nombre de sections dans le fichier, et la date et l'heure de création du fichier.

https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_file_header

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;

La structure possède les champs suivants :

  • Machine : Ce champ spécifie l'architecture cible pour laquelle le fichier a été construit. La valeur de ce champ est déterminée par le compilateur lors de la construction du fichier. Quelques valeurs courantes sont :

    • IMAGE_FILE_MACHINE_I386 : Le fichier est destiné à fonctionner sur une architecture x86, également connue sous le nom de 32 bits.

    • IMAGE_FILE_MACHINE_AMD64 : Le fichier est destiné à fonctionner sur une architecture x64, également connue sous le nom de 64 bits.

    • IMAGE_FILE_MACHINE_ARM : Le fichier est destiné à fonctionner sur une architecture ARM.

  • NumberOfSections : Ce champ spécifie le nombre de sections dans le fichier PE. Un fichier PE est divisé en plusieurs sections, chacune contenant différents types d'informations telles que le code, les données et les ressources. Ce champ est utilisé par le système d'exploitation pour déterminer combien de sections sont présentes dans le fichier.

  • TimeDateStamp : Ce champ contient la date et l'heure de la construction du fichier. La date et l'heure sont stockées sous forme d'une valeur à 4 octets représentant le nombre de secondes écoulées depuis le 1er janvier 1970, 00:00:00 UTC. Ce champ peut être utilisé pour déterminer quand le fichier a été construit pour la dernière fois, ce qui peut être utile pour le débogage ou la gestion des versions.

  • PointerToSymbolTable : Ce champ spécifie le décalage du fichier de la table des symboles COFF (Common Object File Format), si présente. La table des symboles COFF contient des informations sur les symboles utilisés dans le fichier, tels que les noms de fonctions, les noms de variables et les numéros de ligne. Ce champ est uniquement utilisé à des fins de débogage et n'est généralement pas présent dans les builds de sortie.

  • NumberOfSymbols : Ce champ spécifie le nombre de symboles dans la table des symboles COFF, si présente. Ce champ est utilisé conjointement avec PointerToSymbolTable pour localiser la table des symboles COFF dans le fichier.

  • SizeOfOptionalHeader : Ce champ spécifie la taille de l'en-tête optionnel, qui contient des informations supplémentaires sur le fichier. L'en-tête optionnel comprend généralement des informations sur le point d'entrée du fichier, les bibliothèques importées et la taille de la pile et du tas.

  • Characteristics : Ce champ spécifie divers attributs du fichier. Quelques valeurs courantes sont :

    • IMAGE_FILE_EXECUTABLE_IMAGE : Le fichier est un fichier exécutable.

    • IMAGE_FILE_DLL : Le fichier est une bibliothèque de liens dynamiques (DLL).

    • IMAGE_FILE_32BIT_MACHINE : Le fichier est un fichier 32 bits.

    • IMAGE_FILE_DEBUG_STRIPPED : Le fichier a été dépouillé des informations de débogage.

Ces champs fournissent des informations importantes sur le fichier qui sont utilisées par le système d'exploitation lors du chargement du fichier en mémoire et de son exécution. En comprenant les champs de la structure IMAGE_FILE_HEADER, vous pouvez obtenir une compréhension plus approfondie de la manière dont les fichiers PE sont structurés et comment le système d'exploitation les utilise.

La plupart des valeurs possibles pour chaque champ sont déclarées dans le fichier d'en-tête WinNT.h

 

IMAGE_OPTIONAL_HEADER

La structure IMAGE_FILE_HEADER est suivie de l'en-tête optionnel, qui est décrit par la structure IMAGE_OPTIONAL_HEADER. L'en-tête optionnel contient des informations supplémentaires sur l'image, telles que l'adresse du point d'entrée, la taille de l'image, et l'adresse du répertoire d'importation.

https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header32

https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header64

typedef struct _IMAGE_OPTIONAL_HEADER32
{

    WORD Magic;
    BYTE MajorLinkerVersion;
    BYTE MinorLinkerVersion;
    DWORD SizeOfCode;
    DWORD SizeOfInitializedData;
    DWORD SizeOfUninitializedData;
    DWORD AddressOfEntryPoint;
    DWORD BaseOfCode;
    DWORD BaseOfData;
    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;


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;

Voici une description détaillée de chaque champ dans la structure IMAGE_OPTIONAL_HEADER :

  • Magic : Ce champ spécifie le type d'en-tête optionnel présent dans le fichier PE. La valeur la plus courante est IMAGE_NT_OPTIONAL_HDR32_MAGIC pour un fichier 32 bits ou IMAGE_NT_OPTIONAL_HDR64_MAGIC pour un fichier 64 bits.

  • MajorLinkerVersion et MinorLinkerVersion : Ces champs spécifient la version du lieur utilisée pour construire le fichier. Le lieur est un outil utilisé pour combiner des fichiers objets et des bibliothèques en un seul fichier exécutable.

  • SizeOfCode : Ce champ spécifie la taille de la section de code dans le fichier. La section de code contient le code machine pour le fichier exécutable.

  • SizeOfInitializedData : Ce champ spécifie la taille de la section des données initialisées dans le fichier. La section des données initialisées contient des données qui sont initialisées à l'exécution, telles que les variables globales.

  • SizeOfUninitializedData : Ce champ spécifie la taille de la section des données non initialisées dans le fichier. La section des données non initialisées contient des données qui ne sont pas initialisées à l'exécution, comme la section bss.

  • AddressOfEntryPoint : Ce champ spécifie l'adresse virtuelle du point d'entrée du fichier. Le point d'entrée est l'adresse de début du programme et est la première instruction exécutée lorsque le fichier est chargé en mémoire.

  • BaseOfCode : Ce champ spécifie l'adresse virtuelle du début de la section de code.

  • ImageBase : Ce champ spécifie l'adresse virtuelle préférée à laquelle le fichier devrait être chargé en mémoire. Cette adresse est utilisée comme une adresse de base pour toutes les adresses virtuelles à l'intérieur du fichier.

  • SectionAlignment : Ce champ spécifie l'alignement des sections à l'intérieur du fichier. Les sections dans le fichier sont typiquement alignées sur des multiples de cette valeur pour améliorer la performance.

  • FileAlignment : Ce champ spécifie l'alignement des sections à l'intérieur du fichier sur le disque. Les sections dans le fichier sont typiquement alignées sur des multiples de cette valeur pour améliorer la performance du disque.

  • MajorOperatingSystemVersion et MinorOperatingSystemVersion : Ces champs spécifient la version minimale requise du système d'exploitation nécessaire pour exécuter le fichier.

  • MajorImageVersion et MinorImageVersion : Ces champs spécifient la version de l'image. La version de l'image est utilisée pour identifier la version du fichier à des fins de gestion de version.

  • MajorSubsystemVersion et MinorSubsystemVersion : Ces champs spécifient la version du sous-système nécessaire pour exécuter le fichier. Le sous-système est l'environnement dans lequel le fichier s'exécute, comme la console Windows ou l'interface graphique Windows.

  • Win32VersionValue : Ce champ est réservé et généralement défini à 0.

  • SizeOfImage : Ce champ spécifie la taille de l'image, en octets, lorsqu'elle est chargée en mémoire.

  • SizeOfHeaders : Ce champ spécifie la taille des en-têtes, en octets. Les en-têtes incluent le IMAGE_FILE_HEADER et le IMAGE_OPTIONAL_HEADER.

  • CheckSum : Ce champ est utilisé pour vérifier l'intégrité du fichier. La somme de contrôle est calculée en additionnant le contenu du fichier et en stockant le résultat dans ce champ. La somme de contrôle est utilisée pour détecter les modifications du fichier qui peuvent survenir en raison de manipulations ou de corruption.

  • Subsystem : Ce champ spécifie le sous-système requis pour exécuter le fichier. Les valeurs possibles incluent IMAGE_SUBSYSTEM_NATIVE, IMAGE_SUBSYSTEM_WINDOWS_GUI, IMAGE_SUBSYSTEM_WINDOWS_CUI, IMAGE_SUBSYSTEM_OS2_CUI, etc.

  • DllCharacteristics : Ce champ spécifie les caractéristiques du fichier, comme le fait qu'il soit une bibliothèque de liens dynamiques (DLL) ou qu'il puisse être relocalisé au moment du chargement. Les valeurs possibles incluent IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE, IMAGE_DLLCHARACTERISTICS_NX_COMPAT, etc.

  • SizeOfStackReserve : Ce champ spécifie la taille de la pile, en octets, qui est réservée pour le programme. La pile est utilisée pour stocker des données temporaires, telles que des informations d'appel de fonction.

  • SizeOfStackCommit : Ce champ spécifie la taille de la pile, en octets, qui est engagée pour le programme. La pile engagée est la partie de la pile qui est réellement réservée en mémoire.

  • SizeOfHeapReserve : Ce champ spécifie la taille du tas, en octets, qui est réservée pour le programme. Le tas est utilisé pour allouer de la mémoire de manière dynamique en cours d'exécution.

  • SizeOfHeapCommit : Ce champ spécifie la taille du tas, en octets, qui est engagée pour le programme. Le tas engagé est la partie du tas qui est réellement réservée en mémoire.

  • LoaderFlags : Ce champ est réservé et généralement défini à 0.

  • NumberOfRvaAndSizes : Ce champ spécifie le nombre d'entrées de répertoire de données dans le IMAGE_OPTIONAL_HEADER. Les répertoires de données contiennent des informations sur les importations, exportations, ressources, etc. dans le fichier.

  • DataDirectory : Ce champ est un tableau de structures IMAGE_DATA_DIRECTORY qui spécifient l'emplacement et la taille des répertoires de données dans le fichier

 

IMAGE_SECTION_HEADER

Une section, dans le contexte d'un fichier PE (Portable Executable), est un bloc contigu de mémoire dans le fichier qui contient un type spécifique de données ou de code. Dans un fichier PE, les sections sont utilisées pour organiser et stocker différentes parties du fichier, telles que le code, les données, les ressources, etc.

Chaque section dans un fichier PE possède un nom unique et est décrite par une structure IMAGE_SECTION_HEADER, qui contient des informations sur la section telles que sa taille, son emplacement, ses caractéristiques, etc. Voici les champs de IMAGE_SECTION_HEADER :

Un IMAGE_SECTION_HEADER est une structure de données utilisée dans le format de fichier Portable Executable (PE), qui est utilisé sur le système d'exploitation Windows pour définir la disposition d'un fichier en mémoire. Le format de fichier PE est utilisé pour les fichiers exécutables, les DLLs, et d'autres types de fichiers qui sont chargés en mémoire par le système d'exploitation Windows. Chaque en-tête de section décrit un bloc contigu de données dans le fichier, et inclut des informations telles que le nom de la section, l'adresse mémoire virtuelle à laquelle la section doit être chargée, et la taille de la section. Les en-têtes de section peuvent être utilisés pour localiser et accéder à des parties spécifiques du fichier, telles que les sections de code ou de données.

La structure IMAGE_SECTION_HEADER est définie dans le SDK de la plateforme Windows, et peut être trouvée dans le fichier d'en-tête winnt.h. Voici un exemple de comment la structure est définie en C++ :

#pragma pack(push, 1)

typedef struct _IMAGE_SECTION_HEADER
{

    BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 8

    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;

#pragma pack(pop)

 

 

 

Comme vous pouvez le voir, la structure est définie comme une structure C++ et elle contient des champs pour le nom de la section, la taille virtuelle, l'adresse virtuelle, la taille des données brutes, et le pointeur vers les données brutes, les relocalisations, les numéros de ligne, et le nombre de relocalisations et de numéros de ligne. De plus, le champ Caractéristiques contient des drapeaux qui décrivent les caractéristiques de la section, telles que si elle est exécutable, lisible ou inscriptible.

  • Name : Ce tableau de 8 octets est utilisé pour spécifier le nom de la section. Le nom peut être n'importe quelle chaîne terminée par un caractère nul, mais il est généralement utilisé pour donner des noms significatifs à différentes parties du fichier, tels que ".text" pour le code exécutable, ".data" pour les données initialisées, ".rdata" pour les données en lecture seule, et ".bss" pour les données non initialisées. Le nom de la section est utilisé par le système d'exploitation pour localiser la section dans le fichier, et est également utilisé par les débogueurs et autres outils pour identifier la section et son contenu.

  • VirtualSize : Ce champ spécifie la taille de la section en mémoire, en octets. Cette valeur représente la quantité de mémoire que la section occupera en mémoire lorsque le fichier sera chargé en mémoire. La taille virtuelle de la section est utilisée par le système d'exploitation pour déterminer la quantité de mémoire qui doit être allouée pour la section lorsque le fichier est chargé en mémoire.

  • VirtualAddress : Ce champ spécifie l'adresse de départ de la section en mémoire, en octets. Cette valeur est l'adresse à laquelle la section sera chargée en mémoire, et est utilisée par le système d'exploitation pour déterminer l'emplacement en mémoire où la section sera chargée. L'adresse virtuelle de la section est également utilisée par le système d'exploitation pour résoudre les adresses au sein de la section, afin qu'elles puissent être correctement traduites en adresses mémoire lorsque le fichier est chargé en mémoire.

  • SizeOfRawData : Ce champ spécifie la taille de la section dans le fichier, en octets. Cette valeur représente la quantité d'espace dans le fichier que la section occupera, et est utilisée par le système d'exploitation pour déterminer la taille de la section dans le fichier. La taille des données brutes d'une section est utilisée par le système d'exploitation pour localiser la section dans le fichier, et pour déterminer la taille de la section lorsqu'elle est chargée en mémoire.

  • PointerToRawData : Ce champ spécifie le décalage de la section dans le fichier, en octets. Cette valeur représente l'emplacement de la section dans le fichier, et sert à déterminer où peuvent être trouvées les données pour la section. Le pointeur vers les données brutes d'une section est utilisé par le système d'exploitation pour localiser la section dans le fichier, et pour déterminer l'emplacement de la section lorsqu'elle est chargée en mémoire.

  • PointerToRelocations : Ce champ spécifie le décalage des informations de relocalisation pour la section, en octets. Les informations de relocalisation sont utilisées pour corriger les adresses au sein de la section, afin qu'elles puissent être correctement résolues lorsque le fichier est chargé en mémoire. Le pointeur vers les relocalisations d'une section est utilisé par le système d'exploitation pour localiser les informations de relocalisation pour la section, et pour déterminer comment corriger les adresses au sein de la section lorsque le fichier est chargé en mémoire.

  • PointerToLinenumbers : Ce champ spécifie le décalage des informations de numéro de ligne pour la section, en octets. Les informations de numéro de ligne sont utilisées à des fins de débogage et fournissent des informations sur le code source qui a généré la section. Le pointeur vers les numéros de ligne d'une section est utilisé par les débogueurs et autres outils pour identifier le code source qui a généré la section, et pour fournir des informations plus détaillées sur le contenu de la section.

  • NumberOfRelocations : Ce champ spécifie le nombre d'entrées de relocalisation pour la section. Une entrée de relocalisation est un enregistrement qui décrit comment corriger une adresse au sein de la section, afin qu'elle puisse être correctement résolue lorsque le fichier est chargé en mémoire. Le nombre de relocalisations d'une section est utilisé par le système d'exploitation pour déterminer la taille des informations de relocalisation pour la section, et pour savoir combien d'entrées de relocalisation doivent être traitées lorsque le fichier est chargé en mémoire.

  • NumberOfLinenumbers : Ce champ spécifie le nombre d'entrées de numéro de ligne pour la section. Une entrée de numéro de ligne est un enregistrement qui fournit des informations sur le code source qui a généré la section, et est utilisée à des fins de débogage. Le nombre de numéros de ligne d'une section est utilisé par les débogueurs et autres outils pour déterminer la taille des informations de numéro de ligne pour la section, et pour savoir combien d'entrées de numéro de ligne doivent être traitées pour obtenir des informations sur le code source qui a généré la section.

  • Characteristics: Ce champ est un ensemble de drapeaux qui spécifient les attributs de la section. Certains des drapeaux courants utilisés pour les sections sont : IMAGE_SCN_CNT_CODE pour indiquer que la section contient du code exécutable, IMAGE_SCN_CNT_INITIALIZED_DATA pour indiquer que la section contient des données initialisées, IMAGE_SCN_CNT_UNINITIALIZED_DATA pour indiquer que la section contient des données non initialisées, IMAGE_SCN_MEM_EXECUTE pour indiquer que la section peut être exécutée, IMAGE_SCN_MEM_READ pour indiquer que la section peut être lue, et IMAGE_SCN_MEM_WRITE pour indiquer que la section peut être écrite. Ces drapeaux sont utilisés par le système d'exploitation pour déterminer les propriétés de la section, et pour savoir comment gérer la section lorsque le fichier est chargé en mémoire.

Ces champs sont utilisés par le système d'exploitation et d'autres programmes pour gérer la disposition de la mémoire du fichier, et pour localiser et accéder à des parties spécifiques du fichier, telles que les sections de code ou de données.

IMPORTANT : Dans le contexte de la structure IMAGE_NT_HEADER, qui est utilisée dans le format de fichier exécutable portable (PE), les champs VirtualAddress et PhysicalAddress se réfèrent à des choses différentes.

Le champ VirtualAddress est utilisé pour spécifier l'adresse virtuelle à laquelle la section contenant la structure IMAGE_NT_HEADER est chargée en mémoire lors de l'exécution. Cette adresse est relative à l'adresse de base du processus et est utilisée par le programme pour accéder aux données de la section.

Le champ PhysicalAddress est utilisé pour spécifier le décalage de fichier de la section contenant la structure IMAGE_NT_HEADER dans le fichier PE. Il est utilisé par le système d'exploitation pour localiser les données de la section dans le fichier lorsqu'il est chargé en mémoire.

Tous les champs d'en-tête et les décalages pour IMAGE_NT_HEADER sont définis pour la mémoire et fonctionnent sur des adresses virtuelles. Si vous devez décaler un champ sur le disque, vous devez convertir l'adresse virtuelle en adresse physique en utilisant la fonction rva2offset dans le code ci-dessous.

En résumé, VirtualAddress est utilisé par le programme pour accéder à la section dans la mémoire et PhysicalAddress est utilisé par le système d'exploitation pour localiser la section dans le fichier.


 

IMPORTATION

Lorsqu'un programme est compilé, le compilateur génère des fichiers objet qui contiennent le code machine des fonctions du programme. Cependant, les fichiers objet peuvent ne pas contenir toutes les informations nécessaires au fonctionnement du programme. Par exemple, les fichiers objet peuvent contenir des appels à des fonctions qui ne sont pas définies dans le programme mais qui sont fournies par des bibliothèques externes.

C'est ici que la table d'importation intervient. La table d'importation répertorie les dépendances externes du programme et les fonctions que le programme doit importer de ces dépendances. Le lieur dynamique utilise ces informations au moment de l'exécution pour résoudre les adresses des fonctions importées et les lier au programme.

Par exemple, considérez un programme qui utilise les fonctions du système d'exploitation Windows. Le programme peut contenir des appels à la fonction MessageBox de la bibliothèque user32.dll, qui affiche une boîte de dialogue sur l'écran. Pour résoudre l'adresse de la fonction MessageBox, le programme doit inclure une importation pour user32.dll dans sa table d'importation.

De même, si un programme doit utiliser des fonctions issues d'une bibliothèque tierce, il doit inclure un import pour cette bibliothèque dans sa table d'importation. Par exemple, un programme qui utilise les fonctions de la bibliothèque OpenSSL inclurait un import pour la bibliothèque libssl.dll dans sa table d'importation.

 

 

IMAGE_IMPORT_DIRECTORY

Le IMAGE_IMPORT_DIRECTORY est une structure de données utilisée par le système d'exploitation Windows pour importer des fonctions et des données à partir de bibliothèques de liens dynamiques (DLL) dans un fichier exécutable portable (PE). Il fait partie du IMAGE_DATA_DIRECTORY, qui est une table de structures de données stockée dans l'IMAGE_OPTIONAL_HEADER d'un fichier PE.

Le IMAGE_IMPORT_DIRECTORY est utilisé par le chargeur Windows pour résoudre les fonctions importées et les données utilisées par le fichier PE. Il réalise cela en mappant les adresses des fonctions importées et des données aux adresses des fonctions et données correspondantes dans les DLLs. Cela permet au fichier PE d'utiliser les fonctions et les données des DLLs comme si elles faisaient partie intégrante du fichier PE lui-même.

Le IMAGE_IMPORT_DIRECTORY est composé d'une série de structures IMAGE_IMPORT_DESCRIPTOR , chacune décrivant un DLL unique importé par le fichier PE. Chaque structure IMAGE_IMPORT_DESCRIPTOR contient les champs suivants :

  • OriginalFirstThunk : un pointeur vers une table de fonctions importées.

  • TimeDateStamp : la date et l'heure de la dernière mise à jour du DLL.

  • ForwarderChain : une chaîne de fonctions importées transférées.

  • Name : le nom du DLL sous forme de chaîne terminée par un caractère nul.

  • FirstThunk : un pointeur vers une table de fonctions importées qui sont liées au DLL.

typedef struct _IMAGE_IMPORT_DESCRIPTOR
{

    union {
        DWORD Characteristics; // 0 for terminating null import descriptor
        DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    } DUMMYUNIONNAME;

    DWORD TimeDateStamp; // 0 if not bound,
                         // -1 if bound, and real date	ime stamp
                         // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                         // O.W. date/time stamp of DLL bound to (Old BIND)
    
    DWORD ForwarderChain; // -1 if no forwarders
    DWORD Name;
    DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)

} IMAGE_IMPORT_DESCRIPTOR;

OriginalFirstThunk tableau (ou FirstThunk si OriginalFirstThunk est 0)

 

Chaînes pointées par la table des décalages (table OriginalFirstThunk ou FirstThunk si OriginalFirstThunk est à 0)

 

 

COMMENT ÇA MARCHE ?

Le mécanisme d'importation mis en place par Microsoft est compact et magnifique !

Les adresses de toutes les fonctions des bibliothèques tierces (y compris celles du système Windows) utilisées par l'application sont stockées dans une table spéciale - la table des importations. Cette table est remplie lors du chargement du module (nous parlerons plus tard des autres mécanismes de remplissage des importations).

De plus, chaque fois qu'une fonction est appelée depuis une bibliothèque tierce, le compilateur génère généralement le code suivant :

call dword ptr [__cell_with_address_of_function] // for x86 architecture
call qword ptr [__cell_with_address_of_function] // for x64 architecture

 

Ainsi, pour pouvoir appeler une fonction d'une bibliothèque, le chargeur système n'a besoin d'écrire l'adresse de cette fonction qu'une seule fois à un endroit dans l'image.

 

ANALYSEUR C++

Et maintenant, nous allons écrire le parseur le plus simple (compatible avec x86 et x64) de la table dimportation de fichiers exécutables!

#include "stdafx.h"

/*
 *
 *  Copyright (C) 2022, SToFU Systems S.L.
 *  All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 * 
 */

namespace ntpe
{
    static constexpr uint64_t g_kRvaError = -1;

    // These types is defined in NTPEParser.h
    // typedef std::map< std::string, std::set< std::string >> IMPORT_LIST;
    // typedef std::vector< IMAGE_SECTION_HEADER > SECTIONS_LIST;

    //**********************************************************************************
    // FUNCTION: alignUp(DWORD value, DWORD align)
    // 
    // ARGS:
    // DWORD value - value to align.
    // DWORD align - alignment.
    // 
    // DESCRIPTION: 
    // Aligns argument value with the given alignment.
    // 
    // Documentation links:
    // Alignment: https://learn.microsoft.com/en-us/cpp/cpp/alignment-cpp-declarations?view=msvc-170
    // 
    // RETURN VALUE: 
    // DWORD aligned value.
    // 
    //**********************************************************************************
    DWORD alignUp(DWORD value, DWORD align)
    {
        DWORD mod = value % align;
        return value + (mod ? (align - mod) : 0);
    };

    //**********************************************************************************
    // FUNCTION: rva2offset(IMAGE_NTPE_DATA& ntpe, DWORD rva)
    // 
    // ARGS:
    // IMAGE_NTPE_DATA& ntpe - data from PE file.
    // DWORD rva - relative virtual address.
    // 
    // DESCRIPTION: 
    // Parse RVA (relative virtual address) to offset.
    // 
    // RETURN VALUE: 
    // int64_t offset. 
    // g_kRvaError (-1) in case of error.
    // 
    //**********************************************************************************
    int64_t rva2offset(IMAGE_NTPE_DATA& ntpe, DWORD rva)
    {
        /* retrieve first section */
        try
        {
            /* if rva is inside MZ header */
            PIMAGE_SECTION_HEADER sec = ntpe.sectionDirectories;
            if (!ntpe.fileHeader->NumberOfSections || rva < sec->VirtualAddress)
                return rva;

            /* walk on sections */
            for (uint32_t sectionIndex = 0; sectionIndex < ntpe.fileHeader->NumberOfSections; sectionIndex++, sec++)
            {
                /* count section end and allign it after each iteration */
                DWORD secEnd = ntpe::alignUp(sec->Misc.VirtualSize, ntpe.SecAlign) + sec->VirtualAddress;
                if (sec->VirtualAddress <= rva && secEnd > rva)
                    return rva - sec->VirtualAddress + sec->PointerToRawData;
            };
        }
        catch (std::exception&)
        {
        }

        return g_kRvaError;
    };


    //**********************************************************************************
    // FUNCTION: getNTPEData(char* fileMapBase)
    // 
    // ARGS:
    // char* fileMapBase - the starting address of the mapped file.
    // 
    // DESCRIPTION: 
    // Parses following data from mapped PE file.
    //  
    // Documentation links:
    // PE format structure: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
    //
    // RETURN VALUE: 
    // std::optional< IMAGE_NTPE_DATA >. 
    // std::nullopt in case of error.
    // 
    //**********************************************************************************
    #define initNTPE(HeaderType, cellSize) \
    { \
    char* ntstdHeader       = (char*)fileHeader + sizeof(IMAGE_FILE_HEADER); \
    HeaderType* optHeader   = (HeaderType*)ntstdHeader; \
    data.sectionDirectories = (PIMAGE_SECTION_HEADER)(ntstdHeader + sizeof(HeaderType)); \
    data.SecAlign           = optHeader->SectionAlignment; \
    data.dataDirectories    = optHeader->DataDirectory; \
    data.CellSize           = cellSize;	\
    }
    std::optional< IMAGE_NTPE_DATA > getNTPEData(char* fileMapBase, uint64_t fileSize)
    {
        try
        {
            /* PIMAGE_DOS_HEADER from starting address of the mapped view*/
            PIMAGE_DOS_HEADER dosHeader = (IMAGE_DOS_HEADER*)fileMapBase;

            /* return std::nullopt in case of no IMAGE_DOS_SIGNATUR signature */
            if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE)
                return std::nullopt;

            /* PE signature adress from base address + offset of the PE header relative to the beginning of the file */
            PDWORD peSignature = (PDWORD)(fileMapBase + dosHeader->e_lfanew);
            if ((char*)peSignature <= fileMapBase || (char*)peSignature - fileMapBase >= fileSize)
                return std::nullopt;

            /* return std::nullopt in case of no PE signature */
            if (*peSignature != IMAGE_NT_SIGNATURE)
                return std::nullopt;

            /* file header address from PE signature address */
            PIMAGE_FILE_HEADER fileHeader = (PIMAGE_FILE_HEADER)(peSignature + 1);
            if (fileHeader->Machine != IMAGE_FILE_MACHINE_I386 &&
                fileHeader->Machine != IMAGE_FILE_MACHINE_AMD64)
                return std::nullopt;

            /* result IMAGE_NTPE_DATA structure with info from PE file */
            IMAGE_NTPE_DATA data = {};

            /* base address and File header address assignment */
            data.fileBase = fileMapBase;
            data.fileHeader = fileHeader;

            /* addresses of PIMAGE_SECTION_HEADER, PIMAGE_DATA_DIRECTORIES, SectionAlignment, CellSize depending on processor architecture */
            switch (fileHeader->Machine)
            {
            case IMAGE_FILE_MACHINE_I386:
                initNTPE(IMAGE_OPTIONAL_HEADER32, 4);
                return data;

            case IMAGE_FILE_MACHINE_AMD64:
                initNTPE(IMAGE_OPTIONAL_HEADER64, 8);
                return data;
            }
        }
        catch (std::exception&)
        {

        }
        return std::nullopt;
    }


    //**********************************************************************************
    // FUNCTION: getImportList(IMAGE_NTPE_DATA& ntpe)
    // 
    // ARGS:
    // IMAGE_NTPE_DATA& ntpe - data from PE file.
    // 
    // DESCRIPTION: 
    // Retrieves IMPORT_LIST(std::map< std::string, std::set< std::string >>) with all loaded into PE libraries names and imported functions.
    // Map key: loaded dll's names. 
    // Map value: set of imported functions names.
    //
    // Documentation links:
    // Import Directory Table: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#import-directory-table
    //
    // RETURN VALUE: 
    // std::optional< IMPORT_LIST >. 
    // std::nullopt in case of error.
    // 
    //**********************************************************************************
    std::optional< IMPORT_LIST > getImportList(IMAGE_NTPE_DATA& ntpe)
    {
        try
        {
            /* if no imaage import directory in file returns std::nullopt */
            if (ntpe.dataDirectories[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress == 0)
                return std::nullopt;

            IMPORT_LIST result;

            /* import table offset */
            DWORD impOffset = rva2offset(ntpe, ntpe.dataDirectories[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

            /* imoprt table descriptor from import table offset + file base adress */
            PIMAGE_IMPORT_DESCRIPTOR impTable = (PIMAGE_IMPORT_DESCRIPTOR)(impOffset + ntpe.fileBase);

            /* while names in import table */
            while (impTable->Name != 0)
            {
                /* pointer to DLL name from offset of current section name + file base adress */
                std::string modname = rva2offset(ntpe, impTable->Name) + ntpe.fileBase;
                std::transform(modname.begin(), modname.end(), modname.begin(), ::toupper);

                /* start adress of names in look up table from import table name RVA */
                char* cell = ntpe.fileBase + ((impTable->OriginalFirstThunk) ? rva2offset(ntpe, impTable->OriginalFirstThunk) : rva2offset(ntpe, impTable->FirstThunk));

                /* while names in look up table */
                for (;; cell += ntpe.CellSize)
                {
                    int64_t rva = 0;

                    /* break if rva = 0 */
                    memcpy(&rva, cell, ntpe.CellSize);
                    if (!rva)
                        break;

                    /* if rva > 0 function was imported by name. if rva < 0 function was imported by ordinall */
                    if (rva > 0)
                        result[modname].emplace(ntpe.fileBase + rva2offset(ntpe, rva) + 2);
                    else
                        result[modname].emplace(std::string("#ord: ") + std::to_string(rva & 0xFFFF));
                };
                impTable++;
            };
            return result;
        }
        catch (std::exception&)
        {
            return std::nullopt;
        }
    };

    //**********************************************************************************
    // FUNCTION: getImportList(IMAGE_NTPE_DATA& ntpe)
    // 
    // ARGS:
    // std::wstring_view filePath - path to file.
    // 
    // DESCRIPTION: 
    // Retrieves IMPORT_LIST(std::map< std::string, std::set< std::string >>) with all loaded into PE libraries names and imported functions bu path.
    // Map key: loaded dll's names. 
    // Map value: set of imported functions names.
    //
    // Documentation links:
    // Import Directory Table: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#import-directory-table
    //
    // RETURN VALUE: 
    // std::optional< IMPORT_LIST >. 
    // std::nullopt in case of error.
    // 
    //**********************************************************************************
    std::optional< IMPORT_LIST > getImportList(std::wstring_view filePath)
    {
        std::vector< char > buffer;
        /* obtain base address of mapped file from tools::readFile function */
        bool result = tools::readFile(filePath, buffer);
        /* return nullopt if readFile failes or obtained buffer is empty */
        if (!result || buffer.empty())
            return std::nullopt;
        /* get IMAGE_NTPE_DATA from base address of mapped file */
        std::optional< IMAGE_NTPE_DATA > ntpe = getNTPEData(buffer.data(), buffer.size());
        if (!ntpe)
            return std::nullopt;
        /* return result of overloaded getImportList function with IMAGE_NTPE_DATA as argument */
        return getImportList(*ntpe);
    }


    //**********************************************************************************
    // FUNCTION: getSectionsList(IMAGE_NTPE_DATA& ntpe)
    // 
    // ARGS:
    // IMAGE_NTPE_DATA& ntpe - data from PE file.
    // 
    // DESCRIPTION: 
    // Retrieves SECTIONS_LIST from IMAGE_NTPE_DATA.
    // SECTIONS_LIST - vector of sections headers from portable executable file.
    // Sections names exmaple: .data, .code, .src
    //  
    // Documentation links:
    // IMAGE_SECTION_HEADER: https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_section_header
    // Section Table (Section Headers): https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#section-table-section-headers
    // 
    // RETURN VALUE: 
    // std::optional< SECTIONS_LIST >. 
    // std::nullopt in case of error.
    // 
    //**********************************************************************************
    std::optional< SECTIONS_LIST > getSectionsList(IMAGE_NTPE_DATA& ntpe)
    {
        try
        {
            /* result vector of section directories */
            SECTIONS_LIST result;

            /* iterations through all image section headers poiners in IMAGE_NTPE_DATA structure */
            for (uint64_t sectionIndex = 0; sectionIndex < ntpe.fileHeader->NumberOfSections; sectionIndex++)
            {
                /* pushing IMAGE_SECTION_HEADER from iamge section headers */
                result.push_back(ntpe.sectionDirectories[sectionIndex]);
            }
            return result;
        }
        catch (std::exception&)
        {
        }
        /* returns nullopt in case of error */
        return std::nullopt;
    }
}

Vous pouvez trouver le code de l'ensemble du projet sur notre github :

https://github.com/SToFU-Systems/DSAVE

 

Liste des outils utilisés

  1. PE Tools: https://github.com/petoolse/petools Il s'agit d'un outil open-source pour manipuler les champs d'en-tête PE. Prend en charge les fichiers x86 et x64.
  2. WinDbg: https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools Débogueur système de Microsoft. Indispensable dans le travail d'un programmeur système pour Windows OS.
  3. x64Dbg: https://x64dbg.com Débogueur open-source simple et léger pour Windows x64/x86.
  4. WinHex: http://www.winhex.com/winhex/hex-editor.html WinHex est un éditeur hexadécimal universel, particulièrement utile dans le domaine de la criminalistique informatique, de la récupération de données et de l'édition de données de bas niveau.

 

QUELLES SONT LES ÉTAPES SUIVANTES ?

Nous apprécions votre soutien et attendons avec impatience votre engagement continu dans notre communauté

Dans le prochain article, nous écrirons ensemble avec vous le module de hachage flou et aborderons la question des listes noires et blanches, ainsi que l'analyseur de table d'importation le plus simple.

Toutes questions destinées aux auteurs de l'article peuvent être envoyées par e-mail à : articles@stofu.io

 

Merci de votre attention et passez une bonne journée !