C# - 특정 레지스트리 변경 시 알림을 받는 방법
레지스트리 변경에 대한 알림은 Win32 API도 제공하지만,
RegNotifyChangeKeyValue function
; https://learn.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regnotifychangekeyvalue
이럴 때는 간단하게 WMI 기능을 이용하면 됩니다.
Registry class
; https://learn.microsoft.com/en-us/windows/win32/etw/registry
RegistryKeyChangeEvent class
; https://learn.microsoft.com/en-us/previous-versions/windows/desktop/regprov/registrykeychangeevent
Registry Watcher C#
; https://stackoverflow.com/questions/826971/registry-watcher-c-sharp
그래서 대충 다음과 같은 식으로 코딩할 수 있습니다.
using Microsoft.Win32;
using System;
using System.Management;
public class Program
{
public static void Main()
{
using (RegistryMonitor regMon = new RegistryMonitor(Registry.LocalMachine,
@"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"))
{
regMon.Changed += registry_Changed;
do
{
Console.WriteLine("Press 'q' key to exit...");
} while (Console.ReadLine() != "q");
}
}
private static void registry_Changed(object sender, EventArrivedEventArgs e)
{
Console.WriteLine(e.NewEvent.GetText(TextFormat.Mof));
}
}
public class RegistryMonitor : IDisposable
{
ManagementEventWatcher _watcher;
string _wmiQuery;
public RegistryMonitor(RegistryKey hiveRoot, string registryKeyPath)
{
try
{
_wmiQuery = "SELECT * FROM RegistryKeyChangeEvent WHERE " +
$" Hive='{hiveRoot.Name}'" +
$" AND KeyPath='{registryKeyPath}'";
WqlEventQuery wqlEvent = new WqlEventQuery(_wmiQuery);
_watcher = new ManagementEventWatcher(wqlEvent);
_watcher.EventArrived += new EventArrivedEventHandler(HandleEvent);
_watcher.Start();
}
catch (ManagementException e)
{
Console.WriteLine(_wmiQuery + Environment.NewLine + Environment.NewLine + e.ToString());
}
}
private void HandleEvent(object sender, EventArrivedEventArgs e)
{
Changed?.Invoke(sender, e);
}
public void Dispose()
{
if (_watcher != null)
{
_watcher.Dispose();
_watcher = null;
}
}
public event EventHandler<EventArrivedEventArgs> Changed;
}
위에서는 시스템의 환경 변수가 변경되었을 때 알림을 받도록 설정했는데요, 그래서 regedit를 통해 "Computer\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" 경로의 환경 변수를 변경하면 다음과 같은 식의 텍스트가 화면에 출력됩니다.
instance of RegistryKeyChangeEvent
{
Hive = "HKEY_LOCAL_MACHINE";
KeyPath = "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment";
TIME_CREATED = "132643355350866404";
};
개별 값은
e.NewEvent.Properties에서 추출할 수 있는데, 예를 들어 TIME_CREATED는 다음과 같이 C#의 DateTime으로 변환할 수 있습니다.
PropertyData timeCreated = e.NewEvent.Properties["TIME_CREATED"];
ulong timeValue = (ulong)timeCreated.Value;
DateTime changed = DateTime.FromFileTime((long)timeValue);
Console.WriteLine(changed); // 2021-05-01 오후 7:23:56
그나저나, 시간 관련 값이 저렇게 정숫값으로 나올 때는 해석을 어떻게 해야 할지 늘 고민입니다. ^^; (
windbg 내의 DateTime 메모리 값을 해석하는 방법)
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
간단하게 VBScript로 작성하는 것도 가능합니다. ^^
REM RegistryKeyChangeEvent class
REM https://learn.microsoft.com/en-us/previous-versions/windows/desktop/regprov/registrykeychangeevent
Set wmiServices = GetObject("winmgmts:root/default")
Set wmiSink = WScript.CreateObject("WbemScripting.SWbemSink", "SINK_")
wmiServices.ExecNotificationQueryAsync wmiSink, "SELECT * FROM RegistryKeyChangeEvent " _
& "WHERE Hive='HKEY_LOCAL_MACHINE' AND KeyPath='SOFTWARE\\Microsoft\\WBEM\\Scripting'"
WScript.Echo "Listening for Registry Key Change Events..." & vbCrLf
While(True)
WScript.Sleep 1000
Wend
Sub SINK_OnObjectReady(wmiObject, wmiAsyncContext)
WScript.Echo "Received Registry Change Event" & vbCrLf & wmiObject.GetObjectText_()
End Sub
그런데, 가끔은 레지스트리 변경 자체의 알림보다는 특정 레지스트리 값을 변경한 프로세스를 알고 싶을 때가 있습니다. 아쉽게도, Win32 API/WMI 수준에서는 이에 대한 어떠한 정보도 알려주지 않습니다.
이것을 알려면
ProcMon이나 레지스트리의 audit 기능을 사용해야 한다는데,
tinyapps.org / blog - Find out what process changed a registry key or value
; https://tinyapps.org/blog/201509170700_what_process_changed_registry.html
아무래도 ProcMon은 별도 다운로드를 해야 하니, 윈도우의 기본 기능만으로 가능한 auditpol 방법을 소개하겠습니다. 위의 글에 나온 방법에 따르면, ^^ 다음과 같이 레지스트리에 대한 감사 정책을 활성화시키고,
C:\Windows\System32> auditpol /set /subcategory:"Registry" /success:enable
The command was successfully executed.
이후, regedit.exe를 실행해 모니터링을 원하는 레지스트리 키에 대해 우 클릭, "Permissions..." 메뉴를 선택하고 다음과 같이 "Auditing" 탭에 새롭게 "Everyone"으로 "Full Control" / "Read" 권한을 추가합니다.
일단 저렇게 설정이 완료되면, 이벤트 로그의 "Windows Logs" / "Security" 범주에 다음과 같이 변경 이벤트가 남게 됩니다.
위의 예는, regedit.exe(PID == 0x2e68) 프로세스에서 "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" 하위의 환경 변숫값을 바꿨음을 알려주고 있습니다.
// 설정 해제
C:\Windows\System32> auditpol /get /subcategory:"Registry"
System audit policy
Category/Subcategory Setting
Object Access
Registry Success
C:\Windows\System32> auditpol /set /subcategory:"Registry" /success:disable
The command was successfully executed.
C:\Windows\System32>auditpol /get /subcategory:"Registry"
System audit policy
Category/Subcategory Setting
Object Access
Registry No Auditing
만약 ManagementEventWatcher.Start 메서드에서 다음과 같은 예외가 발생한다면?
System.Management.ManagementException: Unparsable query.
at System.Management.ManagementException.ThrowWithExtendedInfo(ManagementStatus errorCode)
at System.Management.ManagementEventWatcher.Start()
at RegistryMonitor..ctor(RegistryKey hiveRoot, String registryKeyPath)
"WqlEventQuery wqlEvent = new WqlEventQuery(_wmiQuery);" 코드에 전달한 _wmiQuery의 텍스트 내용을 잘 살펴볼 필요가 있습니다. 이 글에서 든 예제의 경우 다음과 같은 경로의 Registry 키를 감시하는 건데,
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment
이 값을 넘겨줄 때, escape를 이중으로 고려해 넘겨야 합니다. 즉, C#의 "@" 마크로 감쌌다고 해도 다음과 같이 넘겨야 하고,
string txt = @"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment";
만약 "@" 마크를 사용하지 않았다면 이렇게 해야 합니다.
string txt = "SYSTEM\\\\CurrentControlSet\\\\Control\\\\Session Manager\\\\Environment";
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]