From cf3761a4d87740b0dc2d312bb85af693789ab36d Mon Sep 17 00:00:00 2001 From: Ronny Moreas Date: Mon, 8 Apr 2019 18:25:19 +0200 Subject: [PATCH] Use hashtable output in GetSafePathsToAllow.ps1 supporting "Label" and "RuleCollection" properties --- AaronLocker/Create-Policies.ps1 | 108 ++++++++++++------ .../GetSafePathsToAllow.ps1 | 97 ++++++++++++---- 2 files changed, 148 insertions(+), 57 deletions(-) diff --git a/AaronLocker/Create-Policies.ps1 b/AaronLocker/Create-Policies.ps1 index 92753e2..844e4b6 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% 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\*'; +}