most people think running an exe means writing it to disk first. it doesn't.
as part of building TinyLoad, a Windows PE packer, I had to write a PE loader that maps an executable directly into memory and runs it without ever creating a file. here's how it works.
what is a PE file
PE (Portable Executable) is the format Windows uses for .exe and .dll files. it's basically a structured blob with a header describing how to load it, followed by sections containing code, data, resources etc.
to run a PE file manually you have to do what the Windows loader does — but yourself, in memory.
step 1: parse the headers
every PE starts with a DOS header, then an NT header. the NT header tells you everything you need:
-
SizeOfImage— how much memory to allocate -
ImageBase— where the linker expected the binary to live -
AddressOfEntryPoint— where to jump to start execution -
SizeOfHeaders— how much of the front to copy as-is
IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)data.data();
IMAGE_NT_HEADERS64* nt = (IMAGE_NT_HEADERS64*)(data.data() + dos->e_lfanew);
step 2: allocate memory and copy sections
allocate a block of memory the size of the image, then copy the headers in. after that, iterate the section table and copy each section to its virtual address:
void* base = VirtualAlloc(NULL, nt->OptionalHeader.SizeOfImage,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(base, data.data(), nt->OptionalHeader.SizeOfHeaders);
IMAGE_SECTION_HEADER* sect = IMAGE_FIRST_SECTION(nt);
for (int i = 0; i < nt->FileHeader.NumberOfSections; i++) {
if (sect[i].SizeOfRawData > 0)
memcpy((BYTE*)base + sect[i].VirtualAddress,
data.data() + sect[i].PointerToRawData,
sect[i].SizeOfRawData);
}
step 3: fix relocations
the linker assumed the binary would load at ImageBase. if it lands somewhere else (which it usually does since ASLR), every absolute address in the binary is wrong by delta = actual_base - preferred_base.
the relocation table tells you exactly which addresses need fixing:
size_t delta = (size_t)base - nt->OptionalHeader.ImageBase;
if (delta != 0) {
auto* rel = (IMAGE_BASE_RELOCATION*)((BYTE*)base + relDir->VirtualAddress);
while (rel->VirtualAddress > 0) {
WORD* list = (WORD*)(rel + 1);
DWORD count = (rel->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
for (DWORD i = 0; i < count; i++) {
if ((list[i] >> 12) == IMAGE_REL_BASED_DIR64) {
size_t* p = (size_t*)((BYTE*)base + rel->VirtualAddress + (list[i] & 0xFFF));
*p += delta;
}
}
rel = (IMAGE_BASE_RELOCATION*)((BYTE*)rel + rel->SizeOfBlock);
}
}
step 4: resolve imports
the import directory tells you which DLLs the binary needs and which functions to load from each. iterate the import descriptors, load each DLL, resolve each function by name or ordinal, and write the function addresses into the import address table:
auto* imp = (IMAGE_IMPORT_DESCRIPTOR*)((BYTE*)base + impDir->VirtualAddress);
while (imp->Name) {
HMODULE mod = LoadLibraryA((char*)((BYTE*)base + imp->Name));
auto* thunk = (IMAGE_THUNK_DATA64*)((BYTE*)base + imp->FirstThunk);
auto* orig = (IMAGE_THUNK_DATA64*)((BYTE*)base + imp->OriginalFirstThunk);
while (orig->u1.AddressOfData) {
if (IMAGE_SNAP_BY_ORDINAL64(orig->u1.Ordinal)) {
thunk->u1.Function = (size_t)GetProcAddress(mod,
(char*)(orig->u1.Ordinal & 0xFFFF));
} else {
auto* name = (IMAGE_IMPORT_BY_NAME*)((BYTE*)base + orig->u1.AddressOfData);
thunk->u1.Function = (size_t)GetProcAddress(mod, name->Name);
}
thunk++; orig++;
}
imp++;
}
step 5: jump to entry point
using EntryPoint = void(WINAPI*)();
EntryPoint entry = (EntryPoint)((BYTE*)base + nt->OptionalHeader.AddressOfEntryPoint);
entry();
that's it. the exe runs directly from the allocated memory block, no file on disk, no Windows loader involved.
where this is used in TinyLoad
TinyLoad packs your exe with LZ77 compression and VM encryption. when the packed stub runs, it decrypts and decompresses the original exe in memory, then calls this loader directly. the original exe never exists as a file — it goes straight from encrypted bytes to running process.
full source (single .cpp file, no deps): https://github.com/iamsopotatoe-coder/TinyLoad
Top comments (0)