Afin de ne pas sombrer dans l'ennui, la paresse et le "j'fous rien", je décide de me mettre - moi aussi - à l'étude du format PE. Certes, on retrouve cette documentation un peu partout, mais un des meilleurs moyens (pour ma part) d'assimiler tout ça n'est autre que de rédiger un article avec une démarche, je l'espère, précise et très explicative.
Parlons donc du format PE (un truc dont on ne parlera jamais en cours, c'est bien dommage). Les initiales signifient, respectivement "Portable Executable". Si on fait un cours d'histoire, on va résumer les grandes lignes : c'est un format qui définit l'architecture des programmes microsoft, ainsi que des DLLs voire des pilotes de matériels. Le "Portable" signifie, ici, que les données peuvent être exécutées d'un système à un autre.
En gros, sans vouloir la jouer compliquer, ce put*** de format PE spécifie les programmes qui se terminent en .exe que nous utilisons souvent (voilà, c'était simple).
Vite s'armer (au cas où)
Je vous ai préparé un éditeur hexadécimal, un exécutable accompagné de sa source et de son dump pour faciliter la pratique. Pour l'exécutable, comme vous l'aurez compris, j'ai pas cherché à faire compliqué !
Analysons en douceur les premières lignes :
00000000: 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00 MZ..........ÿÿ..
00000010: B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ¸.......@.......
00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000030: 00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 00 ............€...
00000040: 0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 68 ..º..´.Í!¸.LÍ!Th
00000050: 69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F is program canno
00000060: 74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53 20 t be run in DOS
00000070: 6D 6F 64 65 2E 0D 0D 0A 24 00 00 00 00 00 00 00 mode....$.......
Cette en-tête est peu intéressante par rapport à ce qui nous attend, mais il convient de l'analyser. En premier, on a la suite d'octets 'MZ' qui correspond au "mot magique", c'est-à-dire à la signature de l'en-tête DOS. Effectivement, si nous essayons de lancer ce programme en mode DOS - ce n'est pas ce qu'on veut - alors le programme se basera sur MZ et la suite...
Si vous ne comprenez pas la notion de signature ou mot magique, dites-vous que certains autres formats ont leur propre mot magique. Les fichiers bitmap on 'BM', les archives zip on 'PK', les sons wav on 'RIFF', etc...
Ce morceau de code, si lancé dans un environnement DOS, se contentera d'afficher gentiment "This program cannot be run in DOS mode" grâce au service 9 de l'interruption 0x21 qui permet d'afficher un message à l'écran. En clair, il veut dire "Mais espèce de sac-à-foutre, change d'OS et prends Windows 9x voire NT si t'es pas ringard !". Je sais, c'est brusque, mais je suis pas là pour faire des paroles mystérieuses et intellectuelles comme les grands philosophes. Naturel, quoi.
Je dérive, je dérive... Continuons. A l'offset 0x3C, on a une valeur DWORD (Double Word = double mot = 4 octets) qui correspond à l'offset absolu du header PE, celui qui nous intéresse. Ici, on a 0x80 ; ça signifie que notre en-tête PE commence à l'offset 0x80 (sans déconner ?).
Voyons ça !
00000080: 50 45 00 00 4C 01 05 00 38 5B CE 49 00 16 00 00 PE..L...8[ÎI....
00000090: CD 01 00 00 E0 00 07 03 0B 01 02 38 00 0A 00 00 Í...à......8....
000000A0: 00 12 00 00 00 02 00 00 80 12 00 00 00 10 00 00 ........€.......
000000B0: 00 20 00 00 00 00 40 00 00 10 00 00 00 02 00 00 . ....@.........
000000C0: 04 00 00 00 01 00 00 00 04 00 00 00 00 00 00 00 ................
000000D0: 00 60 00 00 00 04 00 00 97 C8 00 00 03 00 00 00 .`......—È......
000000E0: 00 00 20 00 00 10 00 00 00 00 10 00 00 10 00 00 .. .............
000000F0: 00 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 ................
Apparemment, notre compte est bon. On voit un mot magique "PE" en tout début. La spécification du format PE exige que le mot magique soit codé sur 4 octets et contienne octet pour octet PE#0#0, soit 'PE' suivi de deux octets nuls (ou null bytes). Pour preuve, essayez de mettre autre chose à la place des deux zéros, vous risquerez d'obtenir une cochonnerie du genre :
C:\DOCUME~1\Geoffrey\C\sources>hello
This program cannot be run in DOS mode.
C:\DOCUME~1\Geoffrey\C\sources>
This program cannot be run in DOS mode.
C:\DOCUME~1\Geoffrey\C\sources>
Comme par hasard ! Dans le cas où l'header aurait été valide, on aurait eu tout autant un résultat pourri et inutile qu'est mon "Hello, world!" de la mort qui tue. T'peux pas test.
Après ce fameux mot magique, on a d'autres informations :
[*] 0x80+4 : WORD (2 octets) = type d'architecture de la machine. Ici, on a 0x014C, qui correspond apparemment à I386.
[*] 0x80+6 : WORD (2 octets) = nombre de sections dans le programme.
On va pas s'attarder sur les sections, mais en gros, ça correspond au .data, .bss, .text si vous faites de l'assembleur. La section .data contient des ressources (données basiques) dont le programme peut avoir besoin. Le .bss représente un espace mémoire prêt à recevoir des données (du moins, en asm), et le .text représente votre page de code qui sera exécutée. Là, y'a 5 sections, mais on verra ça plus tard (à vrai dire, je me suis pas plongé dedans, je fais tout en live et ça me forme).
[*]0x80+8 : DWORD (4 octets) = timestamp correspondand à l'heure de compilation du programme. Faudrait essayer, tiens. Ici, on a 0x49CE5B38, soit 1238260536. Que nous donne le script php suivant ?
<?php
echo "Programme compile le ".date('d/m/Y', 1238260536)." a ".date('H:i:s', 1238260536)."\n";
?>
==>
C:\DOCUME~1\Geoffrey\C\sources>php check_timestamp.php
Programme compile le 28/03/2009 a 18:15:36
Programme compile le 28/03/2009 a 18:15:36
Bordel ! Maintenant, je me souviens que j'avais fait ce fameux "Hello, world!" pour tester la réinstallation de mon environnement CodeBlocks. Ça marche vraiment, hohoho !
(Euh, Geo, et si on passait à la suite ? - D'accord)
Pour la suite, on a des données qui parlent de tables des symboles et d'en-têtes optionnelles du PE. Mais en parler ferait trop pour un Part I, et sachant que je débute depuis peu. Ca me fait mal au cerveau et je préfère y aller en douceur (quoi ? Je suis un noob ? mais euh...).
On va se quitter avec un programme de merde qui :
- Ouvre un fichier exe ;
- Dit si c'est un exe valide (en se basant sur la lecture du mot magique au début de l'en-tête PE) ;
- si c'est un exe valide, affiche son architecture (ici, que pour i386), son nombre de sections et sa date de compilation.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define UTILISATION printf("Utilisation : %s\n", argv[0]); \
exit(EXIT_FAILURE);
#define OFFSET_E_LFANEW 0x3C // L'offset absolu de l'en-tête NT
#define SIG_VALID_IMAGE_NT_HEADER 0x00004550 // 'PE#0#O' est la signature valide
int main(int argc, char **argv) {
if(argc < 2) {
UTILISATION
}
int sig,
offset_nt,
archi,
nsec,
tstamp;
FILE *fpe;
if((fpe = fopen(argv[1],"rb")) == NULL) {
printf("Impossible d'ouvrir l'executable %s !\n", argv[1]);
exit(EXIT_FAILURE);
}
fseek(fpe, OFFSET_E_LFANEW, SEEK_SET);
fread(&offset_nt, sizeof(int), 1, fpe);
fseek(fpe, offset_nt, SEEK_SET);
fread(&sig, sizeof(int), 1, fpe);
if(sig == SIG_VALID_IMAGE_NT_HEADER) {
printf("Executable valide !\n");
// On vérifie le type d'architecture
fread(&archi, sizeof(short int), 1, fpe);
// On lit le nombre de sections
fread(&nsec, sizeof(short int), 1, fpe);
// On lit le timestamp de compilation
fread(&tstamp, sizeof(int), 1, fpe);
if((short)archi == 0x014C) {
printf("[*] Architecture : i386\n");
}
printf("[*] Nombre de sections : %d\n"
"[*] Timestamp = %d - %s\n", (short)nsec,
tstamp, asctime(localtime((time_t*)&tstamp)));
} else {
// Si mot magique différent de 'PE#0#0'
printf("Executable invalide !\n");
}
return EXIT_SUCCESS;
}
(Source véritable : http://venom630.free.fr/geo/autre_chose/etude_pe/checkvalidpe_c.txt)
J'ai pas utilisé de structures, car j'aurais fait face à un souci de Data Structure Alignment ; un bon article expliquant ce souci a été écrit par Smoke : c'est ici. J'ai donc utilisé des variables individuelles, mais on se fiche de ça un peu. Ouais, ok, j'aurais pu utiliser windows.h pour me servir des types WORD et DWORD, mais on s'en fout aussi.
Conclusion
Je débute, et remerciement à Ivanlef0u qui m'a conseillé de partir de là si je voulais me tâter un peu sur la compréhension de ouinedouze ; je remercie aussi ceux qui me soutiennent (normal).
Et petit coucou à NiklosKoda qui galère sur son billard en pascal.
Geo

