Coding a PE Loader in C

Coding a PE Loader in C

Rmxzy

Writing a PE Loader in C with Win32 API

The PE (Portable Executable) format is the executable file format used by Windows. It includes all the information needed to load and execute an application or a dynamic link library (DLL). This guide explains the implementation of a basic PE Loader in Windows using the Win32 API.

Overview

A PE Loader is a program that loads, parses, and executes a PE file (such as an EXE or DLL). The following code implements a simple PE Loader that reads a PE file, performs necessary loading steps, including reading headers, allocating memory, mapping sections, resolving imports, applying relocations, and executing the PE file.

Code Implementation

Below is the full implementation of a simple PE Loader in C using the Win32 API.

Main Structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

// Function prototypes
void LoadPEFile(const char* filePath);
void ParseHeaders(const char* peBuffer, IMAGE_NT_HEADERS** ntHeaders);
void* AllocateMemoryForImage(IMAGE_NT_HEADERS* ntHeaders);
void MapSections(const char* peBuffer, IMAGE_NT_HEADERS* ntHeaders, void* baseAddress);
void ResolveImports(IMAGE_NT_HEADERS* ntHeaders, void* baseAddress);
void ApplyRelocations(IMAGE_NT_HEADERS* ntHeaders, void* baseAddress, DWORD_PTR delta);
void ExecutePE(IMAGE_NT_HEADERS* ntHeaders, void* baseAddress);

int main(int argc, char* argv[]) {
if (argc != 2) {
printf("Usage: %s <PE file path>\n", argv[0]);
return 1;
}

LoadPEFile(argv[1]);
return 0;
}

1. LoadPEFile Function

This function reads the PE file from the disk and begins the loading process.

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
void LoadPEFile(const char* filePath) {
FILE* peFile = fopen(filePath, "rb");
if (!peFile) {
printf("Failed to open PE file: %s\n", filePath);
exit(1);
}

// Get the size of the file
fseek(peFile, 0, SEEK_END);
long fileSize = ftell(peFile);
fseek(peFile, 0, SEEK_SET);

// Allocate memory and read the file into the buffer
char* peBuffer = (char*)malloc(fileSize);
fread(peBuffer, 1, fileSize, peFile);
fclose(peFile);

// Parse headers and load sections
IMAGE_NT_HEADERS* ntHeaders;
ParseHeaders(peBuffer, &ntHeaders);
void* baseAddress = AllocateMemoryForImage(ntHeaders);
MapSections(peBuffer, ntHeaders, baseAddress);
ResolveImports(ntHeaders, baseAddress);

// Handle relocations
DWORD_PTR delta = (DWORD_PTR)baseAddress - ntHeaders->OptionalHeader.ImageBase;
if (delta != 0) {
ApplyRelocations(ntHeaders, baseAddress, delta);
}

ExecutePE(ntHeaders, baseAddress);
free(peBuffer);
}
````
---

### 2. **ParseHeaders** Function

This function reads the PE headers from the PE file buffer and verifies whether the PE file is valid.

```c
void ParseHeaders(const char* peBuffer, IMAGE_NT_HEADERS** ntHeaders) {
// DOS Header
IMAGE_DOS_HEADER* dosHeader = (IMAGE_DOS_HEADER*)peBuffer;
if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
printf("Invalid DOS signature.\n");
exit(1);
}

// NT Headers
*ntHeaders = (IMAGE_NT_HEADERS*)(peBuffer + dosHeader->e_lfanew);
if ((*ntHeaders)->Signature != IMAGE_NT_SIGNATURE) {
printf("Invalid PE signature.\n");
exit(1);
}

printf("Valid PE file.\n");
}

3. AllocateMemoryForImage Function

This function allocates memory for the PE image. It first attempts to allocate memory at the preferred base address specified in the PE file, and if that fails, it allocates memory at any available address.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void* AllocateMemoryForImage(IMAGE_NT_HEADERS* ntHeaders) {
void* baseAddress = VirtualAlloc((LPVOID)ntHeaders->OptionalHeader.ImageBase,
ntHeaders->OptionalHeader.SizeOfImage,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);

if (!baseAddress) {
baseAddress = VirtualAlloc(NULL,
ntHeaders->OptionalHeader.SizeOfImage,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
}

if (!baseAddress) {
printf("Failed to allocate memory for image.\n");
exit(1);
}

printf("Memory allocated at: 0x%p\n", baseAddress);
return baseAddress;
}

4. MapSections Function

This function maps the sections of the PE file into the allocated memory. It first copies the headers to the allocated memory and then maps the sections from the PE file to the appropriate addresses.

1
2
3
4
5
6
7
8
9
10
11
void MapSections(const char* peBuffer, IMAGE_NT_HEADERS* ntHeaders, void* baseAddress) {
memcpy(baseAddress, peBuffer, ntHeaders->OptionalHeader.SizeOfHeaders);

IMAGE_SECTION_HEADER* sectionHeader = IMAGE_FIRST_SECTION(ntHeaders);
for (int i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++, sectionHeader++) {
void* dest = (char*)baseAddress + sectionHeader->VirtualAddress;
void* src = (char*)peBuffer + sectionHeader->PointerToRawData;
memcpy(dest, src, sectionHeader->SizeOfRawData);
printf("Mapped section: %s to 0x%p\n", sectionHeader->Name, dest);
}
}

5. ResolveImports Function

This function resolves the imports of the PE file. It extracts the names of the required DLLs and loads the necessary functions from those DLLs.

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
void ResolveImports(IMAGE_NT_HEADERS* ntHeaders, void* baseAddress) {
IMAGE_IMPORT_DESCRIPTOR* importDesc = (IMAGE_IMPORT_DESCRIPTOR*)((char*)baseAddress +
ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

while (importDesc->Name) {
char* dllName = (char*)baseAddress + importDesc->Name;
HMODULE hModule = LoadLibraryA(dllName);

if (!hModule) {
printf("Failed to load library: %s\n", dllName);
exit(1);
}

IMAGE_THUNK_DATA* origFirstThunk = (IMAGE_THUNK_DATA*)((char*)baseAddress + importDesc->OriginalFirstThunk);
IMAGE_THUNK_DATA* firstThunk = (IMAGE_THUNK_DATA*)((char*)baseAddress + importDesc->FirstThunk);

while (origFirstThunk->u1.AddressOfData) {
IMAGE_IMPORT_BY_NAME* importByName = (IMAGE_IMPORT_BY_NAME*)((char*)baseAddress + origFirstThunk->u1.AddressOfData);
FARPROC functionAddress = GetProcAddress(hModule, (LPCSTR)importByName->Name);

if (!functionAddress) {
printf("Failed to resolve import: %s\n", importByName->Name);
exit(1);
}

firstThunk->u1.Function = (DWORD_PTR)functionAddress;
origFirstThunk++;
firstThunk++;
}
importDesc++;
}

printf("Imports resolved.\n");
}

6. ApplyRelocations Function

If the PE file is loaded at an address different from its preferred address, this function applies the necessary relocations to ensure that absolute addresses in the PE file work correctly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void ApplyRelocations(IMAGE_NT_HEADERS* ntHeaders, void* baseAddress, DWORD_PTR delta) {
if (delta == 0) return;

IMAGE_BASE_RELOCATION* reloc = (IMAGE_BASE_RELOCATION*)((char*)baseAddress +
ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);

while (reloc->VirtualAddress) {
WORD* relocItem = (WORD*)((char*)reloc + sizeof(IMAGE_BASE_RELOCATION));
int numRelocs = (reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);

for (int i = 0; i < numRelocs; i++) {
if ((relocItem[i] >> 12) == IMAGE_REL_BASED_HIGHLOW) {
DWORD* patchAddr = (DWORD*)((char*)baseAddress + reloc->VirtualAddress + (relocItem[i] & 0xFFF));
*patchAddr += (DWORD)delta;
}
}
reloc = (IMAGE_BASE_RELOCATION*)((char*)reloc + reloc->SizeOfBlock);
}

printf("Relocations applied.\n");
}

7. ExecutePE Function

This function locates the entry point of the PE file and executes it.

1
2
3
4
5
6
7
void ExecutePE(IMAGE_NT_HEADERS* ntHeaders, void* baseAddress) {
void (*entryPoint)() = (void (*)())((char*)baseAddress + ntHeaders->OptionalHeader.AddressOfEntryPoint);
printf("Executing PE at entry point: 0x%p\n", entryPoint

);
entryPoint();
}

Full Simple PE Loader (x64)

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
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

// Function prototypes
void LoadPEFile(const char* filePath);
void ParseHeaders(const char* peBuffer, IMAGE_NT_HEADERS** ntHeaders);
void* AllocateMemoryForImage(IMAGE_NT_HEADERS* ntHeaders);
void MapSections(const char* peBuffer, IMAGE_NT_HEADERS* ntHeaders, void* baseAddress);
void ResolveImports(IMAGE_NT_HEADERS* ntHeaders, void* baseAddress);
void ApplyRelocations(IMAGE_NT_HEADERS* ntHeaders, void* baseAddress, DWORD_PTR delta);
void ExecutePE(IMAGE_NT_HEADERS* ntHeaders, void* baseAddress);

int main(int argc, char* argv[]) {
if (argc != 2) {
printf("Usage: %s <PE file path>\n", argv[0]);
return 1;
}

LoadPEFile(argv[1]);
return 0;
}

void LoadPEFile(const char* filePath) {
FILE* peFile = fopen(filePath, "rb");
if (!peFile) {
printf("Failed to open PE file: %s\n", filePath);
exit(1);
}

// Get the size of the file
fseek(peFile, 0, SEEK_END);
long fileSize = ftell(peFile);
fseek(peFile, 0, SEEK_SET);

// Allocate memory and read the file into the buffer
char* peBuffer = (char*)malloc(fileSize);
fread(peBuffer, 1, fileSize, peFile);
fclose(peFile);

// Parse headers and load sections
IMAGE_NT_HEADERS* ntHeaders;
ParseHeaders(peBuffer, &ntHeaders);
void* baseAddress = AllocateMemoryForImage(ntHeaders);
MapSections(peBuffer, ntHeaders, baseAddress);
ResolveImports(ntHeaders, baseAddress);

// Handle relocations
DWORD_PTR delta = (DWORD_PTR)baseAddress - ntHeaders->OptionalHeader.ImageBase;
if (delta != 0) {
ApplyRelocations(ntHeaders, baseAddress, delta);
}

ExecutePE(ntHeaders, baseAddress);
free(peBuffer);
}

void ParseHeaders(const char* peBuffer, IMAGE_NT_HEADERS** ntHeaders) {
// DOS Header
IMAGE_DOS_HEADER* dosHeader = (IMAGE_DOS_HEADER*)peBuffer;
if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
printf("Invalid DOS signature.\n");
exit(1);
}

// NT Headers
*ntHeaders = (IMAGE_NT_HEADERS*)(peBuffer + dosHeader->e_lfanew);
if ((*ntHeaders)->Signature != IMAGE_NT_SIGNATURE) {
printf("Invalid PE signature.\n");
exit(1);
}

printf("Valid PE file.\n");
}

void* AllocateMemoryForImage(IMAGE_NT_HEADERS* ntHeaders) {
// Allocate memory for the image, aligned to the preferred base address
void* baseAddress = VirtualAlloc((LPVOID)ntHeaders->OptionalHeader.ImageBase,
ntHeaders->OptionalHeader.SizeOfImage,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);

if (!baseAddress) {
// If allocation at preferred address fails, allocate memory at any available address
baseAddress = VirtualAlloc(NULL,
ntHeaders->OptionalHeader.SizeOfImage,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
}

if (!baseAddress) {
printf("Failed to allocate memory for image.\n");
exit(1);
}

printf("Memory allocated at: 0x%p\n", baseAddress);
return baseAddress;
}

void MapSections(const char* peBuffer, IMAGE_NT_HEADERS* ntHeaders, void* baseAddress) {
// Copy the headers to the allocated memory
memcpy(baseAddress, peBuffer, ntHeaders->OptionalHeader.SizeOfHeaders);

// Map sections to memory
IMAGE_SECTION_HEADER* sectionHeader = IMAGE_FIRST_SECTION(ntHeaders);
for (int i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++, sectionHeader++) {
void* dest = (char*)baseAddress + sectionHeader->VirtualAddress;
void* src = (char*)peBuffer + sectionHeader->PointerToRawData;
memcpy(dest, src, sectionHeader->SizeOfRawData);
printf("Mapped section: %s to 0x%p\n", sectionHeader->Name, dest);
}
}

void ResolveImports(IMAGE_NT_HEADERS* ntHeaders, void* baseAddress) {
IMAGE_IMPORT_DESCRIPTOR* importDesc = (IMAGE_IMPORT_DESCRIPTOR*)((char*)baseAddress +
ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

while (importDesc->Name) {
char* dllName = (char*)baseAddress + importDesc->Name;
HMODULE hModule = LoadLibraryA(dllName);

if (!hModule) {
printf("Failed to load library: %s\n", dllName);
exit(1);
}

IMAGE_THUNK_DATA* origFirstThunk = (IMAGE_THUNK_DATA*)((char*)baseAddress + importDesc->OriginalFirstThunk);
IMAGE_THUNK_DATA* firstThunk = (IMAGE_THUNK_DATA*)((char*)baseAddress + importDesc->FirstThunk);

while (origFirstThunk->u1.AddressOfData) {
IMAGE_IMPORT_BY_NAME* importByName = (IMAGE_IMPORT_BY_NAME*)((char*)baseAddress + origFirstThunk->u1.AddressOfData);
FARPROC functionAddress = GetProcAddress(hModule, (LPCSTR)importByName->Name);

if (!functionAddress) {
printf("Failed to resolve import: %s\n", importByName->Name);
exit(1);
}

firstThunk->u1.Function = (DWORD_PTR)functionAddress;
origFirstThunk++;
firstThunk++;
}
importDesc++;
}

printf("Imports resolved.\n");
}

void ApplyRelocations(IMAGE_NT_HEADERS* ntHeaders, void* baseAddress, DWORD_PTR delta) {
if (delta == 0) return;

IMAGE_BASE_RELOCATION* reloc = (IMAGE_BASE_RELOCATION*)((char*)baseAddress +
ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);

while (reloc->VirtualAddress) {
WORD* relocItem = (WORD*)((char*)reloc + sizeof(IMAGE_BASE_RELOCATION));
int numRelocs = (reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);

for (int i = 0; i < numRelocs; i++) {
if ((relocItem[i] >> 12) == IMAGE_REL_BASED_HIGHLOW) {
DWORD* patchAddr = (DWORD*)((char*)baseAddress + reloc->VirtualAddress + (relocItem[i] & 0xFFF));
*patchAddr += (DWORD)delta;
}
}
reloc = (IMAGE_BASE_RELOCATION*)((char*)reloc + reloc->SizeOfBlock);
}

printf("Relocations applied.\n");
}

void ExecutePE(IMAGE_NT_HEADERS* ntHeaders, void* baseAddress) {
// Get the entry point address and call it
void (*entryPoint)() = (void (*)())((char*)baseAddress + ntHeaders->OptionalHeader.AddressOfEntryPoint);
printf("Executing PE at entry point: 0x%p\n", entryPoint);
entryPoint();
}

Important Note

There is no PE loader that can load all PEs, the most logical way is to write a custom loader for an EXE (in malware development), for example, the above code cannot load Notepad.exe, but it can load clac.exe, the reason for this The difference is the type of PE contents.

Conclusion

This simple PE Loader demonstrates the basic steps required to load and execute a PE file in Windows. In real-world scenarios, more advanced PE Loaders might include additional features like manipulating internal PE structures, more complex header analysis, and various security mechanisms. These types of tools can be used for security testing, malware analysis, or custom software loading and execution.