PE analysis from Kharneth
Sujet
En étudiant le Bugtraq#1, j'ai exécuté le virus par inadvertance et plusieurs de mes fichiers ont été corrompus! Je n'arrive pas à les réparer. S'il vous plait, aidez moi!! Le fichier est un simple petit programme dont certaines structures ont été modifiées. Le but est de retrouver un exécutable valide (Une MessageBox est affichée). Il y a en tout 16 membres effacés (remplacés par des 00). Retrouvez lesquels ainsi que la valeur qu'ils doivent contenir. Un membre peut être du type BYTE, WORD, DWORD ou chaine de caractères. Vous ne devez pas toucher aux autres valeurs. Analysez bien chaque structure du PE (PEHeader, SectionHeader, ImportTable...). Vous pouvez utiliser les outils que vous voulez mais je ne pense pas qu'autre chose qu'un éditeur Hexa soit utile.
Les différents PE torturés pour cet exercice sont téléchargeables ici :
Définition
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]; LONG e_lfanew; } IMAGE_DOS_HEADER,*PIMAGE_DOS_HEADER;
Cet entête est encore présent dans les exécutables PE pour assurer une compatibilité descendante. Dans un environnement DOS (16bits) le système lit cet entête et exécute le DOS stub, un programme minimaliste qui indique que ce programme doit être lancé sous Win32 (la famille Windows en 32 bits).
Champ | FileOffset | remarque |
---|---|---|
e_magic | 0 | les exécutables DOS se reconnaissent grâce à la signature (le magic number) contenue dans les deux premier octets qui est ‘MZ’ |
e_lfanew | 03Ch | Il doit contenir l’adresse physique du l’entête spécifique au PE. Ce champs permet un DOS stub d’une longueur variable. Il nous faut corriger cette valeur en y mettant l’offset de l’entête des exécutables windows, ici c’est 0B0h. |
On peut trouver de nombreuse documentation sur les deux structures contenue dans IMAGE_NT_HEADERS. Microsoft en propose un grand nombre dans la MSDN (Structures PE)
Définition
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER OptionalHeader; } IMAGE_NT_HEADERS,*PIMAGE_NT_HEADERS; 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; #define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16 typedef struct _IMAGE_OPTIONAL_HEADER { 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 Reserved1; 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_HEADER,*PIMAGE_OPTIONAL_HEADER;
Autant les champs de l’entête DOS sont facilement manipulables, autant ceux de l’entête Windows le sont avec difficulté car bien plus nombreux. Pour avoir une vision globale sur les offsets et les enregistrements des champs j’ai utilisé PEview.
En navigant dans l’arborescence des différentes structures on regarde quels sont les champs qui ne sont pas remplis alors qu’ils devraient l’être dans un PE normalement constitué.
champ | FileOffset | Remarque |
---|---|---|
NumberOfSections | 0C6h | On va le mettre à 4. C’est que que nous donne visuellement l’exécutable dans un éditeur héxadécimale. |
SizeOfOptionalHeader | 0D4h | Je vais prendre la valeur par défaut de ce champ, c’est à dire 0E0h. On pourrait faire le calcul à la main (Adresse début structure section - Adresse début IMAGE_OPTIONAL_HEADER |
Characteristics | 0D6h | Nous allons prendre la valeur de IMAGE_NT_OPTIONAL_HDR_MAGIC c-à-d 0x10B. Cette valeur indique au système qu’il s’agit d’un executable |
Champ | FileOffset | Remarque |
---|---|---|
ImageBase | 0F4h | On va mettre à 400000h car c’est une valeur classique. |
SizeofImage | 110h | Ce champs est la taille de l’image mappée en mémoire, ce qui nous fait 5000h (4 section de 1000h + l’entête du PE) |
Sizeofheader | 114h | on met 400h (⇒ début de la section code.) |
Subsystem | 11Ch | On met à 2 (IMAGE_SUBSYSTEM_WINDOWS_GUI, qui n’affiche pas de console). |
NumberOfDirectories | 134h | On met 10h (16) parce que c’est standard1). :) |
Définition
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;
L’entrée de la section ‘.text’, contenant le code exécutable et l’entrypoint, est fausse ; Nous allons remplire les valeurs manquantes.
Champ | FileOffset | Remarque |
---|---|---|
VirtualAddress | 1C4h | que l’on met à 1000h |
SizeOfRawData | 1C8h | La taille de VirtualSize indique la taille du code mais puisque les sections sont alignées et que la taille du code est inférieure à un alignement, nous allons utiliser la valeur de SectionAligment, soit 200h |
PointerToRawData | 1CCh | L’entête du PE plus l’alignement de section fait 400h octets, on met donc 400h |
Définition
typedef struct _IMAGE_IMPORT_DESCRIPTOR { _ANONYMOUS_UNION union { DWORD Characteristics; DWORD OriginalFirstThunk; } DUMMYUNIONNAME; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD Name; DWORD FirstThunk; } IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR;
Tout d’abord il faut comprendre le fonctionnement des imports et la résolution des addresses. Lorsque le loader charge le programme il va résoudre les fonctions importées et placer leur adresse à un endroit spécifique (IAT) dont le contenu servira de saut de destinations dans le code du programme.
La table contenant les éléments IMAGE_IMPORT_DESCRIPTOR doit aussi se terminer par un élément à 0. Le PE de Kharneth posséde le premièr élément IMAGE_IMPORT_DESCRIPTOR à 0, de ce fait il ne peut pas y avoir de résolution des imports.
L’adresse RVA précisant le début de cette table est dans le DataDirectory relatif (Import table address and size) de la structure IMAGE_OPTIONAL_HEADER, ici 2010 RVA pour l’adresse de la table des IMAGE_IMPORT_DESCRIPTOR. Il faut donc trouver les différents tables utilisées pour résoudre les adresses des imports afin de remplir les bons champs.
00002000 5C 20 00 00 00 00 00 00 78 20 00 00 00 00 00 00 \ ......x ...... 00002010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00402020 00 00 00 00 54 20 00 00 00 00 00 00 00 00 00 00 ....T .......... 00002030 86 20 00 00 08 20 00 00 00 00 00 00 00 00 00 00 ................ 00002040 00 00 00 00 00 00 00 00 00 00 00 00 5C 20 00 00 ............\ .. 00002050 00 00 00 00 78 20 00 00 00 00 00 00 80 00 45 78 ....x ......¤.Ex 00002060 69 74 50 72 6F 63 65 73 73 00 6B 65 72 6E 65 6C itProcess.kernel 00002070 33 32 2E 64 6C 6C 00 00 9D 01 4D 65 73 73 61 67 32.dll....Messag 00002080 65 42 6F 78 41 00 75 73 65 72 33 32 2E 64 6C 6C eBoxA.user32.dll
En repérant les noms des DLL et des fonctions à importer, on voit les deux tables similaires (celles de FirstThunk et OriginalFirstThunk) car nous avons deux fois les adresses 205Ch et 2078h, soit deux tables d’un elément. Mais une table comme une table verra ses valeurs réécrites par le système (l’IAT) lors du chargement du programme, il nous faut trouvé laquelle. Cela ne sera pas bien dur car les appels vers ces adresses se trouvent dans le code du programme.
Après correction des valeurs on a
Champ | FileOffset | Valeur |
---|---|---|
OriginalFirstThunk | 610h | 204Ch |
TimeDateStamp | 614h | 0 |
ForwarderChain | 618h | 0 |
Name | 61Ch | 206Ah (⇒ kernel.dll) |
FirstThunk | 620h | 2000h (⇒IAT) |
PE, da leet version from Kharneth.
La première surprise en ouvrant ce PE est qu’il est très petit (666 octets), puis, deuxième surprise, il y a un paquet d’octets mis à 0. Du travail en perspective, mais c’est un PE pour leets.
J’ai été rapide sur certains points sur ce tutoriel. La lecture du texte suivant est donc réservée au personnes ayant déjà acquis une bonne connaisance sur le format des PE, soit en lisant depuis le début ce document, soit en ayant farfouillé précédement dedans.
Je ne voulais pas faire un cours sur le PE2), j’ai juste voulu que ce soit un document pouvant apporter quelques réponses lors de l’étrippage de PE.
Let’s get in!
On regarde tout d’abord à e_lfanew (3Ch) pour voir où se situe la structure IMAGE_NT_HEADERS dans le PE. Il est à zéro et il semble y avoir peu de place à la suite pour l’écrire car on ne doit pas modifier les octets existant et la section (.code) à l’offset 9Ch va nous géner. On va se faire chevaucher IMAGE_DOS_HEADER et IMAGE_NT_HEADERS. Pour ce faire il faudrait déterminer en calculant les offsets dans les différentes structures afin que les octets existants ou à venir ne perturbent ni l’une ni l’autre. J’avoue avoir choisi un peu au hasard de mettre IMAGE_NT_HEADERS à l’offset 0Ch, ce qui a été un choix judicieux puisque une valeur présente (10Ch) se trouve dans le champs AddressOfEntryPoint, et qu’à cet adresse se trouve une séquence qui semble être une suite d’instructions.
Champ | commentaire |
---|---|
Machine | on met 14Ch |
NumberOfSections | il semble n’y avoir qu’une section. |
PointerToSymbolTable | est un élément de IMAGE_DOS_HEADER qui est à 40h |
NumberOfSymbols | on le laisse à 0 pour ne pas interférer avec la valeur ci-dessus. |
SizeOfOptionalHeader | on fait la soustraction de l’offset de la section ‘.code’ avec celui de l’adresse où débuteIMAGE_NT_HEADERS ce qui donne 78h. |
Characteristics | on garde traditionnellement 10Fh. |
Champ | commentaire |
---|---|
Magic | on met 10Bh car c’est le magic number pour un exécutable win32 |
MajorLinkerVersion | (optionel) j’ai mis 6... |
MinorLinkerVersion | (optionel) ...et 66d pour avoir le linker 6.66 comme version :) |
SizeOfCode | On le remplira après |
SizeOfInitializedData | idem |
SizeOfUninitializedData | idem |
AddressOfEntryPoint | ce champ était déjà rempli (10Ch). |
BaseOfCode | mis à 0 car le code semble se trouver dans l’entête du PE. |
BaseOfData | il s’y trouve déjà l’octet de e_lfanew |
ImageBase | on le remplira plus tard. |
SectionAlignment | on met 1000h, un clasique |
FileAlignment | cela semble être 200h |
MajorOperatingSystemVersion | on met 4 |
MinorOperatingSystemVersion | on garde 0 |
MajorImageVersion | on s’en fout un peu |
MinorImageVersion | idem |
MajorSubsystemVersion | il faut avoir 4 au minium (merci Kharneth pour cette correction sur mon PE réticent :)) |
MinorSubsystemVersion | on garde à 0 |
SizeOfImage | on le remplira plus tard |
SizeOfHeaders | puisqu’on a un FileAlignment à 200h on garde cette valeur. |
Subsystem | on met 2 (voir le PE normal) |
SizeOfStackReserve | on met 1000h pour les trois champs suivants. Ce n’est pas bien important ici. |
NumberOfRvaAndSizes | étant donné la taille de IMAGE_OPTIONAL_HEADERS, il ne peut y avoir que 3 éléments au maximum dans le tableau suiant. On met 3. |
Maintenant que le PE est mieux formé, on peut naviguer dedans avec quelques outils (PEiD, Stud_PE,...) pour remplir les quelques champs qui nous manque.
Faisons le plan mémoire RVA du PE chargé avec l’aide des informations de la section.
nom | offset | taille |
---|---|---|
Header | 0 | 200 |
.code | 1000 | 9A |
Plan mémoire
Grâce à cette représentation du PE en mémoire, on peut facilement définir la valeur de SizeOfImage : 109Ah.
Champs que l’on peut encore combler :
Il faut maintenant reconstruire l’IAT. On va prendre PEiD afin de déterminer où se trouvent les éléments de cette table. En ouvrant le PE avec et en cliquant sur la flèche à coté de First bytes on obtient de désassemblage du code de l’éxecutable.
On voit dans le désassemblage deux calls vers des destinations contenues dans des dwords qui sont, respectivement, 29A1079h et 29A106Ch. Ce sont les adresses VA des éléments de l’IAT.
On regardant nos offsets relatifs et les offsets absolus des deux adresses trouvées dans le listing pour déterminer la valeur de ImageBase de façon à ce qu’elles soient dans l’espace mémoire de la section ‘.code’ une fois mappée. L’ImageBase est donc 29A000h.
Nous en avons fini pour l’entête du PE, il est presque prêt à être exécuter, manque seulement la résolution des imports.
J’ai choisi de mettre cette table dans la section et non pas dans le header car cela provoque une erreur lors du chargement. Je ne vais pas expliquer en détail les modification -car si vous avez tout compris il est inutile de repéter- je vais simplement mettre le rapport généré par un outil servant à lister les imports. Pour cela j’ai utilisé le programme logimports d’elooo qui donne les valeurs nécessaires. C’est utile de savoir où trouver de bons outils.
On rempli la table IMPORT dans IMAGE_OPTIONAL_HEADER avec en offset RVA 1030h et comme taille 3Ch (la taille de trois structures IMAGE_IMPORT_DESCRIPTOR)
******************************************************** * LogImports 1.1 * * coded in asm 32 bits * * by elooo * ******************************************************** -----> Log for : D4_1337_V3RSi0N.exe _______________________________________________________ IMAGE_DOS_SIGNATURE (MZ) at : 0x00000000 IMAGE_NT_SIGNATURE (PE) at : 0x0000000c FileHeader at : 0x00000010 OptionalHeader at : 0x00000024 Number of sections : 1 RVA of the Import Directory : 0x00001030 Length of the Import Directory : 0x0000003c Section .code RVA = 0x00001000 Offset = 0x00000200 Length = 0x0000009a (Section of Import Table !) _______________________________________________________ IMAGE_IMPORT_DESCRIPTOR _______________________________________________________ OriginalFirstThunk : 0x00000000 TimeDateStamp : 0x00000000 ForwarderChain : 0x00000000 Name : 0x0000010d ; offset = 0x0000010d FirstThunk : 0x00001074 ; offset = 0x00000274 -----> USER32.DLL ------------------------------------------------------- Hint | Name ------------------------------------------------------- 413 | MessageBoxA _______________________________________________________ _______________________________________________________ IMAGE_IMPORT_DESCRIPTOR _______________________________________________________ OriginalFirstThunk : 0x00000000 TimeDateStamp : 0x00000000 ForwarderChain : 0x00000000 Name : 0x00000100 ; offset = 0x00000100 FirstThunk : 0x0000106c ; offset = 0x0000026c -----> KERNEL32.DLL ------------------------------------------------------- Hint | Name ------------------------------------------------------- 128 | ExitProcess _______________________________________________________
Les informations contenues dans ce listing sont suffisement explicites pour remplir la table à la main.
Pfiou... ce dernier exercice n’a pas été de tout repos intellectuellement. J’ai même un peu raccourci le tutoriel pour des raisons personnelles. Ce double TP de Karneth était très bien. Une fois que vous aurez compris toutes les subtilités du PE (je m’y mets progressivement) et compris ce tutoriel, vous deviendrez un vrai leet. :)