Bypassing Kernel Patch Protections on Windows
Kernel Patch Protection (KPP) is part of the NT kernel introduced with consumer versions of Windows NT, and in this post we'll break it
Kernel Patch Protection (KPP) is part of the NT kernel security system introduced with Windows XP and Windows Server 2000 SP1. In Win9x kernels, the system allowed you to freely manipulate OS kernel objects and functions.
Until KPP was deployed, it wasn’t uncommon for kernel components of various security products to aggressively set inline hooks to OS kernel functions to enhance detection. But this is also true for malware who also use the same kernel hooks to evade detection and manipulate system functions on a low-level basis . Thats why KPP is one of the major features supporting modern Windows security in the NT era.
The NT Kernel has an API called PsSetCreateProcessNotifyRoutine
that can register callbacks for driver-specified routines when processes are created and deleted. A similar function is the PsSetCreateProcessNotifyRoutineEx
API. The API with the Ex suffix is notified when processes are about to be created and deleted, and callback routines can refuse to load. PsSetCreateProcessNotifyRoutine
only does the notification.
Due to its nature, this function is monitored by KPP because it can be a huge attack surface if it’s able to be abused. PsSetCreateProcessNotifyRoutineEx
, PsSetCreateThreadNotifyRoutine
and PsSetLoadImageNotifyRoutine
are also protected as KPP protects key kernel objects.
KPP will immediately interrupt a software interrupt, an ISR called KiRaiseSecurityCheckFailure
and it will raise bugcheck 0x139
if the function is called from an unsigned kernel address space, causing a BSOD that states a kernel check has failed.
CRITICAL_STRUCTURE_CORRUPTION (109)
Arguments:
Arg1: a39fdcd99ec06505, Reserved
Arg2: b3b6e95ff1433664, Reserved
Arg3: 0000000000000002, Failure type dependent information
Arg4: 0000000000000018, Type of corrupted region, can be
18 : Kernel notification callout modification
The NT kernel has a static array that holds registered callbacks like PspCreateProcessNotifyRoutine
, and according to the documentation
For Windows Vista and later versions of Windows, the system can register up to 64 process-creation callback routines.
--CurrentThread->KernelApcDisable;
counter = 0i64;
while ( 1 )
{
CallbackBlock=ExReferenceCallBackBlock(&PspCreatcProcessNotifyRoutine[counter]);// Acquire Rundown Protection
_CallbackBlock = CallbackBlock;
if (CallbackBlock)
{
LODWORD(_Remove) = _Remove & 0xFFFFFFFE;
// Try remove NotifyRoutine if matches
if ( *((_QWORD *)CallbackBlock + 1) == _NotifyRoutine
&& *((_DWORD *)CallbackBlock + 4) == (_DWORD)_Remove
&& ExCompareExchangeCallBack(&PspCreateProcessllotifyRoutine[counter],0164,(__int64)CallbackBlock))
This array is able to be affected by DKOM (Direct Kernel Object Manipulation) attacks due to the callback being inserted directly into the array without going through the system call, thereby evading the KPP. You can get a pointer to this array by calculating the RVA (Relative Virtual Address) from the ntoskrnl
base address.
//
// this array (sizeof 64) contains all callback blocks
//
PVOID* PspCreateProcessNotifyRoutine = &*( PVOID* )
( ( UINT_PTR )NtosKrnlImageBase + RVA_PSP_CREATE_PROCESS_NOTIFY_ROUTINE );
Arrays have a block called Callback Block, which is generated by ExAllocateCallBack
. ExAllocateCallBack
is an undocumented NT kernel internal function. In the original syscall behavior, the function stores a pointer to the callback function that was actually passed to PspSetCreateProcessNotifyRoutine
, and Context stores the value passed as Remove.
typedef struct _CALLBACK_ROUTINE_BLOCK
{
EX_RUNDOWN_REF RundownProtect;
PEX_CALLBACK_FUNCTION Function;
PVOID Context;
} CALLBACK_ROUTINE_BLOCK, * PCALLBACK_ROUTINE_BLOCK;
Insert the callback block into the array with ExCompareExchangeCallBack
, which is also an undocumented internal NT kernel function. If it succeeds in inserting the value into the array, it increments the value of a counter called PspCreateProcessNotifyRoutineCount
which is static like the array.
//
// increment count
//
InterlockedIncrement( ( volatile LONG* )
&*( UINT32* )
( ( UINT_PTR )NtosKrnlImageBase + RVA_PSP_CREATE_PROCESS_NOTIFY_ROUTINE_COUNT ) );
The results are that we can hitch a ride and use PsSetCreateProcessNotifyRoutine
without triggering a KPP bugcheck and a BSOD.
Since Windows 10 21H2, Microsoft implemented to an update for KPP against this method, after publication of this exploit on GitHub. KPP will now check if the callback entry is within the legitimate address. But threat actors can still bypass this measure by placing callback routine stub on the legitimate RWX memory in code cave. We can also hijack existing callback routines by using inline-hooks not monitored by KPP.