Analyzing Windows Boot: bootmgfw.efi and the Early UEFI Boot Flow

About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.

This research is published for educational and defensive purposes. The target sample is anonymized throughout.

Introduction

This article walks through Windows’ early boot process as implemented by bootmgfw.efi.

Analysis

DebugService2

DebugService2 provides the earliest opportunity for WinDbg to attach to the operating system. From the call stack you can see it is invoked from DbgLoadImageSymbols:

kd> lm
start             end                 module name
00000000`cf5f3000 00000000`cf7bd000   bootmgfw   (pdb symbols)
kd> k
 # Child-SP          RetAddr               Call Site
00 00000000`cf570590 00000000`cf6c5023     bootmgfw!DbgLoadImageSymbols+0x67
01 00000000`cf5705e0 00000000`cf6c551a     bootmgfw!BlBdStart+0x193
02 00000000`cf5706d0 00000000`cf66524d     bootmgfw!BlBdInitialize+0x342
03 00000000`cf5707b0 00000000`cf613ae7     bootmgfw!BlInitializeLibrary+0x95
04 00000000`cf5707e0 00000000`cf61344d     bootmgfw!BmMain+0x4eb
05 00000000`cf570960 00000000`ce09283c     bootmgfw!EfiEntry+0x1d
06 00000000`cf570990 00000000`d40b9218     0xce09283c
07 00000000`cf570998 00000000`d4da5918     0xd40b9218
08 00000000`cf5709a0 00000000`cf570a50     0xd4da5918
09 00000000`cf5709a8 00000000`00000000     0xcf570a50

From IDA’s static analysis plus the call stack, the call chain is easy to derive:

EfiEntry-->BmMain-->BlBdStart-->DebugService2

So how does Windows know it should call DebugService2? That’s BmMain’s job.

What BmMain does

It calls BmFwInitializeBootDirectoryPath to initialize the boot application path (\EFI\Microsoft\Boot). Let’s look at BmFwInitializeBootDirectoryPath:

void BmFwInitializeBootDirectoryPath()
{
    // try to open the boot device
    status = BlpDeviceOpen(g_SbControl+0x8e0, 1, NULL, &deviceHandle);
    if (!NT_SUCCESS(status)) {
        goto Cleanup;
    }

    // get the application directory path
    status = BmpFwGetApplicationDirectoryPath(&appDirPath, &appDirPathLen);
    if (!NT_SUCCESS(status) || !appDirPath) {
        goto Cleanup;
    }

    // try to open the target file
    status = BlFileOpen(deviceHandle, tempBuffer, FILE_READ_DATA, &fileHandle);
    if (NT_SUCCESS(status)) {
        BootDirectory = appDirPath;
        appDirPath = NULL;
    }

Cleanup:
    // close the handle here
    // on failure, fall back to \EFI\Boot\bootmgfw.efi
    if (!BootDirectory) {
        BootDirectory = (WCHAR*)DEFAULT_BOOT_DIR;
    }
}

g_SbControl is a global; at offset 0x8e0 it holds a pointer to the EFI_SYSTEM_TABLE, which the HAL wraps.

After initializing the boot path, it calls BmOpenDataStore to mount and read the BCD database file (\EFI\Microsoft\Boot\BCD) through UEFI services (disk I/O):

NTSTATUS BmOpenDataStore(PVOID Context)
{
    part1=NULL;
    part2=NULL;

    // get the data store path
    status = BmGetDataStorePath(&part1, &part2, &freePart2);
    if (!NT_SUCCESS(status)) {
        goto Cleanup;
    }

    // compute the length of the part2 string (excluding null terminator)
    for (part2Length = 0; part2[part2Length] != L'\0'; part2Length++);

    // compute the total bytes for the combined path
    ULONG totalBytes = (part2Length + 1) * sizeof(WCHAR); // includes null terminator
    if (part1->LengthField > (ULONG_MAX - totalBytes)) {
        status = STATUS_INTEGER_OVERFLOW;
        goto Cleanup;
    }
    totalBytes += part1->LengthField;

    // check against the 0xFFFFFFFF limit
    if (totalBytes > 0xFFFFFFFF) {
        status = STATUS_INTEGER_OVERFLOW;
        goto Cleanup;
    }

    // allocate memory
    combinedPath = BlMmAllocateHeap(totalBytes);
    if (!combinedPath) {
        status = STATUS_INSUFFICIENT_RESOURCES;
        goto Cleanup;
    }

    // copy part1 data
    memcpy(combinedPath, part1->Buffer, part1->LengthField);

    // copy part2 data (including null terminator)
    memcpy((PBYTE)combinedPath + part1->LengthField,
           part2,
           (part2Length + 1) * sizeof(WCHAR));

    // set up the UNICODE_STRING, i.e. fullPath
    // open the BCD store
    status = BcdOpenStoreFromFile(&fullPath, Context);

Cleanup:
    // free resources
    if (combinedPath) {
        BlMmFreeHeap(combinedPath);
    }
    if (part1) {
        BlMmFreeHeap(part1);
    }
    if (part2 && freePart2) {
        BlMmFreeHeap(part2);
    }

    return status;
}

During boot-manager startup, this function obtains the BCD (Boot Configuration Data) store path and opens the store to read boot configuration, preparing for the subsequent boot-option loading.

Here is the recovered original prologue of DebugService2:

cf70b9c4 488bc4           mov     rax, rsp
cf70b9c7 4c894018         mov     qword ptr [rax+18h], r8
cf70b9cb 53               push    rbx
cf70b9cc 4883ec40         sub     rsp, 40h
cf70b9d0 488348e0ff       or      qword ptr [rax-20h], 0FFFFFFFFFFFFFFFFh
cf70b9d5 4c8d4818         lea     r9, [rax+18h]
cf70b9d9 4883601800       and     qword ptr [rax+18h], 0
cf70b9de 4533c0           xor     r8d, r8d
cf70b9e1 488bd9           mov     rbx, rcx
cf70b9e4 488950d8         mov     qword ptr [rax-28h], rdx
cf70b9e8 418d4801         lea     ecx, [r8+1]
cf70b9ec e883f6f3ff       call    bootmgfw!RtlImageNtHeaderEx (cf64b074)
cf70b9f1 488b542460       mov     rdx, qword ptr [rsp+60h]
cf70b9f6 4885d2           test    rdx, rdx
cf70b9f9 7410             je      bootmgfw!DbgLoadImageSymbols+0x47 (cf70ba0b)
cf70b9fb 8b4258           mov     eax, dword ptr [rdx+58h]
cf70b9fe 89442430         mov     dword ptr [rsp+30h], eax
cf70ba02 8b4250           mov     eax, dword ptr [rdx+50h]
cf70ba05 89442434         mov     dword ptr [rsp+34h], eax
cf70ba09 eb0d             jmp     bootmgfw!DbgLoadImageSymbols+0x54 (cf70ba18)
cf70ba0b 8364243000       and     dword ptr [rsp+30h], 0
cf70ba10 c744243400001000 mov     dword ptr [rsp+34h], 100000h
cf70ba18 41b803000000     mov     r8d, 3
cf70ba1e 488d542420       lea     rdx, [rsp+20h]
cf70ba23 488bcb           mov     rcx, rbx
cf70ba26 e8e5c80300       call    bootmgfw!DebugService2 (cf748310)
cf70ba2b 4883c440         add     rsp, 40h   ; restored here
cf70ba2f 5b               pop     rbx
cf70ba30 c3               ret

BmpLaunchBootEntry

After DebugService2 is called there’s no convenient place to set a breakpoint, so break directly on BmpLaunchBootEntry. It loads and executes winload.efi. The control flow before BmpLaunchBootEntry selects the correct boot entry based on the BCD store values described earlier. If BitLocker is enabled, the boot manager decrypts the system partition before transferring control to the boot loader.

Figure from an earlier post

Let’s analyze BmpLaunchBootEntry:

bootmgfw!BmpLaunchBootEntry (00000000`cf6150f4)
  bootmgfw!BmpLaunchBootEntry+0x49 (00000000`cf61513d):
    call to bootmgfw!BcdUtilGetBootOption (00000000`cf6a2794)
  bootmgfw!BmpLaunchBootEntry+0x5b (00000000`cf61514f):
    call to bootmgfw!BmOpenDataStore (00000000`cf617dc0)
  bootmgfw!BmpLaunchBootEntry+0x78 (00000000`cf61516c):
    call to bootmgfw!BmPurgeOption (00000000`cf618334)
  bootmgfw!BmpLaunchBootEntry+0x81 (00000000`cf615175):
    call to bootmgfw!BmCloseDataStore (00000000`cf617ed8)
  bootmgfw!BmpLaunchBootEntry+0x8c (00000000`cf615180):
    call to bootmgfw!BcdUtilGetBootOption (00000000`cf6a2794)
  bootmgfw!BmpLaunchBootEntry+0x9e (00000000`cf615192):
    call to bootmgfw!BmOpenDataStore (00000000`cf617dc0)
  bootmgfw!BmpLaunchBootEntry+0xbb (00000000`cf6151af):
    call to bootmgfw!BmPurgeOption (00000000`cf618334)
  bootmgfw!BmpLaunchBootEntry+0xc4 (00000000`cf6151b8):
    call to bootmgfw!BmCloseDataStore (00000000`cf617ed8)
  bootmgfw!BmpLaunchBootEntry+0x10c (00000000`cf615200):
    call to bootmgfw!BlGetBootOptionDevice (00000000`cf6658c0)
  bootmgfw!BmpLaunchBootEntry+0x135 (00000000`cf615229):
    call to bootmgfw!BlGetBootOptionBoolean (00000000`cf6654d0)
  bootmgfw!BmpLaunchBootEntry+0x15a (00000000`cf61524e):
    call to bootmgfw!BmFindAssociatedOsEntry (00000000`cf618ad4)

  bootmgfw!BmpLaunchBootEntry+0x17a (00000000`cf61526e):
    call to bootmgfw!BlGetBootOptionDevice (00000000`cf6658c0)
  bootmgfw!BmpLaunchBootEntry+0x19e (00000000`cf615292):
    call to bootmgfw!BlMmFreeHeap (00000000`cf68bd48)
  bootmgfw!BmpLaunchBootEntry+0x1ab (00000000`cf61529f):
    call to bootmgfw!BlMmFreeHeap (00000000`cf68bd48)
  bootmgfw!BmpLaunchBootEntry+0x1cf (00000000`cf6152c3):
    call to bootmgfw!BlMmFreeHeap (00000000`cf68bd48)
  bootmgfw!BmpLaunchBootEntry+0x1d7 (00000000`cf6152cb):
    call to bootmgfw!BmpCreateDevices (00000000`cf615e6c)
  bootmgfw!BmpLaunchBootEntry+0x1f7 (00000000`cf6152eb):
    call to bootmgfw!McTemplateK0qq_McGenEventWriteBoot (00000000`cf61350c)

  bootmgfw!BmpLaunchBootEntry+0x205 (00000000`cf6152f9):
    call to bootmgfw!BlFveRegisterBootEntryForTrustedWimBoot (00000000`cf62b9e8)
  bootmgfw!BmpLaunchBootEntry+0x243 (00000000`cf615337):
    call to bootmgfw!FvebpUnlockSinglePartition (00000000`cf62be60)

  bootmgfw!BmpLaunchBootEntry+0x255 (00000000`cf615349):
    call to bootmgfw!BlFveRegisterDeviceForTrustedWimBoot (00000000`cf62b964)
  bootmgfw!BmpLaunchBootEntry+0x274 (00000000`cf615368):
    call to bootmgfw!FvebpLogError (00000000`cf62bb28)
  bootmgfw!BmpLaunchBootEntry+0x2a1 (00000000`cf615395):
    call to bootmgfw!BlGetBootOptionBoolean (00000000`cf6654d0)
  bootmgfw!BmpLaunchBootEntry+0x2b8 (00000000`cf6153ac):
    call to bootmgfw!BlAppendBootOptionBoolean (00000000`cf665e2c)
  bootmgfw!BmpLaunchBootEntry+0x2c3 (00000000`cf6153b7):
    call to bootmgfw!BlRemoveBootOption (00000000`cf666128)
  bootmgfw!BmpLaunchBootEntry+0x2ce (00000000`cf6153c2):
    call to bootmgfw!BlRemoveBootOption (00000000`cf666128)
  bootmgfw!BmpLaunchBootEntry+0x2e2 (00000000`cf6153d6):
    call to bootmgfw!BlGetBootOptionBoolean (00000000`cf6654d0)
  bootmgfw!BmpLaunchBootEntry+0x2f9 (00000000`cf6153ed):
    call to bootmgfw!BlAppendBootOptionBoolean (00000000`cf665e2c)
  bootmgfw!BmpLaunchBootEntry+0x304 (00000000`cf6153f8):
    call to bootmgfw!BlRemoveBootOption (00000000`cf666128)
  bootmgfw!BmpLaunchBootEntry+0x30f (00000000`cf615403):
    call to bootmgfw!BlRemoveBootOption (00000000`cf666128)
  bootmgfw!BmpLaunchBootEntry+0x31f (00000000`cf615413):
    call to bootmgfw!BmTransferExecution (00000000`cf61457c)

  bootmgfw!BmpLaunchBootEntry+0x32f (00000000`cf615423):
    call to bootmgfw!BlFveRegisterBootEntryForTrustedWimBoot (00000000`cf62b9e8)
  bootmgfw!BmpLaunchBootEntry+0x383 (00000000`cf615477):
    call to bootmgfw!BcdUtilGetBootOption (00000000`cf6a2794)
  bootmgfw!BmpLaunchBootEntry+0x3be (00000000`cf6154b2):
    call to bootmgfw!BlGetBootOptionBoolean (00000000`cf6654d0)
  bootmgfw!BmpLaunchBootEntry+0x3ea (00000000`cf6154de):
    call to bootmgfw!BlGetBootOptionBoolean (00000000`cf6654d0)
  bootmgfw!BmpLaunchBootEntry+0x417 (00000000`cf61550b):
    call to bootmgfw!BlFveRegisterBootEntryForTrustedWimBoot (00000000`cf62b9e8)
  bootmgfw!BmpLaunchBootEntry+0x421 (00000000`cf615515):
    call to bootmgfw!BmLaunchRecoverySequence (00000000`cf6143ac)
  bootmgfw!BmpLaunchBootEntry+0x436 (00000000`cf61552a):
    call to bootmgfw!BlFveRegisterBootEntryForTrustedWimBoot (00000000`cf62b9e8)
  bootmgfw!BmpLaunchBootEntry+0x451 (00000000`cf615545):
    call to bootmgfw!BlRemoveBootOption (00000000`cf666128)
  bootmgfw!BmpLaunchBootEntry+0x474 (00000000`cf615568):
    call to bootmgfw!BmFatalErrorEx (00000000`cf61a020)
  bootmgfw!BmpLaunchBootEntry+0x479 (00000000`cf61556d):
    call to bootmgfw!BmpPreserveBootmgrLogToBootstat (00000000`cf615998)
  bootmgfw!BmpLaunchBootEntry+0x483 (00000000`cf615577):
    call to bootmgfw!BmDisplayDumpError (00000000`cf616870)
  bootmgfw!BmpLaunchBootEntry+0x48b (00000000`cf61557f):
    call to bootmgfw!BmErrorPurge (00000000`cf61a5a4)
  bootmgfw!BmpLaunchBootEntry+0x4e2 (00000000`cf6155d6):
    call to bootmgfw!BlAppendBootOptionBoolean (00000000`cf665e2c)
  bootmgfw!BmpLaunchBootEntry+0x514 (00000000`cf615608):
    call to bootmgfw!BmOpenDataStore (00000000`cf617dc0)
  bootmgfw!BmpLaunchBootEntry+0x526 (00000000`cf61561a):
    call to bootmgfw!BmProcessCustomAction (00000000`cf614538)
  bootmgfw!BmpLaunchBootEntry+0x535 (00000000`cf615629):
    call to bootmgfw!BmCloseDataStore (00000000`cf617ed8)

FvebpUnlockSinglePartition

This is BitLocker’s decryption function. Let’s analyze it:

NTSTATUS FvebpUnlockSinglePartition(
    PDEVICE_OBJECT Device,       // RCX
    PVOID BootOption,             // RDX (possibly BOOT_OPTION)
    DWORD Index,                  // R8
    USHORT Flags,                 // R9
    PVOID* OutDeviceData          // [RBP+50h] (optional output)
)
{
    HANDLE deviceHandle = INVALID_HANDLE_VALUE;
    PDEVICE_OBJECT bootDevice = NULL;
    PDEVICE_IDENTIFIER deviceId = NULL;
    PVOID deviceData = NULL;
    NTSTATUS status = STATUS_SUCCESS;
    USHORT modifiedFlags = Flags;

    // increment the global counter
    InterlockedIncrement(&FvebGlobalData.Counter);

    // check the device type; use the default device if necessary
    if (*(DWORD*)Device == DEVICE_TYPE_5) {
        Device = SbControlDevice;
    }

    // check whether a special flag should be added
    if (!(Flags & FLAG_600H) &&
        Index > 1 &&
        BootOption != NULL)
    {
        // get the boot device
        status = BlGetBootOptionDevice(
            *(PVOID*)((PBYTE)BootOption + 0x18),
            0x21000001,
            NULL,
            &bootDevice
        );

        if (NT_SUCCESS(status) &&
            Device != NULL &&
            bootDevice != NULL)
        {
            // use the default device if necessary
            PDEVICE_OBJECT dev1 = (*(DWORD*)Device == DEVICE_TYPE_5)
                ? SbControlDevice : Device;

            PDEVICE_OBJECT dev2 = (*(DWORD*)bootDevice == DEVICE_TYPE_5)
                ? SbControlDevice : bootDevice;

            // compare whether the devices are the same
            if (BlpDeviceCompare(dev1, dev2)) {
                modifiedFlags |= FLAG_200H;
            }
        }
    }

    // check whether the device is already locked
    if (*(BYTE*)((PBYTE)Device + 4) & 1) {
        status = STATUS_UNSUCCESSFUL;
        goto Cleanup;
    }

    // special handling on first call
    if (FvebGlobalData.Counter == 1) {
        if (FvebGlobalData.UnkD8 != 0) {
            status = STATUS_UNSUCCESSFUL;
            goto Cleanup;
        }
        FvebGlobalData.UnkD8 = Index;
    }
    // special handling on nested calls
    else if (FvebGlobalData.Counter > 1) {
        if (!(Flags & FLAG_600H) && Index > 1) {
            modifiedFlags |= FLAG_400H;
        }
    }

    // handle VHD devices (recursively unlock the host device)
    DWORD devType = *(DWORD*)Device;
    if ((devType == 2 || devType == 3) &&
        *(DWORD*)((PBYTE)Device + 0x20) == DEVICE_TYPE_6)
    {
        status = FvebpGetVhdHostDevice(
            (PVOID)((PBYTE)Device + 0x38),
            &bootDevice
        );

        if (NT_SUCCESS(status)) {
            // recursively unlock the host device
            status = FvebpUnlockSinglePartition(
                bootDevice,
                NULL,
                Index,
                FLAG_400H,
                NULL
            );
            if (!NT_SUCCESS(status)) goto Cleanup;
        }
    }

    // open the target device
    status = BlpDeviceOpen(
        Device,
        FILE_READ_DATA,
        NULL,
        &deviceHandle
    );
    if (!NT_SUCCESS(status)) goto Cleanup;

    // get the device identifier
    status = BlGetDeviceIdentifier(deviceHandle, &deviceId);
    if (!NT_SUCCESS(status)) goto Cleanup;

    // get device data
    deviceData = FvebpGetDeviceData(deviceId);
    if (!deviceData) {
        status = STATUS_INSUFFICIENT_RESOURCES;
        goto Cleanup;
    }

    // query the key (perform the unlock)
    status = FvebpQueryKey(
        deviceData,
        Index,
        modifiedFlags,
        NULL
    );

Cleanup:
    // set the output parameter
    if (OutDeviceData && NT_SUCCESS(status)) {
        *OutDeviceData = deviceData;
    } else if (deviceData) {
        // free the unused device data
        BlMmFreeHeap(deviceData);
    }

    // close the device handle
    if (deviceHandle != INVALID_HANDLE_VALUE) {
        BlDeviceClose(deviceHandle);
    }

    // free the boot device object
    if (bootDevice) {
        BlMmFreeHeap(bootDevice);
    }

    // update the global counter
    if (InterlockedDecrement(&FvebGlobalData.Counter) == 0) {
        FvebGlobalData.UnkD8 = 0;
    }

    return status;
}

Device handling: if the device type is 5, the system default control device is used, and when devices match, the FLAG_200H flag is added. For VHD virtual disks, the host physical device is unlocked first, with the special FLAG_400H flag on the recursive call.

The core decryption function:

FvebpQueryKey(deviceData, Index, modifiedFlags, NULL);
// perform the actual unlock using the combined flags
// the index likely indicates the key-slot position

BmFindAssociatedOsEntry, BmTransferExecution

These two functions only do basic argument conversion; the call flow is shown below:

Call flow

BmpLaunchBootEntry then calls BmpTransferExecution to check the boot option, and ImgArchEfiStartBootApplication initializes winload.efi’s protected-memory mode. After that, Archpx64TransferTo64BitApplicationAsm runs, completing the preparation to start winload.efi.

After this critical point, all execution flow transfers to winload.efi, which is responsible for loading and initializing the Windows kernel. Before that, execution proceeds in the UEFI environment through boot services, operating under flat memory.

If Secure Boot is disabled, malicious code could make arbitrary modifications to memory at this stage of the boot process, because no PatchGuard exists for kernel-mode modules yet. PatchGuard is only initialized in later boot steps.

Conclusion

If I have time later, I’ll write up an analysis of winload.efi. Comments and corrections are welcome.

Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).

Get in touch