Insights
Blackbird: Defeating PatchGuard, One Layer Deeper.

How do you maintain observability without sacrificing system stability? Diving into the depths of virtualization and extended page tables.

Blackbird: Defeating PatchGuard, One Layer Deeper.

Intro

Ok I admit, the title is clickbait.

My earlier post, Blackbird: Doing What EDRs Won't, covered the first kernel hook engine. It used inline hooksA detour replaces the first instructions of a function with a jump to another handler. The displaced instructions are preserved so the original function can still run. inside ntoskrnl.exeThe Windows kernel executable. It contains core operating-system logic and the kernel implementations of native Nt and Zw routines..

In plain English, Blackbird replaced the function prologueThe prologue is the first group of machine instructions at the entry point of a function. A hook must copy complete instructions rather than cutting one in half. with a jump to its own handler. A trampolineA small executable bridge that replays the displaced original instructions, then jumps back into the untouched remainder of the function. replayed the instructions that had been moved and returned execution to Windows. Making that safe required correct instruction decoding, register preservation, cross-processor synchronization, and reliable rollback.

It technically worked, but proper analysis environments need stability. Kernel inline hooking changes protected bytes inside ntoskrnl.exe. Windows Kernel Patch Protection—PatchGuardAn x64 Windows integrity mechanism that checks protected kernel code and critical structures for unauthorized modification. periodically validates protected kernel code and structures, and when it finds the modification Windows blue-screens with CRITICAL_STRUCTURE_CORRUPTION (0x109). It soon became obvious this method was unsustainable, so I started researching ways around it.

Windows bugcheck screen showing CRITICAL_STRUCTURE_CORRUPTION with stop code 0x109

Initially I researched bypassing PatchGuard, but I quickly realized I would be building against an intentionally undocumented moving target. Even if I got it working, a Windows update could turn the bypass into mass instability. Of course I had heard of hypervisors, but I imagined they were way outside my skillset and had no clue Second Level Address Translation (SLAT)A processor feature that adds a hypervisor-controlled memory translation after the page tables owned by the guest operating system. Intel calls its implementation EPT. hooks existed.

The new hook path keeps the same instrumentation model but moves the modified bytes somewhere else. Blackbird still intercepts selected Windows Nt* routines, enters the same kernel handlers, captures the same state, and calls the original implementation through a trampoline.

The difference is that the original ntoskrnl.exe page is never patched.

Instead Archangel, the Blackbird hypervisorA privileged layer below the guest operating system that controls virtual processors, memory translation, and selected hardware events.—uses Intel Extended Page Tables (EPT)The Intel implementation of Second Level Address Translation. EPT adds a hypervisor-controlled translation from guest physical memory to the actual machine page. to give selected processes an executable shadow view of that page.

The 0x109 Problem

Because the crash arrived late, I initially treated it as an ordinary hook bug. I checked the instruction decoder, register preservation, cross-processor patching, and teardown. Short runs became completely stable, but after extended periods KPP became a reoccuring problem.

An inline hook changes the physical page backing a kernel routine:

ntoskrnl physical page
┌──────────────────────────────────────────┐
│ original prologue → overwritten jump     │
└──────────────────────────────────────────┘

Every process shares that page. PatchGuard, other drivers, debuggers, and Blackbird's target all resolve the address to the same modified bytes. Making the patch more reliable would never make it less visible.

I could start lying to PatchGuard, or I could stop changing the thing it protects.

Kernel inline hooking compared with target-scoped Blackbird SLAT hooking

Finding the Gap Below Windows

The requirement was strange but precise: keep the same kernel address, execute different bytes for selected processes, and leave Windows' copy untouched.

EPTIntel's implementation of SLAT. It lets a hypervisor control the final mapping and permissions of guest physical memory. provided exactly that gap. Windows still translates a process virtual addressThe address a process sees. Windows page tables translate it before memory is accessed. through its page tablesCPU-readable structures that map virtual addresses to physical pages.. The active CR3The register containing the root of the current page-table hierarchy. In practice it identifies the address space currently running. selects which address space is in use. Archangel then controls one final translation below Windows:

guest virtual address
        │
        ▼
Windows page tables
        │
        ▼
guest physical address
        │
        ▼
EPT hierarchy
        │
        ▼
actual machine page

Windows decides which guest page it wants. Archangel decides which real page backs it. That let me build two views:

  • an identity view that maps the clean Windows kernel page;
  • a hook view that maps Blackbird's private shadow copy.
Identity EPT                         Hook EPT
────────────                         ────────
guest physical ─► original page      guest physical ─► shadow page
               clean bytes                         patched copy

The SSDTWindows uses this table to route a system-call number to the corresponding Nt routine inside the kernel. still sends execution to the real ntoskrnl address. Blackbird changes only the final backing page, and only for a monitored process.

Moving the Hook Instead of Rebuilding It

The nice surprise was that the old hook engine was not wasted. It already knew how to decode complete instructions, build trampolines, and enter Blackbird's kernel handlers. I only needed to move where the jump was written.

For each page containing a target routine, the driver creates a private 4 KB shadow copy. Stripped down to the important part, the staging path looks like this:

rawAllocation = ExAllocatePool2(
    POOL_FLAG_NON_PAGED,
    BK_NTAPI_SLAT_SHADOW_ALLOC_SIZE,
    BK_NTAPI_SLAT_SHADOW_POOL_TAG
);
if (rawAllocation == NULL)
    return NULL;

shadowPage = BkntkiSlatAlignShadowAllocation(rawAllocation);
RtlCopyMemory(shadowPage, (PVOID)kernelPageVa, PAGE_SIZE);

livePa   = MmGetPhysicalAddress((PVOID)kernelPageVa);
shadowPa = MmGetPhysicalAddress(shadowPage);

The jump goes into the copy at the same offset as the original function:

original page                       shadow page
─────────────                       ───────────
NtAllocateVirtualMemory:            NtAllocateVirtualMemory:
  original prologue                   jump BlackbirdHandler
  original body                       original body

Targets on the same page share one copy. Once the redirects are staged, the driver gives Archangel the page through a hypercallA controlled call from the guest into the hypervisor, similar in spirit to a system call into the operating system..

The shadow redirect uses a 13-byte absolute jump through R11, a general-purpose CPU register used here to hold the handler address:

mov r11, <Blackbird hook handler>
jmp r11

Preparation rejects any target without enough complete instructions or whose patch would cross a page boundary. Once execution enters the handler, the old path takes over: recursion guard, target check, argument capture, trampoline, then back to Windows.

So yes, these are still kernel hooks. The hypervisor chooses the page; the driver still performs the instrumentation.

Blackbird SLAT hook flow from the monitored process through the shadow page, kernel handler, trampoline, and original routine

The Annoying Read Problem

The obvious first mapping was a readable and executable shadow page. It ran perfectly, but anything reading kernel code as data could see the jump. I had moved the mutation without solving visibility.

The policy became:

  • execute from the shadow page;
  • serve clean reads from the original page;
  • reject writes.

Execution stays on the shadow page without exiting. A data read lacks permission, causing an EPT violationA hardware event raised when guest memory access conflicts with the permissions in the active EPT entry. and a VM exitA controlled transition from guest execution into the hypervisor. Useful, but much more expensive than ordinary execution.. Archangel handles that read in four steps:

  1. temporarily map the original clean page;
  2. invalidate the stale EPT translation;
  3. let the guest execute exactly one instruction;
  4. regain control through the VMX Monitor Trap FlagA virtualization control that causes a VM exit after the guest completes one instruction. Archangel uses it to restore the shadow mapping immediately after a clean read. and restore the execute-only shadow page.

The useful part of the violation handler is small. It swaps in the clean page, records what must be restored, enables the one-instruction trap, and invalidates the cached translation:

if (ReadAccess != FALSE && hookRoot != nullptr) {
    const UINT64 clean = EPT_ENTRY_READ | EPT_ENTRY_EXECUTE;

    if (EptSetExisting4KbLeafMappingForRoot(
            hookRoot, gpa, originalPa, clean) != FALSE &&
        EptQueuePendingSlatRestore(gpa, shadowPa) != FALSE &&
        VmxSetMonitorTrapFlag(TRUE) != FALSE) {
        EptInvalidateLocalContext();
        return TRUE;
    }
}

After that one instruction, the VM-exit dispatcher restores the execute-only shadow mapping:

case VMX_EXIT_REASON_MONITOR_TRAP_FLAG:
    EptRestorePendingSlatHooks();
    VmxSetMonitorTrapFlag(FALSE);
    EptInvalidateLocalContext();
    return FALSE;

Then came the same-page case... An instruction can execute from a page while reading data from that same page. Switching the mapping underneath it can invalidate the code currently running, so the handler checks RIPThe x86-64 instruction pointer: the address of the instruction currently being executed. and keeps the shadow view for that instruction when required.

Writes are rejected rather than silently modifying either copy. A write to a protected hook page becomes a guest page fault and increments a diagnostic counter.

Process Scoping & Performance

The next version worked, and then immediately started firing Blackbird's hook handlers across every process on the system. Performance did not like that.

The old inline hooks could do a quick PID lookup inside the handler, but doing that for thousands of intercepted calls every second was already wasteful. Doing more work in the hypervisor would turn Blackbird into a very advanced way of making Windows feel like it was running on a calculator.

Windows identifies a process by PID. Archangel has something better: CR3, the page-table root already describing the address space running on that processor. One complication: KPTIA mitigation that separates user and kernel page-table views, so one process can use different CR3 values during a system call. means the same process can have two:

  • the user address-space CR3;
  • the kernel CR3 used during kernel transitions.

Whenever the guest changes CR3, Archangel asks one question: does this address space belong to a monitored process? If yes, that processor receives the hook EPT view. If not, it stays on the clean identity view and never touches Blackbird's hooks.

So, start clean, scan for a matching target, and write the hook EPT pointer only when both the user or kernel CR3 and the target configuration match:

GuestCr3 &= AAHV_CR3_ADDRESS_MASK;
selectedEptp = EptGetIdentityMapPointer();

for (LONG i = 0; i < targetCount; ++i) {
    if (EptTargetRequiresHookRoot(&g_ArchangelTargets[i], GuestCr3)) {
        selectedEptp = EptGetHookMapPointerForCurrentProcessor();
        break;
    }
}

if (currentEptp != selectedEptp)
    __vmx_vmwrite(EPT_VMCS_EPT_POINTER, selectedEptp);

My first attempt only tracked one CR3, so the hooks disappeared halfway through a system-call transition. Tracking both fixed that and, moreover, kept PatchGuard and ordinary system activity on the identity view.

Guest CR3 changes select the original kernel page for unrelated processes and the shadow kernel page for monitored targets

That solved most of the performance problem. Normal execution from the shadow page does not cause a VM exitA transition from guest execution into the hypervisor. VM exits enable interception but cost far more than an ordinary instruction.; the patched prologue jumps straight into Blackbird's kernel handler. Exits are reserved for things Archangel actually needs to handle, such as clean reads, rejected writes, and EPT view changes.

The rest came down to not being stupid with the expensive parts:

  • only split the 2 MB EPT region containing a hooked 4 KB page, leaving the rest with better TLBA processor cache of recent address translations. Larger mappings generally use it more efficiently. behaviour;
  • keep the active hook view and pending one-instruction restore per processor;
  • use INVEPTAn Intel instruction that invalidates cached EPT translations after a mapping or permission changes. only for the EPT context that changed;
  • reject unwanted telemetry before doing stack capture, correlation, or event construction.

Why General-Purpose EDRs Rarely Take This Route

At this point the obvious question was: if this works, why do EDRs not all do it?

They can. The difficult part is not the proof of concept; it is deploying a competing hypervisor across millions of machines the vendor does not control.

VBS and HVCI use the Windows hypervisor as part of the platform's security boundary. Microsoft also documents the practical ownership problem plainly: when Hyper-V, Memory Integrity, or Credential Guard is active, another virtualization stack cannot simply take over the same hardware virtualization extensions. Windows applications that need to act as virtualization hosts are expected to use the Windows Hypervisor Platform so they remain compatible with VBS.

An EDR has to survive consumer laptops, servers, VDI, strange firmware, old drivers, new drivers, other security software, and whatever the OEM preloaded that week. It cannot reasonably tell every customer to disable a platform security boundary so its sensor can own VMX and EPT. The support matrix would be brutal even if the engineering were perfect.

Blackbird gets to make a different trade because it is built for controlled analysis environments. The analyst owns the VM template. Nested virtualization can be exposed deliberately. The guest build, driver set, CPU features, snapshot state, and boot configuration can be pinned and tested together. When Archangel needs direct ownership of VMX and EPT, the analysis profile can run with VBS and HVCI disabled inside that disposable guest instead of negotiating with an unknown endpoint configuration.

Special Thanks

Special thanks to: