Le format PE (Portable Executable) est le format utilisé sur Windows pour organiser les fichiers exécutables ainsi que les fichiers objets, donc, chaque .exe et .dll (pour ne citer qu’eux) sont créés selon ce format. Maintenant nous allons voir comment analyser ces deux types de fichiers pour obtenir toutes les informations utiles qui nous sont disponibles.
Voici un schéma représentant la structure d’un fichier PE :
+--------------------+ | Section n | +--------------------+ | ... | +--------------------+ | Section 2 | +--------------------+ | Section 1 | +--------------------+ | Tableau de | tableau de | section header | IMAGE_SECTION_HEADER +--------------------+----------------------- | PE Optional header | IMAGE_OPTIONAL_HEADER +--------------------+ | PE File header | IMAGE_FILE_HEADER IMAGE_NT_HEADERS +--------------------+ / | PE Signature | PE\0\0 / +--------------------+-----------------------/ | | +--------------------+ | MS-DOS header | IMAGE_DOS_HEADER +--------------------+ offset 0
L’en-tête MS-DOS (IMAGE_DOS_HEADER) est là pour permettre au format PE de rester compatible avec DOS, nous allons nous occuper seulement de deux des champs de cette structure :
– Le champ e_magic qui doit être à IMAGE_DOS_SIGNATURE ou encore 0x5A4D qui signifie MZ (les initiales de Mark Zbikowski : l’un des développeurs MS-DOS) et qui sert à vérifier que le fichier est bien valide.
– Le champ e_lfanew qui est l’offset vers la structure IMAGE_NT_HEADERS. Il regroupe donc la PE Signature, le PE File header et le PE Optional header.
Toutes les structures concernant le format PE se trouvent dans le fichier « winnt.h ».
Nous allons commencer par chercher puis afficher les informations qui nous sont disponibles dans la structure IMAGE_FILE_HEADER, ceci grâce à deux fonctions en C : fread() et fseek(). Il faudra lire IMAGE_DOS_HEADER, puis aller à l’offset e_lfanew et enfin lire IMAGE_NT_HEADERS.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | #include <windows.h> #include <stdio.h> #include <time.h> typedef struct { WORD flag; PCHAR name; }CHARACTERISTICS; CHARACTERISTICS arrCharacteristics[] = { {IMAGE_FILE_RELOCS_STRIPPED, "RELOCS_STRIPPED"}, {IMAGE_FILE_EXECUTABLE_IMAGE, "EXECUTABLE_IMAGE"}, {IMAGE_FILE_LINE_NUMS_STRIPPED, "LINE_NUMS_STRIPPED"}, {IMAGE_FILE_LOCAL_SYMS_STRIPPED, "LOCAL_SYMS_STRIPPED"}, {IMAGE_FILE_AGGRESIVE_WS_TRIM, "AGGRESIVE_WS_TRIM"}, {IMAGE_FILE_LARGE_ADDRESS_AWARE, "LARGE_ADDRESS_AWARE"}, {IMAGE_FILE_BYTES_REVERSED_LO, "BYTES_REVERSED_LO"}, {IMAGE_FILE_32BIT_MACHINE, "32BIT_MACHINE"}, {IMAGE_FILE_DEBUG_STRIPPED, "DEBUG_STRIPPED"}, {IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP, "REMOVABLE_RUN_FROM_SWAP"}, {IMAGE_FILE_NET_RUN_FROM_SWAP, "NET_RUN_FROM_SWAP"}, {IMAGE_FILE_SYSTEM, "SYSTEM"}, {IMAGE_FILE_DLL, "DLL"}, {IMAGE_FILE_UP_SYSTEM_ONLY, "UP_SYSTEM_ONLY"}, {IMAGE_FILE_BYTES_REVERSED_HI, "BYTES_REVERSED_HI"} }; void DumpImageFileHeader(IMAGE_FILE_HEADER iFileHeader); int main(int argc, char *argv[]) { IMAGE_DOS_HEADER iDosHeader; IMAGE_NT_HEADERS iNtHeaders; FILE *pfile = NULL; if(argc != 2) { printf("USAGE: DumpImageFileHeader.exe <fichier>\n"); printf("EXAMPLE: DumpImageFileHeader.exe C:\\Windows\\System32\\kernel32.dll\n"); printf("\n"); system("PAUSE"); exit(1); } pfile = fopen(argv[1], "rb"); if(pfile == NULL) { printf("ERREUR : impossible d'ouvrir %s\n", argv[1]); exit(1); } // On lit l'en-tête DOS : fread(&iDosHeader, sizeof(IMAGE_DOS_HEADER), 1, pfile); // On check la signature : if(iDosHeader.e_magic != IMAGE_DOS_SIGNATURE) { printf("ERREUR : le fichier n'est pas valide!\n"); exit(1); } // On se positionne à l'offset de l'en-tête NT : fseek(pfile, iDosHeader.e_lfanew, SEEK_SET); // On lit l'en-tête NT : fread(&iNtHeaders, sizeof(IMAGE_NT_HEADERS), 1, pfile); // On check la signature : if(iNtHeaders.Signature != IMAGE_NT_SIGNATURE) { printf("ERREUR : le fichier n'est pas valide!\n"); exit(1); } DumpImageFileHeader(iNtHeaders.FileHeader); return 0; } void DumpImageFileHeader(IMAGE_FILE_HEADER iFileHeader) { // Machine if(iFileHeader.Machine == IMAGE_FILE_MACHINE_I386) printf("[*] Machine: %.4X (x86)\n", iFileHeader.Machine); else if(iFileHeader.Machine == IMAGE_FILE_MACHINE_AMD64) printf("[*] Machine: %.4X (x64)\n", iFileHeader.Machine); else printf("[*] Machine: %.4X (ni x86 ni x64)\n", iFileHeader.Machine); printf("\n"); // NumberOfSections printf("[*] NumberOfSections: %hu\n", iFileHeader.NumberOfSections); printf("\n"); // TimeDateStamp printf("[*] TimeDateStamp: %lu\n", iFileHeader.TimeDateStamp); printf(" %s", ctime((time_t*)&iFileHeader.TimeDateStamp)); printf("\n"); // PointerToSymbolTable printf("[*] PointerToSymbolTable: %.8X\n", iFileHeader.PointerToSymbolTable); printf("\n"); // NumberOfSymbols printf("[*] NumberOfSymbols: %lu\n", iFileHeader.NumberOfSymbols); printf("\n"); // SizeOfOptionalHeader printf("[*] SizeOfOptionalHeader: %hu\n", iFileHeader.SizeOfOptionalHeader); printf("\n"); // Characteristics printf("[*] Characteristics: %.4X\n", iFileHeader.Characteristics); for(unsigned u = 0; u < sizeof(arrCharacteristics) / sizeof(CHARACTERISTICS); u++) { if(iFileHeader.Characteristics & arrCharacteristics[u].flag) { printf(" %s\n", arrCharacteristics[u].name); } } } |
Maintenant, occupons-nous de lister toutes les informations concernant chaque section. Le tableau des sections se trouve juste après IMAGE_NT_HEADERS et dans le code précédent nous avons obtenu le nombre de sections disponibles, nous savons donc combien de IMAGE_SECTION_HEADER il faut lire.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | #include <windows.h> #include <stdio.h> typedef struct { DWORD flag; PCHAR name; }CHARACTERISTICS; CHARACTERISTICS arrCharacteristics[] = { {IMAGE_SCN_TYPE_NO_PAD, "TYPE_NO_PAD"}, {IMAGE_SCN_CNT_CODE, "CNT_CODE"}, {IMAGE_SCN_CNT_INITIALIZED_DATA, "CNT_INITIALIZED_DATA"}, {IMAGE_SCN_CNT_UNINITIALIZED_DATA, "CNT_UNINITIALIZED_DATA"}, {IMAGE_SCN_LNK_OTHER, "LNK_OTHER"}, {IMAGE_SCN_LNK_INFO, "LNK_INFO"}, {IMAGE_SCN_LNK_REMOVE, "LNK_REMOVE"}, {IMAGE_SCN_LNK_COMDAT, "LNK_COMDAT"}, {IMAGE_SCN_NO_DEFER_SPEC_EXC, "NO_DEFER_SPEC_EXC"}, {IMAGE_SCN_GPREL, "GPREL"}, {IMAGE_SCN_MEM_FARDATA, "MEM_FARDATA"}, {IMAGE_SCN_MEM_PURGEABLE, "MEM_PURGEABLE"}, {IMAGE_SCN_MEM_16BIT, "MEM_16BIT"}, {IMAGE_SCN_MEM_LOCKED, "MEM_LOCKED"}, {IMAGE_SCN_MEM_PRELOAD, "MEM_PRELOAD"}, {IMAGE_SCN_ALIGN_1BYTES, "ALIGN_1BYTES"}, {IMAGE_SCN_ALIGN_2BYTES, "ALIGN_2BYTES"}, {IMAGE_SCN_ALIGN_4BYTES, "ALIGN_4BYTES"}, {IMAGE_SCN_ALIGN_8BYTES, "ALIGN_8BYTES"}, {IMAGE_SCN_ALIGN_16BYTES, "ALIGN_16BYTES"}, {IMAGE_SCN_ALIGN_32BYTES, "ALIGN_32BYTES"}, {IMAGE_SCN_ALIGN_64BYTES, "ALIGN_64BYTES"}, {IMAGE_SCN_ALIGN_128BYTES, "ALIGN_128BYTES"}, {IMAGE_SCN_ALIGN_256BYTES, "ALIGN_256BYTES"}, {IMAGE_SCN_ALIGN_512BYTES, "ALIGN_512BYTES"}, {IMAGE_SCN_ALIGN_1024BYTES, "ALIGN_1024BYTES"}, {IMAGE_SCN_ALIGN_2048BYTES, "ALIGN_2048BYTES"}, {IMAGE_SCN_ALIGN_4096BYTES, "ALIGN_4096BYTES"}, {IMAGE_SCN_ALIGN_8192BYTES, "ALIGN_8192BYTES"}, {IMAGE_SCN_ALIGN_MASK, "ALIGN_MASK"}, {IMAGE_SCN_LNK_NRELOC_OVFL, "LNK_NRELOC_OVFL"}, {IMAGE_SCN_MEM_DISCARDABLE, "MEM_DISCARDABLE"}, {IMAGE_SCN_MEM_NOT_CACHED, "MEM_NOT_CACHED"}, {IMAGE_SCN_MEM_NOT_PAGED, "MEM_NOT_PAGED"}, {IMAGE_SCN_MEM_SHARED, "MEM_SHARED"}, {IMAGE_SCN_MEM_EXECUTE, "MEM_EXECUTE"}, {IMAGE_SCN_MEM_READ, "MEM_READ"}, {IMAGE_SCN_MEM_WRITE, "MEM_WRITE"} }; void GetSegmentsInfos(IMAGE_SECTION_HEADER iSectionHeader); int main(int argc, char *argv[]) { IMAGE_DOS_HEADER iDosHeader; IMAGE_NT_HEADERS iNtHeaders; IMAGE_SECTION_HEADER iSectionHeader; FILE *pfile = NULL; if(argc != 2) { printf("USAGE: GetSegmentsInfos.exe <fichier>\n"); printf("EXAMPLE: GetSegmentsInfos.exe C:\\Windows\\System32\\kernel32.dll\n"); printf("\n"); system("PAUSE"); exit(1); } pfile = fopen(argv[1], "rb"); if(pfile == NULL) { printf("ERREUR : impossible d'ouvrir %s\n", argv[1]); exit(1); } // On lit l'en-tête DOS : fread(&iDosHeader, sizeof(IMAGE_DOS_HEADER), 1, pfile); // On check la signature : if(iDosHeader.e_magic != IMAGE_DOS_SIGNATURE) { printf("ERREUR : le fichier n'est pas valide!\n"); exit(1); } // On se positionne à l'offset de l'en-tête NT : fseek(pfile, iDosHeader.e_lfanew, SEEK_SET); // On lit l'en-tête NT : fread(&iNtHeaders, sizeof(IMAGE_NT_HEADERS), 1, pfile); // On check la signature : if(iNtHeaders.Signature != IMAGE_NT_SIGNATURE) { printf("ERREUR : le fichier n'est pas valide!\n"); exit(1); } for(WORD w = 0; w < iNtHeaders.FileHeader.NumberOfSections; w++) { // On lit une structure IMAGE_SECTION_HEADER : fread(&iSectionHeader, sizeof(IMAGE_SECTION_HEADER), 1, pfile); GetSegmentsInfos(iSectionHeader); } fclose(pfile); return 0; } void GetSegmentsInfos(IMAGE_SECTION_HEADER iSectionHeader) { // Name printf("[*] Name: %s\n", iSectionHeader.Name); printf("\n"); // VirtualSize : printf("[*] VirtualSize: %lu\n", iSectionHeader.Misc.VirtualSize); printf("\n"); // VirtualAddress : printf("[*] VirtualAddress: %.8X\n", iSectionHeader.VirtualAddress); printf("\n"); // SizeOfRawData : printf("[*] SizeOfRawData: %lu\n", iSectionHeader.SizeOfRawData); printf("\n"); // PointerToRawData : printf("[*] PointerToRawData: %.8X\n", iSectionHeader.PointerToRawData); printf("\n"); // PointerToRelocations : printf("[*] PointerToRelocations: %.8X\n", iSectionHeader.PointerToRelocations); printf("\n"); // PointerToLinenumbers : printf("[*] PointerToLinenumbers: %.8X\n", iSectionHeader.PointerToLinenumbers); printf("\n"); // NumberOfRelocations : printf("[*] NumberOfRelocations: %hu\n", iSectionHeader.NumberOfRelocations); printf("\n"); // NumberOfLinenumbers : printf("[*] NumberOfLinenumbers: %hu\n", iSectionHeader.NumberOfLinenumbers); printf("\n"); // Characteristics : printf("[*] Characteristics: %.8X\n", iSectionHeader.Characteristics); for(unsigned u = 0; u < sizeof(arrCharacteristics) / sizeof(CHARACTERISTICS); u++) { if(iSectionHeader.Characteristics & arrCharacteristics[u].flag) { printf(" %s\n", arrCharacteristics[u].name); } } printf("\n"); printf(" -------------------------\n\n\n"); } |
Pour dumper une section, il suffit de se placer à PointerToRawData de la section et de lire les bytes, exemple pour dumper le segment « .text » :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | #include <windows.h> #include <stdio.h> void DumpTextSegment(IMAGE_SECTION_HEADER iSectionHeader, FILE *pfile); int main(int argc, char *argv[]) { IMAGE_DOS_HEADER iDosHeader; IMAGE_NT_HEADERS iNtHeaders; IMAGE_SECTION_HEADER iSectionHeader; FILE *pfile = NULL; if(argc != 2) { printf("USAGE: DumpTextSegment.exe <fichier>\n"); printf("EXAMPLE: DumpTextSegment.exe C:\\Windows\\System32\\kernel32.dll\n"); printf("\n"); system("PAUSE"); exit(1); } pfile = fopen(argv[1], "rb"); if(pfile == NULL) { printf("ERREUR : impossible d'ouvrir %s\n", argv[1]); exit(1); } // On lit l'en-tête DOS : fread(&iDosHeader, sizeof(IMAGE_DOS_HEADER), 1, pfile); // On check la signature : if(iDosHeader.e_magic != IMAGE_DOS_SIGNATURE) { printf("ERREUR : le fichier n'est pas valide!\n"); exit(1); } // On se positionne à l'offset de l'en-tête NT : fseek(pfile, iDosHeader.e_lfanew, SEEK_SET); // On lit l'en-tête NT : fread(&iNtHeaders, sizeof(IMAGE_NT_HEADERS), 1, pfile); // On check la signature : if(iNtHeaders.Signature != IMAGE_NT_SIGNATURE) { printf("ERREUR : le fichier n'est pas valide!\n"); exit(1); } for(WORD w = 0; w < iNtHeaders.FileHeader.NumberOfSections; w++) { // On lit une section : fread(&iSectionHeader, sizeof(IMAGE_SECTION_HEADER), 1, pfile); // Est-ce la section .text? if(!strcmp((char*)iSectionHeader.Name, ".text")) { DumpTextSegment(iSectionHeader, pfile); break; } } printf("\n"); return 0; } void DumpTextSegment(IMAGE_SECTION_HEADER iSectionHeader, FILE *pfile) { BYTE by = 0; DWORD i; unsigned u = 0; // On se place au debut de la section : fseek(pfile, iSectionHeader.PointerToRawData, SEEK_SET); for(i = 0, u = 0; i < iSectionHeader.Misc.VirtualSize; i++, u++) { // On lit byte par byte : fread(&by, sizeof(BYTE), 1, pfile); if(u == 16) { printf("\n"); u = 0; } printf("%.2X ", by); } } |
Maintenant, allons-y pour la table d’import, cette table contient les fonctions importées, c’est à dire les fonctions utilisées par un programme ou une dll mais qui ne réside pas dans ces derniers. Elles sont en fait définies dans d’autres dll. Par exemple, pour utiliser la fonction ExitProcess dans un programme, on doit l’importer de kernel32.dll ; pour la fonction MessageBox on l’importe de user32.dll.
Il est nécessaire pour lister les fonctions importées de savoir ce qu’est une RVA (Relative Virtual Address) : c’est enfaîte une adresse relative à une autre, l’adresse de base, par exemple si ImageBase est à 0x00400000 et que AddressOfEntryPoint est à 0x00001000 alors l’adresse du point d’entré de l’exécutable sera à 0x00401000.
On accède à la table d’importation grâce au champ DataDirectory de la structure IMAGE_OPTIONAL_HEADER, qui est un tableau de structure d’IMAGE_DATA_DIRECTORY contenant 2 membres : la RVA d’une structure de données et sa taille.
On trouve la structure de données qui nous intéresse, c’est à dire celle des importations, qui se nomme IMAGE_IMPORT_DESCRIPTOR, et tout cela grâce à la constante IMAGE_DIRECTORY_ENTRY_IMPORT qui correspond à l’index du tableau d’IMAGE_DATA_DIRECTORY.
Il y a autant d’IMAGE_IMPORT_DESCRIPTOR que de dll depuis lesquelles on importe des fonctions, le dernier a tous ses champs mis à 0.
Le champ Name de la structure IMAGE_IMPORT_DESCRIPTOR contient la RVA vers le nom de la dll. Les champs OriginalFirstThunk et FirstThunk sont normalement identiques tant que le programme n’est pas chargé. Ils peuvent être la RVA vers une structure appelée IMAGE_IMPORT_BY_NAME ou un simple DWORD, car une fonction peut être importée par son nom mais aussi par son ordinal/index (vous comprendrez tout cela dans la section suivante (celle qui parle des fonctions exportées).
Pour savoir si la fonction est importée par son nom ou par son index/ordinal, le bit de poids fort du « Thunk » sera égal à 0 sinon à 1. L’index sera le word de poids faible.
Pour tester ce bit, il existe une constante appelée IMAGE_ORDINAL_FLAG32.
Si la fonction est importée par son nom, on utilise la structure IMAGE_IMPORT_BY_NAME contenant deux champs, le premier : Hint qui contient l’index/ordinal de la fonction dans la table d’exportation de la dll, et le second : Name qui est le nom de la fonction.
Le dernier « Thunk » est à 0.
Si le programme ou la dll sont chargés en mémoire, alors FirstThunk devient l’adresse effective de la fonction, OriginalFirstThunk reste inchangé.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | #include <windows.h> #include <stdio.h> typedef FILE *PFILE; typedef struct _S_PE { PFILE pfile; IMAGE_DOS_HEADER iDosHeader; IMAGE_NT_HEADERS iNtHeaders; PIMAGE_SECTION_HEADER piSectionHeader; }S_PE, *PS_PE; void ListImportedFunctions(PS_PE pspe); void Init(PS_PE pspe); DWORD RvaToOffset(PS_PE pspe, DWORD rva); void ReadCstring(PS_PE pspe, char *name); int main(int argc, char *argv[]) { S_PE spe; if(argc != 2) { printf("USAGE: ListImportedFunctions.exe <fichier>\n"); printf("EXAMPLE: ListImportedFunctions.exe C:\\Windows\\System32\\kernel32.dll\n"); printf("\n"); system("PAUSE"); exit(1); } memset(&spe, 0, sizeof(S_PE)); spe.pfile = fopen(argv[1], "rb"); if(spe.pfile == NULL) { printf("ERREUR : impossible d'ouvrir %s\n", argv[1]); exit(1); } Init(&spe); ListImportedFunctions(&spe); printf("\n"); fclose(spe.pfile); return 0; } void Init(S_PE *pspe) { // On lit l'en-tête DOS : fread(&pspe->iDosHeader, sizeof(IMAGE_DOS_HEADER), 1, pspe->pfile); // On check la signature : if(pspe->iDosHeader.e_magic != IMAGE_DOS_SIGNATURE) { printf("ERREUR : le fichier n'est pas valide!\n"); exit(1); } // On se positionne à l'offset de l'en-tête NT : fseek(pspe->pfile, pspe->iDosHeader.e_lfanew, SEEK_SET); // On lit l'en-tête NT : fread(&pspe->iNtHeaders, sizeof(IMAGE_NT_HEADERS), 1, pspe->pfile); // On check la signature : if(pspe->iNtHeaders.Signature != IMAGE_NT_SIGNATURE) { printf("ERREUR : le fichier n'est pas valide!\n"); exit(1); } // On lit toutes les sections : pspe->piSectionHeader = (PIMAGE_SECTION_HEADER)malloc(sizeof(IMAGE_SECTION_HEADER) * pspe->iNtHeaders.FileHeader.NumberOfSections); for(unsigned i = 0; i < pspe->iNtHeaders.FileHeader.NumberOfSections; i++) { fread(&pspe->piSectionHeader[i], sizeof(IMAGE_SECTION_HEADER), 1, pspe->pfile); } } DWORD RvaToOffset(PS_PE pspe, DWORD rva) { for(WORD i = 0; i < pspe->iNtHeaders.FileHeader.NumberOfSections; i++) { // La RVA est-elle dans cette section ? if((rva >= pspe->piSectionHeader[i].VirtualAddress) && (rva < pspe->piSectionHeader[i].VirtualAddress + pspe->piSectionHeader[i].SizeOfRawData)) { rva -= pspe->piSectionHeader[i].VirtualAddress; rva += pspe->piSectionHeader[i].PointerToRawData; return rva; } } return -1; } void ReadCstring(PS_PE pspe, char *name) { DWORD n = 0; do { fread(name+n, sizeof(char), 1, pspe->pfile); n++; }while(name[n-1] != 0 && n < 1023); name[n] = 0; } void ListImportedFunctions(PS_PE pspe) { IMAGE_IMPORT_DESCRIPTOR iImportDesc; char name[1024]; DWORD thunkData = 0; WORD hint = 0; // On se positionne a la table des fonctions importées : DWORD offset = RvaToOffset(pspe, pspe->iNtHeaders.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); for(DWORD i = 0;;i++) { // On se place à la structure IMAGE_IMPORT_DESCRIPTOR qu'il nous faut : fseek(pspe->pfile, offset + i * sizeof(IMAGE_IMPORT_DESCRIPTOR), SEEK_SET); // On la lit : fread(&iImportDesc, sizeof(IMAGE_IMPORT_DESCRIPTOR), 1, pspe->pfile); // Est-ce la dernière IMAGE_IMPORT_DESCRIPTOR? if((iImportDesc.Characteristics == 0) && (iImportDesc.FirstThunk == 0) && (iImportDesc.ForwarderChain == 0) && (iImportDesc.Name == 0) && (iImportDesc.OriginalFirstThunk == 0) && (iImportDesc.TimeDateStamp == 0)) break; // On va à la RVA du nom de la dll : fseek(pspe->pfile, RvaToOffset(pspe, iImportDesc.Name), SEEK_SET); // On lit le nom de la dll : ReadCstring(pspe, name); printf("[+] %s\n", name); for(DWORD j = 0;;j++) { // On se place au Thunk qu'il nous faut : if(iImportDesc.OriginalFirstThunk != 0) fseek(pspe->pfile, RvaToOffset(pspe, iImportDesc.OriginalFirstThunk) + j * sizeof(DWORD), SEEK_SET); else fseek(pspe->pfile, RvaToOffset(pspe, iImportDesc.FirstThunk) + j * sizeof(DWORD), SEEK_SET); // On le lit : fread(&thunkData, sizeof(DWORD), 1, pspe->pfile); // Est-ce le dernier? if(thunkData == 0) break; // Est-ce une fonction importée par son nom? if((thunkData & IMAGE_ORDINAL_FLAG32) == 0) { // On va à la RVA sur IMAGE_IMPORT_BY_NAME : fseek(pspe->pfile, RvaToOffset(pspe, thunkData), SEEK_SET); // On lit le Hint : fread(&hint, sizeof(WORD), 1, pspe->pfile); printf(" %d - ", hint); // On lit le nom : ReadCstring(pspe, name); printf("%s\n", name); } } } } |
Et pour finir, l’inverse des fonctions importées c’est à dire les fonction exportées, ce sont les fonctions contenues dans les dll qui peuvent être appelées par tout autre programme ou dll. Comme pour les fonctions importées, on y accède grâce au tableau d’IMAGE_DATA_DIRECTORY obtenu depuis le champ DataDirectory de la structure IMAGE_OPTIONAL_HEADER. Cette fois-ci, on tombe sur la structure de données qui nous intéresse, c’est à dire la structure IMAGE_EXPORT_DIRECTORY, et tout cela en utilisant la constante IMAGE_DIRECTORY_ENTRY_EXPORT comme index pour le tableau d’IMAGE_DATA_DIRECTORY.
Le nombre de fonctions exportées par leur nom est contenu dans le champ NumberOfNames, les RVA des noms dans AddressOfNames, les ordinaux dans AddressOfNameOrdinals.
Ces deux champs doivent être parcourus en parallèle. Les adresses des fonctions sont dans AddressOfFunctions.
Quand un nom est trouvé, l’ordinal est utilisé comme index dans le tableau AddressOfFunctions, si la fonction est une forwarder, l’adresse correspond à une RVA vers la string indiquant où elle est forwardée.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | #include <windows.h> #include <stdio.h> typedef FILE *PFILE; typedef struct _S_PE { PFILE pfile; IMAGE_DOS_HEADER iDosHeader; IMAGE_NT_HEADERS iNtHeaders; PIMAGE_SECTION_HEADER piSectionHeader; }S_PE, *PS_PE; void ListExportedFunctions(PS_PE pspe); void Init(PS_PE pspe); DWORD RvaToOffset(PS_PE pspe, DWORD rva); void ReadCstring(PS_PE pspe, char *name); int main(int argc, char *argv[]) { S_PE spe; if(argc != 2) { printf("USAGE: ListExportedFunctions.exe <fichier>\n"); printf("EXAMPLE: ListExportedFunctions.exe C:\\Windows\\System32\\kernel32.dll\n"); printf("\n"); system("PAUSE"); exit(1); } memset(&spe, 0, sizeof(S_PE)); spe.pfile = fopen(argv[1], "rb"); if(spe.pfile == NULL) { printf("ERREUR : impossible d'ouvrir %s\n", argv[1]); exit(1); } Init(&spe); ListExportedFunctions(&spe); fclose(spe.pfile); return 0; } void Init(S_PE *pspe) { // On lit l'en-tête DOS : fread(&pspe->iDosHeader, sizeof(IMAGE_DOS_HEADER), 1, pspe->pfile); // On check la signature : if(pspe->iDosHeader.e_magic != IMAGE_DOS_SIGNATURE) { printf("ERREUR : le fichier n'est pas valide!n"); exit(1); } // On se positionne à l'offset de l'en-tête NT : fseek(pspe->pfile, pspe->iDosHeader.e_lfanew, SEEK_SET); // On lit l'en-tête NT : fread(&pspe->iNtHeaders, sizeof(IMAGE_NT_HEADERS), 1, pspe->pfile); // On check la signature : if(pspe->iNtHeaders.Signature != IMAGE_NT_SIGNATURE) { printf("ERREUR : le fichier n'est pas valide!\n"); exit(1); } // On lit toutes les sections : pspe->piSectionHeader = (PIMAGE_SECTION_HEADER)malloc(sizeof(IMAGE_SECTION_HEADER) * pspe->iNtHeaders.FileHeader.NumberOfSections); for(unsigned i = 0; i < pspe->iNtHeaders.FileHeader.NumberOfSections; i++) { fread(&pspe->piSectionHeader[i], sizeof(IMAGE_SECTION_HEADER), 1, pspe->pfile); } } void ListExportedFunctions(PS_PE pspe) { IMAGE_EXPORT_DIRECTORY iExportDir; DWORD namePos = 0; WORD ordinal = 0; DWORD address = 0; char name[1024]; // On se positionne à la table des fonctions exportées : fseek(pspe->pfile, RvaToOffset(pspe, pspe->iNtHeaders.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress), SEEK_SET); // On lit la structure IMAGE_EXPORT_DIRECTORY : fread(&iExportDir, sizeof(IMAGE_EXPORT_DIRECTORY), 1, pspe->pfile); printf("NumberOfNames: %d\n\n", iExportDir.NumberOfNames); printf("ordinal / index -- name -- address\n\n"); for(DWORD i = 0; i < iExportDir.NumberOfNames; i++) { // Ordinal fseek(pspe->pfile, RvaToOffset(pspe, iExportDir.AddressOfNameOrdinals) + i * sizeof(WORD), SEEK_SET); fread(&ordinal, sizeof(WORD), 1, pspe->pfile); ordinal += (WORD)iExportDir.Base; printf("%d -- ", ordinal); // Name fseek(pspe->pfile, RvaToOffset(pspe, iExportDir.AddressOfNames) + i * sizeof(DWORD), SEEK_SET); fread(&namePos, sizeof(DWORD), 1, pspe->pfile); fseek(pspe->pfile, RvaToOffset(pspe, namePos), SEEK_SET); ReadCstring(pspe, name); printf("%s -- ", name); // Address fseek(pspe->pfile, RvaToOffset(pspe, iExportDir.AddressOfFunctions) + (ordinal - iExportDir.Base) * sizeof(DWORD), SEEK_SET); fread(&address, sizeof(DWORD), 1, pspe->pfile); printf("%.8X", address); // Is forwarded ? if(pspe->iNtHeaders.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress < address && address < pspe->iNtHeaders.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress + pspe->iNtHeaders.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size) { fseek(pspe->pfile, RvaToOffset(pspe, address), SEEK_SET); ReadCstring(pspe, name); printf(" (forwarder -> %s)\n", name); } else printf("\n"); } } DWORD RvaToOffset(PS_PE pspe, DWORD rva) { for(WORD i = 0; i < pspe->iNtHeaders.FileHeader.NumberOfSections; i++) { // La RVA est-elle dans cette section? if((rva >= pspe->piSectionHeader[i].VirtualAddress) && (rva < pspe->piSectionHeader[i].VirtualAddress + pspe->piSectionHeader[i].SizeOfRawData)) { rva -= pspe->piSectionHeader[i].VirtualAddress; rva += pspe->piSectionHeader[i].PointerToRawData; return rva; } } return -1; } void ReadCstring(PS_PE pspe, char *name) { DWORD n = 0; do { fread(name+n, sizeof(char), 1, pspe->pfile); n++; }while(name[n-1] != 0 && n < 1023); name[n] = 0; } |
Voilà tout pour cet article 🙂
Références :
Peering Inside the PE: A Tour of the Win32 Portable Executable File Format
An In-Depth Look into the Win32 Portable Executable File Format
An In-Depth Look into the Win32 Portable Executable File Format, Part 2
Microsoft PE and COFF Specification