logo

Registry Monitor for PowerShell

Posted by Marbenz Antonio on January 31, 2023

PowerShell Commands Every Developer Should Know

During a work conversation, a colleague asked me if it was possible to track changes to a Windows registry key. Microsoft was aware that changes to files can be monitored using the System.IO.FileSystemWatcher.NET class, but they were unaware of registry monitoring. However, they later learned that Windows has an API for it and that it can be called from PowerShell using Interop Services.

About tools

To achieve this, Microsoft will utilize Platform Invoke, also known as PinVoke. PinVoke is a .NET library that allows native APIs to be accessed by managed .NET code. This library is included in Windows via the Global Assembly Cache and also in PowerShell Core.

Moreover, they will utilize several Windows API functions, as listed below:

  • RegOpenKeyEx: Responsible for opening a handle to the key.
  • RegNotifyChangeKeyValue: Responsible for monitoring the key, and triggering an event when a change happens.
  • CreateEvent: Responsible for creating the event.
  • WaitForSingleObject: This will monitor the event, and return a result based on the outcome.
  • RegCloseKey: To close the handle to our registry key.
  • CloseHandle: To close the handle to the event created.

The final two commands are optional, as Interop Services provides a Safe Handle to wrap the handles. This handle is automatically freed by the Garbage Collector, but it is still good practice and builds the habit of tracking object lifecycles. If you plan to frequently interact with Windows, it’s important to become familiar with its memory management to avoid any unexpected behavior.

About definition

To utilize System.Runtime.InteropServices, Microsoft will need to write some of their code in C#. Don’t be intimidated, as C# and PowerShell are quite similar and it won’t be difficult. We’ll begin by defining our functions.

Want to know more about PowerShell? Visit our course now.

They will show step-by-step how to use RegOpenKeyEx, and the other functions will follow the same process. According to Microsoft’s documentation, the function definition appears as follows:

LSTATUS RegOpenKeyExW(
  [in]           HKEY    hKey,
  [in, optional] LPCWSTR lpSubKey,
  [in]           DWORD   ulOptions,
  [in]           REGSAM  samDesired,
  [out]          PHKEY   phkResult
);

Don’t be concerned about the “W” at the end. Many Windows functions have both ANSI and UNICODE versions. Functions ending in “A” are ANSI-compliant and those ending in “W” are UNICODE-compliant. When you call RegOpenKeyEx, Windows will automatically use one of the two versions.

  • HKEY: This represents a handle, which is a type of pointer and can be represented as System.IntPtr in C#. As memory addresses are numerical, System.IntPtr is a specific type of integer.
  • LPCWSTR: A pointer to a constant string with 16-bit Unicode characters, represented as System.String in our case.
  • DWORD: A 32-bit unsigned integer, equivalent to System.UInt32.
  • REGSAM: A Registry Security Access Mask, which they will discuss later.
  • PHKEY: A pointer to a variable that will receive the opened key handle, which can be represented as System.IntPtr.
  • LSTATUS: The function’s return type, mapped to a long, which we will represent as System.Int in C#.

The REGSAM data type is a collection of definitions that map Registry Key security to unsigned integers and can be represented as a System.UInt32 in C#. Microsoft will use the KEY_NOTIFY REGSAM, which corresponds to 0x0010. The final function definition will look similar to this:

[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern int RegOpenKeyExW(
    IntPtr hKey,
    string lpSubKey,
    uint ulOptions,
    uint samDesired,
    out IntPtr phkResult
);

The first line in square brackets is the DllImport Attribute. It specifies the DLL containing the definition for RegOpenKeyExW. The CharSet = CharSet.Unicode sets Unicode as the encoding and SetLastError = true sets the last error with the corresponding Win32 error if the function call fails, which is important for debugging and problem resolution.

Following the same approach, we write the full code:

using System;
using System.Runtime.InteropServices;

namespace Win32
{
    public class Regmon
    {
        [DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern int RegOpenKeyExW(
            int hKey,
            string lpSubKey,
            int ulOptions,
            uint samDesired,
            out IntPtr phkResult
        );

        [DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern int RegNotifyChangeKeyValue(
            IntPtr hKey,
            bool bWatchSubtree,
            int dwNotifyFilter,
            IntPtr hEvent,
            bool fAsynchronous
        );

        [DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern int RegCloseKey(IntPtr hKey);

        [DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern int CloseHandle(IntPtr hKey);

        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern IntPtr CreateEventW(
            int lpEventAttributes,
            bool bManualReset,
            bool bInitialState,
            string lpName
        );

        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern int WaitForSingleObject(
            IntPtr hHandle,
            int dwMilliseconds
        );
    }
}

The original lpEventAttributes parameter is from the LPSECURITY_ATTRIBUTES structure, but since we won’t be using it, defining it as int will not cause any issues. If Microsoft needed to use it, they would have to define LPSECURITY_ATTRIBUTES.

Writing the PowerShell code

With the necessary setup completed, we can now write the PowerShell code that utilizes these functions. To simplify the view, the previous definition text is represented as $signature. You just need to create a string variable to hold the C# code, which can be done using here-strings.

$signature = @'
    Your code goes here.
'@

The final script looks like this:

using namespace System.Runtime.InteropServices

[CmdletBinding()]
param (
    [Parameter(Mandatory)]
    [string]$KeyPath,

    [Parameter()]
    [string]$LogPath = "$PSScriptRoot\RegMon-$(Get-Date -Format 'yyyyMMdd-hhmmss').log",

    [Parameter()]
    [int]$Timeout = 0xFFFFFFFF #INFINITE
)

Add-Type -TypeDefinition $signature

if (!(Test-Path -Path $KeyPath)) { throw "Registry key not found." }

switch -Wildcard ((Get-Item $KeyPath).Name) {
    'HKEY_CLASSES_ROOT*' { $regdefault = 0x80000000 }
    'HKEY_CURRENT_USER*' { $regdefault = 0x80000001 }
    'HKEY_LOCAL_MACHINE*' { $regdefault = 0x80000002 }
    'HKEY_USERS*' { $regdefault = 0x80000003 }
    Default { throw 'Unsuported hive.' }
}

$handle = [IntPtr]::Zero
$result = [Win32.Regmon]::RegOpenKeyExW($regdefault, ($KeyPath -replace '^.*:\\'), 0, 0x0010, [ref]$handle)
$event = [Win32.Regmon]::CreateEventW($null, $true, $false, $null)

<#
This will run indefinitely until it fails or reaches a timeout.
Adjust accordingly.
#>
:Outer while ($true) {
    $result = [Win32.Regmon]::RegNotifyChangeKeyValue(
        $handle,
        $false,
        0x00000001L -bor #REG_NOTIFY_CHANGE_NAME
        0x00000002L -bor #REG_NOTIFY_CHANGE_ATTRIBUTES
        0x00000004L -bor #REG_NOTIFY_CHANGE_LAST_SET
        0x00000008L, #REG_NOTIFY_CHANGE_SECURITY
        $event,
        $true
    )
    $wait = [Win32.Regmon]::WaitForSingleObject($event, $Timeout)

    switch ($wait) {
        0xFFFFFFFF { break Outer } #WAIT_FAILED

        0x00000102L { #WAIT_TIMEOUT
            $outp = 'Timeout reached.'
            Write-Host $outp -ForegroundColor DarkGreen
            Add-Content -FilePath $LogPath -Value $outp
            break Outer
        }

        0 { #WAIT_OBJECT_0 ~> Change detected.
            $outp = "Change triggered on the specified key. Timestamp: $(Get-Date -Format 'hh:mm:ss - dd/MM/yyyy')."
            Write-Host $outp -ForegroundColor DarkGreen
            Add-Content -FilePath $LogPath -Value $outp
        }
    }
}

[Win32.Regmon]::CloseHandle($event)
[Win32.Regmon]::RegCloseKey($handle)

Note

When calling RegOpenKeyExW for the first time, we don’t have the handle to the key yet, so we specify which root key we want to use. The parameter lpSubKey is optional. When not specified, the function will monitor the root key.


Here at CourseMonster, we know how hard it may be to find the right time and funds for training. We provide effective training programs that enable you to select the training option that best meets the demands of your company.

For more information, please get in touch with one of our course advisers today or contact us at training@coursemonster.com

Verified by MonsterInsights