Due to a security review at my employer, we had to beef up password complexity. Minimum 8 chars, at least two numeric, and at least upper and lower, as well as special char (special chars available on standard (?) US keyboard).
Found an article on DevX (http://www.devx.com/security/Article/21522/0) using C++ and RegEx to write your own filter. RegEx was overkill, so I decided to try using PB and here is the result.
You'll notice that I address the three strings passed by AD, using different addressing techniques - they all seem to work!!! Figuring out the "UNICODE_STRING" structure, and how to work with it, took the longest amount of time.
Two registry entries control filtering and logging. Using the registry entry to enable/disable filtering is much faster than removing the DLL and rebooting your Domain Controller!!!
The Datasection at the end contains disallowed strings - some of them are obviously to filter swearwords - just add to the list however many you like.
Thanks to several PB users for their code, specifically, Luis for his Logging code, and pcfreak for his wildcard matching stuff.
Code: Select all
; Active Directory Password Filter
; April 2010
EnableExplicit
Global AccountName$, FullName$, Password$, c$, i
Global CntLowerCase, CntUpperCase, CntNumeric, CntSpecial
Global *p.String, PwdLen, NoFilter = 0, NoLog = 0
#WildCardsStringMatchEscapeCharacter = '/'
Declare.l EvalWildCardsStringMatch(string.s,wildcards.s,flags.l)
Declare.l WildCardsStringMatch(string.s,wildcards.s,flag.l)
Structure UNICODE_STRING
Length.w
MaximumLength.w
Buffer.s
EndStructure
#MinPwdLen = 8
#OffsetOfLength=OffsetOf(UNICODE_STRING\Length)
#OffsetOfMaximumLength=OffsetOf(UNICODE_STRING\MaximumLength)
#OffsetOfBuffer=OffsetOf(UNICODE_STRING\Buffer)
Define SizeOfChar=SizeOf(Character)
#L2F_ENABLE=1
IncludeFile "C:\Program Files\PureBasic\INCLUDES\PBLogger.pbi"
ProcedureDLL AttachProcess(Instance)
L2F_SET_LOGFILE("PwdDLL.log")
NoFilter = Val(RegGetValue("HKEY_LOCAL_MACHINE\SOFTWARE\FTC Active Directory Password Filter","Filter","."))
NoLog = Val(RegGetValue("HKEY_LOCAL_MACHINE\SOFTWARE\FTC Active Directory Password Filter","Filter","."))
If Not NoLog
L2F("AttachProcess "+Hex(Instance))
EndIf
Global NewList pattern.s()
Restore Patterns
Protected NextPattern$
Read.s NextPattern$
While NextPattern$ <> "__END__"
AddElement(pattern())
PrintN("Read pattern: "+NextPattern$)
pattern() = NextPattern$
Read.s NextPattern$
Wend
Global PatternCnt = ListSize(pattern()) - 1
ProcedureReturn #True
EndProcedure
ProcedureDLL PasswordFilter(*AccountName.UNICODE_STRING, *FullName.UNICODE_STRING, *Password.UNICODE_STRING, SetOperation)
NoFilter = Val(RegGetValue("HKEY_LOCAL_MACHINE\SOFTWARE\FTC Active Directory Password Filter","Filter","."))
NoLog = Val(RegGetValue("HKEY_LOCAL_MACHINE\SOFTWARE\FTC Active Directory Password Filter","Filter","."))
Protected *p.String
Protected rc = #True ; true by default
If Not NoLog
L2F("Entering PasswordFilter")
EndIf
If NoFilter
If Not NoLog
L2F("NoFilter selected")
EndIf
Goto ExitPasswordFilter
EndIf
Protected PasswordLength.w = PeekW(*Password+#OffsetOfLength)
Protected PasswordMaximumLength.w = PeekW(*Password+#OffsetOfMaximumLength)
Password$ = PeekS(@*Password\Buffer, PasswordLength/2, #PB_Unicode)
If Not NoLog
L2F("Password$: "+Password$)
EndIf
If (PasswordLength/2) < #MinPwdLen
rc = #False
If Not NoLog
L2F("Invalid: PasswordLength < #MinPwdLen, leaving PasswordFilter" + #CRLF$)
EndIf
Goto ExitPasswordFilter
EndIf
Protected AccountNameLength.w = PeekW(@*AccountName\Length)
Protected AccountNameMaximumLength.w = PeekW(@*AccountName\MaximumLength)
AccountName$ = PeekS(@*AccountName\Buffer, AccountNameLength/2, #PB_Unicode)
If Not NoLog
L2F("AccountName$: "+AccountName$)
EndIf
Protected FullNameLength.w = PeekW(@*FullName\Length)
Protected FullNameMaximumLength.w = PeekW(@*FullName\MaximumLength)
FullName$ = PeekS(@*FullName\Buffer, FullNameLength/2, #PB_Unicode)
If Not NoLog
L2F("FullName$: "+FullName$)
EndIf
CntLowerCase = 0
CntUpperCase = 0
CntNumeric = 0
CntSpecial = 0
*p = @Password$
PwdLen=Len(Password$)
Define c.s
For i = 0 To (PwdLen*2)-1 Step 2
c = PeekS(*p+i, 1, #PB_Unicode)
PrintN("c: " + c + ", chr(" + Str(Asc(c)) + ")")
Debug ("c: " + c + ", chr(" + Str(Asc(c)) + ")")
If c >= "a" And c <="z"
CntLowerCase + 1
EndIf
If c >= "A" And c <="Z"
CntUpperCase + 1
EndIf
If c >= "0" And c <="9"
CntNumeric + 1
EndIf
; Special chars:
; $21 to $2e = !"#$%&'()*+,-./ (15 chars)
; $3a to $40 = :;<=>?@ (7 chars)
; $5b to $60 = [\]^_` (6 chars)
; $7b to $7e = {|}~ (4 chars)
; Total = 32 chars
If c >= "!" And c <="/"
CntSpecial + 1
EndIf
If c >= ":" And c <="@"
CntSpecial + 1
EndIf
If c >= "[" And c <="`"
CntSpecial + 1
EndIf
If c >= "{" And c <="~"
CntSpecial + 1
EndIf
Next i
L2F("Total lowercase: " + Str(CntLowerCase))
L2F("Total uppercase: " + Str(CntUpperCase))
L2F("Total digits: " + Str(CntNumeric))
L2F("Total special: " + Str(CntSpecial))
; Check if password is valid before checking forbidden strings
If (CntNumeric) < 2
If Not NoLog
L2F ("Invalid password, not enough numeric chars")
EndIf
rc = #False
Goto ExitPasswordFilter
EndIf
If Not (CntLowerCase And CntUpperCase And CntNumeric And CntSpecial)
If Not NoLog
L2F ("Invalid password, did not meet mix requirements")
EndIf
rc = #False
Goto ExitPasswordFilter
EndIf
Define Match
; remove '*' and '?', replace with '.' (just for patternmatching)
Define s$ = Password$
ReplaceString(s$, "*", ".", #PB_String_InPlace)
ReplaceString(s$, "?", ".", #PB_String_InPlace)
FirstElement(pattern())
FirstElement(pattern())
MeasureHiResIntervalStart()
For i = 0 To PatternCnt
Match = EvalWildCardsStringMatch(s$, pattern(), 0)
If Match
If Not NoLog
L2F ("Method2: Invalid password, contains '"+pattern()+"'")
EndIf
rc = #False
Goto ExitPasswordFilter
EndIf
NextElement(pattern())
Next i
If Not NoLog
L2F("Method2: " + StrF(MeasureHiResIntervalStop()) + "s ")
EndIf
; Zero memory before returning
FillMemory(@AccountName$,Len(AccountName$))
FillMemory(@FullName$,Len(FullName$))
FillMemory(@Password$,Len(Password$))
FillMemory(@s$,Len(s$))
If Not NoLog
L2F("Leaving PasswordFilter" + #CRLF$)
EndIf
ExitPasswordFilter:
ProcedureReturn rc
EndProcedure
ProcedureDLL InitializeChangeNotify()
NoFilter = Val(RegGetValue("HKEY_LOCAL_MACHINE\SOFTWARE\FTC Active Directory Password Filter","Filter","."))
NoLog = Val(RegGetValue("HKEY_LOCAL_MACHINE\SOFTWARE\FTC Active Directory Password Filter","Filter","."))
If Not NoLog
L2F("InitializeChangeNotify called")
EndIf
ProcedureReturn #True
EndProcedure
ProcedureDLL PasswordChangeNotify(*UserName.UNICODE_STRING, RelativeId, *NewPassword.UNICODE_STRING)
NoFilter = Val(RegGetValue("HKEY_LOCAL_MACHINE\SOFTWARE\FTC Active Directory Password Filter","Filter","."))
NoLog = Val(RegGetValue("HKEY_LOCAL_MACHINE\SOFTWARE\FTC Active Directory Password Filter","Filter","."))
If Not NoLog
L2F("PasswordChangeNotify called")
EndIf
ProcedureReturn #True
EndProcedure
ProcedureDLL DetachProcess(Instance)
If Not NoLog
L2F("DetachProcess "+Hex(Instance))
EndIf
ProcedureReturn #True
EndProcedure
ProcedureDLL AttachThread(Instance)
If Not NoLog
L2F("AttachThread "+Hex(Instance))
EndIf
ProcedureReturn #True
EndProcedure
ProcedureDLL DetachThread(Instance)
If Not NoLog
L2F("DetachThread "+Hex(Instance))
EndIf
ProcedureReturn #True
EndProcedure
Procedure.l EvalWildCardsStringMatch(string.s,wildcards.s,flags.l)
Protected *char.CHARACTER=@wildcards
Protected lastChar.c=0
Protected Part$=""
Protected Result$=""
Protected Negate=#False
While *char\c<>0
Select *char\c
Case '|'
Select lastChar
Case #WildCardsStringMatchEscapeCharacter
Part$+Chr(*char\c)
Default
Result$+Str(WildCardsStringMatch(string,Part$,flags) ! Negate)+"|"
Part$=""
Negate=#False
EndSelect
lastChar=0
Case '&'
Select lastChar
Case #WildCardsStringMatchEscapeCharacter
Part$+Chr(*char\c)
Default
Result$+Str(WildCardsStringMatch(string,Part$,flags) ! Negate)+"&"
Part$=""
Negate=#False
EndSelect
lastChar=0
Case '~'
Select lastChar
Case #WildCardsStringMatchEscapeCharacter
Part$+Chr(*char\c)
Default
If Part$="" And Negate=#False
Negate=#True
Else
Part$+Chr(*char\c)
EndIf
EndSelect
lastChar=0
Case #WildCardsStringMatchEscapeCharacter
Select lastChar
Case #WildCardsStringMatchEscapeCharacter
Part$+Chr(*char\c)
lastChar=0
Default
lastChar=#WildCardsStringMatchEscapeCharacter
EndSelect
Default
If lastChar=0
Part$+Chr(*char\c)
Else
Part$+Chr(lastChar)+Chr(*char\c)
lastChar=0
EndIf
EndSelect
*char+1+#PB_Compiler_Unicode
Wend
Result$+Str(WildCardsStringMatch(string,Part$,flags) ! Negate)
If Len(Result$)>1
DataSection
!@@wcsm_eval_ORs:
Data.l 0
!@@wcsm_eval_ANDs:
Data.l 0
EndDataSection
!MOV esi, dword [p.v_Result$]
CompilerIf #PB_Compiler_Unicode
!MOVZX ebx, word [esi]
CompilerElse
!MOVZX ebx, byte [esi]
CompilerEndIf
!CMP ebx, 0
!JZ @@wcsm_eval_wend
!@@wcsm_eval_while:
!CMP ebx, '|'
!JNE @@wcsm_eval_case2
!@@wcsm_eval_case1:;case '|'
!MOV ecx, dword [@@wcsm_eval_ORs]
!JECXZ @@wcsm_eval_subwend1
!@@wcsm_eval_subwhile1:
!POP eax
!POP edx
!OR eax, edx
!PUSH eax
!DEC ecx
!JNZ @@wcsm_eval_subwhile1
!@@wcsm_eval_subwend1:
!INC ecx
!MOV dword [@@wcsm_eval_ORs], ecx
!JMP @@wcsm_eval_endselect
!@@wcsm_eval_case2:;case '&'
!CMP ebx, '&'
!JNE @@wcsm_eval_default
!INC dword [@@wcsm_eval_ANDs]
!JMP @@wcsm_eval_endselect
!@@wcsm_eval_default:;default
!SUB ebx, 30h
!PUSH ebx
!MOV ecx, dword [@@wcsm_eval_ANDs]
!JECXZ @@wcsm_eval_subwend2
!@@wcsm_eval_subwhile2:
!POP eax
!POP edx
!AND eax, edx
!PUSH eax
!DEC ecx
!JNZ @@wcsm_eval_subwhile2
!@@wcsm_eval_subwend2:
!MOV dword [@@wcsm_eval_ANDs], ecx
!@@wcsm_eval_endselect:
CompilerIf #PB_Compiler_Unicode
!INC esi
!INC esi
!MOVZX ebx, word [esi]
CompilerElse
!INC esi
!MOVZX ebx, byte [esi]
CompilerEndIf
!CMP ebx, 0
!JNZ @@wcsm_eval_while
!@@wcsm_eval_wend:
!MOV ecx, dword [@@wcsm_eval_ORs]
!JECXZ @@wcsm_eval_subwend3
!@@wcsm_eval_subwhile3:
!POP eax
!POP edx
!OR eax, edx
!PUSH eax
!DEC ecx
!JNZ @@wcsm_eval_subwhile3
!@@wcsm_eval_subwend3:
!MOV dword [@@wcsm_eval_ORs], ecx
!POP eax
ProcedureReturn
Else
ProcedureReturn Val(Result$)
EndIf
EndProcedure
Procedure.l WildCardsStringMatch(string.s,wildcards.s,flag.l)
If flag=0
string=LCase(string)
wildcards=LCase(wildcards)
EndIf
If wildcards=""
ProcedureReturn #True
EndIf
If Left(wildcards,1)<>"*" And Left(wildcards,1)<>"?" And Left(wildcards,1)<>"#" And Right(wildcards,1)<>"*" And Right(wildcards,1)<>"?" And Right(wildcards,1)<>"#" : wildcards="*"+wildcards+"*" : EndIf
Protected *Wide1.CHARACTER
Protected *Wide2.CHARACTER
Protected *pos.CHARACTER=@string
Protected *char.CHARACTER=@wildcards
Protected *sPos.CHARACTER
CompilerIf #PB_Compiler_Unicode
Macro UnicodeNumberWildCardCompare(var)
((var>='0' And var<='9') Or (var>=$FF10 And var<=$FF19) Or (var>=$00BC And var<=$00BE) Or (var>=2153 And var<=$2182) Or (var>=$2070 And var<=$2079) Or (var>=$2080 And var<=$2089) Or (var>=$2460 And var<=$249B) Or (var>=$2776 And var<=$2793) Or (var>=$3220 And var<=$3229) Or (var>=$3280 And var<=$3289))
EndMacro
CompilerElse
Macro UnicodeNumberWildCardCompare(var)
((var>='0' And var<='9') Or var=$B9 Or var=$B2 Or var=$B3 Or (var>=$BC And var<=$BE))
EndMacro
CompilerEndIf
Repeat
Select *char\c
Case '*'
*Wide1=*pos
*Wide2=*char+1+#PB_Compiler_Unicode
If *Wide2\c=0 Or (*Wide1\c=0 And RemoveString(wildcards,"*")="")
ProcedureReturn #True
EndIf
If *Wide1\c=0
ProcedureReturn #False
EndIf
*sPos=*char+1+#PB_Compiler_Unicode
While *sPos\c='*'
*char=*sPos
*sPos+1+#PB_Compiler_Unicode
Wend
If *sPos\c=*pos\c Or *sPos\c='?' Or (*sPos\c='#' And UnicodeNumberWildCardCompare(*pos\c))
*Wide2=*sPos
While *Wide2\c<>'*' And *Wide2\c<>0
If *Wide1\c=*Wide2\c Or *Wide2\c='?' Or (*Wide2\c='#' And UnicodeNumberWildCardCompare(*Wide1\c))
*Wide1+1+#PB_Compiler_Unicode
*Wide2+1+#PB_Compiler_Unicode
Else
If *sPos\c=*Wide1\c
*Wide2=*char+1+#PB_Compiler_Unicode
Else
*Wide1+1+#PB_Compiler_Unicode
*Wide2=*sPos
EndIf
EndIf
If *Wide1\c=0
While *Wide2\c='*'
*Wide2+1+#PB_Compiler_Unicode
Wend
If *Wide2\c=0
ProcedureReturn #True
Else
ProcedureReturn #False
EndIf
EndIf
Wend
If *Wide2\c='*'
*pos=*Wide1
*sPos=*Wide2
Else
If *Wide1\c=*Wide2\c And *Wide2\c=0
ProcedureReturn #True
Else
If *Wide2\c=0 And *sPos\c<>'*'
If *Wide1\c=0
ProcedureReturn #False
Else
*pos+1+#PB_Compiler_Unicode
*sPos=*char
EndIf
Else
*pos=*Wide1
EndIf
EndIf
EndIf
*char=*sPos
Else
*pos+1+#PB_Compiler_Unicode
EndIf
Case '?'
If *pos\c<>0
*pos+1+#PB_Compiler_Unicode
*char+1+#PB_Compiler_Unicode
If *pos\c<>0 And *char\c=0
ProcedureReturn #False
EndIf
Else
ProcedureReturn #False
EndIf
Case '#'
If UnicodeNumberWildCardCompare(*pos\c)
*pos+1+#PB_Compiler_Unicode
*char+1+#PB_Compiler_Unicode
If *pos\c<>0 And *char\c=0
ProcedureReturn #False
EndIf
Else
ProcedureReturn #False
EndIf
Default
If *pos\c=*char\c
*pos+1+#PB_Compiler_Unicode
*char+1+#PB_Compiler_Unicode
Else
ProcedureReturn #False
EndIf
EndSelect
Until *char\c=0
ProcedureReturn #True
EndProcedure
DataSection
Patterns:
Data.s _
"*00*" _
,"*11*" _
,"*123*" _
,"*22*" _
,"*234*" _
,"*33*" _
,"*345*" _
,"*44*" _
,"*456*" _
,"*55*" _
,"*567*" _
,"*66*" _
,"*678*" _
,"*77*" _
,"*789*" _
,"*88*" _
,"*890*" _
,"*99*" _
,"*arse*" _
,"*asdfgh*" _
,"*aug*" _
,"*cu?t*" _
,"*dec*" _
,"*feb*" _
,"*fri*" _
,"*frm*" _
,"*fu?k*" _
,"*jan*" _
,"*jul*" _
,"*jun*" _
,"*mar*" _
,"*may*" _
,"*mon*" _
,"*nov*" _
,"*qwerty*" _
,"*sat*" _
,"*sep*" _
,"*sh?t*" _
,"*sun*" _
,"*thu*" _
,"*tue*" _
,"*wed*" _
,"__END__"
EndDataSection
I have yet to write a GUI to control the two registry entries - one to turn on/off logging, and the other to enable/disable the filter itself. I may update this post with it (if I ever get around to writing it!).
One immediate piece of (testing) code I need to write, is something that will bombard the DC with password changes, and stress test everything. I thought of using the Conficker worm, but thought better of it

Hope someone finds this useful.
Almost forgot: uses registry functions from Droopy's Lib, and compiled with 4.41.