diff --git a/.gitignore b/.gitignore index 3e759b7..84164ab 100644 --- a/.gitignore +++ b/.gitignore @@ -328,3 +328,9 @@ ASALocalRun/ # MFractors (Xamarin productivity tool) working folder .mfractor/ +AaronLocker/MergeRules-Dynamic/*.xml +AaronLocker/Outputs/*.xml +AaronLocker/ScanResults/*.xml +AaronLocker/ScanResults/*.txt + +.vscode/launch.json diff --git a/AaronLocker/Create-Policies.ps1 b/AaronLocker/Create-Policies.ps1 index 92753e2..c33dcae 100644 --- a/AaronLocker/Create-Policies.ps1 +++ b/AaronLocker/Create-Policies.ps1 @@ -414,44 +414,82 @@ $csvExeBlacklistData | foreach { # Remove the placeholder element $xExcepts.RemoveChild($xPlaceholder) | Out-Null -Write-Host "Processing additional safe paths to whitelist..." -ForegroundColor Cyan -# Get additional whitelisted paths from the script that produces that list and incorporate them into the document +Write-Host "Processing safe paths to whitelist..." -ForegroundColor Cyan +# Get whitelisted paths from the script that produces that list and incorporate them into the document $PathsToAllow = (& $ps1_GetSafePathsToAllow) -# Add "allow" for Everyone for Exe, Dll, and Script rules -$xRuleCollections = $xDocument.SelectNodes("//RuleCollection[@Type='Exe' or @Type='Script' or @Type='Dll']") -foreach($xRuleCollection in $xRuleCollections) -{ - $PathsToAllow | foreach { - # If path is an existing directory and doesn't have trailing "\*" appended, fix it so that it does. - # If path is a file, don't append \*. If the path ends with \*, no need for further validation. - # If it doesn't end with \* but Get-Item can't identify it as a file or a directory, write a warning and accept it as is. - $pathToAllow = $_ - if (!$pathToAllow.EndsWith("\*")) - { - $pathItem = Get-Item $pathToAllow -Force -ErrorAction SilentlyContinue - if ($pathItem -eq $null) - { - Write-Warning "Cannot verify path $pathItem; adding to rule set as is." - } - elseif ($pathItem -is [System.IO.DirectoryInfo]) - { - Write-Warning "Appending `"\*`" to rule for $pathToAllow" - $pathToAllow = [System.IO.Path]::Combine($pathToAllow, "*") - } + +# Pattern that can be replaced by %LOCALAPPDATA% +$LocalAppDataPattern = "^(%OSDRIVE%|C:)\\Users\\[^\\]*\\AppData\\Local\\" +# Pattern that can be replaced by %APPDATA% +$RoamingAppDataPattern = "^(%OSDRIVE%|C:)\\Users\\[^\\]*\\AppData\\Roaming\\" +# Pattern that can be replaced by %USERPROFILE% (after the above already done) +$UserProfilePattern = "^(%OSDRIVE%|C:)\\Users\\[^\\]*\\" +# Pattern that can be replaced by %WINDIR% +$WinDirPattern = "^(%OSDRIVE%|C:)\\Windows\\" +# Pattern that can be replaced by %SYSTEM32% +$System32Pattern = "^(%OSDRIVE%\\Windows|C:\\Windows|%WINDIR%)\\System32\\" +# Pattern that can be replaced by %PROGRAMFILES% +$ProgramFilesPattern = "^(%OSDRIVE%|C:)\\Program Files(| \(x86\))\\" +# Pattern that can be replaced by %OSDRIVE% +$OSDrivePattern = "^C:\\" + +$PathsToAllow | ForEach-Object { + + if (-not $_.Label) { + # Each hashtable must have a label. + Write-Error -Message ("Invalid syntax in $ps1_GetSafePathsToAllow. No `"Label`" specified.") + } + if (-not $_.Path) { + # Each hashtable must have a path + Write-Error -Message ("Invalid syntax in $ps1_GetSafePathsToAllow. No `"Path`" specified.") + } + + $GenericPath = (((((($_.Path ` + -ireplace $LocalAppDataPattern,"%LOCALAPPDATA%\") ` + -ireplace $RoamingAppDataPattern, "%APPDATA%\") ` + -ireplace $UserProfilePattern, "%USERPROFILE%\") ` + -ireplace $System32Pattern, "%SYSTEM32%\") ` + -ireplace $WinDirPattern, "%WINDIR%\") ` + -ireplace $ProgramFilesPattern, "%PROGRAMFILES%\") ` + -ireplace $OSDrivePattern, "%OSDRIVE%\" + + $RulePath = (($GenericPath ` + -replace "^%LOCALAPPDATA%\\","%OSDRIVE%\Users\*\AppData\Local\") ` + -replace "^%APPDATA%\\","%OSDRIVE%\Users\*\AppData\Roaming\") ` + -replace "^%USERPROFILE%\\","%OSDRIVE%\Users\*\" + + $RuleName = "{0}: Path rule for {1}" -f $_.Label, $GenericPath + Write-Host ("`t" + $RuleName) -ForegroundColor Cyan + + $elemRule = $xDocument.CreateElement("FilePathRule") + $elemRule.SetAttribute("Action", "Allow") + $elemRule.SetAttribute("UserOrGroupSid", "S-1-1-0") + $elemRule.SetAttribute("Id", [GUID]::NewGuid().Guid) + $elemRule.SetAttribute("Name", $RuleName) + $elemRule.SetAttribute("Description", "Allows Everyone to execute from " + $RulePath) + $elemConditions = $xDocument.CreateElement("Conditions") + $elemCondition = $xDocument.CreateElement("FilePathCondition") + $elemCondition.SetAttribute("Path", $RulePath) + $elemConditions.AppendChild($elemCondition) | Out-Null + $elemRule.AppendChild($elemConditions) | Out-Null + + if ($_.RuleCollection) { + $CollectionNode = $xDocument.SelectSingleNode("//RuleCollection[@Type='$($_.RuleCollection)']") + if ($CollectionNode -eq $null) { + Write-Warning ("Couldn't find RuleCollection Type = " + $_.RuleCollection + " (RuleCollection is case-sensitive)") + } else { + $elemRule.Id = [string]([GUID]::NewGuid().Guid) + $CollectionNode.AppendChild($elemRule) | Out-Null + } + } else { + # Add to Exe, Dll, and Script rules + $xDocument.SelectNodes("//RuleCollection[@Type='Exe' or @Type='Script' or @Type='Dll']") | ForEach-Object { + $elemRuleCloned = $elemRule.CloneNode($true) + $elemRuleCloned.Id = [string]([GUID]::NewGuid().Guid) + $_.AppendChild($elemRuleCloned) | Out-Null } - $elemRule = $xDocument.CreateElement("FilePathRule") - $elemRule.SetAttribute("Action", "Allow") - $elemRule.SetAttribute("UserOrGroupSid", "S-1-1-0") - $elemRule.SetAttribute("Id", [GUID]::NewGuid().Guid) - $elemRule.SetAttribute("Name", "Additional allowed path: " + $pathToAllow) - $elemRule.SetAttribute("Description", "Allows Everyone to execute from " + $pathToAllow) - $elemConditions = $xDocument.CreateElement("Conditions") - $elemCondition = $xDocument.CreateElement("FilePathCondition") - $elemCondition.SetAttribute("Path", $pathToAllow) - $elemConditions.AppendChild($elemCondition) | Out-Null - $elemRule.AppendChild($elemConditions) | Out-Null - $xRuleCollection.AppendChild($elemRule) | Out-Null } + } # Incorporate path-exception rules for the user-writable directories under %windir% @@ -800,6 +838,8 @@ else if ($null -ne $_.noRecurse) { $recurse = !$_.noRecurse } $enforceMinFileVersion = $true if ($null -ne $_.enforceMinVersion) { $enforceMinFileVersion = $_.enforceMinVersion } + $customUserOrGroupSid = "S-1-1-0" + if ($null -ne $_.customUserOrGroupSid) { $customUserOrGroupSid = $_.customUserOrGroupSid } $outfile = [System.IO.Path]::Combine($mergeRulesDynamicDir, $label + " Rules.xml") # If it already exists, create a name that doesn't exist yet $ixOutfile = [int]2 @@ -809,7 +849,7 @@ else $ixOutfile++ } Write-Host ("Scanning $label`:", $paths) -Separator "`n`t" -ForegroundColor Cyan - & $ps1_BuildRulesForFilesInWritableDirectories -FileSystemPaths $paths -RecurseDirectories: $recurse -EnforceMinimumVersion: $enforceMinFileVersion -RuleNamePrefix $label -OutputFileName $outfile + & $ps1_BuildRulesForFilesInWritableDirectories -FileSystemPaths $paths -RecurseDirectories: $recurse -EnforceMinimumVersion: $enforceMinFileVersion -CustomUserOrGroupSid: $customUserOrGroupSid -RuleNamePrefix $label -OutputFileName $outfile } } diff --git a/AaronLocker/CustomizationInputs/GetSafePathsToAllow.ps1 b/AaronLocker/CustomizationInputs/GetSafePathsToAllow.ps1 index c011375..303f320 100644 --- a/AaronLocker/CustomizationInputs/GetSafePathsToAllow.ps1 +++ b/AaronLocker/CustomizationInputs/GetSafePathsToAllow.ps1 @@ -1,26 +1,60 @@ <# .SYNOPSIS -Customizable script used by Create-Policies.ps1 that produces a list of additional "safe" paths to allow for non-admin execution. +Customizable script used by Create-Policies.ps1 that produces a list of "safe" +paths to allow for non-admin execution. .DESCRIPTION -This script outputs a simple list of directories that can be considered "safe" for non-admins to execute programs from. -The list is consumed by Create-Policies.ps1, which incorporates the paths into AppLocker rules allowing execution of -EXE, DLL, and Script files. -NOTE: DIRECTORY/FILE PATHS IDENTIFIED IN THIS SCRIPT MUST NOT BE WRITABLE BY NON-ADMIN USERS!!! +This script outputs zero or more hashtables containing information to define path rules +for files of directories that can be considered "safe" for non-admins. + +The list is consumed by Create-Policies.ps1, which incorporates the paths +into AppLocker rules allowing execution of Exe, Dll, and/or Script files. + +NOTE: DIRECTORY/FILE PATHS IDENTIFIED IN THIS SCRIPT SHOULD NOT BE WRITABLE +BY NON-ADMIN USERS!!! You can edit this file as needed for your environment. -Note that each directory name must be followed by \*, as in these examples: - "C:\ProgramData\App-V\*" - "\\MYSERVER\Apps\*" -Individual files can be allowed by path, also. Do not end those with "\*" +The hashtables need to have the following required properties: +* Label: String is incorporated into the rule name and description +* Path: Path of file or directory to be whitelisted. Directories path always + need to end with "\*"", e.g.: "C:\ProgramData\App-V\*", "\\MYSERVER\Apps\*". Individual + files should not end with "\*". + +The following properties are optional: +* RuleCollection: to apply the trust only within a single RuleCollection. +RuleCollection must be one of "Exe", "Dll", "Script", or "Msi", and it is CASE-SENSITIVE. + +Specify paths using only fixed local drive letters or UNC paths. Do not use +mapped drive letters or SUBST drive letters, as the user can change their +definitions. If X: is mapped to the read-only \\MYSERVER\Apps file share, +and you allow execution in \\MYSERVER\Apps\*, the user can run MyProgram.exe +in that share whether it is referenced as \\MYSERVER\Apps\MyProgram.exe or +as X:\MyProgram.exe. Similarly, AppLocker does the right thing with +SUBSTed drive letters. + +Paths may contain all supported AppLocker path variables for well-known +directories in Windows such as %WINDIR%, %SYSTEM32%, %OSDRIVE%, %PROGRAMFILES%, +%REMOVABLE% and %HOT%. -Specify paths using only fixed local drive letters or UNC paths. Do not use mapped drive letters or -SUBST drive letters, as the user can change their definitions. If X: is mapped to the read-only -\\MYSERVER\Apps file share, and you allow execution in \\MYSERVER\Apps\*, the user can run MyProgram.exe -in that share whether it is referenced as \\MYSERVER\Apps\MyProgram.exe or as X:\MyProgram.exe. Similarly, -AppLocker does the right thing with SUBSTed drive letters. +See also https://docs.microsoft.com/en-us/windows/security/threat-protection/windows-defender-application-control/applocker/understanding-the-path-rule-condition-in-applocker -TODO: At some point, reimplement with hashtable output supporting "label" and "RuleCollection" properties so that path rules have more descriptive names, and can be applied to specific rule collections> +Besides the known AppLocker path variables, the following special path +variables %USERPROFILE%, %LOCALAPPDATA% and %APPDATA% are also supported and +will substituted to "%OSDRIVE%\Users\*", "%OSDRIVE%\Users\*\AppData\Local" +and "%OSDRIVE%\Users\*\AppData\Roaming" resp. + +Examples: + +@{ + Label = 'Anaconda'; + Path = '%LOCALAPPDATA%\Continuum\Anaconda3\PKGS\*'; + RuleCollection = 'Exe' +} + +@{ + Label = 'App-V'; + Path = '%OSDRIVE%\App-V\*'; +} #> @@ -35,13 +69,26 @@ if ($null -ne $cs) if ($cs.PartOfDomain) { $computerDomain = $cs.Domain - "\\$computerDomain\netlogon\*" - "\\$computerDomain\sysvol\*" + @{ + Label = "DC Shares"; + Path = "\\$computerDomain\netlogon\*"; + } + @{ + Label = "DC Shares"; + Path = "\\$computerDomain\sysvol\*"; + } + $userDomain = $env:USERDNSDOMAIN if ($null -ne $userDomain -and $userDomain.ToUpper() -ne $computerDomain.ToUpper()) { - "\\$userDomain\netlogon\*" - "\\$userDomain\sysvol\*" + @{ + Label = "DC Shares"; + Path = "\\$userDomain\netlogon\*"; + } + @{ + Label = "DC Shares"; + Path = "\\$userDomain\sysvol\*"; + } } } else @@ -51,7 +98,13 @@ if ($null -ne $cs) } ### Windows Defender put their binaries in ProgramData for a while. Comment this back out when they move it back. -"%OSDRIVE%\PROGRAMDATA\MICROSOFT\WINDOWS DEFENDER\PLATFORM\*" +@{ + Label = "Windows Defender"; + Path = "%OSDRIVE%\PROGRAMDATA\MICROSOFT\WINDOWS DEFENDER\PLATFORM\*"; +} -# Windows upgrade -'C:\$WINDOWS.~BT\Sources\*' +# Windows upgrade sources +@{ + Label = "Windows Upgrade"; + Path = '%OSDRIVE%\$WINDOWS.~BT\Sources\*'; +} diff --git a/AaronLocker/CustomizationInputs/UnsafePathsToBuildRulesFor.ps1 b/AaronLocker/CustomizationInputs/UnsafePathsToBuildRulesFor.ps1 index b082404..531982e 100644 --- a/AaronLocker/CustomizationInputs/UnsafePathsToBuildRulesFor.ps1 +++ b/AaronLocker/CustomizationInputs/UnsafePathsToBuildRulesFor.ps1 @@ -48,6 +48,7 @@ Examples of valid hash tables: label = "OneDrive"; paths = "$env:LOCALAPPDATA\Microsoft\OneDrive"; enforceMinVersion = $false; +customUserOrGroupSid = "S-1-5-32-547" } diff --git a/AaronLocker/Support/BuildRulesForFilesInWritableDirectories.ps1 b/AaronLocker/Support/BuildRulesForFilesInWritableDirectories.ps1 index 3f44007..d2d5e2d 100644 --- a/AaronLocker/Support/BuildRulesForFilesInWritableDirectories.ps1 +++ b/AaronLocker/Support/BuildRulesForFilesInWritableDirectories.ps1 @@ -76,6 +76,11 @@ param( [switch] $EnforceMinimumVersion, + # Optional prefix incorporated into each rule name + [parameter(Mandatory=$false)] + [String] + $CustomUserOrGroupSid, + # Name of output file [parameter(Mandatory=$true)] [String] @@ -84,7 +89,7 @@ param( # Optional prefix incorporated into each rule name [parameter(Mandatory=$false)] [String] - $RuleNamePrefix + $RuleNamePrefix ) #################################################################################################### @@ -152,13 +157,23 @@ foreach($fsp in $FileSystemPaths) # Get-AppLockerFileInformation -Directory inspects files with these extensions: # .com, .exe, .dll, .ocx, .msi, .msp, .mst, .bat, .cmd, .js, .ps1, .vbs, .appx # But this script drops .msi, .msp, .mst, and .appx + [array]$scanFileTypes = @('*.bat','*.com','*.exe','*.dll','*.ocx','*.js','*.ps1','*.pyd','*.vbs','*.xll') + if ($RecurseDirectories) { - $arrALFI += Get-AppLockerFileInformation -FileType Exe,Dll,Script -Directory $fsp -Recurse + $files += Get-ChildItem * -Path $fsp -File -Force -Recurse -Include $scanFileTypes + for ($i = 0; $i -lt $files.Count; $i++) + { + $arrALFI += Get-AppLockerFileInformation -Path $files[$i].FullName + } } else { - $arrALFI += Get-AppLockerFileInformation -FileType Exe,Dll,Script -Directory $fsp + $files += Get-ChildItem * -Path $fsp -File -Force -Include $scanFileTypes + for ($i = 0; $i -lt $files.Count; $i++) + { + $arrALFI += Get-AppLockerFileInformation -Path $files[$i].FullName + } } } elseif ($fspInfo -is [System.IO.FileInfo]) @@ -199,7 +214,7 @@ if ($arrALFI.Length -eq 0) foreach($alfi in $arrALFI) { # Favor publisher rule; hash rule otherwise - $pol = New-AppLockerPolicy -FileInformation $alfi -RuleType Publisher,Hash + $pol = New-AppLockerPolicy -FileInformation $alfi -RuleType Publisher,Hash -User $CustomUserOrGroupSid foreach ($ruleCollection in $pol.RuleCollections) { diff --git a/AaronLocker/accesschk.exe b/AaronLocker/accesschk.exe new file mode 100644 index 0000000..e7e64c7 Binary files /dev/null and b/AaronLocker/accesschk.exe differ