Portable Executable File Format
|
|
Cet article est en cours de réécriture ou de restructuration importante (indiquez la date de pose grâce au paramètre date).
Un utilisateur prévoit de modifier cet article pendant quelques jours. Vous êtes invité(e) à en discuter en page de discussion et à participer à son amélioration de préférence en concertation pour des modifications de fond.
Bandeau apposé par G.lardoux (lui écrire). |
| Portable Executable | |
|---|---|
|
|
|
| Extension | .cpl, .exe, .dll, .ocx, .sys, .scr, .drv |
| Type de format | Binaire, Exécutable, Code Objet, DLL |
| Extension du | MZ-DOS Executable COFF |
| modifier |
|
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 :
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
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
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 :
-
- 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.
- 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.
- 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).
- 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%).
[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
- Module pefile.py
- Module Pydasm.py
- Immunity Debugger
- PeiD - Analyse des Packers/Crypteur
- LordPE - Éditeur de Fichier PE
- W32Dasm - Désassembleur
- OllyDbg - Le Fameux Débogueur Windows
- ImpRec - Outil de Reconstruction d'IAT
- ResHacker - Éditeur de Ressource
- WinHex - Éditeur Hexadécimale
