PatchGuardEncryptorDriver

March 29, 2025 ยท View on GitHub

I created My own Patch Guard driver that dynamically monitors the following mechanisms in kernel space:

  • System Service Descriptor Table (SSDT)
  • Interrupt Dispatch Table (IDT)
  • Model Specific Registers (MSRs)

At DriverEntry() the driver starts by initially capturing each mechanism's state (initial MSR state, initial IDT state and initial SSDT state) at driver load time and saves it into kernel memory.

The monitoring is performed through allocating 3 timers (_KTIMER objects) in kernel space (for each monitored mechanism) and each timer has an associated DPC attached to it.

  • The SSDT monitoring timer has an associated DPC routine that gets the base address of the SSDT and compares each SSDT entry with the inital state's relative SSDT entry. The number of SSDT entries available on the system is resolved at DriverEntry() using the function FillNumberOfSSDTEntries() function in the driver and is saved into a global driver variable.

  • The IDT monitoring timer has an associated DPC routine that runs over each entry in the IDT and compares each entry with the initial IDT entry value captured at driver load.

  • The MSRs monitoring timer has an associated DPC routine that read the values of certain MSRs and dynamically compares it to their relevant MSR state. The number of MSRs that are being checked is different based on if VMX is available on the system (I performed the check by reading the CR4.VMXE bit value which is the 13th bit in the CR4 register).

At the start of the Each DPC routine, the IRQL of the processor immediately increases to HIGH_LEVEL (0xf), this is because an attacker can potentially overwrite an entry in one of the monitored mechanisms and also immediately increase the IRQL to HIGH_LEVEL to avoid the timer's DPCs from invoking. At the end of the DPC routine, the IRQL is lowered back to DISPATCH_LEVEL (0x2) using the KeLowerIrql() function.

The second thing I implemented is an IntegrityCheck structure that allocates another 3 timer (_KTIMER) objects. Each IntegrityCheck timer object is responsible to perform an integrity check on its relative patch guard timer.

Each integrity check timer has an associated DPC that is responsible to check that the following didn't occur:

  • The _KTIMER->Dpc field of the patch guard timer object isn't being overwritten with a malicious _KDPC routine potentially overwritten by an attacker.
  • The DeferredRoutine value of the patch guard's DPC isn't being overwritten with a malicious function pointer by an attacker.

The first check with _KTIMER->Dpc was a bit tricky since the value of the DPC shown in the _KTIMER field is bitwise manipulated:

dt nt!_KTIMER 0xFFFFF8073BA68080

image

To discover how it's constructed, I needed to reverse the KeSetTimerEx() kernel function: image

After reversing the bitwise operations shown above, I created a function called CalculateTimerDPCValue() and a helper function called ROR8() function. The CalculateTimerDPCValue() takes 2 arguments:

  • The _KDPC base address of the associated timer.
  • The _KTIMER kernel object base address.
// Right rotate function for 64-bit values
static inline ULONG_PTR ROR8(ULONG_PTR value, BYTE shift)
{
	return (value >> shift) | (value << (64 - shift));
}

ULONG_PTR IntegrityCheck::CalculateTimerDPCValue(PKDPC Dpc, PKTIMER KTimer)
{
	ULONG_PTR KernelBaseAddress = (ULONG_PTR)g_KernelInfo->KernelBaseAddress;

	// Offsets for nt!KiWaitAlways and nt!KiWaitNever (These offsets change between builds!!!)
	ULONG_PTR KiWaitAlwaysAddress = KernelBaseAddress + 0x00fc5260;
	ULONG_PTR KiWaitNeverAddress = KernelBaseAddress + 0x00fc4f80;

	// Read values from memory
	ULONG_PTR KiWaitAlwaysValue = *(ULONG_PTR*)KiWaitAlwaysAddress;
	ULONG_PTR KiWaitNeverValue = *(ULONG_PTR*)KiWaitNeverAddress;

	// Read the shift count from KiWaitNever (ensuring it's a valid rotation amount)
	BYTE shift = *(BYTE*)(KiWaitNeverAddress) & 0x3F; // Masking to avoid invalid shifts (0x3F = 63)

	ULONG_PTR intermediate = _byteswap_uint64((ULONG_PTR)Dpc ^ KiWaitAlwaysValue);
	ULONG_PTR rotated = ROR8(intermediate ^ (ULONG_PTR)KTimer, shift);

	return rotated ^ KiWaitNeverValue;
}

After calculating the bitwised DPC value of the relative patch guard timer's DPC, I compared this value, with the currently setted value within the patch guard's timer.

The second check performed takes the original DeferredRoutine function pointer associated with the patch guard timer's DPC and dynamically compares it with the current DeferredRoutine function pointer setted in the _KDPC object.

These checks are performed in each IntegrityCheck timer responsible for the integrity of each patch guard's timer initially created by the driver.

--PLEASE NOTE THAT THE OFFSETS USED IN THIS DRIVER ARE DIFFERENT BETWEEN OS BUILDS!!--