├── .gitignore ├── Improsec Password Filter.sln ├── LICENSE ├── Layout.png ├── README.md ├── ipf ├── adler32.cpp ├── adler32.hpp ├── blacklist.cpp ├── blacklist.hpp ├── hash_file.cpp ├── hash_file.hpp ├── hash_scanner.cpp ├── hash_scanner.hpp ├── ipf.vcxproj ├── ipf.vcxproj.filters ├── ipf.vcxproj.user ├── logger.cpp ├── logger.hpp └── main.cpp ├── ipf_test ├── ipf_test.vcxproj ├── ipf_test.vcxproj.filters └── main.cpp └── scripts ├── InstallFilter.ps1 ├── UninstallFilter.ps1 └── weak-phrases.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific files 2 | *.rsuser 3 | *.suo 4 | *.user 5 | *.userosscache 6 | *.sln.docstates 7 | 8 | # Build results 9 | [Dd]ebug/ 10 | [Dd]ebugPublic/ 11 | [Rr]elease/ 12 | [Rr]eleases/ 13 | x64/ 14 | x86/ 15 | [Ww][Ii][Nn]32/ 16 | [Aa][Rr][Mm]/ 17 | [Aa][Rr][Mm]64/ 18 | bld/ 19 | [Bb]in/ 20 | [Oo]bj/ 21 | [Ll]og/ 22 | [Ll]ogs/ 23 | 24 | # Visual Studio 2015/2017 cache/options directory 25 | .vs/ 26 | # Uncomment if you have tasks that create the project's static files in wwwroot 27 | #wwwroot/ 28 | 29 | # Visual Studio 2017 auto generated files 30 | Generated\ Files/ 31 | 32 | # Files built by Visual Studio 33 | *_i.c 34 | *_p.c 35 | *_h.h 36 | *.ilk 37 | *.meta 38 | *.obj 39 | *.iobj 40 | *.pch 41 | *.pdb 42 | *.ipdb 43 | *.pgc 44 | *.pgd 45 | *.rsp 46 | *.sbr 47 | *.tlb 48 | *.tli 49 | *.tlh 50 | *.tmp 51 | *.tmp_proj 52 | *_wpftmp.csproj 53 | *.log 54 | *.vspscc 55 | *.vssscc 56 | .builds 57 | *.pidb 58 | *.svclog 59 | *.scc 60 | 61 | # Visual C++ cache files 62 | ipch/ 63 | *.aps 64 | *.ncb 65 | *.opendb 66 | *.opensdf 67 | *.sdf 68 | *.cachefile 69 | *.VC.db 70 | *.VC.VC.opendb 71 | 72 | # MSBuild Binary and Structured Log 73 | *.binlog 74 | 75 | # Local History for Visual Studio 76 | .localhistory/ -------------------------------------------------------------------------------- /Improsec Password Filter.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.645 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ipf", "ipf\ipf.vcxproj", "{D55587B5-A1B3-4E09-8508-A00193764EE7}" 7 | EndProject 8 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ipf_test", "ipf_test\ipf_test.vcxproj", "{4BC9B4DC-BE72-457F-B089-60B654F7AD34}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|x64 = Debug|x64 13 | Debug|x86 = Debug|x86 14 | Release|x64 = Release|x64 15 | Release|x86 = Release|x86 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {D55587B5-A1B3-4E09-8508-A00193764EE7}.Debug|x64.ActiveCfg = Debug|x64 19 | {D55587B5-A1B3-4E09-8508-A00193764EE7}.Debug|x64.Build.0 = Debug|x64 20 | {D55587B5-A1B3-4E09-8508-A00193764EE7}.Debug|x86.ActiveCfg = Debug|Win32 21 | {D55587B5-A1B3-4E09-8508-A00193764EE7}.Debug|x86.Build.0 = Debug|Win32 22 | {D55587B5-A1B3-4E09-8508-A00193764EE7}.Release|x64.ActiveCfg = Release|x64 23 | {D55587B5-A1B3-4E09-8508-A00193764EE7}.Release|x64.Build.0 = Release|x64 24 | {D55587B5-A1B3-4E09-8508-A00193764EE7}.Release|x86.ActiveCfg = Release|Win32 25 | {D55587B5-A1B3-4E09-8508-A00193764EE7}.Release|x86.Build.0 = Release|Win32 26 | {4BC9B4DC-BE72-457F-B089-60B654F7AD34}.Debug|x64.ActiveCfg = Debug|x64 27 | {4BC9B4DC-BE72-457F-B089-60B654F7AD34}.Debug|x64.Build.0 = Debug|x64 28 | {4BC9B4DC-BE72-457F-B089-60B654F7AD34}.Debug|x86.ActiveCfg = Debug|Win32 29 | {4BC9B4DC-BE72-457F-B089-60B654F7AD34}.Debug|x86.Build.0 = Debug|Win32 30 | {4BC9B4DC-BE72-457F-B089-60B654F7AD34}.Release|x64.ActiveCfg = Release|x64 31 | {4BC9B4DC-BE72-457F-B089-60B654F7AD34}.Release|x64.Build.0 = Release|x64 32 | {4BC9B4DC-BE72-457F-B089-60B654F7AD34}.Release|x86.ActiveCfg = Release|Win32 33 | {4BC9B4DC-BE72-457F-B089-60B654F7AD34}.Release|x86.Build.0 = Release|Win32 34 | EndGlobalSection 35 | GlobalSection(SolutionProperties) = preSolution 36 | HideSolutionNode = FALSE 37 | EndGlobalSection 38 | GlobalSection(ExtensibilityGlobals) = postSolution 39 | SolutionGuid = {F181EA5E-28A4-43EE-95A7-63855D0CAEFB} 40 | EndGlobalSection 41 | EndGlobal 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Improsec A/S 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/improsec/ImprosecPasswordFilter/3beb402ee761961ee20df156c1962f1adcd085c4/Layout.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Improsec Password Filter (IPF) 2 | [![License](https://img.shields.io/badge/License-MIT-red.svg)](https://opensource.org/licenses/MIT) ![Windows Server 2008 R2 | 2012 R2 | 2016](https://img.shields.io/badge/Windows%20Server-2008%20R2%20|%202012%20R2%20|%202016-007bb8.svg) ![Visual Studio 2017](https://img.shields.io/badge/Visual%20Studio-2017-383278.svg) 3 | 4 | Block known weak passwords in your Active Directory environment(s). 5 | 6 | ## Introduction 7 | 8 | The LSA process on Windows systems are responsible for handling password requests pertaining to password changes and resets. Windows allows for the configuration of arbitrary password filters and will automatically load these and ask them to verify the compliance or validity of a password every time a password modification is requested. Upon booting, the LSA process will load all password filters described in the following registry key: 9 | * HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\Notification Packages 10 | 11 | The password filters available in the above described registry key are only base filenames, i.e. without extensions, and will be loaded from the following path: 12 | * C:\Windows\System32 13 | 14 | The following image presents a high-level illustration of the flow between the LSA process and our custom password filter DLL: 15 | 16 | ![picture](Layout.png) 17 | 18 | Additionally, our password filter creates a thread, that will perform constant runtime monitoring of the contents of files in the "C:\improsec-filter" directory. Upon detection of file modifications, our password filter will reload the necessary files to make sure all configurations can be modified without rebooting the system. 19 | 20 | **Important**: Since the DLL is being loaded by a core component of the Operating System, it is very important that the DLL has been built for the same platform (x86 or x64) as the underlying system – otherwise, the LSA process will not be able to load the filter DLL. 21 | 22 | ## Dependencies 23 | 24 | ### Microsoft Visual C++ Redistributable Package 25 | Microsoft Visual C++ Redistributable Package is required by our *ipf.dll* filter. 26 | 27 | ##### Installation step-by-step 28 | * Go to [Microsoft Latest Supported Visual C++ Download](https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads). 29 | * Download and install the Visual Studio 2017 version of the Visual C++ Redistributable Package for your target platform: 30 | * For 32-bit systems: vc_redist.x86.exe 31 | * For 64-bit systems: vc_redist.x64.exe (recommended) 32 | * No restart required. 33 | 34 | It is important that the installed version of the Visual C++ Redistributable package matches the platform and the Visual Studio version used to compile the solution (default: x64 with Visual Studio 2017). If not, the DLL will fail to locate its dependencies and will not load. 35 | 36 | ## Install 37 | 38 | The filter must be installed on a Domain Controller in the Active Directory domain. Note that if you have multiple Domain Controllers serving password change requests in the Active Directory domain, you must install the filter on each of them for full coverage. In order to install the solution, you can use either of the following methods. 39 | * Automated installation 40 | * Configure the pass phrases you would like to block in *weak-phrases.txt* file 41 | * Run the installation PowerShell-script ([link](scripts/InstallFilter.ps1)) 42 | * Restart the Domain Controller 43 | * Manual installation 44 | * Create directory "*C:\\improsec-filter\\*" 45 | * Create a *weak-phrases.txt* file in the "*C:\\improsec-filter\\*" directory 46 | * Set the contents of the *weak-phrases.txt* file to a list of pass phrases you would like to block. 47 | * Create a file named *weak-enabled.txt* in the "*C:\\improsec-filter\\*" directory and set the contents of the file to '1' 48 | * Do this only if you want to enable the basic pass phrase blocking feature. 49 | * Create a file named *leaked-enabled.txt* in the "*C:\\improsec-filter\\*" directory and set the contents of the file to '1' 50 | * Do this only if you want to enable the compromised passwords blocking feature. 51 | * Move or copy the filter DLL into the "*C:\\Windows\\System32\\*" directory 52 | * Append the name of the filter DLL (default: *ipf*) to the following registry key 53 | * HKLM\\SYSTEM\\CurrentControlSet\\Control\\LSA\\Notification Packages 54 | * Restart the Domain Controller 55 | 56 | Upon booting up, LSASS will now load our filter and use it to validate password changes going forward. 57 | 58 | ## Uninstall 59 | 60 | In order to stop the solution from actively rejecting blocked password changes without having to restart the Domain Controller, simply set the contents of the "*C:\\improsec-filter\\weak-enabled.txt*" and "*C:\\improsec-filter\\leaked-enabled.txt*" files to '0'. 61 | 62 | In order to completely uninstall the solution, you can use either of the following methods. 63 | * Automated uninstallation 64 | * Run the uninstallation PowerShell-script ([link](scripts/UninstallFilter.ps1)) 65 | * Restart the Domain Controller 66 | * Manual uninstallation 67 | * Remove the name of the filter DLL (default: *ipf.dll*) from the following registry key 68 | * HKLM\\SYSTEM\\CurrentControlSet\\Control\\LSA\\Notification Packages 69 | * Restart the Domain Controller 70 | 71 | Upon booting up, LSASS will no longer load our filter going forward. Optionally, you can now delete the "*C:\\improsec-filter\\*" directory as well as the filter DLL from the "*C:\\Windows\\System32\\*" directory. 72 | 73 | ## Usage 74 | 75 | Once installed on a server, the filter will hook into the LSASS validation cycle and reject any password change involving passwords that are present in the list of disallowed pass phrases. 76 | 77 | ## Authors 78 | * [**Valdemar Carøe**](https://github.com/st4ckh0und) 79 | * Danske Bank 80 | 81 | ## License 82 | 83 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 84 | -------------------------------------------------------------------------------- /ipf/adler32.cpp: -------------------------------------------------------------------------------- 1 | /* adler32.c -- compute the Adler-32 checksum of a data stream 2 | * Copyright (C) 1995-2011 Mark Adler 3 | * For conditions of distribution and use, see copyright notice in zlib.h 4 | */ 5 | 6 | /* Copyright message from zlib.h: 7 | Copyright (C) 1995-2017 Jean-loup Gailly and Mark Adler 8 | This software is provided 'as-is', without any express or implied 9 | warranty. In no event will the authors be held liable for any damages 10 | arising from the use of this software. 11 | Permission is granted to anyone to use this software for any purpose, 12 | including commercial applications, and to alter it and redistribute it 13 | freely, subject to the following restrictions: 14 | 1. The origin of this software must not be misrepresented; you must not 15 | claim that you wrote the original software. If you use this software in 16 | a product, an acknowledgment in the product documentation would be 17 | appreciated but is not required. 18 | 2. Altered source versions must be plainly marked as such, and must not 19 | be misrepresented as being the original software. 20 | 3. This notice may not be removed or altered from any source distribution. 21 | Jean-loup Gailly Mark Adler 22 | jloup@gzip.org madler@alumni.caltech.edu 23 | The data format used by the zlib library is described by RFCs (Request for 24 | Comments) 1950 to 1952 in the files http://tools.ietf.org/html/rfc1950 25 | (zlib format), rfc1951 (deflate format) and rfc1952 (gzip format). 26 | */ 27 | 28 | // Modifications has been made to this file, to remove dependencies on zlib header files 29 | 30 | /* @(#) $Id$ */ 31 | 32 | #include "adler32.hpp" 33 | 34 | #ifndef z_off_t 35 | # define z_off_t long 36 | #endif 37 | 38 | #if !defined(_WIN32) && defined(Z_LARGE64) 39 | # define z_off64_t off64_t 40 | #else 41 | # if defined(_WIN32) && !defined(__GNUC__) && !defined(Z_SOLO) 42 | # define z_off64_t __int64 43 | # else 44 | # define z_off64_t z_off_t 45 | # endif 46 | #endif 47 | 48 | #define local static 49 | #define Z_NULL 0 /* for initializing zalloc, zfree, opaque */ 50 | 51 | #define BASE 65521 /* largest prime smaller than 65536 */ 52 | #define NMAX 5552 53 | /* NMAX is the largest n such that 255n(n+1)/2 + (n+1)(BASE-1) <= 2^32-1 */ 54 | 55 | #define DO1(buf,i) {adler += (buf)[i]; sum2 += adler;} 56 | #define DO2(buf,i) DO1(buf,i); DO1(buf,i+1); 57 | #define DO4(buf,i) DO2(buf,i); DO2(buf,i+2); 58 | #define DO8(buf,i) DO4(buf,i); DO4(buf,i+4); 59 | #define DO16(buf) DO8(buf,0); DO8(buf,8); 60 | 61 | /* use NO_DIVIDE if your processor does not do division in hardware -- 62 | try it both ways to see which is faster */ 63 | #ifdef NO_DIVIDE 64 | /* note that this assumes BASE is 65521, where 65536 % 65521 == 15 65 | (thank you to John Reiser for pointing this out) */ 66 | # define CHOP(a) \ 67 | do { \ 68 | unsigned long tmp = a >> 16; \ 69 | a &= 0xffffUL; \ 70 | a += (tmp << 4) - tmp; \ 71 | } while (0) 72 | # define MOD28(a) \ 73 | do { \ 74 | CHOP(a); \ 75 | if (a >= BASE) a -= BASE; \ 76 | } while (0) 77 | # define MOD(a) \ 78 | do { \ 79 | CHOP(a); \ 80 | MOD28(a); \ 81 | } while (0) 82 | # define MOD63(a) \ 83 | do { /* this assumes a is not negative */ \ 84 | z_off64_t tmp = a >> 32; \ 85 | a &= 0xffffffffL; \ 86 | a += (tmp << 8) - (tmp << 5) + tmp; \ 87 | tmp = a >> 16; \ 88 | a &= 0xffffL; \ 89 | a += (tmp << 4) - tmp; \ 90 | tmp = a >> 16; \ 91 | a &= 0xffffL; \ 92 | a += (tmp << 4) - tmp; \ 93 | if (a >= BASE) a -= BASE; \ 94 | } while (0) 95 | #else 96 | # define MOD(a) a %= BASE 97 | # define MOD28(a) a %= BASE 98 | # define MOD63(a) a %= BASE 99 | #endif 100 | 101 | /* ========================================================================= */ 102 | unsigned long adler32(unsigned long adler, const unsigned char *buf, unsigned int len) 103 | { 104 | unsigned long sum2; 105 | unsigned n; 106 | 107 | /* split Adler-32 into component sums */ 108 | sum2 = (adler >> 16) & 0xffff; 109 | adler &= 0xffff; 110 | 111 | /* in case user likes doing a byte at a time, keep it fast */ 112 | if (len == 1) 113 | { 114 | adler += buf[0]; 115 | if (adler >= BASE) 116 | adler -= BASE; 117 | sum2 += adler; 118 | if (sum2 >= BASE) 119 | sum2 -= BASE; 120 | return adler | (sum2 << 16); 121 | } 122 | 123 | /* initial Adler-32 value (deferred check for len == 1 speed) */ 124 | if (buf == Z_NULL) 125 | return 1L; 126 | 127 | /* in case short lengths are provided, keep it somewhat fast */ 128 | if (len < 16) 129 | { 130 | while (len--) 131 | { 132 | adler += *buf++; 133 | sum2 += adler; 134 | } 135 | if (adler >= BASE) 136 | adler -= BASE; 137 | MOD28(sum2); /* only added so many BASE's */ 138 | return adler | (sum2 << 16); 139 | } 140 | 141 | /* do length NMAX blocks -- requires just one modulo operation */ 142 | while (len >= NMAX) 143 | { 144 | len -= NMAX; 145 | n = NMAX / 16; /* NMAX is divisible by 16 */ 146 | do 147 | { 148 | DO16(buf); /* 16 sums unrolled */ 149 | buf += 16; 150 | } 151 | while (--n); 152 | MOD(adler); 153 | MOD(sum2); 154 | } 155 | 156 | /* do remaining bytes (less than NMAX, still just one modulo) */ 157 | if (len) 158 | { /* avoid modulos if none remaining */ 159 | while (len >= 16) 160 | { 161 | len -= 16; 162 | DO16(buf); 163 | buf += 16; 164 | } 165 | while (len--) 166 | { 167 | adler += *buf++; 168 | sum2 += adler; 169 | } 170 | MOD(adler); 171 | MOD(sum2); 172 | } 173 | 174 | /* return recombined sums */ 175 | return adler | (sum2 << 16); 176 | } -------------------------------------------------------------------------------- /ipf/adler32.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef OF /* function prototypes */ 4 | # ifdef STDC 5 | # define OF(args) args 6 | # else 7 | # define OF(args) () 8 | # endif 9 | #endif 10 | 11 | unsigned long adler32(unsigned long adler, const unsigned char *buf, unsigned int len); -------------------------------------------------------------------------------- /ipf/blacklist.cpp: -------------------------------------------------------------------------------- 1 | #include "blacklist.hpp" 2 | #include "logger.hpp" 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #pragma comment(lib, "Shlwapi.lib") 10 | 11 | namespace filter { 12 | 13 | blacklist::blacklist() : 14 | ready_(false), 15 | enabled_(false) 16 | { 17 | 18 | } 19 | 20 | void blacklist::enable() 21 | { 22 | enabled_ = true; 23 | } 24 | 25 | void blacklist::disable() 26 | { 27 | enabled_ = false; 28 | } 29 | 30 | bool blacklist::load_file(std::wstring const& path) 31 | { 32 | std::lock_guard lg(mtx_); 33 | 34 | try 35 | { 36 | list_.clear(); 37 | 38 | std::wifstream file(path); 39 | std::wstring line; 40 | 41 | while (std::getline(file, line)) 42 | list_.push_back(line); 43 | 44 | return (ready_ = true); 45 | } 46 | catch (std::exception const& e) 47 | { 48 | filter::logger::get().write("[error] an exception occured while loading blacklist wildcard file"); 49 | filter::logger::get().write("[except] " + std::string(e.what())); 50 | return (ready_ = false); 51 | } 52 | } 53 | 54 | bool blacklist::contains(UNICODE_STRING* p) 55 | { 56 | std::lock_guard lg(mtx_); 57 | 58 | if (ready_ && enabled_ && p != NULL && p->Buffer != NULL) 59 | { 60 | auto iter = std::find_if(list_.begin(), list_.end(), 61 | [&](std::wstring const& w) -> bool 62 | { 63 | USHORT dwLength = static_cast(w.length() * sizeof(WCHAR)); 64 | USHORT dwLength2 = static_cast(p->Length / sizeof(WCHAR)); 65 | 66 | return (p->Length == dwLength && _wcsnicmp(p->Buffer, w.data(), w.length()) == 0) || 67 | (p->Length >= dwLength && StrStrIW(p->Buffer, w.data()) != NULL); 68 | }); 69 | 70 | return iter != list_.end(); 71 | } 72 | 73 | return false; 74 | } 75 | 76 | } // namespace filter 77 | -------------------------------------------------------------------------------- /ipf/blacklist.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | namespace filter { 11 | 12 | class blacklist 13 | { 14 | bool ready_; 15 | bool enabled_; 16 | std::mutex mtx_; 17 | std::vector list_; 18 | 19 | private: 20 | blacklist(); 21 | 22 | public: 23 | static blacklist& get() 24 | { 25 | static blacklist instance; 26 | return instance; 27 | } 28 | 29 | void enable(); 30 | void disable(); 31 | 32 | bool load_file(std::wstring const& path); 33 | bool contains(UNICODE_STRING* p); 34 | }; 35 | 36 | } // namespace filter 37 | -------------------------------------------------------------------------------- /ipf/hash_file.cpp: -------------------------------------------------------------------------------- 1 | #include "hash_file.hpp" 2 | 3 | #include 4 | #include 5 | 6 | namespace filter { 7 | 8 | hash_file::hash_file() : 9 | handle_(INVALID_HANDLE_VALUE), 10 | size_(0), 11 | elements_(0) 12 | { 13 | 14 | } 15 | 16 | hash_file::~hash_file() 17 | { 18 | close(); 19 | } 20 | 21 | void hash_file::open(std::wstring const& filename) 22 | { 23 | close(); 24 | 25 | if ((handle_ = CreateFileW(filename.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL)) == INVALID_HANDLE_VALUE) 26 | throw std::runtime_error("cannot open file"); 27 | else 28 | { 29 | LARGE_INTEGER file_size; 30 | memset(&file_size, 0, sizeof(LARGE_INTEGER)); 31 | 32 | if (!GetFileSizeEx(handle_, &file_size)) 33 | throw std::runtime_error("cannot query file size"); 34 | else if (read(&elements_, sizeof(uint64_t)) != sizeof(uint64_t)) 35 | throw std::runtime_error("cannot read entire element count object"); 36 | else 37 | size_ = (static_cast(file_size.QuadPart) - sizeof(uint64_t)); 38 | } 39 | } 40 | 41 | void hash_file::close() 42 | { 43 | if (handle_ != INVALID_HANDLE_VALUE) 44 | { 45 | CloseHandle(handle_); 46 | handle_ = INVALID_HANDLE_VALUE; 47 | } 48 | 49 | size_ = 0; 50 | elements_ = 0; 51 | } 52 | 53 | void hash_file::reset() const 54 | { 55 | if (handle_ == INVALID_HANDLE_VALUE) 56 | throw std::runtime_error("cannot reset uninitialized file"); 57 | else if (SetFilePointer(handle_, sizeof(uint64_t), NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER && GetLastError() != NO_ERROR) 58 | throw std::runtime_error("cannot reset file pointer"); 59 | } 60 | 61 | uint32_t hash_file::read(void* const buffer, std::size_t length) const 62 | { 63 | if (handle_ == INVALID_HANDLE_VALUE) 64 | throw std::runtime_error("cannot read from uninitialized file"); 65 | else 66 | { 67 | DWORD bytes_read = 0; 68 | 69 | if (!ReadFile(handle_, buffer, static_cast(length), &bytes_read, NULL)) 70 | throw std::runtime_error("cannot read file data"); 71 | else 72 | return static_cast(bytes_read); 73 | } 74 | } 75 | 76 | uint64_t hash_file::size() const 77 | { 78 | return size_; 79 | } 80 | 81 | uint64_t hash_file::elements() const 82 | { 83 | return elements_; 84 | } 85 | 86 | } // namespace filter -------------------------------------------------------------------------------- /ipf/hash_file.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace filter { 6 | 7 | class hash_file 8 | { 9 | public: 10 | hash_file(); 11 | ~hash_file(); 12 | 13 | void open(std::wstring const& filename); 14 | void close(); 15 | void reset() const; 16 | 17 | uint32_t read(void* const buffer, std::size_t length) const; 18 | 19 | uint64_t size() const; 20 | uint64_t elements() const; 21 | 22 | private: 23 | void* handle_; 24 | uint64_t size_; 25 | uint64_t elements_; 26 | }; 27 | 28 | } // namespace filter -------------------------------------------------------------------------------- /ipf/hash_scanner.cpp: -------------------------------------------------------------------------------- 1 | #include "hash_scanner.hpp" 2 | #include "logger.hpp" 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | namespace filter { 15 | 16 | hash_scanner::hash_scanner() : 17 | ready_(false), 18 | enabled_(false) 19 | { 20 | 21 | } 22 | 23 | void hash_scanner::enable() 24 | { 25 | enabled_ = true; 26 | } 27 | 28 | void hash_scanner::disable() 29 | { 30 | enabled_ = false; 31 | } 32 | 33 | bool hash_scanner::open(std::wstring const& path) 34 | { 35 | std::lock_guard lg(mtx_); 36 | 37 | try 38 | { 39 | hash_file file; 40 | file.open(path); 41 | file.reset(); 42 | 43 | if ((file.size() % 16) != 0) 44 | throw std::logic_error("cannot read hashes to a non-aligned buffer"); 45 | else 46 | { 47 | data_.clear(); 48 | data_.reserve(file.elements()); 49 | 50 | std::vector buffer; 51 | std::array temp; 52 | 53 | for (uint64_t remains = file.size(), length = 0; remains > 0;) 54 | { 55 | if ((length = file.read(&temp[0], std::min(temp.size(), remains - buffer.size()))) != 0) 56 | { 57 | buffer.insert(buffer.end(), temp.begin(), temp.begin() + length); 58 | std::vector::const_iterator iter = buffer.cbegin(); 59 | 60 | while (iter != buffer.cend() && std::distance(iter, buffer.cend()) >= 16) 61 | { 62 | data_.push_back(hash_data()); 63 | std::copy(iter, iter + 16, data_.back().data()); 64 | 65 | std::advance(iter, 16); 66 | remains -= 16; 67 | } 68 | 69 | buffer.erase(buffer.begin(), iter); 70 | } 71 | } 72 | } 73 | 74 | return (ready_ = true); 75 | } 76 | catch (std::exception const& e) 77 | { 78 | filter::logger::get().write("[error] an exception occured while loading leaked list file"); 79 | filter::logger::get().write("[except] " + std::string(e.what())); 80 | return (ready_ = false); 81 | } 82 | } 83 | 84 | bool hash_scanner::test(UNICODE_STRING* password) 85 | { 86 | std::lock_guard lg(mtx_); 87 | 88 | if (ready_ && enabled_) 89 | { 90 | std::array hash; 91 | 92 | if (nthash(password, hash)) 93 | return find(hash); 94 | } 95 | 96 | return false; 97 | } 98 | 99 | bool hash_scanner::find(std::array const& entry) const 100 | { 101 | return std::binary_search(data_.begin(), data_.end(), entry); 102 | } 103 | 104 | bool hash_scanner::nthash(UNICODE_STRING* input, std::array& digest) const 105 | { 106 | HCRYPTPROV hCryptProvider = NULL; 107 | HCRYPTHASH hCryptHash = NULL; 108 | 109 | bool result = false; 110 | 111 | if (CryptAcquireContextW(&hCryptProvider, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) 112 | { 113 | if (CryptCreateHash(hCryptProvider, CALG_MD4, 0, 0, &hCryptHash)) 114 | { 115 | if (CryptHashData(hCryptHash, reinterpret_cast(input->Buffer), static_cast(input->Length), 0)) 116 | { 117 | uint8_t md4_hash[16]; 118 | memset(&md4_hash[0], 0, sizeof(md4_hash)); 119 | 120 | uint32_t md4_length = sizeof(md4_hash); 121 | 122 | if (CryptGetHashParam(hCryptHash, HP_HASHVAL, &md4_hash[0], reinterpret_cast(&md4_length), 0)) 123 | { 124 | memcpy(&digest[0], md4_hash, sizeof(md4_hash)); 125 | result = true; 126 | } 127 | } 128 | 129 | CryptDestroyHash(hCryptHash); 130 | } 131 | 132 | CryptReleaseContext(hCryptProvider, 0); 133 | } 134 | 135 | return result; 136 | } 137 | 138 | } // namespace filter -------------------------------------------------------------------------------- /ipf/hash_scanner.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "hash_file.hpp" 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | namespace filter { 13 | 14 | class hash_scanner 15 | { 16 | using hash_data = std::array; 17 | 18 | bool ready_; 19 | bool enabled_; 20 | std::mutex mtx_; 21 | 22 | private: 23 | hash_scanner(); 24 | 25 | public: 26 | static hash_scanner& get() 27 | { 28 | static hash_scanner instance; 29 | return instance; 30 | } 31 | 32 | void enable(); 33 | void disable(); 34 | 35 | bool open(std::wstring const& path); 36 | bool test(UNICODE_STRING* password); 37 | 38 | private: 39 | bool find(std::array const& entry) const; 40 | bool nthash(UNICODE_STRING* input, std::array& digest) const; 41 | 42 | private: 43 | std::vector data_; 44 | }; 45 | 46 | } // namespace filter -------------------------------------------------------------------------------- /ipf/ipf.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 15.0 23 | {D55587B5-A1B3-4E09-8508-A00193764EE7} 24 | Win32Proj 25 | ipf 26 | 10.0 27 | 28 | 29 | 30 | DynamicLibrary 31 | true 32 | v142 33 | Unicode 34 | 35 | 36 | DynamicLibrary 37 | false 38 | v142 39 | true 40 | Unicode 41 | 42 | 43 | DynamicLibrary 44 | true 45 | v142 46 | Unicode 47 | 48 | 49 | DynamicLibrary 50 | false 51 | v142 52 | true 53 | Unicode 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | true 75 | 76 | 77 | true 78 | 79 | 80 | false 81 | 82 | 83 | false 84 | 85 | 86 | 87 | Level3 88 | Disabled 89 | true 90 | WIN32;_DEBUG;IPF_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 91 | true 92 | 93 | 94 | true 95 | Windows 96 | 97 | 98 | 99 | 100 | Level3 101 | Disabled 102 | true 103 | _DEBUG;IPF_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 104 | true 105 | 106 | 107 | true 108 | Windows 109 | 110 | 111 | 112 | 113 | Level3 114 | MaxSpeed 115 | true 116 | true 117 | true 118 | WIN32;NDEBUG;IPF_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 119 | true 120 | 121 | 122 | true 123 | true 124 | true 125 | Windows 126 | 127 | 128 | 129 | 130 | Level3 131 | MaxSpeed 132 | true 133 | true 134 | true 135 | NDEBUG;IPF_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 136 | true 137 | 138 | 139 | true 140 | true 141 | true 142 | Windows 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /ipf/ipf.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {ff029f9d-ee25-4665-a1eb-9a0b39f6d8f7} 6 | 7 | 8 | {f57b51b6-ebc5-4469-bcd3-05cb4318ab98} 9 | 10 | 11 | {53da7fc8-45b6-4c1f-9b37-e6263009328c} 12 | 13 | 14 | {0106ba4b-d629-46ca-9a11-4e6dcd4c5bab} 15 | 16 | 17 | 18 | 19 | logger 20 | 21 | 22 | adler 23 | 24 | 25 | blacklist 26 | 27 | 28 | leaklist 29 | 30 | 31 | leaklist 32 | 33 | 34 | 35 | 36 | logger 37 | 38 | 39 | adler 40 | 41 | 42 | blacklist 43 | 44 | 45 | 46 | leaklist 47 | 48 | 49 | leaklist 50 | 51 | 52 | -------------------------------------------------------------------------------- /ipf/ipf.vcxproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /ipf/logger.cpp: -------------------------------------------------------------------------------- 1 | #include "logger.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace filter { 9 | 10 | void logger::open(std::wstring const& path) 11 | { 12 | file_ = path; 13 | } 14 | 15 | void logger::write(std::string const& message) 16 | { 17 | std::lock_guard lg(mtx_); 18 | 19 | if (!file_.empty()) 20 | { 21 | std::ofstream f(file_, std::ios_base::out | std::ios_base::app); 22 | 23 | if (f.good()) 24 | { 25 | time_t t = std::time(nullptr); 26 | 27 | tm tt; 28 | memset(&tt, 0, sizeof(tm)); 29 | 30 | if (localtime_s(&tt, &t) == 0) 31 | f << std::put_time(&tt, "[%d-%m-%Y %T]") << message << std::endl; 32 | 33 | f.close(); 34 | } 35 | } 36 | } 37 | 38 | } // namespace filter -------------------------------------------------------------------------------- /ipf/logger.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace filter { 7 | 8 | class logger 9 | { 10 | std::mutex mtx_; 11 | std::wstring file_; 12 | 13 | private: 14 | logger() = default; 15 | 16 | public: 17 | static logger& get() 18 | { 19 | static logger instance; 20 | return instance; 21 | } 22 | 23 | void open(std::wstring const& path); 24 | void write(std::string const& message); 25 | }; 26 | 27 | } // namespace filter -------------------------------------------------------------------------------- /ipf/main.cpp: -------------------------------------------------------------------------------- 1 | #include "hash_scanner.hpp" 2 | #include "adler32.hpp" 3 | #include "blacklist.hpp" 4 | #include "logger.hpp" 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | /* 12 | Password Filter reference used throughout this project can be found here: 13 | https://docs.microsoft.com/da-dk/windows/win32/secmgmt/password-filters 14 | */ 15 | 16 | #define STATUS_SUCCESS 0x00000000 17 | 18 | static bool fIncludeLeaked = false; 19 | static wchar_t lpDirectory[256] = { 0 }; 20 | 21 | static constexpr LPCWSTR lpDirectoryPath = L"C:\\improsec-filter"; 22 | static constexpr LPCWSTR lpLogFile = L"errorlog.txt"; 23 | 24 | static constexpr LPCWSTR lpListFile1 = L"weak-phrases.txt"; 25 | static constexpr LPCWSTR lpConfFile1 = L"weak-enabled.txt"; 26 | static constexpr LPCWSTR lpListFile2 = L"leaked-passwords.bin"; 27 | static constexpr LPCWSTR lpConfFile2 = L"leaked-enabled.txt"; 28 | 29 | void HandleFilterEnabling(std::vector const& data, bool weak) 30 | { 31 | if (weak) 32 | { 33 | if (!data.empty() && data[0] == '1') 34 | filter::blacklist::get().enable(); 35 | else 36 | filter::blacklist::get().disable(); 37 | } 38 | else if (fIncludeLeaked) 39 | { 40 | if (!data.empty() && data[0] == '1') 41 | filter::hash_scanner::get().enable(); 42 | else 43 | filter::hash_scanner::get().disable(); 44 | } 45 | } 46 | 47 | bool RetrieveFileData(std::wstring const& path, std::vector& data) 48 | { 49 | HANDLE hFile = CreateFile(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); 50 | 51 | if (hFile != INVALID_HANDLE_VALUE && hFile != NULL) 52 | { 53 | DWORD dwSize = GetFileSize(hFile, NULL); // We don't expect blacklists to exceed 4 GB. 54 | DWORD dwRead = 0; 55 | 56 | if (dwSize != INVALID_FILE_SIZE && dwSize != 0) 57 | { 58 | data.resize(dwSize); 59 | 60 | while (dwRead < dwSize) 61 | { 62 | DWORD dwBytes = 0; 63 | 64 | if (!ReadFile(hFile, &data[dwRead], static_cast(data.size()) - dwRead, &dwBytes, NULL)) 65 | break; 66 | 67 | dwRead += dwBytes; 68 | } 69 | 70 | return (dwRead == dwSize); 71 | } 72 | 73 | CloseHandle(hFile); 74 | } 75 | 76 | return false; 77 | } 78 | 79 | bool CompareFileInfo(std::wstring const& directory, LPCWSTR lpFile, FILE_NOTIFY_INFORMATION* info, DWORD* adler, std::vector& data) 80 | { 81 | DWORD dwLength = static_cast(wcslen(lpFile)); 82 | 83 | if (info->FileNameLength == dwLength * sizeof(WCHAR) && 84 | _wcsnicmp(info->FileName, lpFile, dwLength) == 0) 85 | { 86 | data.clear(); 87 | 88 | if (RetrieveFileData(directory + lpFile, data)) 89 | { 90 | DWORD crc = adler32(0, &data[0], static_cast(data.size())); 91 | 92 | if (crc != *adler) 93 | { 94 | *adler = crc; 95 | return true; 96 | } 97 | } 98 | } 99 | 100 | return false; 101 | } 102 | 103 | void ValidateModification(std::wstring const& directory, FILE_NOTIFY_INFORMATION* info) 104 | { 105 | if (info == NULL) 106 | filter::logger::get().write("[warning] null-pointer given for file change information"); 107 | else 108 | { 109 | static DWORD adler_conf = 0; 110 | static DWORD adler_list = 0; 111 | 112 | do 113 | { 114 | if (info->Action == FILE_ACTION_MODIFIED) 115 | { 116 | std::vector data; 117 | 118 | /* Check if modifications were made to the blacklist file */ 119 | if (CompareFileInfo(directory, lpListFile1, info, &adler_list, data)) 120 | filter::blacklist::get().load_file(directory + lpListFile1); 121 | else if (CompareFileInfo(directory, lpListFile2, info, &adler_conf, data)) 122 | filter::hash_scanner::get().open(directory + lpListFile2); 123 | else if (CompareFileInfo(directory, lpConfFile1, info, &adler_conf, data)) 124 | HandleFilterEnabling(data, true); 125 | else if (CompareFileInfo(directory, lpConfFile2, info, &adler_conf, data)) 126 | HandleFilterEnabling(data, false); 127 | } 128 | 129 | info = reinterpret_cast(reinterpret_cast(info) + info->NextEntryOffset); 130 | } 131 | while (info->NextEntryOffset != 0); 132 | } 133 | } 134 | 135 | void MonitorThread() 136 | { 137 | /* Start monitoring changes to files in the root directory */ 138 | HANDLE hDirectory = CreateFile(lpDirectory, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); 139 | 140 | if (hDirectory == NULL || hDirectory == INVALID_HANDLE_VALUE) 141 | { 142 | do 143 | { 144 | Sleep(2000); 145 | hDirectory = CreateFile(lpDirectory, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); 146 | } while (hDirectory == NULL || hDirectory == INVALID_HANDLE_VALUE); 147 | } 148 | 149 | std::wstring directory = std::wstring(lpDirectory) + L'\\'; 150 | filter::logger::get().open(directory + lpLogFile); 151 | 152 | /* Load configuration file to check if the password filter should be active */ 153 | std::vector data; 154 | 155 | if (RetrieveFileData(directory + lpConfFile1, data)) 156 | HandleFilterEnabling(data, true); 157 | else 158 | { 159 | filter::blacklist::get().disable(); 160 | filter::logger::get().write("[warning] failed to read 'weak-enabled.txt' file - enabling might be problematic"); 161 | } 162 | 163 | if (RetrieveFileData(directory + lpConfFile2, data)) 164 | HandleFilterEnabling(data, false); 165 | else 166 | { 167 | filter::hash_scanner::get().disable(); 168 | filter::logger::get().write("[warning] failed to read 'leaked-enabled.txt' file - enabling might be problematic"); 169 | } 170 | 171 | /* Load filter list */ 172 | auto start = std::chrono::high_resolution_clock::now(); 173 | std::cout << "Loading filter list" << std::endl; 174 | 175 | try 176 | { 177 | filter::blacklist::get().load_file(directory + lpListFile1); 178 | } 179 | catch (std::exception const& e) 180 | { 181 | std::cout << "Exception: " << e.what() << std::endl; 182 | } 183 | 184 | auto end = std::chrono::high_resolution_clock::now(); 185 | auto time_span = std::chrono::duration_cast>(end - start); 186 | 187 | std::cout << "Finished in " << time_span.count() << " seconds" << std::endl; 188 | 189 | /* Load leaked list */ 190 | if (fIncludeLeaked) 191 | { 192 | start = std::chrono::high_resolution_clock::now(); 193 | std::cout << "Loading leaked list" << std::endl; 194 | 195 | try 196 | { 197 | filter::hash_scanner::get().open(directory + lpListFile2); 198 | } 199 | catch (std::exception const& e) 200 | { 201 | std::cout << "Exception: " << e.what() << std::endl; 202 | } 203 | 204 | end = std::chrono::high_resolution_clock::now(); 205 | time_span = std::chrono::duration_cast>(end - start); 206 | 207 | std::cout << "Finished in " << time_span.count() << " seconds" << std::endl; 208 | } 209 | 210 | while (true) 211 | { 212 | DWORD bytes = 0; 213 | std::vector buf(4096); 214 | 215 | if (!ReadDirectoryChangesW(hDirectory, &buf[0], static_cast(buf.size()), FALSE, FILE_NOTIFY_CHANGE_LAST_WRITE, &bytes, NULL, NULL)) 216 | filter::logger::get().write("[error] failed to read directory changes from improsec root folder"); 217 | else if (bytes > 0) 218 | ValidateModification(directory, reinterpret_cast(&buf[0])); 219 | 220 | Sleep(1000); 221 | } 222 | 223 | CloseHandle(hDirectory); 224 | } 225 | 226 | /* 227 | The InitializeChangeNotify function is implemented by a password filter DLL. This function initializes the DLL. 228 | InitializeChangeNotify is called by the Local Security Authority (LSA) to verify that the password notification DLL is loaded and initialized. 229 | */ 230 | extern "C" __declspec(dllexport) BOOLEAN NTAPI InitializeChangeNotify() 231 | { 232 | // TRUE = The password filter DLL is initialized 233 | // FALSE = The password filter DLL is not initialized 234 | 235 | return TRUE; 236 | } 237 | 238 | /* 239 | The PasswordChangeNotify function is implemented by a password filter DLL. It notifies the DLL that a password was changed. 240 | The PasswordChangeNotify function is called after the PasswordFilter function has been called successfully and the new password has been stored. 241 | */ 242 | extern "C" __declspec(dllexport) NTSTATUS NTAPI PasswordChangeNotify(UNICODE_STRING* UserName, ULONG RelativeId, UNICODE_STRING* NewPassword) 243 | { 244 | if (NewPassword != NULL && NewPassword->Buffer != NULL) 245 | SecureZeroMemory(NewPassword->Buffer, NewPassword->Length); 246 | 247 | return STATUS_SUCCESS; 248 | } 249 | 250 | /* 251 | The PasswordFilter function is implemented by a password filter DLL. 252 | The value returned by this function determines whether the new password is accepted by the system. 253 | All of the password filters installed on a system must return TRUE for the password change to take effect. 254 | Password change requests may be made when users specify a new password, accounts are created and when administrators override a password. 255 | => SetOperation = TRUE if the password was set rather than changed 256 | */ 257 | extern "C" __declspec(dllexport) BOOLEAN NTAPI PasswordFilter(UNICODE_STRING* AccountName, UNICODE_STRING* FullName, UNICODE_STRING* Password, BOOLEAN SetOperation) 258 | { 259 | BOOL fResult = filter::blacklist::get().contains(Password) ? TRUE : FALSE; 260 | 261 | if (fResult == FALSE && fIncludeLeaked) 262 | fResult = filter::hash_scanner::get().test(Password) ? TRUE : FALSE; 263 | 264 | // TRUE = The password is accepted by the filter (LSA evaluates the rest of the filter chain) 265 | // FALSE = The password is rejected by the filter (LSA returns the ERROR_ILL_FORMED_PASSWORD (1324) status code to the source of the password change request) 266 | 267 | return (fResult != TRUE); 268 | } 269 | 270 | BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpvReserved) 271 | { 272 | static HANDLE hThread = NULL; 273 | 274 | if (dwReason == DLL_PROCESS_ATTACH) 275 | { 276 | ExpandEnvironmentStrings(lpDirectoryPath, lpDirectory, sizeof(lpDirectory) / sizeof(wchar_t)); 277 | return ((hThread = CreateThread(NULL, 0, LPTHREAD_START_ROUTINE(&MonitorThread), NULL, 0, NULL)) != NULL); 278 | } 279 | else if (dwReason == DLL_PROCESS_DETACH) 280 | return TerminateThread(hThread, 0); 281 | 282 | return TRUE; 283 | } 284 | -------------------------------------------------------------------------------- /ipf_test/ipf_test.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 15.0 23 | {4BC9B4DC-BE72-457F-B089-60B654F7AD34} 24 | Win32Proj 25 | ipftest 26 | 10.0.17763.0 27 | 28 | 29 | 30 | Application 31 | true 32 | v141 33 | Unicode 34 | 35 | 36 | Application 37 | false 38 | v141 39 | true 40 | Unicode 41 | 42 | 43 | Application 44 | true 45 | v141 46 | Unicode 47 | 48 | 49 | Application 50 | false 51 | v141 52 | true 53 | Unicode 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | true 75 | 76 | 77 | true 78 | 79 | 80 | false 81 | 82 | 83 | false 84 | 85 | 86 | 87 | Level3 88 | Disabled 89 | true 90 | WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 91 | true 92 | 93 | 94 | true 95 | Console 96 | 97 | 98 | 99 | 100 | Level3 101 | Disabled 102 | true 103 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 104 | true 105 | 106 | 107 | true 108 | Console 109 | 110 | 111 | 112 | 113 | Level3 114 | MaxSpeed 115 | true 116 | true 117 | true 118 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 119 | true 120 | 121 | 122 | true 123 | true 124 | true 125 | Console 126 | 127 | 128 | 129 | 130 | Level3 131 | MaxSpeed 132 | true 133 | true 134 | true 135 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 136 | true 137 | 138 | 139 | true 140 | true 141 | true 142 | Console 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /ipf_test/ipf_test.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ipf_test/main.cpp: -------------------------------------------------------------------------------- 1 | #ifndef _CRT_SECURE_NO_WARNINGS 2 | #define _CRT_SECURE_NO_WARNINGS 3 | #endif 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | typedef BOOLEAN (WINAPI* InitializeChangeNotify_t)(); 14 | typedef BOOLEAN (WINAPI* Passwordfilter_t)(PUNICODE_STRING AccountName, PUNICODE_STRING FullName, PUNICODE_STRING Password, BOOLEAN SetOperation); 15 | 16 | void timed_event(std::string const& description, std::function functor) 17 | { 18 | auto start = std::chrono::high_resolution_clock::now(); 19 | std::cout << description << std::endl; 20 | 21 | try 22 | { 23 | functor(); 24 | } 25 | catch (std::exception const& e) 26 | { 27 | std::cout << "Exception: " << e.what() << std::endl; 28 | } 29 | 30 | auto end = std::chrono::high_resolution_clock::now(); 31 | auto time_span = std::chrono::duration_cast>(end - start); 32 | 33 | std::cout << "Finished in " << time_span.count() << " seconds" << std::endl; 34 | } 35 | 36 | #include 37 | #include 38 | #include 39 | 40 | bool nthash(std::wstring const& input, std::array& digest) 41 | { 42 | HCRYPTPROV hCryptProvider = NULL; 43 | HCRYPTHASH hCryptHash = NULL; 44 | 45 | bool result = false; 46 | 47 | if (CryptAcquireContextW(&hCryptProvider, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) 48 | { 49 | if (CryptCreateHash(hCryptProvider, CALG_MD4, 0, 0, &hCryptHash)) 50 | { 51 | if (CryptHashData(hCryptHash, reinterpret_cast(input.c_str()), input.size() * 2, 0)) 52 | { 53 | uint8_t md4_hash[16]; 54 | memset(&md4_hash[0], 0, sizeof(md4_hash)); 55 | 56 | uint32_t md4_length = sizeof(md4_hash); 57 | 58 | if (CryptGetHashParam(hCryptHash, HP_HASHVAL, &md4_hash[0], reinterpret_cast(&md4_length), 0)) 59 | { 60 | memcpy(&digest[0], md4_hash, sizeof(md4_hash)); 61 | result = true; 62 | } 63 | } 64 | 65 | CryptDestroyHash(hCryptHash); 66 | } 67 | 68 | CryptReleaseContext(hCryptProvider, 0); 69 | } 70 | 71 | return result; 72 | } 73 | 74 | int wmain(int argc, wchar_t* argv[]) 75 | { 76 | HMODULE hModule = LoadLibrary(L"ipf.dll"); 77 | 78 | if (hModule == NULL) 79 | std::cout << "Cannot load 'filter.dll'" << std::endl; 80 | else 81 | { 82 | InitializeChangeNotify_t initialize = reinterpret_cast(GetProcAddress(hModule, "InitializeChangeNotify")); 83 | 84 | if (initialize == nullptr) 85 | std::cerr << "Failed to locate initialization function" << std::endl; 86 | else if (!initialize()) 87 | std::cerr << "Failed to initialize filter" << std::endl; 88 | 89 | std::cin.get(); 90 | 91 | timed_event("Scanning for password", [&]() -> void { 92 | Passwordfilter_t filter = reinterpret_cast(GetProcAddress(hModule, "PasswordFilter")); 93 | 94 | if (filter == nullptr) 95 | std::cerr << "Failed to locate filter function" << std::endl; 96 | else 97 | { 98 | wchar_t const* v = L"fakepassword"; 99 | 100 | UNICODE_STRING p = { 0 }; 101 | p.Buffer = const_cast(v); 102 | p.Length = wcslen(v) * sizeof(WCHAR); 103 | p.MaximumLength = p.Length + sizeof(WCHAR); 104 | 105 | if (filter(NULL, NULL, &p, FALSE)) 106 | std::cout << "ALLOWED" << std::endl; 107 | else 108 | std::cout << "FILTERED" << std::endl; 109 | } 110 | }); 111 | 112 | timed_event("Scanning for password", [&]() -> void { 113 | Passwordfilter_t filter = reinterpret_cast(GetProcAddress(hModule, "PasswordFilter")); 114 | 115 | if (filter == nullptr) 116 | std::cerr << "Failed to locate filter function" << std::endl; 117 | else 118 | { 119 | wchar_t const* v = L"Pa$$w0rd"; 120 | 121 | UNICODE_STRING p = { 0 }; 122 | p.Buffer = const_cast(v); 123 | p.Length = wcslen(v) * sizeof(WCHAR); 124 | p.MaximumLength = p.Length + sizeof(WCHAR); 125 | 126 | if (filter(NULL, NULL, &p, FALSE)) 127 | std::cout << "ALLOWED" << std::endl; 128 | else 129 | std::cout << "FILTERED" << std::endl; 130 | } 131 | }); 132 | } 133 | 134 | return 0; 135 | } -------------------------------------------------------------------------------- /scripts/InstallFilter.ps1: -------------------------------------------------------------------------------- 1 | function Test-RegistryValue { 2 | param( 3 | [Parameter(Mandatory=$true)] 4 | [ValidateNotNullOrEmpty()] 5 | $Path, 6 | 7 | [Parameter(Mandatory=$true)] 8 | [ValidateNotNullOrEmpty()] 9 | $Key 10 | ) 11 | 12 | try { 13 | Get-ItemProperty -Path $Path | Select-Object -ExpandProperty $Key -ErrorAction Stop | Out-Null 14 | return $true 15 | } catch { 16 | return $false 17 | } 18 | } 19 | 20 | $path = "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" 21 | $name = "Notification Packages" 22 | $filt = "ipf" 23 | 24 | New-Item -Path "C:\" -Name "improsec-filter" -ItemType Directory -Force | Out-Null 25 | Set-Content -Path "C:\improsec-filter\weak-enabled.txt" -Value "1" -Force | Out-Null 26 | Set-Content -Path "C:\improsec-filter\leaked-enabled.txt" -Value "1" -Force | Out-Null 27 | 28 | Copy-Item -Path "$PSScriptRoot\weak-phrases.txt" -Destination "C:\improsec-filter\weak-phrases.txt" -Force 29 | Copy-Item -Path "$PSScriptRoot\$filt.dll" -Destination "C:\Windows\System32\$filt.dll" -Force 30 | 31 | if (Test-RegistryValue -Path $path -Key $name) { 32 | $values = @((Get-ItemPropertyValue -Path $path -Name $name)) 33 | 34 | if (!$values.Contains($filt)) { 35 | $values += $filt 36 | } 37 | 38 | Set-ItemProperty -Path $path -Name $name -Value $values 39 | } else { 40 | New-ItemProperty -Path $path -Name $name -Value @($filt) -PropertyType MultiString 41 | } 42 | -------------------------------------------------------------------------------- /scripts/UninstallFilter.ps1: -------------------------------------------------------------------------------- 1 | function Test-RegistryValue { 2 | param( 3 | [Parameter(Mandatory=$true)] 4 | [ValidateNotNullOrEmpty()] 5 | $Path, 6 | 7 | [Parameter(Mandatory=$true)] 8 | [ValidateNotNullOrEmpty()] 9 | $Key 10 | ) 11 | 12 | try { 13 | Get-ItemProperty -Path $Path | Select-Object -ExpandProperty $Key -ErrorAction Stop | Out-Null 14 | return $true 15 | } catch { 16 | return $false 17 | } 18 | } 19 | 20 | $path = "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" 21 | $name = "Notification Packages" 22 | $filt = "ipf" 23 | 24 | Set-Content -Path "C:\improsec-filter\weak-enabled.txt" -Value "0" -Force | Out-Null 25 | Set-Content -Path "C:\improsec-filter\leaked-enabled.txt" -Value "0" -Force | Out-Null 26 | 27 | if (Test-RegistryValue -Path $path -Key $name) { 28 | $oldval = @((Get-ItemPropertyValue -Path $path -Name $name)) 29 | $newval = @() 30 | 31 | foreach ($v in $oldval) { 32 | if ($v -ne $filt) { 33 | $newval += $v 34 | } 35 | } 36 | 37 | Set-ItemProperty -Path $path -Name $name -Value $newval 38 | } 39 | -------------------------------------------------------------------------------- /scripts/weak-phrases.txt: -------------------------------------------------------------------------------- 1 | password 2 | --------------------------------------------------------------------------------