Dissecting a Windows Kernel Shield Driver: Inside Callback Internals
Kernel shield
Table of Contents
- Introduction
- General Information About the Driver
- Decoding DriverEntry
- Manipulating LDR_DATA_TABLE_ENTRY
- Process termination IOCTL function
- Process protection via ObRegisterCallbacks
- Conclusion
Introduction
In this analysis, we examine a 64-bit Windows driver called driver_challenge, which implements a kernel shield—that is, kernel-level protection for certain processes. The primary purpose of this driver is to make processes persistent and protect them from being terminated by the user or by third-party tools, such as Task Manager.
General information about the driver
- Name: driver_challenge
- Type: PE32+ executable for MS Windows 6.01 (native)
- Arch: x86-64
- Sections: 7 sections
The driver’s main entry point initializes structures, creates a device, and registers kernel callbacks to intercept certain operations on processes.
Decoding DriverEntry
Here is the code for the function that is called in DriverEntry
ulonglong FUN_14000114c(longlong param_1)
{
uint *puVar1;
uint uVar2;
int iVar3;
ulonglong uVar4;
undefined8 local_res8 [4];
undefined local_28 [16];
undefined local_18 [16];
puVar1 = (uint *)(*(longlong *)(param_1 + 0x28) + 0x68);
*puVar1 = *puVar1 | 0x20;
_DAT_140003018 = 0;
RtlInitUnicodeString(local_28,L"\\Device\\NSecKrnl");
RtlInitUnicodeString(local_18,L"\\DosDevices\\NSecKrnl");
*(code **)(param_1 + 0x70) = FUN_140001010;
*(code **)(param_1 + 0x80) = FUN_140001010;
*(code **)(param_1 + 0xe0) = FUN_140001030;
*(code **)(param_1 + 0x68) = FUN_1400010e0;
uVar4 = IoCreateDevice(param_1,0,local_28,0x22,0,0,local_res8);
if (-1 < (int)uVar4) {
uVar2 = IoCreateSymbolicLink(local_18,local_28);
uVar4 = (ulonglong)uVar2;
if ((int)uVar2 < 0) {
IoDeleteDevice(local_res8[0]);
}
else {
iVar3 = PsSetCreateProcessNotifyRoutine(FUN_140001480,0);
DAT_140003010 = -1 < iVar3;
iVar3 = PsSetLoadImageNotifyRoutine(_guard_check_icall);
DAT_140003011 = -1 < iVar3;
FUN_140001518();
}
}
return uVar4;
}
The purpose of the *puVar1 variable within param_1 is unclear. To determine this, we examine the subsequent call to IoCreateDevice(), which receives a PDRIVER_OBJECT as an argument.
- param_1 = PDRIVER_OBJECT
- puVar1 = DRIVER_OBJECT
PDRIVER_OBJECT is a pointer to DRIVER_OBJECT.
https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_driver_object
typedef struct _DRIVER_OBJECT {
CSHORT Type; // 0x02
CSHORT Size; // 0x02
PDEVICE_OBJECT DeviceObject; // 0x08
ULONG Flags; // 0x04
PVOID DriverStart; // 0x08
ULONG DriverSize;
PVOID DriverSection;
PDRIVER_EXTENSION DriverExtension;
UNICODE_STRING DriverName;
PUNICODE_STRING HardwareDatabase;
PFAST_IO_DISPATCH FastIoDispatch;
PDRIVER_INITIALIZE DriverInit;
PDRIVER_STARTIO DriverStartIo;
PDRIVER_UNLOAD DriverUnload;
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT, *PDRIVER_OBJECT;
Now that we know that puVar1 and param_1 are structures for DRIVER, let’s break down these two lines:
puVar1 = (uint *)(*(longlong *)(param_1 + 0x28) + 0x68);
*puVar1 = *puVar1 | 0x20;
When padding is added to the structure and variables along with their sizes, we end up at DriverSection for the + 0x28.
Note: Windows x64 aligns all pointers to 8 bytes and adds padding if necessary.
CSHORT Type; // 0x00
CSHORT Size; // 0x02
// 0x04-0x07 padding for align DeviceObject
PDEVICE_OBJECT DeviceObject; // 0x08–0x0F
ULONG Flags; // 0x10–0x13
// 0x14–0x17 padding for align DriverStart
PVOID DriverStart; // 0x18–0x1F
ULONG DriverSize; // 0x20–0x23
// 0x24–0x27 padding for align DriverSection
PVOID DriverSection; // 0x28–0x2F
Regarding DriverSection, Microsoft states:
- Points to the driver’s section object, which represents the driver image in the memory manager. This is an opaque system structure used internally by the memory manager and loader. Drivers should not access or modify this member.
Working with LDR_DATA_TABLE_ENTRY
Often, PVOID DriverStart points to an LDR_DATA_TABLE_ENTRY structure, which Microsoft does not officially document. However, researchers have managed to document it:
https://www.geoffchappell.com/studies/windows/km/ntoskrnl/inc/api/ntldr/ldr_data_table_entry.htm
According to the documentation, the offset 0x68 in the LDR_DATA_TABLE_ENTRY corresponds to ULONG Flags;
https://www.tssc.de/winint/Win11_22631_2428_ntoskrnl/_LDR_DATA_TABLE_ENTRY.htm The OR operation with 0x20 sets the ProcessStaticImport flag, marking the driver as “loaded and active” for the Windows loader. To explain what this actually does: Windows checks drivers using data in LDR_DATA_TABLE_ENTRY, so if the driver itself modifies that data, it can bypass Windows checks. It changes its status, thereby circumventing certain checks. Normally, a driver never modifies LDR_DATA_TABLE_ENTRY; it uses the official APIs.
UINT8 FlagGroup[4]; // 0x0068; 0x0004 Bytes
ULONG32 Flags; // 0x0068; 0x0004 Bytes
struct // 0x0068; 29 elements; 0x0004 Bytes
{
ULONG32 PackagedBinary : 1; // 0x0068; Bit: 0
ULONG32 MarkedForRemoval : 1; // 0x0068; Bit: 1
ULONG32 ImageDll : 1; // 0x0068; Bit: 2
ULONG32 LoadNotificationsSent : 1; // 0x0068; Bit: 3
ULONG32 TelemetryEntryProcessed : 1; // 0x0068; Bit: 4
ULONG32 ProcessStaticImport : 1; // 0x0068; Bit: 5
ULONG32 InLegacyLists : 1; // 0x0068; Bit: 6
ULONG32 InIndexes : 1; // 0x0068; Bit: 7
ULONG32 ShimDll : 1; // 0x0068; Bit: 8
ULONG32 InExceptionTable : 1; // 0x0068; Bit: 9
ULONG32 ReservedFlags1 : 2; // 0x0068; Bits: 10 - 11
ULONG32 LoadInProgress : 1; // 0x0068; Bit: 12
ULONG32 LoadConfigProcessed : 1; // 0x0068; Bit: 13
ULONG32 EntryProcessed : 1; // 0x0068; Bit: 14
ULONG32 ProtectDelayLoad : 1; // 0x0068; Bit: 15
ULONG32 ReservedFlags3 : 2; // 0x0068; Bits: 16 - 17
ULONG32 DontCallForThreads : 1; // 0x0068; Bit: 18
ULONG32 ProcessAttachCalled : 1; // 0x0068; Bit: 19
ULONG32 ProcessAttachFailed : 1; // 0x0068; Bit: 20
ULONG32 CorDeferredValidate : 1; // 0x0068; Bit: 21
ULONG32 CorImage : 1; // 0x0068; Bit: 22
ULONG32 DontRelocate : 1; // 0x0068; Bit: 23
ULONG32 CorILOnly : 1; // 0x0068; Bit: 24
ULONG32 ChpeImage : 1; // 0x0068; Bit: 25
ULONG32 ChpeEmulatorImage : 1; // 0x0068; Bit: 26
ULONG32 ReservedFlags5 : 1; // 0x0068; Bit: 27
ULONG32 Redirected : 1; // 0x0068; Bit: 28
ULONG32 ReservedFlags6 : 2; // 0x0068; Bits: 29 - 30
ULONG32 CompatDatabaseProcessed : 1; // 0x0068; Bit: 31
};
Next, the array PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];, is initialized in the following code:
*(code **)(param_1 + 0x70) = FUN_140001010;
*(code **)(param_1 + 0x80) = FUN_140001010;
*(code **)(param_1 + 0xe0) = FUN_140001030;
*(code **)(param_1 + 0x68) = FUN_1400010e0;
Process termination IOCTL function
We now want to know which IOCTL code can be used to force a process to terminate.
In the function FUN_140001030:
undefined4 FUN_140001030(undefined8 param_1,longlong param_2)
{
int iVar1;
longlong *plVar2;
char cVar3;
ulonglong uVar4;
undefined4 uVar5;
plVar2 = *(longlong **)(param_2 + 0x18);
uVar5 = 0xc0000001;
iVar1 = *(int *)(*(longlong *)(param_2 + 0xb8) + 0x18);
if (iVar1 == 0x2248d4)
{
if (plVar2 != (longlong *)0x0)
{
cVar3 = FUN_1400012b8(*plVar2);
uVar5 = 0xc0000001;
if (cVar3 != '\0') {
uVar5 = 0;
}
}
}
else {
if (iVar1 == 0x2248d8) {
if (plVar2 == (longlong *)0x0) goto LAB_1400010be;
uVar4 = FUN_140001614(*plVar2);
cVar3 = (char)uVar4;
}
else if (iVar1 == 0x2248dc) {
if (plVar2 == (longlong *)0x0) goto LAB_1400010be;
cVar3 = FUN_140001240(*plVar2);
}
else {
if ((iVar1 != 0x2248e0) || (plVar2 == (longlong *)0x0)) goto LAB_1400010be;
uVar4 = FUN_1400013e8(*plVar2);
cVar3 = (char)uVar4;
}
if (cVar3 != '\0') {
uVar5 = 0;
}
}
LAB_1400010be:
*(undefined4 *)(param_2 + 0x30) = uVar5;
IofCompleteRequest(param_2,0);
return uVar5;
}
We can see that plVar2 equals param_2. The following function will help us determine which variable is param_2 actually is.
In the FUN_1400013e8 function, param_2 is passed to the PsLookupProcessByProcessId function, which takes a HANDLE, so we can conclude that param_2 is a handle and plVar2 is as well. The process termination code is 0x2248e0.
ulonglong FUN_1400013e8(undefined8 param_1)
{
ulonglong uVar1;
undefined8 local_res10;
longlong local_res18 [2];
local_res18[0] = 0;
local_res10 = 0;
uVar1 = PsLookupProcessByProcessId(param_1,local_res18);
if (-1 < (int)uVar1)
{
uVar1 = ObOpenObjectByPointer
(local_res18[0],0x200,0,1,*(undefined8 *)PsProcessType_exref,0,&local_res10);
if (-1 < (int)uVar1)
{
ZwTerminateProcess(local_res10,0);
uVar1 = ZwClose(local_res10);
}
}
if (local_res18[0] != 0) {
uVar1 = ObfDereferenceObject();
}
return uVar1 & 0xffffffffffffff00;
}
Returning to the first function, we can see a call to IoCreateDevice() function. This function is used to create an object that represents a hardware device, in order to interact with the hardware.
The value passed is 0x22, which is FILE_DEVICE_UNKNOWN. This means that no specific behavior is enforced; the driver is free to define its own IOCTLs. This is very commonly used in vulnerable drivers or custom drivers.
https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/specifying-device-types
NTSTATUS IoCreateDevice(
[in] PDRIVER_OBJECT DriverObject,
[in] ULONG DeviceExtensionSize,
[in, optional] PUNICODE_STRING DeviceName,
[in] DEVICE_TYPE DeviceType,
[in] ULONG DeviceCharacteristics,
[in] BOOLEAN Exclusive,
[out] PDEVICE_OBJECT *DeviceObject
);
Process protection using ObRegisterCallbacks
The function below shows that a callback is registered via the ObRegisterCallbacks function, which is used to take control of kernel objects—in this case, processes.
The ObRegisterCallbacks function takes the OB_CALLBACK_REGISTRATION structure as its first parameter, which itself contains a pointer to the OB_OPERATION_REGISTRATION structure that holds the callback function.
This means that every time someone tries to open or manipulate a process, it triggers the callback function.
In our case, this is the FUN_1400014b0 function.
This is often used in EDRs, anti-cheat systems, or malware.
void FUN_140001518(void)
{
int iVar1;
code *local_58;
undefined8 local_50;
code *local_48;
undefined8 local_40;
undefined local_38 [8];
undefined auStack_30 [8];
undefined local_28 [16];
code **local_18;
local_58 = PsProcessType_exref;
local_50 = 3;
local_48 = FUN_1400014b0;
local_18 = (code **)0x0;
local_40 = 0;
local_28 = ZEXT816(0);
stack0xffffffffffffffcc = SUB1612(ZEXT816(0),4);
local_38._0_4_ = 0x10100;
RtlInitUnicodeString(auStack_30,L"328987");
local_18 = &local_58;
local_28._8_8_ = 0;
iVar1 = ObRegisterCallbacks(local_38,&DAT_140003020);
if (iVar1 < 0) {
DAT_140003020 = 0;
}
return;
}
Here is an example function for initializing and registering a callback:
Source: https://medium.com/@s12deff/register-windows-object-callbacks-from-kernel-driver-e7bf4fd1e30c
Next, the prototype of a callback function generally looks like this: OB_PREOP_CALLBACK_STATUS CreateCallback(PVOID RegistrationContext, POB_PRE_OPERATION_INFORMATION OperationInformation)
To understand exactly what our callback does, we need to look at the function FUN_1400014b0 and understand what it does.
- FUN_1400014b0:
undefined8 FUN_1400014b0(undefined8 param_1,longlong param_2)
{
char cVar1;
longlong lVar2;
longlong lVar3;
if (((param_2 != 0) && (lVar3 = *(longlong *)(param_2 + 8), lVar3 != 0)) &&
(*(longlong *)(param_2 + 0x20) != 0)) {
lVar2 = IoGetCurrentProcess();
if (lVar2 != lVar3) {
lVar3 = PsGetProcessId(lVar3);
cVar1 = FUN_14000138c(lVar3);
if (cVar1 != '\0') {
lVar3 = PsGetCurrentProcessId();
cVar1 = FUN_140001330(lVar3);
if (cVar1 == '\0') {
**(uint **)(param_2 + 0x20) = **(uint **)(param_2 + 0x20) & 0xfffffffe;
}
}
}
}
return 0;
}
We now know that ‘param_1’ is a ‘PVOID’ and ‘param_2’ is a ‘POB_PRE_OPERATION_INFORMATION’.
- POB_PRE_OPERATION_INFORMATION : https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_ob_pre_operation_information
*(longlong *)(param_2 + 8) =PVOID Object.
*(longlong *)(param_2 + 0x20) = POB_PRE_OPERATION_PARAMETERS Parameters;
If (lVar2 != lVar3), the code checks whether the current process differs from the target process.
What’s really interesting is the line **(uint **)(param_2 + 0x20) = **(uint **)(param_2 + 0x20) & 0xfffffffe;.
**(uint **)(param_2 + 0x20) is dereferenced, so this takes us further into the structure; here is a quick diagram:
POB_PRE_OPERATION_PARAMETERS –> _OB_PRE_OPERATION_PARAMETERS –> _OB_PRE_CREATE_HANDLE_INFORMATION
- _OB_PRE_CREATE_HANDLE_INFORMATION: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_ob_pre_create_handle_information
typedef struct _OB_PRE_CREATE_HANDLE_INFORMATION {
ACCESS_MASK DesiredAccess;
ACCESS_MASK OriginalDesiredAccess;
} OB_PRE_CREATE_HANDLE_INFORMATION, *POB_PRE_CREATE_HANDLE_INFORMATION;
So **(uint **)(param_2 + 0x20) = DesiredAccess.
A bitmask operation using & 0xfffffffe is performed on Parameters->CreateHandleInformation.DesiredAccess.
DesiredAccess holds a value that controls the permissions the program requests when it starts a process.
The values for this variable can be found in WinNT.h. https://gist.github.com/JamesMenetrey/d3f494262bcab48af1d617c3d39f34cf
Here is a list of the defines for DesiredAccess:
#define PROCESS_TERMINATE (0x0001)
#define PROCESS_CREATE_THREAD (0x0002)
#define PROCESS_SET_SESSIONID (0x0004)
#define PROCESS_VM_OPERATION (0x0008)
#define PROCESS_VM_READ (0x0010)
#define PROCESS_VM_WRITE (0x0020)
#define PROCESS_DUP_HANDLE (0x0040)
#define PROCESS_CREATE_PROCESS (0x0080)
#define PROCESS_SET_QUOTA (0x0100)
#define PROCESS_SET_INFORMATION (0x0200)
#define PROCESS_QUERY_INFORMATION (0x0400)
#define PROCESS_SUSPEND_RESUME (0x0800)
#define PROCESS_QUERY_LIMITED_INFORMATION (0x1000)
#define PROCESS_SET_LIMITED_INFORMATION (0x2000)
In our case, it sets & 0xfffffffe, which is equivalent to
- 0xfffffffe = ~0x0001
Parameters->CreateHandleInformation.DesiredAccess &= ~0x0001
This clears the first bit of DesiredAccess, revoking the PROCESS_TERMINATE permission while leaving all other rights intact. In summary, this makes the process persistent, preventing termination by the user or any third-party tool, including Task Manager.
https://www.sentinelone.com/vulnerability-database/cve-2025-68947/
Conclusion
This driver implements a kernel shield with two main objectives:
Camouflage & bypass: modifying LDR_DATA_TABLE_ENTRY to bypass load checks. Process protection: using ObRegisterCallbacks to revoke termination rights (PROCESS_TERMINATE).
Additionally, it exposes a customizable IOCTL that allows for forced process termination, illustrating the dual nature of a driver capable of both protecting and manipulating processes at the kernel level.
In summary, this type of driver is powerful and sensitive; it can be used for legitimate protections such as EDR or anti-cheat systems, or for malicious behaviors such as rootkits.