-1

I’m building a lightweight process monitoring tool in C# (.NET Framework 4.6.2). Here’s my situation:

I cannot use WMI due to it's speed; (this is slow for antivirus app).

I also cannot rely on ETW (Microsoft.Diagnostics.Tracing.TraceEvent) because it brings too many external dependencies for my use case.

Basically, I want to detect when new processes are created or terminated, similar to what Win32_ProcessStartTrace / Win32_ProcessStopTrace provide in WMI

I’m fine with using P/Invoke or low-level Windows APIs.

So my questions are:

Is there a pure WinAPI approach to subscribe to process creation/termination notifications?

Are there any examples or references for implementing this in C# safely (without polling every second)?

Any practical guidance or native API references would be greatly appreciated.

2
  • 1
    You should have to use Detours (but maybe an existing hooking package) to hook NtCreateProcessEx, or probably better CreateProcessInternal (which is undocumented, but it's the same thing as CreateProcess, except it has a first DWORD parameter, which represents the execution context, and a last DWORD parameter which is reserved). The former is a hassle, the latter is undocumented, but it's the function that advapi32.dll actually uses when you call CreateProcessAsUser Commented Nov 1 at 18:54
  • WinRM/WMI is your real solution. If WinRM is disabled how do you hope to get remote info? Why is WinRM disabled anyway, is there a specific security concern with it? Commented Nov 1 at 19:04

1 Answer 1

1

There is actually no easy way to do it with all restrictions you have got. ETW is the lowest you can go, however given your restriction of not using it, you have 2 options:

  • Write kernel driver yourself within C and expose its interface, then use P/Invoke from c#
  • Use evntrace.dll to monitor ETW events (OpenTrace and ProcessTrace) and further tdh.dll if you need detailed information.

You can find example of parsing detailed data using tdh here

Bellow is example of the implementation to give you an idea (AI generated, not tested):
* Note 1 - This is Event Id for Start Process event. You can also subscribe to Close (id 2) or others. Referee to the documentation
* Note 2 - This is guid of Kernel Process

using System;
using System.Runtime.InteropServices;

public class EtwProcessMonitor
{
    private const int PROCESS_START_EVENT_ID = 1; // Note 1
    private static readonly Guid KernelProcessGuid = new Guid("3d6fa8d1-fe05-11d0-9dda-00c04fd7ba7c"); // Note 2

    private delegate void EventRecordCallback(ref EVENT_RECORD eventRecord);

    [StructLayout(LayoutKind.Sequential)]
    public struct EVENT_TRACE_LOGFILE
    {
        [MarshalAs(UnmanagedType.LPWStr)]
        public string LogFileName;
        [MarshalAs(UnmanagedType.LPWStr)]
        public string LoggerName;
        public IntPtr ProcessTraceMode;
        public EventRecordCallback EventCallback;
        public uint BufferSize;
        public uint MinimumBuffers;
        public uint MaximumBuffers;
        public uint MaximumFileSize;
        public uint LogFileMode;
        public uint FlushTimer;
        public uint EnableFlags;
        public int AgeLimit;
        public IntPtr Context;
        public Guid Guid;
        public IntPtr UserContext;
        public IntPtr EventCallback2;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct EVENT_HEADER
    {
        public ushort Size;
        public ushort HeaderType;
        public ushort Flags;
        public ushort EventProperty;
        public uint ThreadId;
        public uint ProcessId;
        public long TimeStamp;
        public Guid ProviderId;
        public EVENT_DESCRIPTOR EventDescriptor;
        public ulong ProcessorTime;
        public Guid ActivityId;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct EVENT_DESCRIPTOR
    {
        public ushort Id;
        public byte Version;
        public byte Channel;
        public byte Level;
        public byte Opcode;
        public ushort Task;
        public ulong Keyword;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct EVENT_RECORD
    {
        public EVENT_HEADER EventHeader;
        public IntPtr BufferContext;
        public ushort ExtendedDataCount;
        public ushort UserDataLength;
        public IntPtr ExtendedData;
        public IntPtr UserData;
        public IntPtr UserContext;
    }

    [DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
    private static extern ulong OpenTrace(ref EVENT_TRACE_LOGFILE logfile);

    [DllImport("advapi32.dll")]
    private static extern uint ProcessTrace(ulong[] handleArray, uint handleCount, IntPtr startTime, IntPtr endTime);

    public void Start()
    {
        EVENT_TRACE_LOGFILE logfile = new EVENT_TRACE_LOGFILE
        {
            LoggerName = "NT Kernel Logger",
            ProcessTraceMode = (IntPtr)(0x00000100 | 0x00000020), // PROCESS_TRACE_MODE_REAL_TIME | PROCESS_TRACE_MODE_EVENT_RECORD
            EventCallback = new EventRecordCallback(OnEvent),
            Guid = KernelProcessGuid
        };

        ulong traceHandle = OpenTrace(ref logfile);
        if (traceHandle == ulong.MaxValue)
        {
            Console.WriteLine("Failed to open trace.");
            return;
        }

        Console.WriteLine("Listening for process start events...");
        ProcessTrace(new[] { traceHandle }, 1, IntPtr.Zero, IntPtr.Zero);
    }

    private void OnEvent(ref EVENT_RECORD record)
    {
        if (record.EventHeader.EventDescriptor.Id == PROCESS_START_EVENT_ID)
        {
            Console.WriteLine($"Process started: PID = {record.EventHeader.ProcessId}");
        }
    }
}
Sign up to request clarification or add additional context in comments.

3 Comments

"ETW is the lowest you can go" - It is not. A kernel-mode driver can register a process creation/deletion callback via PsSetCreateProcessNotifyRoutine.
You're right - I misspoke. I meant ETW is the lowest level you should go. While your point stands, I advise against using PsSetCreateProcessNotifyRoutine. It has notable limitations: to my knowledge, only 8 subscribers are supported, and it intercepts process creation synchronously. ETW, on the other hand, is asynchronous and provides precise insights into process creation and termination without interfering with execution. It’s a cleaner, non-blocking alternative that still delivers the observability needed. Again, you're right — PsSetCreateProcessNotifyRoutine is the lowe
The question has the antivirus tag. A malware protection system will want to be notified synchronously, before the primary thread starts executing.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.