4 ways to monitor Windows Registry Using C#

During the development of Acting Admin, I have been searching for the ultimate way to monitor changes in the Windows Registry with C# code.

Registry monitoring with C# is a jungle. Depending on the technology we choose, we will face different challenges. There is no perfect way – all implementations have their own problems. In this article, I will explain the pros and cons of the methods we have researched, and provide code examples for each approach.

Method 1: Monitor Registry using WMI callbacks

WMI, or Windows Management Instrumentation makes it possible for our application to register as a temporary event consumer. Using WQL (WMI Query Language) we can listen for different events that we specify in the query.

Pros

  • Easy to implement
  • No external libraries needed for .NET Framework applications

Cons

  • This method only tells us that something has changed. It does not tell us what changed.
  • No information about which process – or which user modified the registry

The WMI RegistryEvent Classes

If we take a look in the ROOT\CIMV2 Namespace with WMI Explorer, we can see four different Registry Event Classes:

Registry Event Classes in WMI
  • RegistryEvent – Just the base class
  • RegistryKeyChangeEvent – Monitor changes made to a single key
  • RegistryTreeChangeEvent – Monitor changes made to a registry tree
  • RegistryValueChangeEvent – Monitor change made to single value

Sample Code for WMI Registry Monitoring

Here’s a sample class in C# (works in .NET Core and .NET Framework) which monitors the HKLM\Software path and all subkeys for any changes.

public class WmiRegistryEventListener : IDisposable
{
    private const string query = @"SELECT * FROM RegistryTreeChangeEvent " +
            "WHERE Hive='HKEY_LOCAL_MACHINE' " +
            "AND RootPath='SOFTWARE'";

    private ManagementEventWatcher watcher;

    public WmiRegistryEventListener()
    {
        watcher = new ManagementEventWatcher(query);
        watcher.EventArrived += 
            new EventArrivedEventHandler(registryEventHandler);
        watcher.Start();
    }

    public void Dispose()
    {
        this.watcher?.Dispose();
    }

    private void registryEventHandler(object sender, EventArrivedEventArgs e)
    {
        Console.WriteLine("Received an event:");

        //Iterate over the properties received from the event and print them out.
        foreach (var prop in e.NewEvent.Properties)
        {
            Console.WriteLine($"{prop.Name}:{prop.Value.ToString()}");
        }

        Console.WriteLine();
    }
}

This class can be easily used in a .NET console application like this:

class Program
    {
        static void Main(string[] args)
        {
            var registryListener = new WmiRegistryEventListener();

            Console.WriteLine("Listening for registry changes.");
            Console.WriteLine("Press any key to exit.");

            Console.ReadKey();

            registryListener.Dispose();

        }
    }

The limitations for WMI-based monitoring

With this approach (WMI) we will only get information that something has changed. We will not receive any data about which registry key, value name, or value data which was modified.

There are certainly cases where we might only need to know that something has happened – but the common scenario most likely requires that we at least can see what the changes were. The other methods in this article provide us with much more information we can use.


Method 2: Programmatically monitor using Windows Registry Auditing

Windows has a built-in way of monitoring the registry – the auditing functionality. When registry auditing has been enabled and configured, any changes to the registry which meet our configured criteria will generate an entry in the Windows event log’s Security Events Log.

Since it’s possible both to enable and configure the auditing programmatically – and since we can read the security event log from our application – no manual steps are needed to make this work.

Pros

  • Events contain information about both user and process responsible for the registry modification
  • No external libraries needed

Cons

  • Requires configuration of Windows which may clash with current auditing settings
  • The Security Event Log will grow very fast
  • No ‘Create Key’ events due to a bug in Windows Auditing
  • A bit tricky to implement

First Step – Enable Registry Auditing

The first thing we need to do is to tell Windows that we want to enable registry auditing. This can be achieved using auditpol.exe. Process.Start is used to start the process from our application.

Process.Start(
    [email protected]"{Environment.GetEnvironmentVariable("SystemRoot")}\System32\auditpol.exe",
    "/set /subcategory:{0CCE921E-69AE-11D9-BED3-505054503030} /success:enable");

Since auditpol.exe is located in System32, we use GetEnvironmentVariable to expand the environment variable SystemRoot (most often C:\Windows).

We provide 3 arguments to auditpol.exe letting it know that we want to enable auditing for successful registry changes. Using a GUID for the subcategory ensures we avoid any Windows localization issues. This GUID corresponds to the “Registry” subcategory.

Second Step – Set the SACL

system access control list (SACL) enables administrators to log attempts to access a secured object.

https://docs.microsoft.com/en-us/windows/win32/secauthz/access-control-lists

SACLs tells Windows which objects we want to enable auditing for and lets us specify which operations should generate audit entries.

Please note: The Create Subkey auditing does not seem to work properly. This is most likely due to a bug in Windows. In order to catch all events for registry key creation, we need to use other methods.

Manually creating Auditing Rules (SACLs)

  1. Open Regedit
  2. Go to Permissions… for the root key where auditing should be enabled.
  3. Click Advanced. Select the Auditing tab.
  4. Click Add, then Select a principal and select the user or group for which auditing should be enabled.
  5. Make sure Success is selected for type and This key and subkeys for ‘Applies to‘.
  6. Choose the operations you want to enable auditing for.

Create SACLs for Registry with code

Please note: This code must be run as a 64-bit process for inherited rules to propagate correctly. Please make sure that your output settings in Visual Studio specify x64.

Let’s say we want to enable auditing for the built-in group Interactive Users (Well-known SID S-1-5-4). The first thing we must do is check if auditing has already been enabled for this user, and if so – which rules have been specified for auditing.

var key = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
RegistrySecurity rs = key.GetAccessControl(AccessControlSections.Audit);

var existing = rs.GetAuditRules(true, false, typeof(SecurityIdentifier))
    .OfType<RegistryAuditRule>().FirstOrDefault(r => r.IdentityReference.Value == "S-1-5-4");

This code reads the current audit rules for the HKLM hive into the variable existing.

var auditFlags = RegistryRights.SetValue |
                 RegistryRights.TakeOwnership |
                 RegistryRights.Delete |
                 RegistryRights.CreateSubKey |
                 RegistryRights.ChangePermissions |
                 RegistryRights.CreateLink;

if (existing != null)
{
    PropertyInfo prop =
        typeof(RegistryAuditRule).GetProperty("AccessMask", BindingFlags.NonPublic | BindingFlags.Instance);

    MethodInfo getter = prop.GetGetMethod(nonPublic: true);
    var mask = (RegistryRights)getter.Invoke(existing, null);

    if (mask.HasAllFlags(auditFlags))
    {
        Console.WriteLine("Enable Registry Auditing: All flags present.");
        return;
    }
}

First, we specify the operations we would like to enable auditing for using RegistryRights flags.

If existing equals null, we know that there are some auditing rules configured for the SID we specified, and in this case, we check if all flags have already been enabled for this principle – then we return. The HasAllFlags is an Enum extension which looks like this (thanks weston):

public static bool HasAllFlags<TEnum>(this TEnum op, params TEnum[] checkflags)
{
    foreach (TEnum checkflag in checkflags)
    {
        if (((int)(object)op & (int)(object)checkflag) != (int)(object)checkflag)
            return false;
    }
    return true;
}

At this stage, if our code has concluded that all flags are not present, we need to add our audit rules:

rs.AddAuditRule(new RegistryAuditRule(
    account, auditFlags, InheritanceFlags.ContainerInherit, PropagationFlags.None, AuditFlags.Success));
rs.SetAuditRuleProtection(true, true);
key.SetAccessControl(rs);

That’s it! We have now enabled auditing for a set of Registry operations. Next, we will read the events generated in the Security Event Log.

Third Step – Read the Events

I will write this section when I can find the time. If you found this post and need the solution, please drop me an email at erik[at]erikengberg[dot]com..


Method 3: Process Hooking

Pros

  • Events contain information about both user and process responsible for the registry modification

Cons

  • Potentially very unstable
  • Third-party library EasyHook not actively maintained
  • Re-routes all registry operations (from the injected process(es)) through our monitoring process
  • Antivirus software might detect your application as malware

Method 4: Kernel Event Trace

Pros

  • Events contain information about both user and process responsible for the registry modification

Cons

  • Any ‘Delete’ event will not contain the registry key or value (if applicable)
  • Thin or non-existing documentation

Leave a Reply

Your email address will not be published.