Executive Summary
Part 2 of Aidan Khoury’s PatchGuard introspection-detection series covers two LSTAR-focused checks. The first, KiErrata420Present (Khoury’s informal name for an unnamed Windows 10 1903+ routine), briefly overwrites the LSTAR MSR with a tiny stub that lives inside the PatchGuard context — one byte for RET, two bytes for the SYSCALL opcode — then dispatches SYSCALL through KeGuardDispatchICall. A naive hypervisor that silently blocks WRMSR on LSTAR (to protect its own syscall hook) ends up executing the guest’s real hooked handler instead of the dummy stub, and the entire mechanism unravels. The fix is a transparent shadow: on RDMSR(LSTAR) return the guest’s original value, on WRMSR(LSTAR) swap the guest’s “original” value back to the hook address.
The second technique, KiErrata1337Present, is Khoury’s own derived detection. It deliberately triggers a #PF inside the syscall handler by zeroing IA32_KERNEL_GS_BASE before SYSCALL — the very first SWAPGS+MOV gs:[KPCR.UserRsp], rsp sequence then dereferences a NULL segment base and faults. A temporary IDT with a custom #PF handler catches the fault, reads the true syscall handler RIP off the trap frame, and uses a single XCHG qword [rsp], rcx to swap the trap-frame RIP with the SYSCALL-saved return address held in RCX. The IRET branches cleanly back into the detection routine while RCX now holds the real LSTAR target. Full PoC is published at ajkhoury/Errata1337.
No errata for U!
This is Part 2 of the series. Part 1 walked through three PatchGuard routines that catch a hypervisor cheating on architectural state via FMASK/TF/SYSCALL, MOV SS/POP SS single-step quirks, and the ICEBP pending-BS VMCS trap. This part pivots to the most common hypervisor introspection knob — the LSTAR MSR — and shows two checks that catch a sloppy or naively-implemented LSTAR hook in flight.
KiErrata420Present
Trapping RDMSR/WRMSR on LSTAR is the canonical way to hook system calls in a Windows guest from a hypervisor — it’s the entry-point register for SYSCALL and a single MSR exit lets a VMM redirect kernel-mode dispatch wherever it likes. The catch, despite a lot of online folklore to the contrary, is that this leaves a stack of detection vectors if the VMM isn’t careful about transparency. Windows 10 build 18362 (1903) added several of them. The check below was one of the simpler ones — not even good enough to earn a meme “errata” name from Microsoft. Khoury calls it KiErrata420Present for posterity.
KiErrata420Present:
cli ; disable interrupts
mov r9d, 0C0000082h ;
mov ecx, r9d ;
rdmsr ; read LSTAR MSR value
shl rdx, 32 ;
or rax, rdx ; store LSTAR value in rax
lea rdx, [rdi+87Ah] ; store temp LSTAR value in rdx read from pg context
mov rbx, rax ; rbx = original LSTAR value
mov rax, rdx ; rax = temp LSTAR value
shr rdx, 32 ;
wrmsr ; write temporary LSTAR MSR value
mov r14d, 20000h ;
lea rax, [rdi+87Ch] ; rax = stub to execute syscall
mov rsi, 0A3A03F5891C8B4E8h ; rsi = constant to obfuscate pg context pointer
test [rdi+994h], r14d ; test if should store pg check data?
jnz short trigger_syscall ; if nz, skip tracing
mov r8, gs:KPCR.CurrentPrcb ;
lea rdx, [rdi+rsi] ;
mov rcx, [rdi+4C0h] ;
mov [rcx], rdx ;
mov rcx, [rdi+4C8h] ; store pg check related data
mov [rcx], r8 ;
mov rcx, [rdi+4D0h] ;
mov [rcx], r9 ;
mov rcx, [rdi+4D8h] ;
mov qword ptr [rcx], 112h ;
trigger_syscall:
call KeGuardDispatchICall ; dispatch call to syscall instruction stub
test [rdi+994h], r14d ; test if pg check should be traced?
jnz short restore_lstar ; if nz, skip tracing
mov rax, [rdi+4C0h] ;
mov [rax], rsi ;
mov rax, [rdi+4C8h] ;
mov [rax], r13 ; wipe pg check related data
mov rax, [rdi+4D0h] ;
mov [rax], r13 ;
mov rax, [rdi+4D8h] ;
mov [rax], r13 ;
restore_lstar:
mov rdx, rbx ; restore original LSTAR value
mov rax, rbx ;
shr rdx, 32 ;
mov ecx, 0C0000082h ;
wrmsr ; write original LSTAR MSR value
sti ; reenable interrupts
The shape is straightforward: save the current LSTAR, overwrite it with a temporary value that lives at PgContext + 0x87A, dispatch SYSCALL via KeGuardDispatchICall, restore the original LSTAR. The interesting question is what gets written into that PG-context slot.
What ends up at context + 0x87A
The relevant bits of the temporary-LSTAR setup are:
lea rdx, [rdi+87Ah] ; store temp LSTAR value in rdx read from pg context
mov rbx, rax ; rbx = original LSTAR value
mov rax, rdx ; rax = temp LSTAR value
shr rdx, 32 ;
So the temporary LSTAR value is RDI + 0x87A. RDI here is the runtime base of the current PatchGuard context. Hunting back into PG initialisation tells us what the kernel puts at that offset:
mov byte ptr [r14+87Ah], 0C3h ; store RET instruction
That’s the byte C3 — RET. So byte 0 of the synthesised “syscall handler” is an immediate return. The next piece of evidence is the KeGuardDispatchICall argument:
lea rax, [rdi+87Ch] ; rax = stub to execute syscall
KeGuardDispatchICall branches to whatever RAX points at, and here it’s PgContext + 0x87C. PG init at that offset writes:
mov eax, 050Fh
mov [r14+87Ch], ax ; store SYSCALL instruction
That two-byte sequence 0F 05 is the encoding of SYSCALL. Stitching the parts together, KiErrata420Present is doing something equivalent to:
_disable();
OriginalSyscall64 = __readmsr(MSR_LSTAR);
__writemsr(MSR_LSTAR, &PgContext->DummySyscallHandler); // C3 -> ret
KeGuardDispatchICall(&PgContext->Syscall); // 0F 05 -> syscall
__writemsr(MSR_LSTAR, OriginalSyscall64);
_enable();
It writes a dummy syscall handler (one RET byte) to LSTAR, executes a SYSCALL through a tiny PG-context-resident trampoline that holds the actual SYSCALL opcode, and restores the real LSTAR. If the hypervisor is hooking LSTAR and refusing to let the guest overwrite it, the SYSCALL will dispatch to the hypervisor’s hook handler rather than to the one-byte RET stub — and the routine doesn’t return cleanly. This is exactly what a naive implementation (e.g. early Hyperbone) trips on.
Bypass: shadow the MSR
The right behaviour for the VMM is to let the guest overwrite LSTAR — treat the “real” value as a shadow — and silently restore the hook value when the guest writes its own original back. PatchGuard has a separate cross-check against LSTAR tampering, so the guest itself can’t leave the MSR rewritten permanently without triggering a different bugcheck. In the VMM, the read/write handlers look like this:
VMM_EVENT_STATUS
HVAPI
VmmHandleMsrRead(
_In_ PVIRTUAL_CPU Vcpu
)
{
// ...
//
// Hide our LSTAR syscall hook handler address.
//
case MSR_LSTAR:
if (Vcpu->OriginalLSTAR) {
MsrValue = Vcpu->OriginalLSTAR;
} else {
MsrValue = __readmsr(MSR_LSTAR);
}
break;
// ...
}
VMM_EVENT_STATUS
HVAPI
VmmHandleMsrWrite(
_In_ PVIRTUAL_CPU Vcpu
)
{
// ...
//
// Let the guest overwrite our hook to avoid possible detection.
//
// If and only if the guest is writing the original LSTAR, we replace
// the MSR value with the hook LSTAR value.
//
// N.B. We do this to get around one of PatchGuard's syscall hook
// detections which works like this:
//
// _disable();
// OriginalSyscall64 = __readmsr(MSR_LSTAR);
// __writemsr(MSR_LSTAR, &PgCtx->PgSyscallDummy); // C3 -> ret
// KeGuardDispatchICall(&PgCtx->SyscallOpcode1); // 0F 05 -> syscall
// __writemsr(MSR_LSTAR, OriginalSyscall64);
// _enable();
//
case MSR_LSTAR:
if (MsrValue == Vcpu->OriginalLSTAR) {
MsrValue = Vcpu->HookLSTAR;
}
__writemsr(MSR_LSTAR, MsrValue);
break;
// ...
}
KiErrata1337Present — a derived LSTAR detection
Khoury’s own contribution: using critical thinking on the patterns above, here is a fresh LSTAR-hook detection that doesn’t depend on tampering with the MSR at all. The hook detection now sits inside the syscall handler itself.
Both Linux and Windows 64-bit syscall handlers open with SWAPGS, which exchanges the current IA32_GS_BASE with IA32_KERNEL_GS_BASE (MSR 0xC0000102). The instruction immediately after is a segmented store. From nt!KiSystemCall64:
KiSystemCall64 proc near
swapgs ; swap GS base with IA32_KERNEL_GS_BASE
mov gs:KPCR.UserRsp, rsp ; store user mode stack in processor control region
mov rsp, gs:KPCR.Prcb.RspBase ; set the kernel stack from processor control region
The implication: if we deliberately corrupt IA32_KERNEL_GS_BASE before issuing SYSCALL, the SWAPGS at the start of the kernel handler will load a bogus GS base, the very next MOV gs:[KPCR.UserRsp], rsp will fault, and we’ll get a #PF inside the syscall handler. The #PF trap frame contains the faulting RIP — which is the real syscall handler address. That’s what we want to read.
First attempt: just fault
KiErrata1337Present:
swapgs ; swapgs to emulate coming from user mode
mov ecx 0C0000102h ;
xor eax, eax ; set KERNEL_GS_BASE MSR to zero
xor edx, edx ;
wrmsr ;
syscall ; execute the syscall instruction to trigger fault
ret
This will fault, which is the right outcome — but the real #PF handler will bugcheck on the corrupted GS bases, and even if it survived, the next context switch would explode. We need a temporary IDT and we need to restore the GS bases.
Hooking the IDT for the duration of the trick
Six-step recipe: disable interrupts, snapshot the IDTR, swap in a temporary IDT whose #PF vector points at our handler, run the detection, restore the original IDT, re-enable interrupts. Pseudocode:
TempIdtr.Limit = sizeof(TempIdt) - 1;
TempIdtr.Base = (UINT64)&TempIdt[0];
for (IdtEntry in KPCR->IdtBase)
TempIdt[i] = IdtEntry; // Fill in temporary IDT
_disable(); // Disable interrupts
__sidt(&OriginalIdtr); // Backup original IDT
__lidt(&TempIdtr); // Load our temporary hook IDT
// Hook page fault handler.
TempIdt[PF] = PageFaultHookHandler;
// Trigger syscall that will purposely page fault!
KiErrata1337Present(); // This must be lean enough not to timeout watchdog!
__lidt(&OriginalIdtr); // Restore the original IDT.
_enable(); // Re-enable interrupts.
The minimal #PF handler — for now — just discards the error code and returns:
PageFaultHookHandler:
add rsp, 8 ; skip fault code on stack
iretq ; return from interrupt
And the detection routine now saves and restores both GS-base MSRs so the OS doesn’t fall over after we come back:
KiErrata1337Present:
mov ecx, 0C0000101h ; read original GS_BASE MSR
rdmsr ;
push rdx ; backup original GS_BASE MSR
push rax ;
mov ecx, 0C0000102h ; read original KERNEL_GS_BASE MSR
rdmsr ;
push rdx ; backup original KERNEL_GS_BASE MSR
push rax ;
swapgs ; swapgs to emulate coming from user mode
xor eax, eax ;
xor edx, edx ; set KERNEL_GS_BASE MSR to zero
wrmsr ;
syscall ; execute syscall instruction which executes swapgs immediately
mov ecx, 0C0000102h ;
pop rax ;
pop rdx ; restore original KERNEL_GS_BASE MSR
wrmsr ;
mov ecx, 0C0000101h ;
pop rax ;
pop rdx ; restore original GS_BASE MSR
wrmsr ;
ret ; return back to caller
This already works in the sense of “doesn’t crash” — but it still doesn’t tell us the real syscall handler address. Time for the actual trick.
The juicy part: XCHG the trap-frame RIP with the SYSCALL return in RCX
SYSCALL saves the address of the instruction following itself in RCX (for SYSRET to return to). The Intel SDM semantics:
RCX ← RIP; (* Will contain address of next instruction *)
RIP ← IA32_LSTAR;
R11 ← RFLAGS;
RFLAGS ← RFLAGS AND NOT(IA32_FMASK);
// .... memes
So after our SYSCALL faults inside the kernel handler, RCX already holds the post-SYSCALL return target inside our detection routine. The trap frame at [rsp] (after we’ve discarded the error code) holds the kernel-handler RIP — the value we actually want to read out. A single XCHG swaps the two, and IRETQ branches us back into our detection code with the real syscall-handler address sitting in RCX. Two birds, one stone.
PageFaultHookHandler:
add rsp, 8 ; skip fault code on stack
xchg qword [rsp], rcx ; xchg trap frame RIP with syscall return address in RCX
iretq ; return from interrupt
PoC || GTFO
The full implementation, ready to paste:
// detect.c
VOID
DoTheThing(
VOID
)
{
KIDTENTRY64 TempIdt[19];
X64_DESCRIPTOR TempIdtr;
PVOID SyscallHandler;
TempIdtr.Limit = sizeof(TempIdt) - 1;
TempIdtr.Base = (UINT64)&TempIdt[0];
RtlCopyMemory(TempIdt, KeGetPcr()->IdtBase, TempIdtr.Limit + 1);
_disable(); // Disable interrupts
__sidt(&OriginalIdtr); // Backup original IDT
__lidt(&TempIdtr); // Load our temporary hook IDT
// Hook page fault handler.
TempIdt[X86_TRAP_PF].OffsetLow = (UINT16)(UINTN)PageFaultHookHandler;
TempIdt[X86_TRAP_PF].OffsetMiddle = (UINT16)((UINTN)PageFaultHookHandler >> 16);
TempIdt[X86_TRAP_PF].OffsetHigh = (UINT32)((UINTN)PageFaultHookHandler >> 32);
// Trigger syscall that will purposely page fault!
SyscallHandler = KiErrata1337Present();
__lidt(&OriginalIdtr); // Restore the original IDT.
_enable(); // Re-enable interrupts.
LOG_INFO("REAL SYSCALL Handler = 0x%p", SyscallHandler);
}
; detect.asm
PageFaultHookHandler:
add rsp, 8 ; skip fault code on stack
xchg qword [rsp], rcx ; xchg trap frame RIP with syscall return address in RCX
iretq
KiErrata1337Present:
push rbx ; backup RBX which is to be clobbered
mov ecx, 0C0000101h ; read original GS_BASE MSR
rdmsr ;
push rdx ; backup original GS_BASE MSR
push rax ;
mov ecx, 0C0000102h ; read original KERNEL_GS_BASE MSR
rdmsr ;
push rdx ; backup original KERNEL_GS_BASE MSR
push rax ;
swapgs ; swapgs to emulate coming from user mode
xor eax, eax ;
xor edx, edx ; set KERNEL_GS_BASE MSR to zero
wrmsr ;
syscall ; execute syscall instruction which executes swapgs immediately
mov rbx, rcx ; store result syscall handler address in RBX for now
mov ecx, 0C0000102h ;
pop rax ;
pop rdx ; restore original KERNEL_GS_BASE MSR
wrmsr ;
mov ecx, 0C0000101h ;
pop rax ;
pop rdx ; restore original GS_BASE MSR
wrmsr ;
mov rax, rbx ; return result in RAX
pop rbx ; restore original RBX
ret ; return back to caller
Full PoC repository: github.com/ajkhoury/Errata1337.
Key Takeaways
LSTARhooking is the obvious attack surface and the obvious detection surface. TrappingRDMSR/WRMSRonLSTARis the most efficient way to hook syscalls in a Windows guest — and that’s exactly why PatchGuard now probes it from multiple angles starting with Windows 10 1903.KiErrata420Presentabuses byte-level PG-context layout. One byte ofC3atPgContext+0x87A+ two bytes0F 05atPgContext+0x87Cform a one-shot dummy syscall handler + opcode trampoline. The check stands or falls on whether the VMM allows the guest’sWRMSRonLSTARto actually land.- Naive “just block
WRMSR(LSTAR)” hypervisors die here. Hyperbone-class implementations that disallow guest writes toLSTARoutright will execute their hooked handler instead of the one-byteRETstub, then fail to return cleanly. Fix: shadow the MSR transparently on both read and write. - PatchGuard has a separate
LSTAR-tamper check, so the shadow is safe. A guest can’t permanently rewriteLSTARfrom kernel mode without separately tripping PG — that gives the VMM the cover it needs to let the user-visible MSR fluctuate. KiErrata1337Presentreads the realLSTARwithout ever touching the MSR. ZeroIA32_KERNEL_GS_BASE, issueSYSCALL, let the kernel handler’sSWAPGS+segmented store fault, capture the faultingRIPon the trap frame. The MSR is read through an architectural side-channel.- The
XCHG qword [rsp], rcxis the elegant kernel-level pivot.SYSCALLpopulatesRCXwith its return address; the trap frame holds the kernel-handlerRIP. OneXCHGexchanges them,IRETQbranches back to the detector, andRCXcarries the truth. - Both detections require an IDT swap to avoid bugchecking. Temporary IDT, custom
#PFhandler, careful GS-base backup/restore — classic kernel-introspection plumbing. The detection routine has to stay lean enough not to trip the DPC/watchdog timeout while interrupts are disabled.
Hardening Checklist for Hypervisor Authors
- Shadow
LSTAR, don’t block it. OnRDMSR(LSTAR)return the guest’s saved original; onWRMSR(LSTAR)accept the write, but when the guest is restoring the value it expected to find, swap your hook back in. TheVmmHandleMsrRead/VmmHandleMsrWritepattern shown in the article is the right shape. - Don’t silently reject guest writes to security-relevant MSRs. A failed
WRMSR(LSTAR)is more detectable than a transparent one — PatchGuard’s probes are sensitive to behaviour, not just to the final MSR value. - Watch for byte-level pokes into the PatchGuard context.
C3and0F 05at constant PG-context offsets are a very specific fingerprint — if you ever introspect a PG callback, those bytes are a probe. - Assume
IA32_KERNEL_GS_BASEcan be deliberately corrupted from kernel mode. A faultingSWAPGSin the syscall handler is now an LSTAR-discovery technique, not just a bug. If you intercept#PFin the host, validate the trap frame and don’t leak the guest-side handler address. - Audit any IDT-related VMEXIT path.
KiErrata1337Present-style detections install a temporary IDT and revert it; that’s a strong signal a VMM can hook on, but only if it’s actually trackingLIDTand IDTR shadowing. - Don’t hook syscalls by remapping the
LSTARtarget page either. Both checks ultimately read the architectural targetRIP; nested-paging tricks that hide the page contents fromRDMSRstill expose theRIPvia the trap frame. - Treat any disable of PatchGuard as the loudest thing on the system. Disabling PG removes the cross-check that protects the shadow strategy — legitimate guest software won’t do it, and the system itself will broadcast the act of doing so.
- Read the PoC. The Errata1337 repo is short and complete — build it, run it under your VMM, and make sure both
KiErrata420PresentandKiErrata1337Presentreturn the values you expect.
Conclusion
The two checks in Part 2 reinforce the lesson from Part 1: an introspection hypervisor on Windows has to be architecturally transparent, not just functionally correct. KiErrata420Present doesn’t care that your hook is sophisticated — it cares whether the guest’s own WRMSR(LSTAR) takes effect when it expects to. KiErrata1337Present doesn’t care about MSRs at all; it builds the read out of SWAPGS, the trap frame, and an XCHG. Both are deeply mechanical, both are short, and both are easy to bolt onto a normal anti-cheat or EDR probe. For VMM authors the practical answer is the same in every case: shadow MSR state instead of blocking it, validate guest interruptibility on every relevant exit, and assume that any detection you can think of in five minutes is already inside nt!Ki* somewhere.
Original text: “Patchguard: Detection of Hypervisor Based Introspection [P2]” by Aidan Khoury at Reverse Engineering (revers.engineering).

![PatchGuard’s Detection of Hypervisor-Based Introspection: KiErrata420Present and Errata1337 [P2]](https://core-jmp.org/wp-content/uploads/2026/06/Hyper-V-300x300.png)