Welcome to this new Medium post, today I will show you an interesting way used by security software to detect the remote thread creation in Windows
Remote thread creation is one of the most common techniques used by malware to inject code into another process. Tools like Cobalt Strike, Metasploit, or custom malware use Windows APIs like CreateRemoteThread to start a new thread inside a target process. This allows the attacker to execute arbitrary code in the context of a legitimate process, making detection harder for traditional antivirus solutions
But how do EDR products and security drivers actually detect this? The answer is kernel callbacks, specifically, PsSetCreateThreadNotifyRoutine

Courses: Learn how offensive development works on Windows OS from beginner to advanced taking our courses, all explained in C++.
Technique Database: Access 70+ real offensive techniques with weekly updates, complete with code, PoCs, and AV scan results:
Modules: Dive deep into essential offensive topics with our modular text-training program! Get a new module every 14 days. Start at just $1.99 per module, or unlock lifetime access to all modules for $100.
What is PsSetCreateThreadNotifyRoutine?
Windows provides a kernel API called PsSetCreateThreadNotifyRoutine that allows a driver to register a callback function. Every time a thread is created or destroyed anywhere in the system, the OS will call your function and give you three pieces of information:
- ProcessId: the PID of the process where the thread will run
- ThreadId: the TID of the new thread
- Create: a boolean telling you if the thread is being created or destroyed
The key detail here is that this callback runs in the context of the creator process, not the target process. This is what makes remote thread detection possible
How Do We Detect Remote Threads?
The logic is simple. Inside the callback, we compare two values:
ProcessIdd:the process that owns the new thread (the target)PsGetCurrentProcessId():the process that is currently executing (the creator)
If these two PIDs are different, it means one process is creating a thread inside another process. That is remote thread creation
And this looks like:
VOID ThreadNotifyCallback(
IN HANDLE ProcessId,
IN HANDLE ThreadId,
IN BOOLEAN Create)
{
if (Create) {
HANDLE CurrentProcessId = PsGetCurrentProcessId();
if (CurrentProcessId != ProcessId) {
// Remote thread detected!
// CurrentProcessId is the creator
// ProcessId is the target
}
}
}There is one exception we need to handle: the System process (PID 4). The Windows kernel itself creates threads in other processes as part of normal system behavior, so we filter it out to avoid false positives
Complete code
#include <ntdef.h>
#include <ntifs.h>
#include <ntstatus.h>
#include <wdf.h>
#include "thread_monitor_log.h"
EXTERN_C_START
// Forward declarations
VOID ThreadNotifyCallback(
IN HANDLE ProcessId,
IN HANDLE ThreadId,
IN BOOLEAN Create
);
DRIVER_INITIALIZE DriverEntry;
NTSTATUS
EvtDeviceAdd(
_In_ WDFDRIVER Driver,
_In_ PWDFDEVICE_INIT DeviceInit
);
VOID
EvtDriverUnload(
_In_ WDFDRIVER Driver
);
EXTERN_C_END
// Debug print macro
#define THREAD_MONITOR_DBG(fmt, ...) \
KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, \
"[ThreadMonitor] " fmt "\n", __VA_ARGS__))
#define THREAD_MONITOR_ERR(fmt, ...) \
KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, \
"[ThreadMonitor] ERROR: " fmt "\n", __VA_ARGS__))
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath){
NTSTATUS Status;
WDF_DRIVER_CONFIG Config;
WDFDRIVER WdfDriver;
THREAD_MONITOR_DBG("%s", "Driver loading...");
// Initialize WDF driver configuration
WDF_DRIVER_CONFIG_INIT(&Config, EvtDeviceAdd);
Config.EvtDriverUnload = EvtDriverUnload;
// Create WDF driver object
Status = WdfDriverCreate(
DriverObject,
RegistryPath,
WDF_NO_OBJECT_ATTRIBUTES,
&Config,
&WdfDriver
);
if (!NT_SUCCESS(Status)) {
THREAD_MONITOR_ERR("WdfDriverCreate failed: 0x%X", Status);
return Status;
}
Status = ThreadMonitorInitializeLogFile();
if (!NT_SUCCESS(Status)) {
THREAD_MONITOR_ERR("Log file init failed: 0x%X", Status);
// No hagas return, que el driver siga funcionando sin log
}
// Register thread notification callback
Status = PsSetCreateThreadNotifyRoutine(ThreadNotifyCallback);
if (!NT_SUCCESS(Status)) {
THREAD_MONITOR_ERR("PsSetCreateThreadNotifyRoutine failed: 0x%X", Status);
return Status;
}
THREAD_MONITOR_DBG("%s", "Thread notifications hooked successfully!");
return STATUS_SUCCESS;
}
NTSTATUS EvtDeviceAdd(_In_ WDFDRIVER Driver, _In_ PWDFDEVICE_INIT DeviceInit){
UNREFERENCED_PARAMETER(Driver);
UNREFERENCED_PARAMETER(DeviceInit);
THREAD_MONITOR_DBG("%s", "EvtDeviceAdd called (no device creation needed)");
// For thread monitoring, we don't need to create an actual device
return STATUS_SUCCESS;
}
VOID EvtDriverUnload(_In_ WDFDRIVER Driver){
UNREFERENCED_PARAMETER(Driver);
NTSTATUS Status;
THREAD_MONITOR_DBG("%s", "Driver unloading...");
// Remove the thread notification callback
Status = PsRemoveCreateThreadNotifyRoutine(ThreadNotifyCallback);
if (!NT_SUCCESS(Status)) {
THREAD_MONITOR_ERR("PsRemoveCreateThreadNotifyRoutine failed: 0x%X", Status);
}
ThreadMonitorCloseLogFile();
THREAD_MONITOR_DBG("%s", "Driver unloaded successfully!");
}
VOID ThreadNotifyCallback(IN HANDLE ProcessId, IN HANDLE ThreadId, IN BOOLEAN Create){
// The notification occurs in the context of the CREATOR process,
if (Create) {
HANDLE CurrentProcessId = PsGetCurrentProcessId();
// ===== THREAD CREATED =====
if(CurrentProcessId != ProcessId){
// If thread is created from a different process that means remote thread creation
// We can log this as a potential injection attempt
// Filter out System process (PID 4)
if (CurrentProcessId == (HANDLE)4) {
return; // Normal kernel behavior, ignore
}
THREAD_MONITOR_DBG("Remote Thread Created: PID=%d, TID=%d (Created by PID=%d)",
(ULONG)(ULONG_PTR)ProcessId,
(ULONG)(ULONG_PTR)ThreadId,
(ULONG)(ULONG_PTR)CurrentProcessId);
THREAD_MONITOR_LOG("Remote Thread Created: PID=%d, TID=%d (Created by PID=%d)",
(ULONG)(ULONG_PTR)ProcessId,
(ULONG)(ULONG_PTR)ThreadId,
(ULONG)(ULONG_PTR)CurrentProcessId);
}
else {
// Thread Created in its own process context
}
}
}Testing
To test the detection we can use any tool that performs remote thread injection. For this demo I used a simple program that calls CreateRemoteThread on a target process.
Once the driver is loaded, we can see the detection both in DebugView and in the log file:
Remote Thread Created: PID=7832, TID=1204 (Created by PID=5765)The driver correctly identifies the creator process and the target process, which is exactly the information an EDR would use to decide if the activity is malicious
Conclusion
This is the same fundamental mechanism that real EDR products use to monitor thread creation across the system. Of course, commercial solutions add many more layers on top: they correlate the creator process reputation, inspect the thread start address, check if it points to unbacked memory, and combine this with other telemetry sources like image load callbacks and object handle monitoring.
But the core detection starts here, a simple kernel callback that tells you when one process creates a thread inside another
📌 Follow me: YouTube | 🐦 X | 💬 Discord Server | 📸 Instagram | Newsletter
S12.