This article explains the Windows boot process and demonstrates how a bootkit attack can be reproduced to bypass security solutions such as EDR/XDR. For the rest of this paper, the execution flow of Windows is analyzed, focusing on boot stages, UEFI protocols, privileges, and kernel initialization.

1. UEFI Boot Phases Overview

Understanding how Windows boots is essential to understanding how kernel exploitation and bootkits work.

None
UEFI Boot Phases

1.1 SEC (Security Phase)

  • The Security phase is the very first stage of the UEFI boot process.
  • Its main objective is to verify the authenticity of the firmware before execution begins.
  • A small portion of assembly code may run here to check basic system information and ensure the platform is in a trusted state.
  • This phase establishes the root of trust for the rest of the boot process.

1.2 PEI (Pre-EFI Initialization)

  • The PEI phase is responsible for initializing memory and other essential hardware resources.
  • During this stage, the firmware sets up the environment where the later DXE phase will be stored and executed.
  • Temporary memory structures are created, and the system prepares for more complex initialization routines.

1.3 DXE (Driver Execution Environment)

  • The DXE phase loads the bulk of the UEFI firmware.
  • Here, drivers and protocols become accessible, enabling communication with hardware and higher-level services.
  • This phase essentially transforms the firmware into a modular environment where applications and drivers can interact.

1.4 BDS (Boot Device Selection)

  • The BDS phase defines what to load and who loads it.
  • It determines the boot policy, selects the boot device, and prepares the transition to the next stage.
  • This is where the system decides whether to boot from disk, network, or other sources.

1.5 TSL (Transient System Load)

  • The TSL phase provides privileges to the OS loader.
  • At this point, typical UEFI applications are cleaned up, although EFI Runtime Drivers may remain active.
  • The system is now ready to hand over control to the operating system loader.
  • The critical function ExitBootServices() is called here, signaling that firmware services are no longer available to the OS.

1.6 RT (Runtime Phase)

  • In the Runtime phase, all UEFI applications and most drivers leave memory.
  • The OS loader takes full control of the system.
  • UEFI protocols are no longer accessible, except for a limited set of runtime services that remain available to the operating system (such as variables, time services, and NVRAM access).
  • From this point onward, the firmware has completed its role, and the operating system is responsible for managing the hardware and system state.

2. Windows Boot Sequence

in windows it's possible to understand how resource managed around the fields previous documented.

None
https://antapex.org/winarch2.htm

2.1 bootmgfw.efi – Windows Boot Manager

This is the Windows Boot Manager, stored as an EFI application. It is responsible for loading all critical resources needed to run the operating system loader.

Tasks include:

  • Reading the Boot Configuration Data (BCD) to determine which OS or boot option to start.
  • Initializing the environment for the OS loader.
  • Handling Secure Boot checks if enabled.
  • Presenting boot options (if multiple OSes or recovery modes are available).

In short, bootmgfw.efi prepares the system and then passes control to the next stage: winload.efi.

2.2 winload.efi – Windows OS Loader

This is the Windows OS Loader, also an EFI application. its job is to load the Windows kernel and essential drivers into memory.

Key responsibilities:

  • Load the Windows kernel (ntoskrnl.exe).
  • Load core drivers (HAL, boot-class drivers, file system drivers).
  • Set up the execution environment for the operating system.
  • Finally, it calls ExitBootServices(), which signals the end of UEFI firmware services and hands full control to the Windows kernel.

2.3 ntoskrnl.exe – Windows Kernel

Ntoskrnl.exe is a system process, and it's also known as the "Windows NT Operating System Kernel Executable". It's related to a kernel, which is a piece of software that connects hardware and software. Many different Windows services depend on this kernel to function efficiently. "Microsoft Documentation"

3. Bootkits (Malicious EFI)

3.1 Definition

Kaspersky Definition

A bootkit is a malicious program designed to load as early as possible in the boot process, in order to control all stages of the operating system start up, modifying system code and drivers before anti-virus and other security components are loaded. The malicious program is loaded from the Master Boot Record (MBR) or boot sector. In effect, a bootkit is a rootkit that loads before the operating system.

4. Attack Chain Overview

we talk about how boot work and where resource are allocated so now we can intercept that execution flow and manipulate critical section.

main function that bootmgfw.efi use for load image in memory:

4.1 UEFI Image Loading

these protocols describe an Image that has been loaded into memory and specifies the device path used when a PE/COFF image was loaded through the EFI Boot Service LoadImage()

https://uefi.org/specs/UEFI/2.10/09_Protocols_EFI_Loaded_Image.html

example:

None
Example of LoadImage()

in this case we take a efi driver path and we load in memory with LoadImage.

after our driver it's in memory we also load windows boot manager and hook LoadImage

None

this permit to take control of what will load in future and analized it.

None
boot with LoadImage() hook

5. Boot Manager Execution Flow

after release the bootmgfw.efi he call bootmgr.efi with BlImgLoadBootApplication() and BlImgStartBootApplication() Functions

None
ImgArchStartBootApplication()

ImgArchStartBootApplication() call BlpArchTransferTo64BitApplication()

None
BlpArchTransferTo64BitApplication()

BlpArchTransferTo64BitApplication() call Archpx64TransferTo64BitApplicationAsm()

None
Archpx64TransferTo64BitApplicationAsm()

Archpx64TransferTo64BitApplicationAsm()

None
Archpx64TransferTo64BitApplicationAsm()

final return long jump and give priority to child process like winload.efi.

6. Winload Attack Chain

Entrypoint OslpMain() call OslPrepareTarget()

None
OslPrepareTarget()

OslPrepareTarget() call OslpLoadAllModules()

None
OslpLoadAllModules()

OslpLoadAllModules() call OslLoadImage()

None
OslLoadImage()

list all allocate module from DXE_Runtime_Driver

None
bootkit take all allocate modules

obtain memory space allocated for windows kernel.

None
obtain info about Windows Kernel

OslFwpKernelSetupPhase1()

this time windows provides all Kernel structure so it's good moment to make hook to kernel function.

None
OslFwpKernelSetupPhase1()

7. Kernel Initialization and Hooking

7.1 Kernel Setup Phase

provides many functions that manage how and when driver can load make some example.

7.2 Targeting IoInitSystem

inizialize all critical structure in windows os. it's good point to make hook and load our driver but there is some problems.

None
IoInitSystem

for use a solid Pattern scan the instructions aren't equal and change in every build.

7.3 Reliable Hook Point

IoInitSystemPreDrivers it's seems equal in every build and it's good point to hook.

IoInitSystemPreDrivers = { 
41 B9 49 63 70 20 ; mov r9d, 20706349h
48 8D 0D ?? ?? ?? ?? ; lea rcx, [rip+disp32] → IopCompletionLookasideList
41 B8 38 00 00 00 ; mov r8d, 38h
}
None
IoInitSystemPreDrivers

the code i used to find pattern IoInitSystemPreDrivers

None
code for IoInitSystemPreDrivers pattern

the result is

None
result of bootkit Execution flow

8. NTFS and Driver Initialization

At early stages:

  • NTFS is not initialized
  • Paths like C: or GLOBAL?? are unavailable
None
NTFS is not initialized

By reversing IoInitSystemPreDrivers, a later execution point is found where:

  • NTFS is initialized
  • All drivers are loaded

At this location:

  • A relative jump redirects execution to a custom assembly wrapper.
None
NTFS is initialized and relative jmp

9. Transition from Physical to Virtual Memory

in this part of code i insert a relative jmp to my asm wrapper that allign the stack for access to my hook function:

None
assembly wrapper

in this wrapper after allignment to stack i call my ManualMapFile, this is the code responsable to this behavior

None
code of hook

pay attention to ConvertPointer this particular protocol convert my wrapper function from physical to virtual, why? in this moment the ExitBootServices has called so Windows Kernel force remmap all address from physical to virtual to optimize space and release UEFI Application Privileges. the protocol it used is SetVirtualAddressMap(). according to UEFI Documentation:

SetVirtualAddressMap()

Changes the runtime addressing mode of EFI firmware from physical to virtual.

typedef
EFI_STATUS
SetVirtualAddressMap (
   IN UINTN                 MemoryMapSize,
   IN UINTN                 DescriptorSize,
   IN UINT32                DescriptorVersion,
   IN EFI_MEMORY_DESCRIPTOR *VirtualMap
  );

Description

The SetVirtualAddressMap() function is used by the OS loader. The function can only be called at runtime, and is called by the owner of the system's memory map: i.e., the component which called EFI_BOOT_SERVICES.ExitBootServices(). All events of type EVT_SIGNAL_VIRTUAL_ADDRESS_CHANGE must be signaled before SetVirtualAddressMap() returns.

This call changes the addresses of the runtime components of the EFI firmware to the new virtual addresses supplied in the VirtualMap. The supplied VirtualMap must provide a new virtual address for every entry in the memory map at ExitBootServices() that is marked as being needed for runtime usage. All of the virtual address fields in the VirtualMap must be aligned on 4 KiB boundaries.

The call to SetVirtualAddressMap() must be done with the physical mappings. On successful return from this function, the system must then make any future calls with the newly assigned virtual mappings. All address space mappings must be done in accordance to the cacheability flags as specified in the original address map.

When this function is called, all events that were registered to be signaled on an address map change are notified. Each component that is notified must update any internal pointers for their new addresses. This can be done with the ConvertPointer() function. Once all events have been notified, the EFI firmware reapplies image "fix-up" information to virtually relocate all runtime images to their new addresses. In addition, all of the fields of the EFI Runtime Services Table except SetVirtualAddressMap and ConvertPointer must be converted from physical pointers to virtual pointers using the ConvertPointer() service. The SetVirtualAddressMap() and ConvertPointer() services are only callable in physical mode, so they do not need to be converted from physical pointers to virtual pointers. Several fields of the EFI System Table must be converted from physical pointers to virtual pointers using the ConvertPointer() service. These fields include FirmwareVendor, RuntimeServices, and ConfigurationTable. Because contents of both the EFI Runtime Services Table and the EFI System Table are modified by this service, the 32-bit CRC for the EFI Runtime Services Table and the EFI System Table must be recomputed.

so after i call this protocol and the execution flow pass to my wrapper function the result is:

None
assembly wrapper init

10. Kernel Payload Execution

The execution chain begins with a custom ManualMapFile() function. Instead of relying on the Windows Image Loader, the logic manually maps the driver into system memory.

None
hook function ManualMapFile()

To remain independent of the standard import table during the early stages, the loader utilizes custom Assembly (ASM) logic to resolve Kernel-Mode APIs at runtime.

  • Pattern Scanning: The code scans ntoskrnl.exe in memory to locate unexported or internal functions.
None
runtime resolve KM APIS
  • Structure Initialization: Once the APIs are resolved, the environment is prepared, and the rootkit's structures are initialized.
None
structure inizialize

the final code allocate buffer with RWX privileges, To achieve stealth, the rootkit avoids standard registration. By reversing IoCreateDriver(), i identify the internal calls to ObCreateObject and ObCreateObjectType.

None
ManualMap RootKit

11. Locating IoDriverObjectType

The pointer to the driver object type is stored within the kernel's code segments.

Pattern Matching: A specific signature is used to find the offset within the instruction stream of IoCreateDriver.

None
https://doxygen.reactos.org/ reference

this function permit to create an object and provide it to drivers, the type of object it's created by IopObCreateObjectType:

None
https://doxygen.reactos.org/ reference

the pointer it's saved in code segments so when windows need it can retrieve from this part of code and use it for creare the object, for find this i reverse the logic of IoCreateDriver and i found a pattern usefull:

None
IoDriverOjectType signature windows 10
None
Dissasebly IoCreateDriver

Pointer Calculation: After the DXE phase identifies the pattern, it performs a relative offset calculation to reach the actual pointer in IoDriverObjectType.

None
DXE relative offset calculation

Object Creation: With this pointer, we can manually invoke ObCreateObject to instantiate a _DRIVER_OBJECT structure.

12. Object Insertion & Stealth

A key observation in the Windows Object Manager is that ObCreateObject allocates the structure but does not immediately link it to the global object directories.

  • The Gap: The object is created but remains "hidden" from system lists.
  • The Link: It is only after ObInsertObject increments the reference counts and inserts the handle that Windows recognizes it in the object list.
None
https://doxygen.reactos.org/ reference
None
_DRIVER_OBJECT of rootkit
  • Execution: Once the handle is valid, the code jumps to the Driver Entry, passing the manually crafted object.
None
DriverEntry of rootkit

13. Rootkit IOCTL: The Communication Bridge

Once the rootkit is resident in memory with a valid _DRIVER_OBJECT, it must establish a command-and-control (C2) channel with User-Mode applications.

The rootkit utilizes IoCreateDevice to create a named device object. To make this accessible to a standard Win32 application, a Symbolic Link (Symlink) is created.

  • Kernel Space: \Device\MyRootkit
None
Rootkit creating IOCTL Channel

User Space: \\.\MyRootkitLink (accessed via CreateFileA with OPEN_EXISTING).

None
User-Mode program connect to Rootkit

Communication is handled via IOCTL (Input/Output Control) codes sent through DeviceIoControl.

None
comunication beetween IOCTL

make an example my program open console and the user press 1, the code it is sent to my rootkit and that it! disable cortex with full control of machine:

None
Cortex disabilitation

also it is possibile open critical process without high privileges and XDR enabled:

None
Lsass dump

disable CrowdStrike visibility:

None
CrowdStrike Evasion

Requirements for this attack

  • boot USB with BlackSeed

Conclusion

It is crucial to note that this bootkit does not bypass Secure Boot itself. Instead, it demonstrates the devastating consequences that follow once Secure Boot is either disabled or bypassed. The security industry often treats Secure Boot as a binary "pass/fail" for system integrity, but this research proves that integrity is a continuous requirement, not a one-time check. The real danger is not just the ability to load a driver, but the ability to subvert the operating system's internal logic before it has fully awakened. This writeup serves as a stark reminder that while Secure Boot protects the "front door," the internal architecture of the Windows Kernel remains vulnerable to early-stage manipulation that can render even the most advanced EDR/XDR solutions completely blind.

Disclaimer

For ethical and responsible purposes:

  • Full source code is not published
  • No reverse engineering of commercial products (e.g., Cortex XDR, CrowdStrike) was performed
  • This research demonstrates attack patterns and defensive blind spots to improve security awareness