PasswordFilter – How to Build and Install a Custom PasswordFilter.dll for Active Directory Password Security

This article walks system administrators through designing, coding, and deploying a custom PasswordFilter.dll for Active Directory. It explains how the filter integrates with LSA, validates passwords using blacklists and regex patterns, and provides build, installation, and rollback steps so you can enforce stronger password security without risking domain controller stability

In the previous two articles, the reasons for using a Password Filter and how it works in the password change flow of Active Directory were explained. There are, of course, many commercial and open‑source alternatives, such as Microsoft Entra Password Protection or several projects available on GitHub.

With the simple logic described earlier, writing your own Password Filter is completely feasible. The code itself is quite simple and mostly about string handling. The main challenges are performance and making sure you never crash the lsass.exe process. Keep the following points in mind:

  1. Handle all exceptions carefully in the code paths you implement. If exceptions are handled well, you almost never have to worry about crashing lsass.exe.

  2. Use an in‑memory cache instead of reading files and recompiling patterns on every request. Only reload data when the underlying files change.

  3. Make sure your code can safely handle multiple threads reading the same data at the same time.

  4. Follow Microsoft MSVC conventions and best practices.

  5. Build the DLL for the correct system architecture (x86 or x64).

  6. Deploy the DLL correctly to Active Directory and monitor its behavior before putting it into production.

Building PasswordFilter.dll

Helper functions

  • ToLower(): Convert a string to lowercase.

  • Utf8ToUtf16(): Convert UTF‑8 strings to std::wstring.

  • TrimWhitespace(): Remove leading and trailing whitespace characters (including \r and \n).

  • ReadUtf8Lines(): Read a UTF‑8 file line by line.

  • UniStrToWstring(): Convert a UNICODE_STRING to std::wstring.

  • LoadPatternsIfNeeded(): Read regex.txt if it has changed, recompile the patterns, and store them in the in‑memory cache.

  • LoadEnforceIfNeeded(): Read the passwordfilter.ini configuration file if it has changed, extract the enforce value, and update the in‑memory cache.

  • LoadBlacklistIfNeeded(): Read blacklist.txt if it has changed and store the updated blacklist in the in‑memory cache.

  • InitializeChangeNotify(): Default function required by lsass.exe. Called when PasswordFilter.dll is loaded into the process.

  • PasswordFilter(): Default function required by lsass.exe. Called whenever there is a password‑change request and is the main logic of the filter.

  • PasswordChangeNotify(): Default function required by lsass.exe. Called after a password has been successfully changed.

Instead of maintaining blacklist entries and patterns in both uppercase and lowercase, normalize everything to one form (for example, lowercase). This way, instead of listing AbcABCABc, and so on, you only need to store abc.

Utf8ToUtf16() is required because LSASS works with UTF‑16 (wchar_t). Files on disk are typically UTF‑8; to let LSASS process them correctly, they must be converted to UTF‑16 to avoid encoding issues.

Similarly, UniStrToWstring() is needed because PasswordFilter() receives the password parameter as a UNICODE_STRING, not as a std::wstring. Your C++ helper functions such as ToLowerstd::wstring::find, or std::regex_search cannot work directly with UNICODE_STRING.

Load***IfNeeded() functions

All three functions—LoadPatternsIfNeededLoadEnforceIfNeeded, and LoadBlacklistIfNeeded—share the same idea: they check the last modified time of the underlying file and compare it with the timestamp stored in the cache. If the times differ, the function reloads the file and updates the cache; otherwise, it keeps using the cached data.

static void LoadBlacklistIfNeeded() {
    try {
        std::error_code ec;
        auto ft = std::filesystem::last_write_time(CONFIG_BLACKLIST_PATH, ec);
        if (ec) return;
        {
            std::shared_lock readLock(g_blacklist_lock);
            if (ft == g_blacklist_last_write_time) return;
        }
        std::unique_lock writeLock(g_blacklist_lock);
        if (ft == g_blacklist_last_write_time) return;
        std::vector<std::wstring> lines;
        if (!ReadUtf8Lines(CONFIG_BLACKLIST_PATH, lines)) {
            return;
        }
        for (auto& s : lines) ToLower(s);
        g_blacklist.swap(lines);
        g_blacklist_last_write_time = ft;
    } catch (...) {
    }
}

The example below shows the logic for LoadBlacklistIfNeeded():

  • Wrap the entire function body in a try/catch block so that any exception is swallowed and LSASS stays safe.

  • Use std::filesystem::last_write_time to obtain the file’s last modified time. If an error occurs (for example, file not found), exit the function and keep using the old cache.

  • Use std::shared_lock for read access so that multiple threads can read the blacklist at the same time. This is useful when LSASS processes many password‑change requests concurrently.

  • Use std::unique_lock for write access. Only one thread can hold this lock, and while it does, no thread can take the shared lock. This guarantees that only one thread updates the cache when blacklist.txt changes, preventing write conflicts or inconsistent data.

shared-mutex.webp

Processing logic for the function:

  • First, acquire a shared_lock and compare g_blacklist_last_write_time with the current file modification time. If they are equal, the file has not changed and the function returns immediately.

  • If the times differ, acquire a unique_lock, read the file, convert all lines to lowercase, and then update g_blacklist and g_blacklist_last_write_time in the cache.

PasswordFilter() function

extern "C" BOOLEAN __stdcall PasswordFilter(
    PUNICODE_STRING AccountName,
    PUNICODE_STRING FullName,
    PUNICODE_STRING Password,
    BOOLEAN SetOperation
) {
    std::wstring pwd;
    try {
        LoadEnforceIfNeeded();
        {
            std::shared_lock enforceLock(g_enforce_lock);
            if (g_enforce == 0) return TRUE;
        }
        if (!UniStrToWstring(Password, pwd)) return TRUE;
        if (pwd.empty()) return TRUE;
        ToLower(pwd);
        LoadBlacklistIfNeeded();
        LoadPatternsIfNeeded();
        {
            std::shared_lock readLock(g_blacklist_lock);
            for (const auto& bl : g_blacklist) {
                if (bl.empty()) continue;
                if (pwd.find(bl) != std::wstring::npos) {
                    SecureZeroMemory(pwd.data(), pwd.size() * sizeof(wchar_t));
                    return FALSE;
                }
            }
        }
        {
            std::shared_lock readLock(g_patterns_lock);
            for (const auto& pattern : g_patterns) {
                if (std::regex_search(pwd, pattern)) {
                    SecureZeroMemory(pwd.data(), pwd.size() * sizeof(wchar_t));
                    return FALSE;
                }
            }
        }
        SecureZeroMemory(pwd.data(), pwd.size() * sizeof(wchar_t));
        return TRUE;
    } catch (...) {
        SecureZeroMemory(pwd.data(), pwd.size() * sizeof(wchar_t));
        return TRUE;
    }
}

The main PasswordFilter() function can be summarized as:

  1. Update the enforce value if the configuration file has changed.

  2. Check whether enforce == 0. This flag works like an on/off switch: 0 means PasswordFilter checks are disabled, 1 means they are enabled. If enforce is 0, return TRUE.

  3. Convert the new password from UNICODE_STRING to std::wstring and normalize it to lowercase.

  4. Reload the blacklist and regex patterns if their files have changed.

  5. Check whether the password contains any substring from the blacklist.

  6. Check whether the password matches any of the compiled regex patterns.

Important points:

  • The entire body of the function is inside a try/catch block to ensure all exceptions are handled, even if the called helper functions already catch their own exceptions.

  • Before every return, always clear the plaintext password from memory by calling SecureZeroMemory(), following Microsoft’s recommendation.

  • If any exception occurs, the function returns TRUE. This ensures that LSASS continues running smoothly and the filter never breaks the overall password‑change process.

You can place the full implementation of these helper functions and the filter logic in a separate “PasswordFilter Code” file for reuse and maintenance.

Building PasswordFilter.dll (Visual Studio)

This section describes how to build PasswordFilter.dll for an x64 Active Directory server using Visual Studio.

Step 1: Create the project

  • File → New → Project/Solution → “Dynamic‑Link Library with exports (DLL)” → fill in project information.

Step 2: Add the C++ source file

  • Create a C++ file (for example, PasswordFilter.cpp) and paste in all code from your PasswordFilter implementation.

  • You can customize the logic inside PasswordFilter() and helper functions as you like, but the three functions InitializeChangeNotifyPasswordFilter, and PasswordChangeNotify are mandatory.

Step 3: Configure the build

  • Right‑click the project → Properties.

  • Configuration: Release.

  • Platform: x64.

  • Configuration Properties → General → C++ Language Standard → ISO C++ 17 Standard (/std:c++17).

  • Configuration Properties → General → C Language Standard → Default (Legacy MSVC).

LSASS calls exactly three functions by their exported names. To keep those names intact in the DLL, you have two options:

  • Use extern "C" with __declspec(dllexport), or

  • Use a .def (module definition) file.

If you do not use one of these methods, the compiler will mangle the names (for example, ?PasswordFilter@@YAHPAU_UNICODE_STRING@@0N@Z), and LSA will not be able to find the correct export names.

Module definition file

  • Create a PasswordFilter.def file in the same folder as PasswordFilter.cpp.

  • Add the following content:

LIBRARY PasswordFilter

EXPORTS

InitializeChangeNotify

PasswordFilter

PasswordChangeNotify

  • If the .def file is not picked up automatically, configure it explicitly:

  • Right‑click project → Properties → Linker → Input → Module Definition File → enter PasswordFilter.def.

Build the project

  • Build → Build Solution (Ctrl + Shift + B).

  • The DLL will be generated at: [Project Folder]\x64\Release\PasswordFilter.dll

Verify exports

  • Open “Developer Command Prompt for VS”.

  • Run: dumpbin /exports [Project Folder]\x64\Release\PasswordFilter.dll.

  • Make sure the three functions InitializeChangeNotifyPasswordChangeNotify, and PasswordFilter are present in the exports list.

dll-dump.webp

Installing PasswordFilter.dll on Active Directory

  1. Copy PasswordFilter.dll to C:\Windows\System32 on the Active Directory server.

  2. Register the DLL as an LSA extension in the registry:

    • Go to HKLM\SYSTEM\CurrentControlSet\Control\LSA.

    • Edit the Notification Packages value.

    • Add PasswordFilter (without the .dll extension) on a new line.

Example before:

scecli

pwdmig

After adding:

scecli

pwdmig

PasswordFilter

  1. Create the files blacklist.txtregex.txt, and passwordfilter.ini under C:\ProgramData (or another directory, matching the paths in your code).

  2. In passwordfilter.ini, configure:

    • enforce=1 → enable blacklist and regex checks.

    • enforce=0 → skip all PasswordFilter checks.

  3. Because LSA runs under the SYSTEM account, make sure the files in C:\ProgramData are readable by SYSTEM.

  4. Reboot the server so that LSA can load the new PasswordFilter DLL.

  5. To verify that the DLL has been loaded, run:

    • tasklist /m PasswordFilter.dll

dll-modules.webp

If the DLL appears in the module list for lsass.exe, your custom PasswordFilter has been successfully installed and is ready to enforce your password rules.

Rollback in Case of Errors

If the password‑change flow fails and you determine that the root cause is an error in the PasswordFilter logic (the error can usually be found in the Security log in Event Viewer), you can apply one of the following rollback options:

  • Clear the contents of the two files C:\ProgramData\regex.txt and C:\ProgramData\blacklist.txt. When these files change, the next time a password change request is received, the PasswordFilter will reload them and refresh the pattern and blacklist data.

  • Edit the enforce value in C:\ProgramData\passwordfilter.ini (1 = Enable, 0 = Disable).

  • Remove the PasswordFilter entry from the registry at HKLM\SYSTEM\CurrentControlSet\Control\Lsa\Notification Packages and reboot the server.

When the Server Cannot Reboot Because of PasswordFilter

  • Boot the server into Directory Services Restore Mode (DSRM) or Safe Mode, then remove the PasswordFilter entry from the registry.

  • From WinRE, rename C:\Windows\System32\PasswordFilter.dll to any other name that does not match the value configured in the registry. On the next boot, LSASS will no longer load that PasswordFilter DLL.

Next/Previous Post

buymeacoffee
PasswordFilter – How to Build and Install a Custom PasswordFilter.dll for Active Directory Password Security - codevel.io | Codevel.io