Le format PE :

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

Ce contenu a été publié dans File format, avec comme mot(s)-clef(s) , , , . Vous pouvez le mettre en favoris avec ce permalien.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

*

Protected by WP Anti Spam

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.