Portable Executable File Format

Un article de Wikipédia, l'encyclopédie libre.
Aller à : Navigation, rechercher
Portable Executable
Visualisation du début d'un fichier PE avec WinHex

Extension .cpl, .exe, .dll, .ocx, .sys, .scr, .drv
Type de format Binaire, Exécutable, Code Objet, DLL
Extension du MZ-DOS Executable
COFF

Le format PE (Portable Executable), est un format de fichier natif spécifique à Windows, qui structure les fichiers exécutable du système d'exploitation. Développé par Microsoft, et dérivé de Unix COFF (Common Object File Format, voir (en) COFF), ce format structure les binaires à extension *.exe (Logiciel), *.ocx (Object Linking and Embbeding), *.dll (Dynamic Link Library) et *.cpl (Panneau de Configuration) sur toutes les plateformes Win32 et supérieur.


D'un point de vue sécuritaire, la connaissance de ce format est en soit une base qu'il est nécessaire de posséder lorsque l'on souhaite se lancer dans le Reverse Engineering. Divers outils, présentés plus bas, se basent sur le fonctionnement de la structure PE pour travailler sur les exécutables.

Sommaire

[modifier] Historique

Microsoft migra vers le format PE avec l'introduction de Windows NT 3.1. Toutes les versions suivantes de Windows, incluant Windows 95/98/ME, supportent ce format. Auparavant les fichiers «exécutables» étaient au format NE — New Executable File Format, « New » faisant référence à CP/M, et aux fichiers *.com —.

La création du format PE a été induite par le fait que Microsoft souhaitait créer une structure de fichier portable, de sorte qu'elle puisse s'adapter aux différents systèmes de Windows NT, car il faut savoir que Windows NT était au départ capable de supporter d'autres architectures que le x86 d'Intel, Power PC et Motorola 68000 en faisaient partie. L'idée fût donc de créer une structure commune à ces architectures.

[modifier] Schéma du Format PE

PE étant une structure de fichier, cela signifie qu'il est formaté d'une certaine façon. Voici son formatage :

Structure du Format PE

Tous les fichiers PE respectent ce formatage. Si on essaye, par exemple, d'ouvrir un fichier *.exe avec le Bloc-Note, on s'aperçoit que les deux premiers octets sont MZ, qui correspondent bien aux 2 premiers octets de l'en-tête MZ-DOS décrit ci-dessous.

[modifier] En-Tête MZ-DOS

L'en-tête MZ-DOS permet au système d'exploitation de reconnaître le fichier comme étant un exécutable valide dans le cas où celui-ci serait lancé depuis MS-DOS, afin de pouvoir exécuter son segment DOS.

Cet en-tête est également soumis à structuration, nommé IMAGE_DOS_HEADER, cette structure permet de formater l'entête MZ-DOS. Ci-dessous, son prototype en langage C.

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[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;

[modifier] Segment DOS

Le segment DOS est exécuté au cas où Windows ne reconnaîtrait pas le fichier comme étant au format PE. Ce segment étant un exécutable valide, il affiche en général un message comme This program must be run under Win32, autrement dit "Ce programme doit être exécuté sous Win32.".

Il s'agit d'un message implémenté par le compilateur, lors de la compilation du code logiciel, et dans le plupart des cas une exécution de int 21h, une interruption MS-DOS qui permet d'afficher un texte à l'écran.

[modifier] En-Tête PE

L'en-tête PE est un ensemble de structures, regroupées dans une même et unique nommé IMAGE_NT_HEADER, voici son prototype en langage C.

typedef struct _IMAGE_NT_HEADERS {
  DWORD                 Signature;
  IMAGE_FILE_HEADER     FileHeader;
  IMAGE_OPTIONAL_HEADER OptionalHeader;
}IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
  • Signature : Signature permettant d'identifier le fichier, qui doit être égale à 0x00004550, soit "PE\0\0" (\0 étant un octet nul).

Le second champ, FileHeader, est une structure nommé IMAGE_FILE_HEADER, qui contient les informations concernant la structuration du fichier, et prototypé comme suit :

typedef struct _IMAGE_FILE_HEADER {
  WORD  Machine;
  WORD  NumberOfSections;
  DWORD TimeDateStamp;      
  DWORD PointerToSymbolTable;
  DWORD NumberOfSymbols;
  WORD  SizeOfOptionalHeader;
  WORD  Characteristics;
}IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

Voir (en) http://msdn.microsoft.com/en-us/library/windows/desktop/ms680313%28v=vs.85%29.aspx

[modifier] Table des Sections

La Table des Sections est situé juste derrière l'en-tête PE. Il s'agit d'un tableau contenant plusieurs structures.

Ces structures contiennent les informations concernant les sections du binaire, ainsi s'il y a 4 sections, la Table des Sections contiendra 4 structures, sachant que sa capacité maximum est de 96.

C'est l'octet NumberOfSection de la structure _IMAGE_FILE_HEADER (structure définissant l'en-tête PE) qui quantifie les sections du fichier.

La Table des Sections est prototypé comme suit :

typedef struct _IMAGE_SECTION_HEADER {
  BYTE  Name[IMAGE_SIZEOF_SHORT_NAME];
  union {
      DWORD PhysicalAddress;
      DWORD VirtualSize;
  } Misc;
  DWORD VirtualAddress;
  DWORD SizeOfRawData;
  DWORD PointerToRawData;
  DWORD PointerToRelocations;
  DWORD PointerToLinenumbers;
  WORD  NumberOfRelocations;
  WORD  NumberOfLinenumbers;
  DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

[modifier] Table d'Import - IAT

Exemple d'IAT sur un programme Windows

L'IAT, qui signifie Import Address Table, est une section (.idata ou .rdata) contenant les adresses des API importées par un logiciel, ainsi que les noms des DLL important ces fonctions. Les API, contenu dans ces DLL, permettent aux logiciels de fonctionner correctement.

Son existence est dû au fait que les API sont adressées différemment en fonction des OS.

La section .idata (ou .rdata parfois), qui contient cet IAT, est formaté d'une certaine façon. Il faut savoir en premier lieu qu'une structure nommée IMAGE_IMPORT_DESCRIPTOR est utilisé pour chaque DLL appelé; plus une dernière de 5 DWORD mis à zéro qui définit une terminaison.

Pour chaque DLL importé, une structure nommé IMAGE_THUNK_DATA sera utilisé pour chaque API de cette DLL; il y aura donc autant de IMAGE_THUNK_DATA que de fonctions exportées par une DLL, plus un dernier DWORD spécifiant la terminaison de cette DLL.

Une troisième structure, IMAGE_IMPORT_BY_NAME, définit le nom des API ainsi que leur numéro ORDINAL (Nombre de 16 bits identifiant une fonction au sein d'une DLL). Il en existe autant qu'il y a d'API importé par DLL. Par exemple, sur l'image d'exemple ci-dessus, il y a trois IMAGE_IMPORT_BY_NAME définies pour user32.dll, car seulement trois de ses API sont utilisées par le programme.

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    DWORD   Characteristics;        // 0 for terminating null import descriptor
    DWORD   OriginalFirstThunk;     // It points to the first thunk IMAGE_THUNK_DATA
    DWORD   TimeDateStamp;          // 0 if not bound
    DWORD   ForwarderChain;         // -1 if no forwarders
    DWORD   Name;                   // RVA of DLL Name.
    DWORD   FirstThunk;             // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;       //Ordinal Number
    BYTE    Name[1];    //Name of function
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
typedef struct _IMAGE_THUNK_DATA {
    PDWORD                 Function;
    PIMAGE_IMPORT_BY_NAME  AddressOfData;
} IMAGE_THUNK_DATA, *PIMAGE_THUNK_DATA;

[modifier] Table d'Export - EAT

EAT = Export Address Table

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;        /* 0x00 */
    DWORD   TimeDateStamp;          /* 0x04 */
    WORD    MajorVersion;           /* 0x08 */
    WORD    MinorVersion;           /* 0x0a */
    DWORD   Name;                   /* 0x0c */
    DWORD   Base;                   /* 0x10 */
    DWORD   NumberOfFunctions;      /* 0x14 */
    DWORD   NumberOfNames;          /* 0x18 */
    DWORD   AddressOfFunctions;     // 0x1c RVA from base of image
    DWORD   AddressOfNames;         // 0x20 RVA from base of image
    DWORD   AddressOfNameOrdinals;  // 0x24 RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

[modifier] Section des Fichiers PE

Les sections d'un programme vu par PEiD

Chaque fichier PE contient un ensemble de sections, définit dans la Table des Sections. Une section est en fait un « segment » du fichier, possédant certaines particularités. Ci-dessous, un aperçu non exhaustif des différentes sections existantes.

Nom Description
.text Généralement le code (instructions) du programme
.bss Contient des données non-initialisées
.reloc ...
.data Contient des données initialisées
.rsrc Généralement les ressources du fichier (Curseurs, Sons, Menus ...)
.rdata Contient l'IAT d'un fichier
.idata Contient l'IAT d'un fichier
.upx Signe d'une compression UPX, propre au logiciel UPX
.aspack Signe d'un package ASPACK, propre au logiciel ASPACK
.adata Signe d'un package ASPACK, propre au logiciel ASPACK

[modifier] Le PE Loader

Le PE Loader est un élément de Windows permettant de reconnaître et de charger en mémoire des fichiers PE. C'est grâce à lui que Windows peut exécuter les instructions d'un tel fichier.

  • Voici son fonctionnement global :
  1. Le PE Loader examine l'en-tête MZ-DOS afin de trouver l'offset de l'en-tête PE. S'il le trouve il « saute » dessus.
  2. Le PE Loader vérifie la validité de l'en-tête PE. Si tel est le cas il « saute » à la fin de cet en-tête.
  3. Le PE Loader lit les informations concernant les sections puis mappe ces sections en mémoire en employant un procédé de « File Mapping » (copie d'un fichier en mémoire).
  4. Lorsque tout a été mappé en mémoire, le PE Loader se comporte lui-même comme une partie logique du PE File, en tant que table d'importation.

Voir sur : http://ivanlef0u.fr/repo/windoz/pe/_pe-tuts/_pe-tut1.html

[modifier] Reverse Engineering du Format PE

[modifier] Définition

Le Reverse Engineering, parfois raccourci en "Reversing", est un technique d'analyse logiciel qui de par son aspect permet de trouver des «bugs», des failles, de concevoir des exploits, et/ou plus généralement de corriger des problèmes dans un logiciel. Plus globalement, il s'agit donc d'une technique d'ingénierie visant à comprendre le fonctionnement d'un binaire, sans posséder son code source.

Une autre utilisation du Reversing, consiste en l'analyse de Malware dans le but de comprendre son comportement, sa structure, afin de créer un "vaccin" contre ce dernier. Ces dits "vaccins" peuvent ensuite être intégrés dans un anti-virus pour protéger le système. À noter que la plupart des virus/anti-virus se concentrent sur Windows, qui est de loin l'OS le plus utilisé/vendu dans le domaine grand public.

Cette pratique consiste en une analyse poussé du Bytecode d'un programme (quel qu'en soit le format), pour en révéler son fonctionnement, son comportement. Elle fait ressortir des connaissances poussées du système d'exploitation (FreeBSD, Windows, Linux, Mac), de programmation (ASM, C/C++), et architecturaux (x86, SPARC) de la part de celui qui s'y exécute.

Cet article traitant principalement du Format PE (Portable Executable) de Windows, nous allons donc étudier le Reverse Engineering sur des binaires structurés via ce format de fichier.

Notez qu'il est bien entendu possible d'effectuer toutes les démarches suivantes dans des formats tel que ELF (Extended & Linking Format) ou encore Mach-O (Mach Object File Format). En revanche, en changeant de format, les outils vont changer à leurs tours, non pas le principe mais les programmes eux-mêmes; des nuances sur les protections anti-reversing se feront sentir également.

[modifier] Analyse Statique

Une Analyse Statique d'un programme/logiciel consiste en une lecture du Bytecode de ce dernier tel qu'il existe sur le disque dur, c'est-à-dire sans être en exécution.

Une lecture du Bytecode d'un programme n'est ni plus ni moins qu'une transformation des suites d'octet présentent dans le programme, en langage ASM, langage mnémotechnique permettant une compréhension humaine de ce que doit entreprendre le programme.

Des outils ont été spécifiquement développés dans ce but, tel que Win32Dasm, ou Pydasm (Module Python)

[modifier] Analyse Dynamique

Une Analyse Dynamique comprend deux phases : lecture du code - exécution du code.

Pour entreprendre une telle démarche, il est nécessaire de posséder un outil logiciel que l'on nomme Débogueur, tel qu'OllyDbg ou GDB (GNU Debugger).

Le principe étant de désassembler le programme, tout en l'exécutant, de manière à pouvoir modifier son code si besoin en temps réel (donc son exécution) dans le but d'interagir avec, afin de comprendre plus en détail son fonctionnement.

[modifier] Niveaux de Privilèges

La plupart des analyses sont effectués en Ring 3, c'est-à-dire en mode utilisateur. Au dessus on arrive au Ring 0, soit le Kernel, mais ce genre d'analyse poussé est rare et réservé aux aventuriers (développement de driver, hack ...).

Voir Anneau_de_protection

[modifier] Protection Anti-Reversing

Le Reversing étant vu comme une technique intrusive à l'égard des logiciels closed-source (Code source propriétaire et non divulgué), les programmeurs ont donc eu l'idée d'inclure des routines spécifiques dans leurs programmes, qui n'auront qu'un unique but : empêcher au maximum la prise d'information sur le comportement du logiciel.

Pour ce faire il existe de nombreuses de méthodes, dont l'utilisation de Packer (Cryptage/Compression de Code), de code détecteur de débogueur, d'analyse de Checksum (Somme de Contrôle)... seule l'imagination et la créativité peuvent faire freins au développement d'applications "protégées" contre le Reverse-Engineering.

[modifier] Les Packer

Un Packer est un outil logiciel dont le but est de compresser un programme afin de réduire sa taille initiale, tout en conservant son aspect exécutable (sans changer le code d'origine).

Néanmoins, avec le temps, des nouveaux packer sont apparues et ces derniers possèdent également la capacité de crypter le code du programme cible, ce qui aura pour effet de réduire son poids certes, mais qui de plus, va modifier son code d'exécution. Un algorithme des plus connu, et un des plus simple à contourner (clé généralement en dur dans le programme), est sans nul doute XOR (OU-Exclusif).

Bien entendu, la partie "décryptage" effectué lors du lancement du programme packé/crypté sera établie de manière transparente pour l'utilisateur. Mais un Reverser verra cela autrement, en visualisant un code ASM (très) différent de la normale.

Un des Packer sans nul doute le plus connu est UPX, qui s'occupe de réduire la taille d'un exécutable de manière très efficace (près de 50%).

Visualisation du ratio de compression sur notepad.exe

[modifier] Détection de Breakpoint

Le détection de breakpoint (Point d'Arrêt), consiste à déceler lors de l'exécution d'un binaire l'appel à la fonction INT 3, soit 0xCC en héxadécimale. Un code similaire à celui-ci permettrait d'effectuer cette opération :

if(byte XOR 0x55 == 0x99){ // Si Byte = 0xCC -> 0xCC XOR 0x55 == 0x99
    printf("Breakpoint trouvé !!");
}

[modifier] False Breakpoint

La technique du False Breakpoint consiste comme son nom l'indique à programmer (simuler) un point d'arrêt comme le ferait un débogueur, grâce à l'instruction INT 3, soit 0xCC en héxadécimale.

Le fait est que lorsqu'un débogueur rencontrera cette fonction, il se stoppera tout seul par la réception d'un signal SIGTRAP. Lorsque le débogueur voit un opcode 0xCC, il met en pause le programme et attend qu'on lui ordonne de continuer.

Par défaut, un processus se ferme lorsqu'il reçoit un signal SIGTRAP. L'astuce consiste donc à changer ce comportement par défaut grâce à la fonction signal() appliquée à SIGTRAP.

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
 
void sighandler(int signal) {
    printf("Je suis une fonction ordinaire... ");
    exit(0);
}
 
int main(void) {
    signal(SIGTRAP,sighandler);    // On place le sighandler
    __asm__("int3");               // On place un faux Breakpoint
    printf("Pris au piège...");    // Le débogueur arrive ici
    return EXIT_FAILURE;
}

[modifier] False Disassembly

(À venir)

[modifier] Polymorphisme

(À venir)

[modifier] Protection par Cheksum

(À venir)

[modifier] Détection de Débogueur

(À venir)

[modifier] Anti Dumping

(À venir)

[modifier] Les Outils

[modifier] Voir aussi

[modifier] Liens externes

Outils personnels
Espaces de noms
Variantes
Actions
Navigation
Contribuer
Imprimer / exporter
Boîte à outils
Autres langues