Monitoring for malicious password filters
TLDR; Below, I cover some information on password filters and provide a script and instructions on how to monitor against the placement of a malicious password filter. Password Filters can intercept clear-text passwords during password changes and can be planted on systems using phishing or malware.
In a recent post regarding Operation FishMedley, author Matthieu Fau mentions many technologies. One used a malicious password filter. That got me thinking of how you could monitor and be more proactive against a malicious password filter.
Password filters have been around since the late 1990s. Our product, nFront Password Filter, is based on password filter technology. A password filter is called by the LSA (local security authority) on a domain controller when it receives a password change for an Active Directory user. The password change may be sourced from a Windows machine, from a MacOS joined to the domain, or even from an SSPR solution or password change via Azure, etc. All password changes for AD users must reach a writable domain controller. First, the LSA checks the password change against the built-in domain password policy (including any fine-grained policies). If the password meets the requirements, it will call the PasswordFilter() API for all DLLs listed on the “Notification Packages” registry value. The LSA provides 4 input parameters – username, user’s full name, new password, and a setOperation flag to indicate if the password change is an admin reset or end-user password change. The input parameters are all in clear text. An attacker can do anything they want in the password filter DLL. The DLL is running under a SYSTEM context as a thread of the LSA, so it has a lot of capabilities. The hacker can log passwords and other information locally in a clear text or encrypted format, or they may decide to send the data offsite.
Of course, if an attacker can put a DLL in Windows\System32 on your DC, you likely have missed a lot of defenses in your security plan. However, sometimes these DLLs are planted via complex phishing attacks, or the malware is embedded in software that appears to be a legitimate driver or application.
You can use GPO to turn on monitoring for file/object access and get generic event viewer messages. But what if I want to proactively email the IT team? What if I want to control the content of the event viewer message or make it a STOP error instead of a warning?
I decided to explore a solution using PowerShell. I do not work much with Powershell, so I thought I would leverage AI.
I asked ChatGPT to write a PowerShell script to monitor a change of a registry value and log an event to the event viewer. The output worked on a “standard” registry value (like a REG_DWORD and REG_SZ), but a password filter is added to a REG_MULTI_SZ value and the script did not handle it properly. I then asked Gemini (Google’s AI) for a PowerShell script. It produced extremely similar output, which did not function correctly, but it was a little smarter in registering the event source for the script.
After some trial and error, I got a script that runs continuously and checks every XX seconds to see if the target registry value has changed. If it changes, the script logs a STOP error to the Event Viewer Application log with the content showing the old and new value so you can clearly see the name of the newly added DLL.
Below is a PowerShell script that you can copy and modify to your liking. It creates an event source named “RegistryMonitor” and will log any changes to the “Notification Packages” registry value to the Application log in the event viewer. A screenshot further down in this post shows an example Event Viewer log entry. In the code below, I left the “Write-Host….stage XX” entries commented out in case you want to uncomment them when doing your testing. When testing code, I like to put statements like this in place to easily see where the script/code fails.
$registryPath = "HKLM:\System\CurrentControlSet\Control\Lsa"
$registryName = "Notification Packages"
$EventLogName = "Application"
$EventSource = "RegistryMonitor"
$checkInterval = 60 # Time in seconds between checks
# Ensure the event source exists
if (-not (Get-EventLog -List | Where-Object {$_.Source -eq $EventSource})) {
New-EventLog -LogName $EventLogName -Source $EventSource
}
#Write-Host "stage 50"
# Get initial registry value
$prevList = (Get-ItemProperty -Path $registryPath -Name $registryName)."$registryName"
while ($true) {
try{
#Write-Host "stage 100"
$currList = (Get-ItemProperty -Path $registryPath -Name $registryName)."$registryName"
#Write-Host "stage 200"
if (($currList | Compare-Object $prevList) -as [bool]){
Write-Host "array values are not equal"
#Write-Host "stage 300"
$msg1 = "The registry value '$registryName' has changed. `r`n"
$msg2 = "Any new DLLs added here can intercept and read passwords in clear text.`r`n"
$msg3 = "Old Value: $prevList `r`n"
$msg4 = "New Value: $currList `r`n"
$eventMessage = $msg1 + $msg2 + $msg3 + $msg4
# Write event to Event Viewer
Write-EventLog -LogName Application -Source "RegistryMonitor" -EntryType Error -EventId 1001 -Message $eventMessage
#Write-Host $eventMessage
# Update previous value
$prevList = $currList
}
}
catch {
Write-Warning "Error accessing registry: $($_.Exception.Message)"
}
Start-Sleep -Seconds $checkInterval
}
After successfully testing the script, the next challenge was to get the script to start and run continuously even when someone is not logged on. The trick here is to use the Task Scheduler to run the script when the machine starts and run as the user SYSTEM. There may be better ways to do this. If so, please let me know. I do not like running any script under a SYSTEM context because if the script were replaced, bad things could happen! However, if someone is replacing your script on the server, they likely already own it.
Here are some screenshots showing how to configure the script to run continuously using a scheduled task. Be sure to click Change User or Group and set it to SYSTEM. Also, select the option “Run whether the user is logged on or not.” I do not think it matters, but I selected Windows Server 2019 for the “Configure for:” list.

Add a trigger to run at system startup.

For Actions, add the action to start PowerShell and call the file for your script. I put my script in c:\__scripts\ on my test machine.

IMPORTANT: To ensure the task runs continuously, be sure to clear the checkbox for “Stop the task if it runs longer than:”. The default is to stop the task after 3 days, and it would defeat the point of continuous monitoring.

Here is the Event Viewer after a fresh boot.

Here is the Notification Packages registry value before modifying it. It has the DLL for our password filter application (PPRO) and the 2 default Microsoft DLLS (rassf and scecli).

I edited the registry value and added a new string named MALWARE.

Less than a minute later, I got a new event in the Application log showing the change. It is nice to see the old and new values directly in the message instead of a generic event saying the registry value has changed.

The event viewer logging is great and will work well if you have software to aggregate the event viewer errors into a central application and notify the IT staff. However, I would consider adding email notifications as well. Below is a PowerShell example showing how to send an email. Perhaps you want to add this to the script above to email your IT team if a password filter is added or removed. Your email code will depend on your SMTP server and if you are running a local mail server or Exchange in the cloud.
# Email parameters
$EmailFrom = "[email protected]"
$EmailTo = "[email protected]"
$Subject = "Test Email"
$Body = "This is a test email sent from PowerShell."
$SMTPServer = "smtp.example.com"
$SMTPPort = 587 # Or 25, depending on your SMTP server settings
$Username = "your_username"
$Password = "your_password"
# Create SMTP client
$SMTPClient = New-Object Net.Mail.SmtpClient($SmtpServer, $SMTPPort)
$SMTPClient.EnableSsl = $true # Use SSL if required by your SMTP server
$SMTPClient.Credentials = New-Object System.Net.NetworkCredential($Username, $Password)
# Create mail message
$Message = New-Object Net.Mail.MailMessage($EmailFrom, $EmailTo, $Subject, $Body)
try {
# Send email
$SMTPClient.Send($Message)
Write-Host "Email sent successfully!"
}
catch {
Write-Host "Error sending email: $($_.Exception.Message)"
}
Adding this script to your domain controllers is simple and easy. Discovering an unauthorized password filter is something I hope you never experience. However, knowing the moment it happened versus months later can make a big difference.