From fbac1ec635e5ad6da83acf14ece6c243a312a92e Mon Sep 17 00:00:00 2001 From: Ryan Newington Date: Wed, 22 Jan 2025 20:31:24 +1100 Subject: [PATCH 1/2] Adds ability to scope or descope users from the filter Added new test files `PasswordEvaluatorInScopeTests.cpp` and `RegistryTests.cpp` to the `NativeUnitTests.vcxproj` and `NativeUnitTests.vcxproj.filters` project files. Updated `filter.cpp` and `passwordevaluator.cpp` to include the `IsUserInScope` function, generating event log messages for excluded users. Enhanced `registry.cpp` to support multi-string registry values and log errors. Updated `messages.h`, `messages.mc`, `passwordevaluator.h`, and `registry.h` to declare new functions and constants. Modified `lithnet.activedirectory.passwordfilter.adml` and `lithnet.activedirectory.passwordfilter.admx` to add new policies. Updated `messages.res` to reflect message changes. Added necessary includes and implemented test methods in `PasswordEvaluatorInScopeTests` and `RegistryTests` to verify new functionalities. --- src/NativeUnitTests/NativeUnitTests.vcxproj | 2 + .../NativeUnitTests.vcxproj.filters | 6 + .../PasswordEvaluatorInScopeTests.cpp | 191 ++++++++++++++++++ src/NativeUnitTests/RegistryTests.cpp | 98 +++++++++ src/PasswordFilter/filter.cpp | 14 ++ src/PasswordFilter/messages.h | 33 ++- src/PasswordFilter/messages.mc | 28 +++ src/PasswordFilter/messages.res | Bin 4336 -> 4880 bytes src/PasswordFilter/passwordevaluator.cpp | 68 +++++-- src/PasswordFilter/passwordevaluator.h | 2 + src/PasswordFilter/registry.cpp | 105 +++++++++- src/PasswordFilter/registry.h | 19 +- ...ithnet.activedirectory.passwordfilter.adml | 18 +- ...ithnet.activedirectory.passwordfilter.admx | 31 ++- 14 files changed, 585 insertions(+), 30 deletions(-) create mode 100644 src/NativeUnitTests/PasswordEvaluatorInScopeTests.cpp create mode 100644 src/NativeUnitTests/RegistryTests.cpp diff --git a/src/NativeUnitTests/NativeUnitTests.vcxproj b/src/NativeUnitTests/NativeUnitTests.vcxproj index 55a3bc0..9a56858 100644 --- a/src/NativeUnitTests/NativeUnitTests.vcxproj +++ b/src/NativeUnitTests/NativeUnitTests.vcxproj @@ -164,9 +164,11 @@ + + Create Create diff --git a/src/NativeUnitTests/NativeUnitTests.vcxproj.filters b/src/NativeUnitTests/NativeUnitTests.vcxproj.filters index e0e5c4a..21f52e9 100644 --- a/src/NativeUnitTests/NativeUnitTests.vcxproj.filters +++ b/src/NativeUnitTests/NativeUnitTests.vcxproj.filters @@ -62,5 +62,11 @@ Source Files + + Source Files + + + Source Files + \ No newline at end of file diff --git a/src/NativeUnitTests/PasswordEvaluatorInScopeTests.cpp b/src/NativeUnitTests/PasswordEvaluatorInScopeTests.cpp new file mode 100644 index 0000000..498110b --- /dev/null +++ b/src/NativeUnitTests/PasswordEvaluatorInScopeTests.cpp @@ -0,0 +1,191 @@ +#include "stdafx.h" +#include "CppUnitTest.h" +#include "passwordevaluator.h" +#include + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +static void SetRegistryExcludedAccounts(const std::vector& accounts) +{ + HKEY hKey; + LONG result = RegCreateKeyEx(HKEY_LOCAL_MACHINE, REG_BASE_SETTINGS_KEY, 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL); + if (result == ERROR_SUCCESS) + { + // Calculate the size of the MULTI_SZ string + size_t totalSize = 0; + for (const auto& account : accounts) + { + totalSize += (account.size() + 1) * sizeof(wchar_t); + } + totalSize += sizeof(wchar_t); // For the final null terminator + + // Create the MULTI_SZ string + std::vector multiSz(totalSize / sizeof(wchar_t)); + wchar_t* ptr = multiSz.data(); + for (const auto& account : accounts) + { + wcscpy_s(ptr, account.size() + 1, account.c_str()); + ptr += account.size() + 1; + } + *ptr = L'\0'; // Final null terminator + + // Set the registry value + RegSetValueEx(hKey, REG_VALUE_EXCLUDEDACCOUNTS, 0, REG_MULTI_SZ, (const BYTE*)multiSz.data(), totalSize); + RegCloseKey(hKey); + } +} + +static void SetRegistryIncludedAccounts(const std::vector& accounts) +{ + HKEY hKey; + LONG result = RegCreateKeyEx(HKEY_LOCAL_MACHINE, REG_BASE_SETTINGS_KEY, 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL); + if (result == ERROR_SUCCESS) + { + // Calculate the size of the MULTI_SZ string + size_t totalSize = 0; + for (const auto& account : accounts) + { + totalSize += (account.size() + 1) * sizeof(wchar_t); + } + totalSize += sizeof(wchar_t); // For the final null terminator + + // Create the MULTI_SZ string + std::vector multiSz(totalSize / sizeof(wchar_t)); + wchar_t* ptr = multiSz.data(); + for (const auto& account : accounts) + { + wcscpy_s(ptr, account.size() + 1, account.c_str()); + ptr += account.size() + 1; + } + *ptr = L'\0'; // Final null terminator + + // Set the registry value + RegSetValueEx(hKey, REG_VALUE_INCLUDEDACCOUNTS, 0, REG_MULTI_SZ, (const BYTE*)multiSz.data(), totalSize); + RegCloseKey(hKey); + } +} + +static void DeleteRegistryValues() +{ + HKEY hKey; + LONG result = RegOpenKeyEx(HKEY_LOCAL_MACHINE, REG_BASE_SETTINGS_KEY, 0, KEY_WRITE, &hKey); + if (result == ERROR_SUCCESS) + { + RegDeleteValue(hKey, REG_VALUE_EXCLUDEDACCOUNTS); + RegDeleteValue(hKey, REG_VALUE_INCLUDEDACCOUNTS); + RegCloseKey(hKey); + } +} + +namespace NativeUnitTests +{ + TEST_CLASS(PasswordEvaluatorInScopeTests) + { + public: + + TEST_METHOD_INITIALIZE(CleanupRegistry) + { + DeleteRegistryValues(); + } + + TEST_METHOD(TestIsUserInScope_ExcludedAccount) + { + std::wstring accountName = L"krbtgt"; + BOOLEAN result = IsUserInScope(accountName); + Assert::IsFalse(result); + } + + TEST_METHOD(TestIsUserInScope_NotExcludedAccount) + { + std::wstring accountName = L"testuser"; + BOOLEAN result = IsUserInScope(accountName); + Assert::IsTrue(result); + } + + TEST_METHOD(TestIsUserInScope_ExcludedAccountFromRegistry) + { + std::vector excludedAccounts = { L"excludeduser" }; + SetRegistryExcludedAccounts(excludedAccounts); + + std::wstring accountName = L"excludeduser"; + BOOLEAN result = IsUserInScope(accountName); + Assert::IsFalse(result); + } + + TEST_METHOD(TestIsUserInScope_MultipleExcludedAccountsFromRegistry) + { + std::vector excludedAccounts = { L"excludeduser1", L"excludeduser2", L"excludeduser3" }; + SetRegistryExcludedAccounts(excludedAccounts); + + std::wstring accountName1 = L"excludeduser1"; + BOOLEAN result1 = IsUserInScope(accountName1); + Assert::IsFalse(result1); + + std::wstring accountName2 = L"excludeduser2"; + BOOLEAN result2 = IsUserInScope(accountName2); + Assert::IsFalse(result2); + + std::wstring accountName3 = L"excludeduser3"; + BOOLEAN result3 = IsUserInScope(accountName3); + Assert::IsFalse(result3); + + std::wstring accountName4 = L"includeduser"; + BOOLEAN result4 = IsUserInScope(accountName4); + Assert::IsTrue(result4); + } + + TEST_METHOD(TestIsUserInScope_NotExcludedAccountFromRegistry) + { + std::vector excludedAccounts = { L"excludeduser1", L"excludeduser2", L"excludeduser3" }; + SetRegistryExcludedAccounts(excludedAccounts); + + std::wstring accountName = L"includeduser"; + BOOLEAN result = IsUserInScope(accountName); + Assert::IsTrue(result); + } + + // New test cases for 'included user' logic + + TEST_METHOD(TestIsUserInScope_IncludedAccount) + { + std::vector includedAccounts = { L"includeduser" }; + SetRegistryIncludedAccounts(includedAccounts); + + std::wstring accountName = L"includeduser"; + BOOLEAN result = IsUserInScope(accountName); + Assert::IsTrue(result); + } + + TEST_METHOD(TestIsUserInScope_IncludedAccountListEmpty) + { + std::vector includedAccounts = {}; + SetRegistryIncludedAccounts(includedAccounts); + + std::wstring accountName = L"anyuser"; + BOOLEAN result = IsUserInScope(accountName); + Assert::IsTrue(result); + } + + TEST_METHOD(TestIsUserInScope_NotIncludedAccount) + { + std::vector includedAccounts = { L"includeduser1", L"includeduser2" }; + SetRegistryIncludedAccounts(includedAccounts); + + std::wstring accountName = L"notincludeduser"; + BOOLEAN result = IsUserInScope(accountName); + Assert::IsFalse(result); + } + + TEST_METHOD(TestIsUserInScope_IncludedAndExcludedAccount) + { + std::vector includedAccounts = { L"includeduser" }; + std::vector excludedAccounts = { L"includeduser" }; + SetRegistryIncludedAccounts(includedAccounts); + SetRegistryExcludedAccounts(excludedAccounts); + + std::wstring accountName = L"includeduser"; + BOOLEAN result = IsUserInScope(accountName); + Assert::IsFalse(result); + } + }; +} diff --git a/src/NativeUnitTests/RegistryTests.cpp b/src/NativeUnitTests/RegistryTests.cpp new file mode 100644 index 0000000..d64ca17 --- /dev/null +++ b/src/NativeUnitTests/RegistryTests.cpp @@ -0,0 +1,98 @@ +#include "stdafx.h" +#include "CppUnitTest.h" +#include "registry.h" +#include + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +namespace NativeUnitTests +{ + TEST_CLASS(RegistryTests) + { + public: + + TEST_METHOD(TestGetRegValueString) + { + registry reg; + std::wstring defaultValue = L"default"; + std::wstring value = reg.GetRegValue(L"NonExistentValue", defaultValue); + Assert::AreEqual(defaultValue, value); + } + + TEST_METHOD(TestGetRegValueDWORD) + { + registry reg; + DWORD defaultValue = 1234; + DWORD value = reg.GetRegValue(L"NonExistentValue", defaultValue); + Assert::AreEqual(defaultValue, value); + } + + TEST_METHOD(TestGetRegValueMultiString) + { + registry reg; + std::vector defaultValue = { L"default1", L"default2" }; + std::vector value = reg.GetRegValue(L"NonExistentValue", REG_DEFAULT_MAX_ITEMS, defaultValue); + Assert::IsTrue(defaultValue == value); + } + + TEST_METHOD(TestGetRegistryForUser) + { + registry reg = registry::GetRegistryForUser(L"testuser"); + Assert::IsNotNull(®); + } + + TEST_METHOD(TestSetAndGetRegValueString) + { + HKEY hKey; + LPCWSTR subKey = REG_BASE_SETTINGS_KEY; + LPCWSTR valueName = L"TestString"; + std::wstring setValue = L"TestValue"; + + RegCreateKeyEx(HKEY_LOCAL_MACHINE, subKey, 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL); + RegSetValueEx(hKey, valueName, 0, REG_SZ, (const BYTE*)setValue.c_str(), (setValue.size() + 1) * sizeof(wchar_t)); + RegCloseKey(hKey); + + registry reg; + std::wstring getValue = reg.GetRegValue(valueName, L""); + Assert::AreEqual(setValue, getValue); + } + + TEST_METHOD(TestSetAndGetRegValueDWORD) + { + HKEY hKey; + LPCWSTR subKey = REG_BASE_SETTINGS_KEY; + LPCWSTR valueName = L"TestDWORD"; + DWORD setValue = 5678; + + RegCreateKeyEx(HKEY_LOCAL_MACHINE, subKey, 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL); + RegSetValueEx(hKey, valueName, 0, REG_DWORD, (const BYTE*)&setValue, sizeof(setValue)); + RegCloseKey(hKey); + + registry reg; + DWORD getValue = reg.GetRegValue(valueName, 0); + Assert::AreEqual(setValue, getValue); + } + + TEST_METHOD(TestSetAndGetRegValueMultiString) + { + HKEY hKey; + LPCWSTR subKey = REG_BASE_SETTINGS_KEY; + LPCWSTR valueName = L"TestMultiString"; + std::vector setValue = { L"Value1", L"Value2" }; + std::wstring multiString; + for (const auto& str : setValue) + { + multiString.append(str).append(1, L'\0'); + } + multiString.append(1, L'\0'); + + RegCreateKeyEx(HKEY_LOCAL_MACHINE, subKey, 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL); + RegSetValueEx(hKey, valueName, 0, REG_MULTI_SZ, (const BYTE*)multiString.c_str(), (multiString.size() + 1) * sizeof(wchar_t)); + RegCloseKey(hKey); + + registry reg; + std::vector getValue = reg.GetRegValue(valueName, REG_DEFAULT_MAX_ITEMS, std::vector()); + Assert::IsTrue(std::equal(setValue.begin(), setValue.end(), getValue.begin(), getValue.end())); + } + }; +} diff --git a/src/PasswordFilter/filter.cpp b/src/PasswordFilter/filter.cpp index 4ead0bd..6345c49 100644 --- a/src/PasswordFilter/filter.cpp +++ b/src/PasswordFilter/filter.cpp @@ -64,6 +64,13 @@ extern "C" __declspec(dllexport) BOOLEAN __stdcall PasswordFilter( simulate = reg.GetRegValue(REG_VALUE_AUDITONLY, 0) != 0; std::wstring accountName(AccountName->Buffer, AccountName->Length / sizeof(WCHAR)); + + if (!IsUserInScope(accountName)) + { + eventlog::getInstance().logw(EVENTLOG_INFORMATION_TYPE, MSG_USEREXCLUDED, 1, accountName.c_str()); + return TRUE; + } + std::wstring fullName(FullName->Buffer, FullName->Length / sizeof(WCHAR)); SecureArrayT password = UnicodeStringToWcharArray(*Password); @@ -146,6 +153,13 @@ extern "C" __declspec(dllexport) int __stdcall PasswordFilterEx( } std::wstring accountName = AccountName; + + if (!IsUserInScope(accountName)) + { + eventlog::getInstance().logw(EVENTLOG_INFORMATION_TYPE, MSG_USEREXCLUDED, 1, accountName.c_str()); + return TRUE; + } + std::wstring fullName = FullName; SecureArrayT password = StringToWcharArray(Password); diff --git a/src/PasswordFilter/messages.h b/src/PasswordFilter/messages.h index dd18649..c7b913c 100644 --- a/src/PasswordFilter/messages.h +++ b/src/PasswordFilter/messages.h @@ -105,12 +105,43 @@ // // MessageText: // -// There was a problem opening the store file. Check that the store folder exists and is accessible.\n +// There was a problem opening the store file. Check that the store folder exists and is accessible. // Error code: %1 // Path: %2 // #define MSG_STOREERROR ((DWORD)0xC0020009L) +// +// MessageId: MSG_USEREXCLUDED +// +// MessageText: +// +// The user %1 was in the exclusion list and was not evaluated by the password filter. +// +#define MSG_USEREXCLUDED ((DWORD)0x4002000AL) + +// +// MessageId: MSG_USERNOTINCLUDED +// +// MessageText: +// +// The user %1 was not in the inclusion list and was not evaluated by the password filter. +// +#define MSG_USERNOTINCLUDED ((DWORD)0x4002000BL) + +// +// MessageId: MSG_REG_READ_ERROR +// +// MessageText: +// +// A registry read error occurred. +// Error code: %1 +// Key: %2 +// Value: %3 +// Type: %4 +// +#define MSG_REG_READ_ERROR ((DWORD)0xC002000CL) + // // MessageId: MSG_PASSWORD_REJECTED_ON_ERROR // diff --git a/src/PasswordFilter/messages.mc b/src/PasswordFilter/messages.mc index 96641fe..0dc582b 100644 --- a/src/PasswordFilter/messages.mc +++ b/src/PasswordFilter/messages.mc @@ -77,6 +77,34 @@ Error code: %1 Path: %2 . +MessageId=0xA +Severity=Informational +Facility=Runtime +SymbolicName=MSG_USEREXCLUDED +Language=English +The user %1 was in the exclusion list and was not evaluated by the password filter. +. + +MessageId=0xB +Severity=Informational +Facility=Runtime +SymbolicName=MSG_USERNOTINCLUDED +Language=English +The user %1 was not in the inclusion list and was not evaluated by the password filter. +. + +MessageId=0xC +Severity=Error +Facility=Runtime +SymbolicName=MSG_REG_READ_ERROR +Language=English +A registry read error occurred. +Error code: %1 +Key: %2 +Value: %3 +Type: %4 +. + MessageId=0x2001 Severity=Warning Facility=System diff --git a/src/PasswordFilter/messages.res b/src/PasswordFilter/messages.res index 482025cc573b9d1646aa8405ffb64edf64cadf75..3ab06d50fb0c7657f6048817e296f52321546853 100644 GIT binary patch delta 492 zcmbVHJxc>Y5Pf?WF)=0^Qlxc7Pn#fqq*4g6whe-vryhC`PPr(W%G6eYZ7nRUL`1Q% z5w5Yf_CE;zgZO4S1Us>Kvomks%)I%U9W9K^<$E140LJ4op_l>7C>317P$Z<7APxwh zm7qoG6ueHExM8jgh83dd10D)P!L=q2FB9z6WR{neh -int ProcessPassword(const SecureArrayT &password, const std::wstring &accountName, const std::wstring &fullName, const BOOLEAN &setOperation) +BOOLEAN IsUserInScope(const std::wstring& accountName) +{ + if (_wcsicmp(accountName.c_str(), L"krbtgt") == 0) + { + eventlog::getInstance().logw(EVENTLOG_INFORMATION_TYPE, MSG_USEREXCLUDED, 1, accountName.c_str()); + return FALSE; + } + + registry reg; + + for (const auto& excludedAccount : reg.GetRegValue(REG_VALUE_EXCLUDEDACCOUNTS, REG_DEFAULT_MAX_ITEMS, std::vector())) + { + if (_wcsicmp(accountName.c_str(), excludedAccount.c_str()) == 0) + { + eventlog::getInstance().logw(EVENTLOG_INFORMATION_TYPE, MSG_USEREXCLUDED, 1, accountName.c_str()); + return FALSE; + } + } + + // See if we have an INCLUDEDACCOUNTS value. If it is present, then only allow those accounts to be processed. If it is blank, allow all accounts. + + std::vector includedAccounts = reg.GetRegValue(REG_VALUE_INCLUDEDACCOUNTS, REG_DEFAULT_MAX_ITEMS, std::vector()); + + if (!includedAccounts.empty()) + { + for (const auto& includedAccount : includedAccounts) + { + if (_wcsicmp(accountName.c_str(), includedAccount.c_str()) == 0) + { + return TRUE; + } + } + + eventlog::getInstance().logw(EVENTLOG_INFORMATION_TYPE, MSG_USERNOTINCLUDED, 1, accountName.c_str()); + + return FALSE; + } + + return TRUE; +} + +int ProcessPassword(const SecureArrayT& password, const std::wstring& accountName, const std::wstring& fullName, const BOOLEAN& setOperation) { eventlog::getInstance().logw(EVENTLOG_INFORMATION_TYPE, MSG_PROCESSING_REQUEST, 3, setOperation ? L"set" : L"change", accountName.c_str(), fullName.c_str()); @@ -73,9 +115,9 @@ int ProcessPassword(const SecureArrayT &password, const std::wstring &acc return PASSWORD_APPROVED; } -BOOLEAN ProcessPasswordRaw(const SecureArrayT &password, const std::wstring &accountName, const std::wstring &fullName, const BOOLEAN &setOperation, const registry ®) +BOOLEAN ProcessPasswordRaw(const SecureArrayT& password, const std::wstring& accountName, const std::wstring& fullName, const BOOLEAN& setOperation, const registry& reg) { - if ((setOperation && reg.GetRegValue(REG_VALUE_CHECKBANNEDPASSWORDONSET, 0) != 0) || + if ((setOperation && reg.GetRegValue(REG_VALUE_CHECKBANNEDPASSWORDONSET, 0) != 0) || (!setOperation && reg.GetRegValue(REG_VALUE_CHECKBANNEDPASSWORDONCHANGE, 0) != 0)) { OutputDebugString(L"Checking raw password"); @@ -93,10 +135,10 @@ BOOLEAN ProcessPasswordRaw(const SecureArrayT &password, const std::wstri return TRUE; } -BOOLEAN ProcessPasswordNormalizedPasswordStore(const SecureArrayT &password, const std::wstring &accountName, const std::wstring &fullName, const BOOLEAN &setOperation, const registry ®) +BOOLEAN ProcessPasswordNormalizedPasswordStore(const SecureArrayT& password, const std::wstring& accountName, const std::wstring& fullName, const BOOLEAN& setOperation, const registry& reg) { if (setOperation && reg.GetRegValue(REG_VALUE_CHECKNORMALIZEDBANNEDPASSWORDONSET, 0) != 0 || - !setOperation && reg.GetRegValue(REG_VALUE_CHECKNORMALIZEDBANNEDPASSWORDONCHANGE, 0) != 0 ) + !setOperation && reg.GetRegValue(REG_VALUE_CHECKNORMALIZEDBANNEDPASSWORDONCHANGE, 0) != 0) { bool result = TRUE; @@ -121,7 +163,7 @@ BOOLEAN ProcessPasswordNormalizedPasswordStore(const SecureArrayT &passwo } -BOOLEAN ProcessPasswordNormalizedWordStore(const SecureArrayT &password, const std::wstring &accountName, const std::wstring &fullName, const BOOLEAN &setOperation, const registry ®) +BOOLEAN ProcessPasswordNormalizedWordStore(const SecureArrayT& password, const std::wstring& accountName, const std::wstring& fullName, const BOOLEAN& setOperation, const registry& reg) { if (setOperation && reg.GetRegValue(REG_VALUE_CHECKNORMALIZEDBANNEDWORDONSET, 0) != 0 || !setOperation && reg.GetRegValue(REG_VALUE_CHECKNORMALIZEDBANNEDWORDONCHANGE, 0) != 0) @@ -149,7 +191,7 @@ BOOLEAN ProcessPasswordNormalizedWordStore(const SecureArrayT &password, } -BOOLEAN ProcessPasswordLength(const SecureArrayT &password, const std::wstring &accountName, const std::wstring &fullName, const BOOLEAN &setOperation, const registry ®) +BOOLEAN ProcessPasswordLength(const SecureArrayT& password, const std::wstring& accountName, const std::wstring& fullName, const BOOLEAN& setOperation, const registry& reg) { const int minLength = reg.GetRegValue(REG_VALUE_MINIMUMLENGTH, 0); @@ -170,7 +212,7 @@ BOOLEAN ProcessPasswordLength(const SecureArrayT &password, const std::ws return TRUE; } -BOOLEAN ProcessPasswordDoesntContainAccountName(const SecureArrayT &password, const std::wstring &accountName, const std::wstring &fullName, const BOOLEAN &setOperation, const registry ®) +BOOLEAN ProcessPasswordDoesntContainAccountName(const SecureArrayT& password, const std::wstring& accountName, const std::wstring& fullName, const BOOLEAN& setOperation, const registry& reg) { int flag = reg.GetRegValue(REG_VALUE_PASSWORDDOESNTCONTAINACCOUNTNAME, 0); @@ -178,7 +220,7 @@ BOOLEAN ProcessPasswordDoesntContainAccountName(const SecureArrayT &passw { OutputDebugString(L"Checking to see if password contains account name"); - for each (std::wstring substring in SplitString(accountName, L' ')) + for each(std::wstring substring in SplitString(accountName, L' ')) { if (substring.length() > 3) { @@ -197,7 +239,7 @@ BOOLEAN ProcessPasswordDoesntContainAccountName(const SecureArrayT &passw return TRUE; } -BOOLEAN ProcessPasswordDoesntContainFullName(const SecureArrayT &password, const std::wstring &accountName, const std::wstring &fullName, const BOOLEAN &setOperation, const registry ®) +BOOLEAN ProcessPasswordDoesntContainFullName(const SecureArrayT& password, const std::wstring& accountName, const std::wstring& fullName, const BOOLEAN& setOperation, const registry& reg) { int flag = reg.GetRegValue(REG_VALUE_PASSWORDDOESNTCONTAINFULLNAME, 0); @@ -205,7 +247,7 @@ BOOLEAN ProcessPasswordDoesntContainFullName(const SecureArrayT &password { OutputDebugString(L"Checking to see if password contains full name"); - for each (std::wstring substring in SplitString(fullName, L' ')) + for each(std::wstring substring in SplitString(fullName, L' ')) { if (substring.length() > 3) { @@ -224,7 +266,7 @@ BOOLEAN ProcessPasswordDoesntContainFullName(const SecureArrayT &password return TRUE; } -BOOLEAN ProcessPasswordRegexApprove(const SecureArrayT &password, const std::wstring &accountName, const std::wstring &fullName, const BOOLEAN &setOperation, const registry ®) +BOOLEAN ProcessPasswordRegexApprove(const SecureArrayT& password, const std::wstring& accountName, const std::wstring& fullName, const BOOLEAN& setOperation, const registry& reg) { std::wstring regex = reg.GetRegValue(REG_VALUE_REGEXAPPROVE, L""); @@ -247,7 +289,7 @@ BOOLEAN ProcessPasswordRegexApprove(const SecureArrayT &password, const s return TRUE; } -BOOLEAN ProcessPasswordRegexReject(const SecureArrayT &password, const std::wstring &accountName, const std::wstring &fullName, const BOOLEAN &setOperation, const registry ®) +BOOLEAN ProcessPasswordRegexReject(const SecureArrayT& password, const std::wstring& accountName, const std::wstring& fullName, const BOOLEAN& setOperation, const registry& reg) { std::wstring regex = reg.GetRegValue(REG_VALUE_REGEXREJECT, L""); diff --git a/src/PasswordFilter/passwordevaluator.h b/src/PasswordFilter/passwordevaluator.h index 54a896e..b4c4e82 100644 --- a/src/PasswordFilter/passwordevaluator.h +++ b/src/PasswordFilter/passwordevaluator.h @@ -16,6 +16,8 @@ static const int PASSWORD_REJECTED_BANNED_NORMALIZED_WORD = 10; static const int PASSWORD_REJECTED_BLANK = 11; static const int FILTER_ERROR = 100; +BOOLEAN IsUserInScope(const std::wstring& accountName); + int ProcessPassword(const SecureArrayT &password, const std::wstring &accountName, const std::wstring &fullName, const BOOLEAN &setOperation); BOOLEAN ProcessPasswordRaw(const SecureArrayT &password, const std::wstring &accountName, const std::wstring &fullName, const BOOLEAN &setOperation, const registry ®); diff --git a/src/PasswordFilter/registry.cpp b/src/PasswordFilter/registry.cpp index e039faa..b2e2fa9 100644 --- a/src/PasswordFilter/registry.cpp +++ b/src/PasswordFilter/registry.cpp @@ -1,6 +1,9 @@ #include "stdafx.h" #include "registry.h" #include +#include +#include "eventlog.h" +#include "messages.h" registry::registry() { @@ -18,22 +21,27 @@ registry::registry(std::wstring policyGroup) registry::~registry() = default; -std::wstring registry::GetRegValue(const std::wstring & valueName, const std::wstring & defaultValue) const +std::wstring registry::GetRegValue(const std::wstring& valueName, const std::wstring& defaultValue) const { return GetPolicyOrSettingsValue(valueName, defaultValue); } -DWORD registry::GetRegValue(const std::wstring & valueName, DWORD defaultValue) const +std::vector registry::GetRegValue(const std::wstring& valueName, DWORD maxItems, const std::vector& defaultValue) const +{ + return GetPolicyOrSettingsValue(maxItems, valueName, defaultValue); +} + +DWORD registry::GetRegValue(const std::wstring& valueName, DWORD defaultValue) const { return GetPolicyOrSettingsValue(valueName, defaultValue); } -registry registry::GetRegistryForUser(const std::wstring & user) +registry registry::GetRegistryForUser(const std::wstring& user) { return registry(L"default"); } -DWORD registry::GetPolicyOrSettingsValue(const std::wstring & valueName, DWORD defaultValue) const +DWORD registry::GetPolicyOrSettingsValue(const std::wstring& valueName, DWORD defaultValue) const { DWORD dwBufferSize(sizeof(DWORD)); DWORD value(0); @@ -67,7 +75,7 @@ DWORD registry::GetPolicyOrSettingsValue(const std::wstring & valueName, DWORD d return defaultValue; } -const std::wstring registry::GetKeyName(LPCWSTR & key) const +const std::wstring registry::GetKeyName(LPCWSTR& key) const { std::wstring name(key); if (!this->policyGroup.empty()) @@ -79,7 +87,7 @@ const std::wstring registry::GetKeyName(LPCWSTR & key) const return name; } -const std::wstring registry::GetPolicyOrSettingsValue(const std::wstring & valueName, const std::wstring & defaultValue) const +const std::wstring registry::GetPolicyOrSettingsValue(const std::wstring& valueName, const std::wstring& defaultValue) const { DWORD dwBufferSize = 0; @@ -112,7 +120,89 @@ const std::wstring registry::GetPolicyOrSettingsValue(const std::wstring & value return defaultValue; } -const std::wstring registry::GetValueString(DWORD & dwBufferSize, const std::wstring & keyName, const std::wstring & valueName, const std::wstring & defaultValue) const +const std::vector registry::GetPolicyOrSettingsValue(DWORD maxItems, const std::wstring& valueName, const std::vector& defaultValue) const +{ + DWORD dwBufferSize = 0; + + long result = RegGetValue(HKEY_LOCAL_MACHINE, + this->policyKeyName.c_str(), + valueName.c_str(), + RRF_RT_REG_MULTI_SZ, + NULL, + NULL, + &dwBufferSize); + + if (result == ERROR_SUCCESS) + { + return GetValueMultiString(dwBufferSize, maxItems, this->policyKeyName, valueName, defaultValue); + } + + result = RegGetValue(HKEY_LOCAL_MACHINE, + this->settingsKeyName.c_str(), + valueName.c_str(), + RRF_RT_REG_MULTI_SZ, + NULL, + NULL, + &dwBufferSize); + + if (result == ERROR_SUCCESS) + { + return GetValueMultiString(dwBufferSize, maxItems, this->settingsKeyName, valueName, defaultValue); + } + + return defaultValue; +} + +const std::vector registry::GetValueMultiString(DWORD& dwBufferSize, DWORD maxItems, const std::wstring& keyName, const std::wstring& valueName, const std::vector& defaultValue) const +{ + std::vector result; + + std::vector data; + data.resize(dwBufferSize / sizeof(wchar_t)); + + DWORD lResult = RegGetValue(HKEY_LOCAL_MACHINE, + keyName.c_str(), + valueName.c_str(), + RRF_RT_REG_MULTI_SZ, + NULL, + &data[0], + &dwBufferSize + ); + + if (lResult != ERROR_SUCCESS) + { + eventlog::getInstance().logw(EVENTLOG_ERROR_TYPE, MSG_REG_READ_ERROR, 4, std::to_wstring(lResult).c_str(), keyName.c_str(), valueName.c_str(), L"REG_MULTI_SZ"); + return defaultValue; + } + + data.resize(dwBufferSize / sizeof(wchar_t)); + + std::wstring current; + for (DWORD i = 0; i < dwBufferSize / sizeof(WCHAR); i++) + { + if (data[i] == L'\0') + { + if (!current.empty()) + { + if (result.size() >= maxItems) + { + break; + } + + result.push_back(current); + current.clear(); + } + } + else + { + current += data[i]; + } + } + + return result; +} + +const std::wstring registry::GetValueString(DWORD& dwBufferSize, const std::wstring& keyName, const std::wstring& valueName, const std::wstring& defaultValue) const { std::unique_ptr szBuffer = std::make_unique(dwBufferSize / sizeof(WCHAR)); @@ -126,6 +216,7 @@ const std::wstring registry::GetValueString(DWORD & dwBufferSize, const std::wst if (result != ERROR_SUCCESS) { + eventlog::getInstance().logw(EVENTLOG_ERROR_TYPE, MSG_REG_READ_ERROR, 4, std::to_wstring(result).c_str(), keyName.c_str(), valueName.c_str(), L"REG_SZ"); return defaultValue; } diff --git a/src/PasswordFilter/registry.h b/src/PasswordFilter/registry.h index 0cc6082..3aa7cfc 100644 --- a/src/PasswordFilter/registry.h +++ b/src/PasswordFilter/registry.h @@ -1,12 +1,16 @@ #pragma once #include "stdafx.h" +#include +static DWORD REG_DEFAULT_MAX_ITEMS = 32; static LPCWSTR REG_BASE_SETTINGS_KEY = L"SOFTWARE\\Lithnet\\PasswordFilter"; static LPCWSTR REG_BASE_POLICY_KEY = L"SOFTWARE\\Policies\\Lithnet\\PasswordFilter"; static LPCWSTR REG_VALUE_STOREPATH = L"Store"; static LPCWSTR REG_VALUE_DISABLED = L"Disabled"; static LPCWSTR REG_VALUE_AUDITONLY = L"AuditOnly"; +static LPCWSTR REG_VALUE_EXCLUDEDACCOUNTS = L"ExcludedAccounts"; +static LPCWSTR REG_VALUE_INCLUDEDACCOUNTS = L"IncludedAccounts"; static LPCWSTR REG_VALUE_MINIMUMLENGTH = L"MinimumLength"; @@ -76,15 +80,18 @@ class registry registry(); registry(std::wstring policyGroup); ~registry(); - std::wstring GetRegValue(const std::wstring & valueName, const std::wstring & defaultValue) const; - DWORD GetRegValue(const std::wstring & valueName, DWORD defaultValue) const; - static registry GetRegistryForUser(const std::wstring & user); + std::wstring GetRegValue(const std::wstring& valueName, const std::wstring& defaultValue) const; + DWORD GetRegValue(const std::wstring& valueName, DWORD defaultValue) const; + std::vector registry::GetRegValue(const std::wstring& valueName, DWORD maxItems, const std::vector& defaultValue) const; + static registry GetRegistryForUser(const std::wstring& user); private: - DWORD GetPolicyOrSettingsValue(const std::wstring & valueName, DWORD defaultValue) const; const std::wstring GetKeyName(LPCWSTR& key) const; - const std::wstring GetPolicyOrSettingsValue(const std::wstring & valueName, const std::wstring & defaultValue) const; - const std::wstring GetValueString(DWORD & dwBufferSize, const std::wstring & keyName, const std::wstring & valueName, const std::wstring & defaultValue) const; + DWORD GetPolicyOrSettingsValue(const std::wstring& valueName, DWORD defaultValue) const; + const std::vector GetPolicyOrSettingsValue(DWORD maxItems, const std::wstring& valueName, const std::vector< std::wstring>& defaultValue) const; + const std::wstring GetPolicyOrSettingsValue(const std::wstring& valueName, const std::wstring& defaultValue) const; + const std::wstring GetValueString(DWORD& dwBufferSize, const std::wstring& keyName, const std::wstring& valueName, const std::wstring& defaultValue) const; + const std::vector GetValueMultiString(DWORD& dwBufferSize, DWORD maxItems, const std::wstring& keyName, const std::wstring& valueName, const std::vector& defaultValue) const; }; diff --git a/src/PasswordProtection/PolicyDefinitions/en-US/lithnet.activedirectory.passwordfilter.adml b/src/PasswordProtection/PolicyDefinitions/en-US/lithnet.activedirectory.passwordfilter.adml index 121faf0..7162179 100644 --- a/src/PasswordProtection/PolicyDefinitions/en-US/lithnet.activedirectory.passwordfilter.adml +++ b/src/PasswordProtection/PolicyDefinitions/en-US/lithnet.activedirectory.passwordfilter.adml @@ -10,7 +10,13 @@ Disable password filter When enabled, prevents the password filter from evaluating password change requests. If disabled, or set to not configured, the password filter will evaluate password change requests - Minimum password length + Exclude user accounts from password filtering + When enabled, the password filter will ignore password changes for the listed accounts. A maximum of 32 accounts can be listed. If disabled, or set to not configured, the password filter will evaluate password change requests for all accounts + + Enable password filtering only on specified user accounts + When enabled, the password filter will ONLY process password changes for the listed accounts. A maximum of 32 accounts can be listed. If disabled, or set to not configured, the password filter will evaluate password change requests for all accounts. Note if a user is added to the exclusion list, this takes priority over the inclusion list + + Minimum password length When enabled, specifies the minimum password length to enforce. If disabled or not configured, no minimum password length is enforced Passwords must meet specified number of complexity points @@ -85,7 +91,15 @@ If this policy disabled, or set to not configured, the password filter will not - + + List of user account names to exclude (one per line, samAccountName only) + + + + List of user account names to include (one per line, samAccountName only) + + + Minimum password length diff --git a/src/PasswordProtection/PolicyDefinitions/lithnet.activedirectory.passwordfilter.admx b/src/PasswordProtection/PolicyDefinitions/lithnet.activedirectory.passwordfilter.admx index b9b4135..ae06a46 100644 --- a/src/PasswordProtection/PolicyDefinitions/lithnet.activedirectory.passwordfilter.admx +++ b/src/PasswordProtection/PolicyDefinitions/lithnet.activedirectory.passwordfilter.admx @@ -38,7 +38,36 @@ - + + + + + + + + + + + + + + + + + Date: Wed, 22 Jan 2025 20:35:12 +1100 Subject: [PATCH 2/2] Removed event logging when a user is excluded in both PasswordFilter and PasswordFilterEx functions. Changed the return value from TRUE to PASSWORD_APPROVED for consistency and clarity. --- src/PasswordFilter/filter.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PasswordFilter/filter.cpp b/src/PasswordFilter/filter.cpp index 6345c49..a600bd0 100644 --- a/src/PasswordFilter/filter.cpp +++ b/src/PasswordFilter/filter.cpp @@ -67,7 +67,6 @@ extern "C" __declspec(dllexport) BOOLEAN __stdcall PasswordFilter( if (!IsUserInScope(accountName)) { - eventlog::getInstance().logw(EVENTLOG_INFORMATION_TYPE, MSG_USEREXCLUDED, 1, accountName.c_str()); return TRUE; } @@ -156,8 +155,7 @@ extern "C" __declspec(dllexport) int __stdcall PasswordFilterEx( if (!IsUserInScope(accountName)) { - eventlog::getInstance().logw(EVENTLOG_INFORMATION_TYPE, MSG_USEREXCLUDED, 1, accountName.c_str()); - return TRUE; + return PASSWORD_APPROVED; } std::wstring fullName = FullName;