You double-click the exe. Windows maps the PE into memory, sets up the thread, jumps to the entry point. But the entry point is not your code. It is a VMProtect loader stub sitting in its own section, and for the next few milliseconds this stub has to undo everything the packer did. The sections are hollow. Imports are gone. Relocations are wrong. Everything has to be rebuilt from compressed blobs before a single line of the original binary can run.
I traced through this whole process and I want to walk through what I found.
Setup
First thing the loader does is figure out where it is. It grabs the actual load address and compares it to the preferred base the binary was compiled for. The difference is the ASLR delta, used later when fixing up addresses.
Then it checks if it already ran. There is a small state block inside the image, and if that is already initialized the loader returns right away. Prevents double-init if it gets called more than once.
On the first run, it allocates a fresh state block and starts going through its list. All the configuration is baked into the loader code as placeholder constants. During protection, VMProtect compiled the loader with dummy values where real offsets and sizes should be, then went back and patched every placeholder with the actual data for this binary. The accessors that read these values are forced non-inline so the compiler cannot optimize the placeholders away before they can be patched.
Making Sections Writable
Before writing anything the loader needs permission. Most code sections are mapped read-execute, data sections are usually read-only. Writing to them would fault.
It walks a table of section descriptors in the VMProtect section. Each entry has an address, size, and flags. For every section it changes permissions to read-write or read-write-execute. It does not use the normal Win32 API for this. It either makes a direct syscall or manually resolves the function from ntdll, depending on what it set up during init.
After changing protections it looks at what the old protection was. If any section had a guard page flag, something is up. Guard pages on code sections usually mean a debugger with memory watchpoints.
Decompression
With sections writable the loader moves to LZMA decompression. It reads the info table I described in Part 1. First entry gives it the LZMA properties blob, which it uses to set up the decoder state and allocate probability tables.
Then it goes through every remaining entry. Each is a source/destination pair. Source is the RVA of a compressed blob in the VMProtect section. Destination is the RVA of the original section. It calls the LZMA decode function for each, with the size set to -1 for both input and output. That means "go until you hit the end marker." This is why the encoder writes end markers during packing.
If any call fails the loader gives up and returns an error. No retry. A corrupted blob and the binary is dead.
TLS
There is a problem with thread-local storage that I did not think about until I saw how they handle it. When Windows maps a PE it processes the TLS directory early and assigns a TLS slot index. This gets written into the binary at a known address. But decompression is about to overwrite that address with whatever the linker put there, usually zero.
So the loader reads the current TLS index before decompression and writes it back after. Without that, every __declspec(thread) variable in the binary would silently break.
Fixing Addresses
After decompression the section data is back to its original form, but every absolute address in the code assumes the binary is at its preferred base. If ASLR moved it, and it almost always does, those addresses are all wrong.
The loader walks a private fixup table that was saved during packing. Each entry describes a page and a list of offsets within that page that need the ASLR delta added. It handles 32-bit and 64-bit fixups and some less common types.
I noticed the fixup format is close to the standard PE relocation format but the fields are rearranged. The type and offset bits are swapped compared to normal relocation blocks. Not obfuscation really, just a different layout.
Imports
With code in place and addresses fixed, the loader wires up external function calls. The original import table was destroyed during protection. In its place VMProtect stored a custom import table.
It is a stream of DLL entries, each followed by a list of function entries. DLL names and function names are encrypted with a rotation-based cipher. For each DLL it grabs a handle to the already-loaded module. For each function it resolves the address using its own export directory walk, including forwarded exports.
What caught my eye is that the resolved pointers are not stored directly. Each function entry has a key, and the loader subtracts this key from the real address before writing it. So the IAT slot stores real_address - key instead of the actual pointer. The virtualized code adds the key back when calling. If you dump the process and try to rebuild a working PE from the dump, the IAT is garbage because the keys are not stored anywhere obvious.
Internal References
Some entries in the import table point to other locations within the same binary instead of external DLLs. These use the same key-based storage but resolve against the image base. The loader writes image_base + target_rva - key into the slot.
Delay Imports
Delay-loaded DLLs are handled later in the sequence, after the VMProtect runtime has initialized. The difference is that delay imports use LoadLibrary instead of GetModuleHandle since the DLL might not be loaded yet. Same key trick on the resolved addresses.
Restoring Protections
Once all writes are done the loader makes a second pass over the sections and puts the original protections back. Code sections go to read-execute, read-only data goes back to read-only. This pass includes the same guard-page and breakpoint checks as a final sweep.
Handoff
Before jumping to the original code the loader calls the VMProtect runtime entry if one exists. This sets up the virtualizer runtime and licensing. If that succeeds, the loader stores its state pointer, marks itself done, and returns. The stub jumps to the real entry point and the binary runs. The whole thing took a few milliseconds.
Next
In part 3 I will cover the other half of what the loader is doing during all of this. The integrity checks and anti-debug that run between every step of the process.
