Structure du format de fichiers PE - Application avec le Notepad (Remarque : des éléments de ce tut ont déjà été abordé dans d'autres Mementos. Ce tut aborde en détail et depuis le début les notions sur les exes.)Note importante: Pour ne pas s'emméler les pinceaux, DANS TOUT LE TUT, tous les chiffres/nombres/valeurs en hexa seront précédés par un "0x". Le "0x" n'est pas un nombre, mais une information pour dire "Attention, ce qui suit juste après est en hexadécimal". Les nombres décimaux seront écrits normalement. Exemple : Aujourd'hui on va s'attaquer aux exes. Les utilisateurs de Mac nous reprochent toujours de devoir mettre la main dans le camboui en tant qu'utilisateurs de PC, et ça tombe bien car c'est ce qu'on va faire!!! Quant aux utilisateurs de Mac, qu'ils restent avec leurs belle boite bien chère tout fermée, et qu'ils continuent à faire ce qu'on leurs dit de faire et de ne surtout pas réfléchir... Et en plus ils s'en vantent :) les c...! Bon en fait, je suis mauvaise langue car il y a quand
même des rassemblements comme le MacHack (Macaque!?!) aux States, où il
y a des gens qui n'ont rien à envier aux meilleurs coders,crackers.. de
PC. Go! Matos nécessaire: Tout d'abord vous enfermez la copine ( le copain s'il y a des filles qui s'initient au cracking! hello girls :) ), les parents, le frère, la soeur, le chien et le chat dans le placard. Et oui, jai dit bien grand et bien solide. Il y a du monde et il faut que ça tienne! Comme cela on a la paix, et on va pouvoir travailler tranquilloux. On y va... Tout bon crackeur doit très vite avoir une certaine notion des executables ne serait ce que pour savoir/comprendre ce qu'il faitquand il (elle) utilise Procdump par exemple! Je ne me prétend pas du tout être bon dans le domaine, mais je voudrais donner un minimum d'aperçu sur les exes. Petit historique :) Le système d'exploitation Windows NT version 3.1 a introduit un nouveau format de fichier executable appelé le format de fichier Portable Executable (PE). Ce nouveau format fut élaboré principalement à partir de la spécification du COFF (Common Object File Format - COFF entête et entête optionnel) en vigueur dans le monde Unix. De plus pour rester compatible avec les versions antérieures de MS-DOS et de Windows, le format PE comprend le familier en-tête MZ du MS-DOS. Structure des fichiers PE Le format de fichier PE est organisé comme un flux de données de manière linéaire. Il commence avec l'entête MS-DOS, le "program stub" en mode réel et la signature du PE. Ensuite, on trouve l'entête PE et le PE optionnel. Les sections sont déclarées dans un partie qui suit le PE optionnel. On va aborder dans ce tut chacune des parties du PE (i.e. de l'executable) tels qu'elles s'enchainent quand on parcours de haut en bas l'executable sous un éditeur hexadécimal. Toute la structure et la composition d'un PE est donnée dans le fichier WINNT.H. Ce fichier est inclus dans toute application servant au développement d'applications windows. On commence par ouvrir le notepad sous l'éditeur hexa.
Je rappelle que tout ce tut ne traite QUE du notepad. Ainsi quand on visualise un soft sous un éditeur hexa, il est bon de connaitre les differentes sections que l'on peut apercevoir. Voici un petit aperçu depuis son début jusqu'à la fin: ENTETE / DEBUT DE PROGRAMME 00000000 4D5A 9000 0300 0000 0400 0000 FFFF 0000 MZ.............. =>Ce que l'on voit (en gros) au début de tout executable, avec la
fameuse phrase "PARTIE DE ZEROS" (Je l'appelle comme ça, on vera après ce que c'est) 00000390 0000 0000 0000 0000 0000 0000 0000 0000 ................ =>Ca, ca apparait très souvent et c'est très pratique. Cela permet de bricoler, bricoler, bricoler :)) Ici cela apparait de manière flagrante sur plusieurs lignes, mais cela peut être aussi très court, du genre (là où c'est en rouge) : 6865 6C6C 6F2C 2077 6F72 6C64 0A00 0000"PARTIE DE CODE" (Là aussi, on verra après) 000017C0 00E8 320D 0000 E9E2 0000 003D FFFF 0000 ..2........=.... =>Bon bien ici, c'est du code pur et dur, comme par exemple où l'on patch un saut quand on cracke. "PARTIE DE FONCTIONS/DONNEES" (Là aussi, on verra...) 00006640 6E64 436C 6F73 6500 A300 4669 6E64 4669 ndClose...FindFi =>Là vous avez un passage "lisible". On reconnait des noms de fonctions/APIs... "PARTIE DE FONCTIONS/DONNEES" (Là aussi, on verra...) 0000A1D0 1B00 2600 5200 6500 7400 6F00 7500 7200 ..&.R.e.t.o.u.r.
=>... ici, ce sont les menus du programme. Pour ceux qui codent, on remarque le "&" pour souligner une lettre pour les raccourcis... "PARTIE DE FONCTIONS/DONNEES" (Là aussi, on verra...) 0000AC30 6900 6F00 6E00 2E00 2000 5100 7500 6900 i.o.n... .Q.u.i. =>... et là, la partie de toutes les chaines de caractères qui sont manipulées par le soft. Elle se trouve à la fin en général. Bon voilà, on a à peu près fait le tour. On va maintenant passer le soft en revue ligne par ligne et regarder en détail la signification. On revient donc au début du soft. 00-7F : 1er bloc (Par 00-7F j'entend: "bloc allant de l'adresse 0x00 à l'adresse 0x7F". Pareil pour les parties suivantes) 00000000 4D5A 9000 0300 0000 0400 0000 FFFF 0000
MZ.............. Cette partie s'appelle l'entête MS-DOS. Elle porte aussi le nom d'entête Real-Mode. Elle est au début du fichier, car si celui-ci n'est pas executable sous DOS mais sous Windows uniquement, elle affiche "This program cannot be run in DOS mode" au lieu de mettre un truc barbare du style "The name specified is not recognized as an internal or external command, operable program or batch file". C'est ce qui se passerait en commençant directement avec l'entête PE (voir bloc suivante). Dans tout ce bloc, le plus important à retenir est ceci :
Petite précision : bien évidemment, il ne suffit pas de mettre MZ (avec un éditeur hexa) au début d'un fichier pour qu'il "marche tout seul" sous windows... MZ s'appelle le "Magic Number" est correspond à ce qu'on appelle le format d'image (d'un exe, dll, vxd...). On définit ainsi plusieurs "magic numbers" :
Il est à noter que
les exes de OS/2 ont la même structure que les executables windows. 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; 00000000
4D5A 9000 0300 0000 0400 0000 FFFF 0000 MZ..............
Ensuite vous avez la fameuse phase à 2 Francs "This program cannot be run in DOS mode" qui se termine en 0x73. Elle est suivit de 2E 0D 0D 0A 24 et puis des zéros. Vous avez ensuite la ligne 0x80 qui commence avec PE, mais ça c'est pour après avec la section suivante. Entre MZ et la phrase "...DOS mode" il y a différentes variables que je n'aborderai pas ici hormis celle en 0x3C qui vaut 0x80. Il s'agit du pointeur (saut) qui indique ou se trouve l'entête PE dans le soft. Quand le programme est chargé en mémoire, l'ordi lit ce bloc, et arrivé en 0x3C il sait qu'il doit aller à l'adresse 0x80 pour lire le PE. PADDING : ce terme est important. Il signifie "remplissage avec des 0" C'est ce que l'on a a la ligne 0x70. Apès le 0x24 en 0x78, on termine la ligne par des "00 00". C'est ce dont je parlais plus haut dans la présentation des différents blocs. Ce remplissage peut faire quelques bytes ou bien des blocs entiers. Mais, me direz-vous, "pourquoi mettre des 00 au lieu de placer le code directement à la suite?". Bonne question mon cher Watson! Dans le cas de la ligne, c'est pour avoir une ligne complete et faire commencer le bloc suivant à la ligne. On peut ainsi traiter les blocs par adresse de multiple de 16 pour le mode 32 bits. Quant aux blocs entiers de 00, c'est pour remplir la place (la partie de code non occupée) entre la fin du code dans une section et le début de la section suivante. Et pourquoi ne pas rapprocher les blocs au lieu de mettre tant de 00 ? Et bien comme cela, nous (coders, reversers) avons un espace de liberté où nous pouvons nous exprimer! :o) Valà... De plus, le padding n'est pas qu' "ésthetique". Certaines sections ont besoins d'avoir une certaine taille pour marcher sous une version donnée de windows. Donc retenez que le remplissage avec des zéros pour garder une "structure" dans un exe s'appelle le padding. 80-FFF : 2e bloc 00000080 5045 0000 4C01 0500
6591 4635 0000 0000 PE..L...e.F5.... De 0x240 à 0xFFF, il n'y a que des zéros . Il s'agit du padding jusqu'au début de la section suivante. Bien. Nous sommes donc en 0x80 suite au saut en 0x3C de la section précédente. On lit à cette adresse " PE" qui signifie portable executable. On aborde là une notion très importante dans le monde des exes sous zindoz. Le format de fichier PE est un format pour les exes et les dlls pour les systèmes d'exploitation de zindoz 95 à ultérieurs. On trouve ce nombre 0x 5045 (PE) sous l'appellation IMAGE_NT_SIGNATURE. L'entête PE est au programme ce que la table des matières est au livre. Vous voyez dans le code ci-dessus les mots "text", "data", "idata", "rsrc" et "reloc". Ces mots sont en fait nommés avec un point devant ".text", ".data", ".idata", ".rsrc" et ".reloc", bien que le "." ne soit pas indispensable. Ils correspondent chacun à une section de l'exe. Il y a donc 5 sections dans cet exe. Parfois on en trouve plus, parfois moins. Cela dépend de chaque programme. De même, leur nom n'est pas figé dans l'appellation. On peut très bien le changer, où introduire une section avec un nom comme vous voulez. Il faut au maximum sept lettres (soit huit avec le "."), sinon on empiète sur les informations de la section et le programme ne va pas aimer! On va d'ailleurs traiter un petit exemple plus bas :) Chaque section définie dans le PE est codée sur 0x28 bytes (Attention, on est en hexa! Je rappelle qu'un byte ça prend "00" en place). Par exemple pour la section .data, on a (en bleu) : La manière la plus classique et facile pour visualiser ces informations,
est d'utiliser procdump.
Je traite l'exemple avec .text, c'est bien sûr valable pour toutes les sections. La section commence au "Virtual Offset" en 0x1000 (vérifiez le sous un éditeur hexa en allant à cette adresse). Ensuite, on a la taille de cette section en "Virtual Size" qui vaut 0x3E9C. La taille indiquée ici correspond à la place effectivement occupée par du code. Mais comme il y a du padding entre les sections, la taille réelle de la section est plus grande. Cette taille réelle est donnée en "Raw Size". Il s'agit de la différence entre le début de la section suivante (.data en 0x5000) moins le début de notre section (.text en 0x1000) qui fait donc 0x4000. En dans ces 0x4000 bytes, on a 0x3E9C bytes (de la virtual size) qui sont du code, et le reste du padding, soit 0x4000 - 0x3E9C = 0x164 bytes de "00". Donc, on retient les définitions ci-dessus, et le fait que la raw size = virtual size + padding. Donc la raw size est supérieure à la virtual size. On retrouve la propriété du padding où tous les nombres de la raw size sont "ronds" i.e. un multiple du 16, 32 ou 64 bits. Pour la "Raw Offset", cela correspond dans le cas du Notepad à la "Virtual Offset". Mais ceci est rarement le cas, ce qui en général complique un peu les choses. Par exemple, si on regarde les sections de Procdump lui-même : Sections Informations :
On remarque ici que l'on ne peut plus lire directement l'offset sous l'éditeur hexa à partir du dead listing (0x4098A6 donne 0x98A6 qui est différent de 0x8CA6). On utilise en fait la formule suivante qui est applicable à tous les cas :
L'adresse 0x8CA6 est dans la section .text. Il suffit
de regarder la colonne Virtual Offset pour cela. 0x408CA6 = 0x4098A6 - 0x1000 + 0x400 Et le 0x4098A6 que l'on lit sous wdasm ou bien sous softice est appelé le RVA (Relative Virtual Offset). Cette valeur est utilisée pour l'offset (du soft) en mémoire (dans la RAM) quand l'ImageBase est inconnue. Bon, bien maintenant que l'on a vu le petit calcul, ce n'est plus trés bien compliqué... La dernière colonne "Characteristics" indique les propriétés de la section. Ces propriétés sont divisées en deux. Execute, Read, Write On peut avoir des combinaisons de propriétés pour une section. Pour ceux d'entre vous qui ont l'habitude de la gestion des droits des fichiers sous Unix (avec 1,2,4), on a la même procédure ici mais en hexadécimal, par exemple :
Code, Data
Même calcul ici :
Mais ces quelques
caractéristiques ne sont pas les seules qui existent.
De plus, il y a également
des propriétés non encore utilisées mais déjà réservées pour le futur!!
Elles ne sont donc pas attribuées... Si on veut donc se rajouter une section dans un exe (ou en modifier une existante), il sera avantageux de lui coller le maximum : un 0xE0000020 comme caractéristique. Bien! On a fini pour les "characteristics", on va passer à notre petit exemple de ...bien plus haut :) (NDOracle (mise en page): Ouf!) Il s'agit de rajouter une section à l'exe pour se faire la main en manipulant les données des sections. Pourquoi rajouter une section? Et bien parfois, on n'a pas assez de place dans les paddings d'un exe, et il est bon de se tailler soi-même sa place :) Autre avantage, quand on écrit du code dans le padding, on est par définition en dehors de la virtual size, donc si vous désassemblez le fichier, vous ne verrez pas votre code. Pour rajouter une nouvelle section, on va faire cela d'abord
"automatiquement" avec procdump. Puis, comme on est pas esclave
d'un programme qui fait les choses à notre place, on va le faire "à
la main" avec un éditeur hexa pour bien savoir comment cela marche! Vous avez la copie du notepad dans un repertoire. Renommez
la N0.exe (par exemple, c'est ce que j'ai fait). On va utiliser procdump pour N1.exe et on va faire N2.exe à la main. Procdump Dans la fenêtre où les sections sont affichées, cliquer avec le bouton droit de la souris sur une section et choisir "Add section". Rentrer un nom (7 lettres max.) et appuyez sur OK. Procdump rajoute automatiquement la nouvelle section à la fin et caclule les virtual/raw offsets. La caractéristique par défaut est 0xC0000040. Il ne reste plus qu'à éditer la section pour y modifier ses valeurs. Au passage, la taille de l'exe passe de 56 à 60Ko. Voilà pour la méthode "automatique". A la main On part du principe qu'on ne sait rien, et qu'on veut apprendre. Comment rajoute-t-on une section? On peut déjà commencer à calquer notre section à la suite des autres. Ainsi, j'appelle ma section ".anubis", je la mets après la dernière section du notepad, soit .reloc. Son virtual offset sera donc de (= raw size + raw offset) E000. Et on va dire qu'elle fait 1000 en taille, donc de vitual size. Ici on n'a pas besoin de padding puisq'on a une section toute vide. Et pour les caractéristiques, on prendre les même que .rsrc et .idata soit 0x40000040. N'oubliez pas que la déclaration de la section se fait sur 0x28 bytes. On commence donc à l'offset 0x240. En écrivant par analogie (par exemple avec .rsrc) dans le PE sous l'éditeur hexa, on a ceci : 000001F0 2E72 7372 6300
0000 0060 0000 0070 0000 .rsrc....`...p.. Maintenant, sauvez, et regarder N2.exe sous procdump... Rien! Ben oui, il ne s'agit pas d'écrire, il faut aussi dire au programme que quelquechose de nouveau est là. Très certainement il a quelque chose d'autre à changer dans l'en-tête PE. Comment savoir? On va tout simplement comparer N0.exe et N1.exe, le fichier de référence et celui modifié par procdump. Pour cela, pas besoin de se taper la comparaison byte à byte dans l'éditeur hexa. Kro$oft nous fournit les moyens rapides et efficaces de le faire. Sortez une fenêtre DOS, on va se taper des bonnes vieilles commandes. Héhé... La commande DOS "FC" ou FILE COMPARE
On peut utiliser cette commande de 2 manières simples
: On ne pourra jamais se passer du DOS. C'est la meilleure et la pire des choses
que kro$oft ait faite... |
Comparaison des fichiers N0.exe et N1.exe 0000007C: 00 63 ...En plus c'est bien affiché, c'est pas beau ça?! La commande FC nous affiche les premiers changements et après nous dit que les deux exes sont trop différents. On voit ainsi les changements suivants : - de 0x7C à
0x7F : 4 bytes de modifiés. Pas d'explication pour le moment. Je recopie ici le passage qui nous intéresse pour plus de clarté. Il s'agit du passage changé à la main et donc encore incomplet. 00000080 5045 0000 4C01 0500 6591 4635 0000 0000 PE ..L...e.F5.... 00000090 0000 0000 E000
0E01 0B01 030A 0040 0000 .............@.. Donc on reprend notre N2.exe et on change déjà 0x05 en 0x06 à l'offset 0x86. On regarde sous procdump, et... il ne veut pas :( Donc quelque chose manque encore! On va changer le 0xE0 en 0xF0, et si cela ne suffit pas on modifiera de 0x7C à 0x7F. Cette fois ça a marché! Notre nouvelle section est correctement ajoutée. Il ne nous reste plus qu'à ajouter mauellement la place que l'on a déclaré dans l'entête PE. On va tout à la fin de l'exe et on insère 0x1000 bytes de "00" (de 0xDFF0 à 0xEFF0). Mettez le curseur en 0xDFF0, allez dans le menu Edit>Insert et entrez 0x1000 bytes de "00". Sauvegardons, et l'exe passe de 56 à 60 Ko... CQFD! ou AQT! (pour les plus avancés ;) ) Le fait que la modification 0x0E à 0x0F fasse grinchonner procdump nous montre que c'est important, dès que l'on dit à l'ordi que notre programme a une nouvelle section (passage de 0x05 à 0x06). Que peut être cette valeur? 0xF0 - 0xE0 = 0x10. Et 0xF0 étant en 0xD1, il y a fort à parier que le 0x00 en 0xD0 fait aussi parti de cette valeur. Les bytes étant lus "à l'envers", on a donc 0xF000 - 0xE000 = 0x1000. Et la seule manière de rattacher ce 0x1000 à nos données et de le comparer à la taille de la section .anubis. Donc 0xE000 ne peut être que la taille de notre exe qui passe à 0xF000 avec l'ajout de la nouvelle section. Ceci nous amène à finir les explications sur l'entête PE. On va regarder la signification des valeurs qui viennent après les lettres PE jusqu'au début de la déclaration des sections .text,... On s'intéresse donc au passage en jaune : 00000080 5045 0000
4C01 0600 6591
4635 0000 0000 PE..L...e.F5.... En 0x84, la valeur 0x4C01 (lire 0x014C) définie le type de système sur lequel est censé tourner le programme. Voici quelques valeurs pour cette variable qui porte le nom de "Machine" : IMAGE_FILE_MACHINE_I386 0x014C = pour processeur
Intel 80386 et plus (notre cas ici) IMAGE_FILE_MACHINE_ALPHA 0x0184 = pour processeur DEC Alpha AXP IMAGE_FILE_MACHINE_POWERPC 0x01F0 = pour IBM Power PC Basiquement, les informations dans l'entête PE sont utilisées par le système pour déterminer comment traiter le fichier. Ainsi par exemple, si vous changez sous l'éditeur hexa la valeur d'origine 0x4C01 en 0x8401 (pour les processeurs Alpha), on obtient un joli message : On remarque au passage la prose commerciale de kro$oft : "contacter un revendeur..." En 0x86, on a vu que
ce nombre correspond au nombre de sections du PE. Ici 0x06
(soit 0x0600), En 0x88, il y a la
date en "Time in seconds since UTC 1/1/70" : 768027063 En 0x8C et jusqu'à la fin de la ligne en 0x8F on a le "PointerToSymbolTable". Il s'agit d'une valeur qui est la plupart du temps à zéro sur 4 bytes. De même, en 0x90 jusqu'à 0x93 (sur 4 bytes donc), on a le "NumberOfSymbols'". Egalement à zéro. En 0x94, il y a le "SizeOfOptionalHeader" qui vaut 0xE000. Il s'agit de la taille de l'entête optionnel IMAGE_OPTIONAL_HEADER qui se trouve plus bas dans la section PE (dans laquelle on est maintenant). En 0x96, on a les
"Characteristics" sur 2 bytes avec 0x0E01.
(bits: 15,14,13,12,...,2,1,0) La signification du vecteur de base 16 étant :
SE : système d'exploitation Dans le cas du notepad, on est en 0x0E01. On peut ou bien faire la somme des nombres appropriés de la colonne hexa (0x0001 + 0x0200 + 0x0400 + 0x0800 = 0x0E01) ou bien lire le vecteur et voir que les rangs 0, 9, 10, 11 sont allumés. Cela correspond aux caractéristiques suivantes :
Voilà pour ce qui se cache derrière ces quelques bits... :) Jusqu'ici, on peut donc résumer l'entête PE à cela : typedef struct _IMAGE_FILE_HEADER
{ Ensuite, dans l'entête PE il y a ce que l'on appelle l'entête PE optionnel. Cela n'a rien à voir d' "optionnel", bien au contraire. On va trouver dans cette partie des informations comme l'ImageBase, le PEP (Program Entry Point), la version du SE... Décorticons tout cela : L'entête optionnel est divisé en 2 parties. L'une dite standard et l'autre additionnelle pour NT. typedef struct _IMAGE_OPTIONAL_HEADER
{ Regardons la partie standard (par analogie avec le code C++ ci-dessus) : En 0x98, vous avez le Magic Number sur 2 bytes qui vaut 0x010B (il apparait comme 0x0B01) dans tous les executables, sauf preuve du contraire. Ce nombre n'a pas plus de signification particulière. En 0x9A et 0x9B se trouvent 0x03 et 0x0A, les numéros des versions du linker lors de la compilation. Ensuite on a 3 valeurs,
sur 4 bytes chacune, qui valent : En 0xA8 sur 4 bytes, on a LA valeur! Il s'agit ni plus ni moins que du PEP, le program entry point. C'est à partir de cette valeur là que le programme commence à s'executer. Cette valeur vaut ici 0x10CC (elle est écrite "à l'envers" comme tout le reste, CC10 0000). Si vous décompilez le notepad sous windasm et que vous appuyez sur le bouton PEP de la barre menu, vous atterrissez directement ici en 0x10CC. C'est comme cela que cette valeur est déterminée. Les deux valeurs suivantes sont en AC et sur 4 bytes le "BaseOfCode" qui vaut 0010 0000 (lire 0x1000) et en B0, sur 4 bytes aussi, le "BaseOfData" qui vaut lui 0050 0000 (lire 0x5000). Le "BaseOfCode" est l'adresse (l'offset) du début de la section du code (c'est à dire la section .text), et "BaseOfData" l'offset du début de la section .data. Ce renseignement n'est pas très utile ici. Il vaut mieux utiliser les données de la déclaration des sections qui sont plus précises. Ainsi se termine la partie standard, avec la déclaration du "program entry point" pour le plus important. Pour la partie additionnelle NT, voyons ce qu'il y a dedans... On commence en B4 sur 4 bytes avec l'"ImageBase" qui vaut 0x400000. Cette valeur est définie lors de la compilation par le linker. Elle peut être bien sur différente, mais elle vaut 0x400000 dans beaucoup de cas. Sa signification? Quand on lance le programme en mémoire (i.e. quand on le démarre), celui-ci va avoir une adresse relative (RVA ou Relativ Virtual Offset) qui est différente de son adresse (Virtual Offset) dans l'exe. Je prends tout de suite un exemple. Vous avez déjà visualisé un soft sous windasm et sous un hexediteur. Sous windasm, toutes les adresses des lignes de code sont du type 0x40XXXX. Et quand on cracke par exemple, on se réferre dans la barre en bas de windasm pour avoir l'adresse équivalente sous l'éditeur hexa. La différence entre ces 2 valeurs est donnée par la formule citée (beaucoup) plus haut à l'"ImageBase" près. Ensuite il y a des détails sur l'alignement des sections et du fichier de 0xB8 à 0xBF. Chacune valant 0x1000. On passe. De même de 0xC0 à 0xCA , il y a tout un tas de renseignements sur les versions du SE, du programme (notepad)... En 0xCB sur 4 bytes, il y a une valeur réservée. La valeur suivante en 0xD0 sur 4 bytes qui vaut 00F0 0000 (lire 0xF000) est la taille de l'exe, c'est à dire la somme de la taille de toutes les sections (padding compris). Cette valeur s'appelle "SizeOfImage". Dans l'exercice d'ajout d'une nouvelle section plus haut (avec .anubis), on avait eu à changer cette valeur. Elle est définie ici. En 0xD4 sur 4 bytes se trouve le "SizeOfHeaders" qui vaut 0004 0000 (lire 0x0400). Il correspond à la somme de tous les en-têtes. En 0xD8, aussi sur 4 bytes, il y a le checksum 8918 0100 (lire 0x00011889) du fichier .exe. Seul le linker vérifie cette valeur. La changer n'influe pas sur le lancement de l'exe. La valeur 0200 en 0xDC qui vaut 02 sur 2 bytes est le "Subsystem". Cela indique de quelle manière doit tourner le programme. Les différentes possibilités sont énumérées ci-dessous :
Donc comme notre valeur vaut 2, on tourne sous une application windows utilisant le Graphic User Interface (GUI). Ce qui est bien le cas avec le notepad. Ouf, on est sauvé, tout va bien. Et pour tout ce qui est application en mode DOS, on aura un 3 à la place du 2. En 0xDE sur 2 bytes (0000) est ce qu'on appelle le "DllCharacteristics". Ce sont des propriétés pour les Dlls du programme :
Ensuite on a 4 valeurs en rapport avec la pile lors de l'execution du soft par le processeur. Ces valeurs sont "SizeOfStackReserve", "SizeOfStackCommit", "SizeOfHeapReserve", "SizeOfHeapCommit". Elles font chacunes 4 bytes et sont localisées de 0xE0 à 0xEF. En 0xF0 sur 4 bytes, il y a le LoaderFlags qui est une option pour le chargement du programme. Cette valeur est à 0000 si rien de particulier est demandé comme ici. Ensuite en 0xF4 sur 4 bytes, la valeur "NumberOfRvaAndSizes" en F4 qui vaut 0x10 (soit 16 en décimal). Il s'agit de la longueur du tableau "DataDirectory" qui suit. Cette valeur permet de connaître la taille du tableau, mais ne renseigne pas sur les entrées valides du tableau. Le "DataDirectory" est un tableau de 16 valeurs qui donnent diverses informations sur différentes parties de l'exe. Ce tableau s'étend de 0xF8 (inclus) à 0x17C (exclus), et toutes les entrées sont en WORD soit 4 bytes. Un rapide coup d'oeil sur les entrées du tableau donne :
VA = Virtual address Hormis IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT, IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR et la 16ème entrée qui est toujours à zéro, ce tableau est celui que nous donne procdump en cliquant sur "Directory" dans la partie "Structures editor" du PE editor. Dans tous ces renseignements, il est important de voir que l'on trouve ici l'IAT ou ImportAddressTable en 0x62E0 sur 0x240 bytes. En comparant avec les valeurs des sections, on trouve que la IAT est inclue dans .idata. L'IAT est une table qui contient des adresses pour appeler les fonctions dans les DLLd. Pour comprendre en détail la structure de l'IAT, il faut regarder en même temps la déclaration de la section .idata, l'import image data directory, l'entête optionnel et la section .idata elle-même. Toutes les entrées
non utilisées sont à zéro. Les champs renseignés
et donc utilisés dans le cas du notepad sont: On termine ici le
"NT additional field". Juste après toutes ces valeurs
du "DataDirectory" se trouve la déclaration des sections
(.text, .data, ...) Le bloc de 0x28 bytes de la déclaration de la fonction est défini comme suit : typedef struct _IMAGE_SECTION_HEADER { UCHAR Name[IMAGE_SIZEOF_SHORT_NAME]; Pour définir une section, il y a donc dans l'ordre le nom (8 bytes), la Virtual Size (4 bytes), la VirtualAddress (4 bytes), la SizeOfRawData ou Raw Size (4 bytes), le PointerToRawData ou Raw Offset (4 bytes), le PointerToRelocations (4 bytes), le PointerToLinenumbers (4 bytes), le NumberOfRelocations (2 bytes), le NumberOfLinenumbers (2 bytes), les Characteristics (4 bytes). Soit au total 0x28 ou 40 bytes! Comme on peut le voir aussi dans le cas du notepad, PointerToRelocations, PointerToLinenumbers, NumberOfRelocations et NumberOfLinenumbers ne sont jamais utilisés pour le PE d'un .exe, mais seulement celui d'un .obj. Ensuite, on a du padding jusqu'au début de la première section. On peut y écrire n'importe quoi dedans sans qu'il y ait vraiment d'interférences dans l'execution du programme. Voilà pour l'entête PE dans sa totalité! Pour résumer, voici les points importants où il faut avoir l'oeil quand on regarde un exe dans un editeur hexadécimal (les offsets sont relatifs et dépendent du pointeur en 0x3C) :
Bon, on pourrait bien sûr rajouter quelques valeurs, mais cela dépend de chacun et ce tableau est déjà bien complet :) On va maintenant regarder les blocs que j'ai affichés en intro, à savoir les différentes sections de l'exe. Il y a 9 sections de base. Elle ne figurent pas forcément toutes dans l'exe. Ensuite, on peut définir des sections "personnelles" (dans le cas d'un ajout de section à la main), des sections dûes à des compacteurs (UPX, ...), bref les possibilitées sont variées. Les 9 sections de
bases sont : On va les voir en détail. .text Cette section est celle qui contient le code de l'executable et en principe, uniquement du code. Ce code est bien sûr executable dans ses propriétés (voir les "Characteristics" de cette section plus haut). Cette section porte parfois le nom .code ou CODE. Elle contient le PEP définit dans l'en-tête PE. Pour illustrer la section, en voici un extrait tel que je l'ai montré au début de ce tut : 000017D0 7421 83F8
0274 1C83 F805 7417 83F8 0774 t!...t....t....t Cette section commence en 0x1000 et se termine en 0x5000 (padding inclus). Elle doit être 0x60000020 en caractéristiques, cad : lue et executée (0x60000000) comme du code (0x00000020). Les sections data Il existe plusieurs types de sections data. Tout dépend du type de données qu'elles contiennent. Par rapport à la liste ci-dessus, il y a: .bss, .rdata, .data, .edata, .idata, .pdata Je rappelle que toutes les adresses pour les début et fin de sections concernent le Notepad, fournit avec ce tut. .data Cette section peut aussi s'appeler DATA. Elle contient d'une manière générale toutes les données qui ne sont pas dans les autres "data" sections. Dans le cas du notepad, on a les fonctions utilisées qui sont propres au notepad : 00005200 7900 0000
0000 0000 6C66 5069 7463 6841 y.......lfPitchA Cette section commence en 0x5000 et finit en 0x6000. Elle doit être 0xC0000040 en caractéristiques, cad : lecture et écriture (0xC0000000) de données initialisées (0x00000040). .bss Cette section concerne les données non initialisées (uninitialized data) comme par exemple "int i;". Elle peut aussi s'appeler BSS. .rdata Cette section concerne les données en lecture seule telles que les chaines littérales, les constantes ou les information de débugage. Le "r" signifie Read Only. .edata Section qui contient les données exportées (exported symbols). On trouve ces données plus généralement dans les Dlls plutôt que dans les executables (ce qui n'est quand même pas impossible). On trouvera dans cette section les entry point des fonctions exportées. Souvent, cette section est intégrée dans la section .data. La structure de l'"export table" (IMAGE_EXPORT_DIRECTORY) comprends un entête et la donnée exportée. typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; Il n'y a pas de section .edata dans le notepad. .idata Section contenant les "imported data". Ce sont des "import modules" et des "fonctions names", comprendre des Dlls et des APIs. Celles-ci sont d'abord représentés par leurs RVA (Relativ Virtual Address). L'"import directory" devrait résider dans une section qui est "initialized data" et "readable". C'est un tableau de
IMAGE_IMPORT_DESCRIPTORs, une pour chaque Dll utilisée. La liste
est terminée par un IMAGE_IMPORT_DESCRIPTOR, rempli de 00. Pour
illustrer, un IMAGE_IMPORT_DESCRIPTOR possède la structure suivante
: union { typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR; Le nom des APIs Win32 figure toujours AVANT le nom de la Dll à laquelle elle est ratachée. Exemple : dans la Dll GDI32.dll se trouve l'API "CreateDCA". 00006CD0 7445 7874 4578
0000 C600 4765 7454 6578 tExtEx....GetTex Ces APIs sont utilisées par le notepad pour son fonctionnement (gestion des boites de dialogues, changement de la police...). La section commence en 0x6000 et fini en 0x7000, padding compris. Elle est en 0x40000040 en caractéristique, cad : la section peut être lue (0x40000000) et contient des données initialisées (0x00000040). .pdata Section non dévelopée dans le cas du notepad. Se reporter à .data. .debug Les informations relatives au débugage sont normalement placées dans cette section. Le format PE supporte aussi des fichiers débugages séparés. Ils ont alors l'extension ".DBG". La section .debug contient les informations de débugage, mais les "debug directories" sont dans la section .rdata sous IMAGE_DEBUG_DIRECTORY. typedef struct _IMAGE_DEBUG_DIRECTORY { ULONG Characteristics; La section est divisée en plusieurs portions de données représentant différents types d'informations de débugage. Les différentes possibilités sont : #define IMAGE_DEBUG_TYPE_UNKNOWN
0 Il n'y a pas de section debug dans le cas du notepad. .rsrc (Cette section sera dévelopée plus en détail dans un prochain Mémento avec exemple à l'appui) Cette section contient
tout ce qui est appelé ressources. On entend par là tout
ce qui est icones, menus, strings,... Cette section est en général
assez longue, et est structurée sous forme d'arbre/arborescence.
Chaque ressource est définie par son nom ou son ID, ainsi que par
un numéro qui lui permet d'être atteinte dans l'arbre. Pour décrire
cette section, on doit considérer tous les éléments
qui la constituent comme une arborescence. Le Ressource Directory commence en 0x7000 (d'après la valeur ResourceDirectory VA en 0x108) et se termine en 0x7000 + 0x558C = 0xC58C (d'après la valeur ResourceDirectory Size en 0x10C). On remarque d'ailleurs un joli padding à la fin : 0000C550 6100 7200
4600 6900 6C00 6500 4900 6E00 a.r.F.i.l.e.I.n. La racine et les branches de cette arborescence sont dictées par l'IMAGE_RESOURCE_DIRECTORY. typedef struct _IMAGE_RESOURCE_DIRECTORY { DWORD Characteristics; Pour prendre un exemple : En 0x7324, il y a une entrée du Resource Directory... : 00007320 0000 0000
9052 2900 0400 0000 0000 0100 .....R)......... ...qui correspond à l'icone déclarée en 0x7478 et dont le dessin commence en 0x74EA : 00007470 E404 0000
0000 0000 2800 0000 1000 0000 ........(....... Les entrées du Resource Directory commencent par "R)" dans la partie ascii de l'éditeur hexa. Comme resources facilement identifiables, il y a les icones qui sont la plupart du temps repérables grace aux "dessins symétriques" dans la partie ascii, et aux valeurs hexa "uniformes" dans la partie hexadécimale. En fait les dessins des icones sont codées par petits carrés, chacun avec un code hexa pour déterminer la couleur. Toutes les ressources ont un entête qui commence avec "(" dans la partie ascii (soit 0x2800 dans la partie hexa comme en 0x7478). Ainsi pour repérer l'entrée dans le Resource Directory, il suffit de regarder l'offset de "(" (0x2800) et de faire une recherche de chaine hexa sur cette valeur. Obligatoirement, on tombe sur l'offset enregistré dans l'entrée du Resource Directory. Par exemple, de 0x7000 à 0x8300, on a le Resource Directory suivit des icones. En 0x7324 se trouve le dernier "R)" qui déclare entre autres les icones. Est d'abord définie, l'offset (5e colonne avec 7874 pour 0x7478 comme 1ere valeur), puis la taille (2801 dans la 7e colonne, lire 0x0128, donc l'item suivant commence en 0x128 + 0x7478 = 0x75A0 (en dessous de 7874 dans la 5e colonne), puis la caractéristique de l'item avec la colonne de E404 dans la 1ere colonne. 00007320 0000 0000 9052
2900 0400 0000 0000 0100 .....R)......... En 0x9FB8 commence la déclaration des menus (voir ci dessus en 0x73B8)... : 00009FA0 C000 0000
E000 3300 E000 6600 F000 9900 ......3...f..... ...suivis des strings à partir de 0xA384 jusqu'en 0xC0D0... : 0000A570 8000 4F00
7200 6900 6500 6E00 7400 6100 ..O.r.i.e.n.t.a. En 0xC200 se trouve les infos "maison" du programme (qui l'a fait, quelle version, date, copyright...). Bien entendu, si vous utilisez un soft genre exescope, vous avez acces à toutes ces infos et vous pouvez les modifier à loisir. Tous les changements d'offsets sont en principe recalculés par exescope & co pour que l'exe soit toujours "cohérent". Quelques détails sur la structure elle-même du Resource Directory : Le Resource Directory consiste en deux comptes. Le premier est le nombre d'entrées au début du tableau ayant un nom associé avec chaque entrées. Le second est le nombre d'entrées qui suit directement l'entrée nommée. Ce second compte identifie le nombre d'entrées qui ont des IDs de 16 bits en tant que nom. Cette structure permet de rechercher rapidement un item par nom ou nombre (le ou étant exclusif, cad qu'on doit chercher par l'un ou l'autre, mais pas par les deux). Les champs NumberOfNamedEntries et NumberOfIdEntries indiquent le nombre d'entrées attachées au Directory. Les entrées nommées apparaissent en premier en ordre alphabétique croissant, suivies des IDs en ordre numérique croissant. Une entrée est représentée par les deux champs suivant : typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY { ULONG Name; L'ID dépend de la position de l'entrée dans l'arborescence. Si c'est un nombre, son bit haut est vide, sinon c'est un nom et son bit haut est allumé. Dans ce cas-là, les 31 bits bas représentent l'offset du début de la raw data de la section ressource. Une fois les renseignements de la ressource atteints, on a ses caractéristiques suivantes : typedef struct _IMAGE_RESOURCE_DATA_ENTRY { ULONG OffsetToData;
// position de la ressource (en RVA) Puis, étant dans le répertoire racine, on a une représentation de ce type (ces ressources sont prédéfinies) : #define RT_CURSOR
MAKEINTRESOURCE(1) Tout autre nombre est défini par l'utilisateur. A un niveau plus bas dans l'arborescence, l'ID est son nom ou son nombre. Pour décrypter les IDs, il faut les couper en deux groupes de bits de 0 à 9 et de 10 à 15. Les valeurs sont définies dans le fichier "winresrc.h". La section ressources commence en 0x7000 et termine en 0xD000. Elle est en 0x40000040 en caractéristique, cad : la section peut être lue (0x40000000) et contient des données initialisées (0x00000040). .reloc Quelques mots sur cette section... Son nom signifie relocation. Cette section contient les données nécessaires pour permettre au loader de charger l'exe en mémoire à une adresse autre que celle définie en ImageBase (recalculation des offsets). Cette section est définie aussi dans le DataDirectory. Elle commence en 0xD000 et termine en 0xE000. Elle est en 0x42000040 en caractéristique, cad : la section peut être lue (0x40000000) et contient des données initialisées (0x00000040). Deplus, la section peut être annulée à volonté (0x02000000). (--- fin des sections ---) En résumé, on peut retenir les points suivants importants pour les sections :
Pour finir, voici un schéma de la structure du Notepad, et plus généralement d'un .exe :
Vous devriez y voir plus clair maintenant. Regardez par exemple sous wdasm le debut du dead listing du notepad jusqu'au PEP (program entry point). Il n'y a pas tant que ça à lire, et vous allez voir que beaucoup d'informations sont bien mieux "lisibles"... A partir de ce tut, vous avez une connaissance générale sur la structure d'un exe sous windows. Dans les prochains tuts, lorsqu'on bidouillera dans les offsets, ajoutera des sections, recalculera certains élément pour reconstruire le nouvel .exe après transformation, vous saurez à quoi correspond les termes utilisés et où les trouver. Il y a énormément à dire sur le PE en général. Plutôt que de vraiment entrer dans les détails et d'écrire un bouquin (qui existe déjà du reste), j'ai voulu à travers ce tut donner une vision globale de la structure d'un exe tel qu'on le visualise sous un éditeur hexadécimal. Les parties plus spécifiques seront développées dans des prochains tuts avec des exemples détaillés. Je voudrais terminer ce tut par une petite note mondaine :) On touche là un domaine de la programmation "windows" qui a été merveilleusement bien décrite par Matt Pietrek dans son livre Windows 95 System Programming Secrets (IDG Books, 1995). Ce livre est aujourd'hui épuisé et plus ou moins introuvable. En quelques mots, ce livre explique *comment* marche le systeme d'exploitation Windows en explorant les dll, les vxd, la structure des fichiers.... Ce n'est pas un livre qui explique la programmation sous windows. Matt Pietrek était (entre autres) directeur de projet à Numega Technologies. Voici un petit extrait de son parcours : "I joined NuMega in 1993, after being laid off from Borland. I certainly was not one of the founders, but was well within the first 10 people. When I joined, there were the two founders, two programmers (one of them off-site), an office manager, and a sales/marketing guy. A few more great people joined shortly after I did, but it was still pretty small for the first two years. The two founders, Jim and Frank, are great guys. I still see them semi-regularly, and live in the same small town as they do. I wasn’t even formally interviewed. I just talked to them on the phone on two separate occasions, with most of conversation being about what I’d do to improve SoftIce. I was totally shocked when they called with a job offer. I certainly felt under qualified. The great thing about the early days of NuMega was that it was a very family oriented company. It seemed that all the families knew each other, and we were always doing things together. We didn’t work horrible hours either, like most startups do these days. We were all pretty good at what we did, and we worked pretty normal hours (say, 9-5). If you have good products, and a high barrier to entry, you don’t have the same pressures that it seems everybody’s under today. As I recall, we didn’t even bother to track vacation time. Everybody just intrinsically knew that as long as the job got done, having a lot of formal procedures just got in the way. When I joined, the primary products were SoftIce for DOS & Windows, and BoundsChecker for DOS. I jumped right into the middle of the first BoundsChecker for Windows release (you have to recall, in 1993, this would have been for 16 bit Windows 3.1) My first big job was to write the parameter validation system. The team was basically Frank, the SoftIce for Windows guru, and myself. You wouldn’t believe how much of the code was stolen from the DOS products." Sa page web est là : http://www.wheaty.net Il y a des trucs de
fous dessus!! Valaaaaa, le prochain coup on se tappera un vxd ou quelque chose d'équivalent :) Si vous êtes encore debout après ce tut, ou bien vous êtes venu lire directement la fin, ou bien je vous conseille d'aller mettre la tête dans le congélateur pour vous raffraichir...
|