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 .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 .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 .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 .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 .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 .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:
union BUGCHECK_STATE { volatile LONG Value; struct { 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 HalPrivateDispatchTable.HalTimerWatchdogStop(); // 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; } NoNmi: // Call HalPrepareForBugcheck from HalPrivateDispatchTable HalPrivateDispatchTable.HalPrepareForBugcheck(NmiFlag);
Windows 10:
// We will come back to this later... if ( !IsMaster ) goto HowAmISupposedToQuitFromThisSpinlockLol; // Call HalTimerWatchdogStop from HalPrivateDispatchTable HalPrivateDispatchTable.HalTimerWatchdogStop(); // Mask 0x2000 off HV enlightenments HvlEnlightenments &= ~0x4000; // Save BugCheck progress, what ??? IoSaveBugCheckProgress(0x60); // KiNmiInProgress checks bool NmiFlag = KeIsEmptyAffinityEx(&KiNmiInProgress); // Call HalPrepareForBugcheck from HalPrivateDispatchTable HalPrivateDispatchTable.HalPrepareForBugcheck(NmiFlag);
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:
// Define HAL_PRIVATE_DISPATCH_TABLE // #define HAL_PDT_PREPARE_FOR_BUGCHECK_OFFSET 0x108 #define HAL_PDT_PREPARE_FOR_BUGCHECK_MIN_VERSION 6 using FnHalPrepareForBugcheck = void( __stdcall )( BOOLEAN NmiFlag ); #define HAL_PDT_TIMER_WATCHDOG_STOP_OFFSET 0x338 #define HAL_PDT_TIMER_WATCHDOG_STOP_MIN_VERSION 23 using FnHalTimerWatchdogStop = NTSTATUS( __stdcall )(); #pragma pack(push, 1) typedef struct _HAL_PRIVATE_DISPATCH_TABLE { union { ULONG Version; struct { char Pad0[ HAL_PDT_PREPARE_FOR_BUGCHECK_OFFSET ]; FnHalPrepareForBugcheck* HalPrepareForBugcheck; }; struct { char Pad1[ HAL_PDT_TIMER_WATCHDOG_STOP_OFFSET ]; FnHalTimerWatchdogStop* HalTimerWatchdogStop; }; }; } HAL_PRIVATE_DISPATCH_TABLE; #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[] = { PrcbContext->Rdx, PrcbContext->R8, PrcbContext->R9, *( ULONG64* ) ( PrcbContext->Rsp + 0x28 ) }; // Collect information about the exception based on bugcheck code switch ( BugCheckCode ) { case UNEXPECTED_KERNEL_MODE_TRAP: // 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 ]; break; case SYSTEM_THREAD_EXCEPTION_NOT_HANDLED: ExceptionCode = BugCheckArgs[ 0 ]; ExceptionAddress = BugCheckArgs[ 1 ]; ExceptionRecord = ( EXCEPTION_RECORD* ) BugCheckArgs[ 2 ]; ContextRecord = ( CONTEXT* ) BugCheckArgs[ 3 ]; break; case SYSTEM_SERVICE_EXCEPTION: ExceptionCode = BugCheckArgs[ 0 ]; ExceptionAddress = BugCheckArgs[ 1 ]; ContextRecord = ( CONTEXT* ) BugCheckArgs[ 2 ]; break; case KMODE_EXCEPTION_NOT_HANDLED: case KERNEL_MODE_EXCEPTION_NOT_HANDLED: ExceptionCode = BugCheckArgs[ 0 ]; ExceptionAddress = BugCheckArgs[ 1 ]; break; default: 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 ) { break; } 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 ContextRecord->Rip++; BugCheck::Continue( ContextRecord ); } } ); __debugbreak();
Yay, success! Right? No. Try this instead and watch your entire system lock down:
KeIpiGenericCall( [ ] ( ULONG64 x ) { __debugbreak(); 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.
_disable(); KeRaiseIrql( HIGH_LEVEL ); if ( KeGetCurrentProcessorIndex() != ( KiBugCheckActive >> 4 ) ) { while ( 1 ) { if ( KeGetCurrentPrcb()->IpiFrozen == 5 ) KiFreezeTargetExecution( nullptr, nullptr ); _mm_pause(); } }
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 ) VfStopBranchTracing(); _disable(); __writecr8(0xFui64); KiStartDebugAccumulation(KeGetCurrentPrcb()); 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 ); _enable(); } // Extract arguments of the original call, BugCheck::Parse may clobber them ULONG BugCheckCode = BugCheckCtx->Rcx; ULONG64 BugCheckArgs[] = { BugCheckCtx->Rdx, BugCheckCtx->R8, BugCheckCtx->R9, *( ULONG64* ) ( BugCheckCtx->Rsp + 0x28 ) }; // Try parsing parameters CONTEXT* ContextRecord = nullptr; EXCEPTION_RECORD ExceptionRecord; if ( BugCheck::Parse( &ContextRecord, &ExceptionRecord, BugCheckCtx ) ) { // Try handling exception if ( Cb( ContextRecord, &ExceptionRecord ) == EXCEPTION_CONTINUE_EXECUTION ) { // Revert IRQL to match interrupted routine _disable(); __writecr8( BugCheckState->Cr8 ); // Restore context (These flags make the control flow a little simpler :wink:) ContextRecord->ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS | CONTEXT_FLOATING_POINT; 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::Initialize(); 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 ContextRecord->Rip++; return EXCEPTION_CONTINUE_EXECUTION; } return EXCEPTION_EXECUTE_HANDLER; }; KeIpiGenericCall( [ ] ( ULONG64 x ) { for ( int i = 0; i < 4; i++ ) __debugbreak(); 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: https://github.com/can1357/ByePg.
Great read, and interesting PoC. I really like the way you layout the articles too. Great post. Keep it up man!
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?
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.
Does this still work?
Yes
What’s the advantage of this over KeRegisterBugcheckCallback?
You can recover the system from it.
Does it can work in win8 / win7?
Yes.
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?
Hey Yuri, you probably have to change the patterns in the scanner, though I remember having issues with Windows 7 in the first place.
Will this system wide hook (ByePG) work if a detour patch is installed in the SwapContext kernel mode function ?
Hello brother, may I ask how to use this function if it is used to HOOK non-syscall? I want to use page exception, but I do not know how to deal with it
Thanks to the boss, I finally understand this. This is to remove the blue screen through abnormal recovery status