Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow custom SID for writable directories #6

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
112 changes: 76 additions & 36 deletions AaronLocker/Create-Policies.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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%
Expand Down Expand Up @@ -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
Expand All @@ -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
}
}

Expand Down
97 changes: 75 additions & 22 deletions AaronLocker/CustomizationInputs/GetSafePathsToAllow.ps1
Original file line number Diff line number Diff line change
@@ -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\*';
}

#>

Expand All @@ -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
Expand All @@ -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\*';
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Examples of valid hash tables:
label = "OneDrive";
paths = "$env:LOCALAPPDATA\Microsoft\OneDrive";
enforceMinVersion = $false;
customUserOrGroupSid = "S-1-5-32-547"
}


Expand Down
23 changes: 19 additions & 4 deletions AaronLocker/Support/BuildRulesForFilesInWritableDirectories.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -84,7 +89,7 @@ param(
# Optional prefix incorporated into each rule name
[parameter(Mandatory=$false)]
[String]
$RuleNamePrefix
$RuleNamePrefix
)

####################################################################################################
Expand Down Expand Up @@ -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')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wish we could specify arbitrary file extensions, but the PowerShell cmdlets don't support them. The problem is that when you pass the Get-AppLockerFileInformation results to New-AppLockerPolicy, it relies on file extension to determine file type based on its own hardcoded list. If you try to manipulate the path name in the file information object to give it a standard extension before passing it to New-AppLockerPolicy, it will still fail because it goes to look for that file and doesn't find it. I considered and quickly discarded an idea to create hard links to the new names and then deleting them after building. For now I'm just running Scan-Directories.ps1 and pulling data out of there for HashRuleData.ps1, or just insisting on good publisher/productname data.


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])
Expand Down Expand Up @@ -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)
{
Expand Down
Binary file added AaronLocker/accesschk.exe
Binary file not shown.