AppLocker is a software whitelisting product from Microsoft that ships with Windows. It can be used to restrict the software that will execute on a computer.
Overview of Policies
AppLocker policies are typically created and deployed using Group Policy. If multiple policies are deployed to a single computer, these policies are merged into a single "effective" policy (more on this later).
AppLocker policies are composed of rules that are organised into rule collections. Each collection contains rules targeting a specific type of executable code. There are five collections:
- Executables (.exe and .com)
- Dynamic Link Libraries (.dll and .ocx)
- Scripts (.bat, .cmd, .ps1, .js and .vbs)
- Windows Installer (.msi and .msp)
- Universal Applications (.appx)
Each collection can be separately configured in one of three modes: disabled (ignore all rules within the collection), audit (do not prevent code from executing, but log rule violations), or enforced (prevent code from executing and log violations).
Each AppLocker rule includes a unique GUID identifier, a name, and a description. A rule also identifies the SID of the user or group that is to be targeted, and an action of either allow or deny (where allow is used to allow code to execute, and deny is used to prevent execution). Finally, a rule also includes a condition to identify the affected executable code (i.e. the scope). There are three types of conditions:
- Publisher. Code is identified using properties of a publisher certificate.
- Path. Code is identified using a file path (either fully qualified to a specific file, or using wildcards to reference multiple files/folders).
- File Hash. Code is identified using a unique authenticode hash.
The path and hash conditions are straight forward, and each consist of a single value (i.e. the hash or path), but a total of four values are required to build a publisher rule. The values, listed with decreasing specificity, are: file version, file name, product name and publisher. Each of these values can be specifically defined, or generalised by using an asterisk wildcard. For example, by using a wildcard for product name, file name and version, you could "trust" all software published by a single vendor.
Whitelisting Approach
There are many approaches to application whitelisting. As to what is "best" for you will depend upon the risks you are seeking to mitigate, and the effort you are willing to expend on creating and maintaining an effective policy. However, here are a few high-level approaches to whitelisting:
- Limit users and administrators to running explicitly approved software.
- Limit users and administrators to running any software in approved locations (e.g.
c:\windows
andc:\program files
). - Limit users to running explicitly approved software, but allow local administrators to run any software.
- Limit users to running any software in locations that cannot be modified without administrator privileges (similar to option 2).
- Allow users to run any software except programs that are specifically blocked (blacklisting).
Having selected a suitable approach, you next need to decide how the rules will be enforced. For example, you could restrict execution by:
- Hash.
- Explicit path.
- Top level folder.
- Publisher certificate (with various levels of granularity).
Hash and publisher certificate (with all fields qualified) are the two most secure options (as both are independent of modifiable properties of a file). However, these two options also incur the greatest level of administrative effort (as any change to a file will require a corresponding change to the matching AppLocker rule).
Although whitelisting a top level folder (and everything therein) requires minimum effort, it won't prevent users from executing "inappropriate" software that is "incorrectly" installed into an approved location (e.g. an application manually installed by an administrator onto a shared computer becomes accessible to any user of the system).
Similarly, although using a publisher certificate that is scoped only to the publisher name may alleviate the need to modify AppLocker rules to support new product versions, it also opens the possibility that a user will be able to execute any software from the vendor, including software that may not be deemed suitable for your corporate environment.
As I said, there is no "right or wrong" approach. The key is to understand the risks to be mitigated, the support overheads you can sustain, and the agility required of your organisation. However, in this article, I will describe the steps required to implement a whitelisting policy based on the following principles:
- Allow administrators to run any software. Rationale: If you are an administrator, you "own the box", and can disable AppLocker. Therefore, the security benefit of a policy that attempts to limit the actions of administrators is limited, and yet the support overhead is high, for a single misconfigured rule targeting local administrators could render a system inoperable.
- Allow non-privileged users to run any software under c:\windows. Rationale: Typically, only system software is installed into this folder. Authorising this location using a top-level folder rule considerably reduces the size of an AppLocker policy, and limits the effort required to maintain the policy following the deployment of system updates.
- Limit non-privileged users to running approved software outside of c:\windows. Authorise the execution of approved software by publisher certificate (based on vendor and product), falling back to hash when a certificate rule cannot be generated. Rationale: Avoids users from running unapproved software that may get installed via a local administrator. Also avoids the use of rules that can be circumvented by modifying the properties of file (e.g. renaming). And favouring publisher certificates over hashes will result in a smaller policy with less administrative overhead.
- Consider options for consolidating publisher rules. Use a small number of publisher rules based solely on publisher name (i.e. all other rule properties are wildcards) where doing so significantly reduces the number of rules to be maintained without notably increasing the likelihood that users will be able to run unapproved software from the vendor. For example, you may use a suite of products from a single vendor that collectively result in a large number of publisher rules when the product name is incorporated. Rationale: By decreasing the specificity of a publisher rule, and trusting all software from a single vendor, you will collapse many rules into a single rule. If it is unlikely that users will seek to install other software from the vendor, the impact of this consolidation is minimal.
- Amend any path based rule to explicitly exclude sub-folders that are user-writable. Rational: A "trusted" location such as
c:\windows
includes several well-known sub-folders that can be modified by non-privileged users. These folders could be used as a backdoor for executing arbitrary code.
Automating Policy Creation
Before describing the procedure for creating a new AppLocker policy, I will lay out a few assumptions:
- We are implementing an AppLocker policy for Windows 10 (although the approach is equally applicable to other versions of Windows).
- The policy will be applied via Group Policy to computers that are deployed with a corporate standard operating environment (SOE).
- The SOE includes the operating system, drivers, and a selection of "core" applications that are required by all users.
- A catalog of approved optional software is made available to users (e.g. via the Microsoft Configuration Manager Application Catalog).
- A single AppLocker policy will be created to allow users to execute the software within the SOE, and any approved software installed after the SOE is deployed.
These are the steps we will use to create a new AppLocker policy based on the approach outlined above:
- Create a default policy that implements a basic ruleset allowing administrators to run any software, and standard users to run software under
c:\windows
. - Amend the default rules to prevent non-privileged users from executing software in "user-writable" locations.
- Create another policy to include the rules required to enable all software included within the SOE (i.e. a baseline that includes core applications like Microsoft Office).
- Create a new policy for each application that is authorised for optional installation (i.e. approved software that isn't baked into the SOE). Where possible, I recommend performing this step on a virtual machine. This will allow you to revert to a clean snapshot before processing each application.
- Create new "ad hoc" policies to address files that weren't successfully processed during any of the previous steps.
- Merge (and optimise) all of the policies created in the previous steps into a single policy that can be imported into Group Policy.
So let's work through this one step at a time. Firstly, to create the default policy:
- From any Windows 10 computer that does not already include an AppLocker policy, run gpedit.msc as an administrator.
- Expand Local Computer Policy, Computer Configuration, Windows Settings, Security Settings, Application Control Policies, AppLocker.
- Right click the AppLocker node and select Properties.
- Select the Advanced tab, select the option Enable the DLL rule collection, and click OK.
- Right click Executable Rules, and select Create Default Rules.
- Delete the rule named (Default Rule) All files located in the Program Files folder.
- Repeat steps 5 and 6 for Windows Installer Rules, Script Rules, DLL Rules, and Packaged App Rules. Note: Step 6 isn't necessary for Windows Installer Rules and Packaged App Rules.
- Right click the AppLocker node and select Export Policy.
- Save the policy to a network share which we will henceforth refer to as the AppLocker "repository". e.g.
\\server\applocker\default.xml
.
The next step is to modify this policy to exclude all user-writable locations. To help, we will use a script to scan the file system for folders with permissions that grant write access to standard users.
Create a script in your repository named UserWriteableLocations.ps1 with the following content:
# Paths that we've already excluded via AppLocker.
$exclusions = @()
# Paths to process.
$paths = @(
"C:\Windows"
)
# Setup log.
$log = "$PSScriptRoot\UserWritableLocations.log"
$FSR = [System.Security.AccessControl.FileSystemRights]
# Unfortunately the FileSystemRights enum doesn't contain all the values from the Win32 API. Urgh.
$GenericRights = @{
GENERIC_READ = [int]0x80000000;
GENERIC_WRITE = [int]0x40000000;
GENERIC_EXECUTE = [int]0x20000000;
GENERIC_ALL = [int]0x10000000;
FILTER_GENERIC = [int]0x0FFFFFFF;
}
# ... so we need to map them ourselves.
$MappedGenericRights = @{
FILE_GENERIC_READ = $FSR::ReadAttributes -bor $FSR::ReadData -bor $FSR::ReadExtendedAttributes -bor $FSR::ReadPermissions -bor $FSR::Synchronize
FILE_GENERIC_WRITE = $FSR::AppendData -bor $FSR::WriteAttributes -bor $FSR::WriteData -bor $FSR::WriteExtendedAttributes -bor $FSR::ReadPermissions -bor $FSR::Synchronize
FILE_GENERIC_EXECUTE = $FSR::ExecuteFile -bor $FSR::ReadPermissions -bor $FSR::ReadAttributes -bor $FSR::Synchronize
FILE_GENERIC_ALL = $FSR::FullControl
}
Function Map-GenericRightsToFileSystemRights([System.Security.AccessControl.FileSystemRights]$Rights) {
$MappedRights = New-Object -TypeName $FSR
if ($Rights -band $GenericRights.GENERIC_EXECUTE) {
$MappedRights = $MappedRights -bor $MappedGenericRights.FILE_GENERIC_EXECUTE
}
if ($Rights -band $GenericRights.GENERIC_READ) {
$MappedRights = $MappedRights -bor $MappedGenericRights.FILE_GENERIC_READ
}
if ($Rights -band $GenericRights.GENERIC_WRITE) {
$MappedRights = $MappedRights -bor $MappedGenericRights.FILE_GENERIC_WRITE
}
if ($Rights -band $GenericRights.GENERIC_ALL) {
$MappedRights = $MappedRights -bor $MappedGenericRights.FILE_GENERIC_ALL
}
return (($Rights -band $GenericRights.FILTER_GENERIC) -bor $MappedRights) -as $FSR
}
# These are the rights from the FileSystemRights enum we care about.
$WriteRights = @('WriteData', 'CreateFiles', 'CreateDirectories', 'WriteExtendedAttributes', 'WriteAttributes', 'Write', 'Modify', 'FullControl')
# Helper function to match against a list of patterns.
function notlike($string, $patterns) {
foreach ($pattern in $patterns) { if ($string -like $pattern) { return $false } }
return $true
}
# The hard work...
function scan($path, $log) {
$cache = @()
gci $path -recurse -exclude $exclusions -force -ea silentlycontinue |
? {($_.psiscontainer) -and (notlike $_.fullname $exclusions)} | %{
trap { continue }
$directory = $_.fullname
(get-acl $directory -ea silentlycontinue).access |
? {$_.isinherited -eq $false} |
? {$_.identityreference -match ".*USERS|EVERYONE"} | %{
(map-genericrightstofilesystemrights $_.filesystemrights).tostring().split(",") | %{
if ($writerights -contains $_.trim()) {
if ($cache -notcontains $directory) { $cache += $directory }
}
}
}
}
return $cache
}
# Start scanning.
$paths | %{ scan $_ $log } | out-file $log
Now run the script as an administrator:
powershell.exe -file \\server\applocker\userwritablelocation.ps1
The results will be written to the repository as \\server\applocker\UserWritableLocations.log
.
Note: The above script recursively searches for user-writable locations under c:\windows
, however, it can easily be extended to scan other locations by adding the appropriate folders to the $paths variable (e.g. c:\program files (x86)
and c:\program files
).
Out of interest, these are the user-writable sub-folders under c:\windows
on a default installation of Windows 10 Long Term Service Branch:
C:\Windows\Tasks
C:\Windows\Temp
C:\Windows\tracing
C:\Windows\PLA\Reports
C:\Windows\PLA\Rules
C:\Windows\PLA\Templates
C:\Windows\PLA\Reports\en-US
C:\Windows\PLA\Rules\en-US
C:\Windows\Registration\CRMLog
C:\Windows\System32\FxsTmp
C:\Windows\System32\Tasks
C:\Windows\System32\Com\dmp
C:\Windows\System32\LogFiles\WMI
C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys
C:\Windows\System32\spool\PRINTERS
C:\Windows\System32\spool\SERVERS
C:\Windows\System32\spool\drivers\color
C:\Windows\System32\Tasks\Microsoft\Windows\PLA
C:\Windows\System32\Tasks\Microsoft\Windows\RemoteApp and Desktop Connections Update
C:\Windows\System32\Tasks\Microsoft\Windows\PLA\System
C:\Windows\SysWOW64\FxsTmp
C:\Windows\SysWOW64\Tasks
C:\Windows\SysWOW64\Com\dmp
C:\Windows\SysWOW64\Tasks\Microsoft\Windows\PLA
C:\Windows\SysWOW64\Tasks\Microsoft\Windows\RemoteApp and Desktop Connections Update
C:\Windows\SysWOW64\Tasks\Microsoft\Windows\PLA\System
We will now create another script in the repository named InsertUserWritableLocations.ps1 to update the default policy with the output of the previous script.
# The input file, created from UserWritableLocations.ps1.
$input = "$PSScriptRoot\UserWritableLocations.log"
# The default locations trusted by AppLocker.
$trusted = @{
"C:\Windows" = @(
"(Default Rule) All files located in the Windows folder",
"(Default Rule) All scripts located in the Windows folder",
"(Default Rule) Microsoft Windows DLLs"
);
"C:\Program Files" = @(
"(Default Rule) All files located in the Program Files folder",
"(Default Rule) All scripts located in the Program Files folder",
"(Default Rule) All DLLs located in the Program Files folder"
)
}
# First run UserWritableLocations.ps1 to create an input file.
if (-not (test-path $input)) { write-host "File not found: $input"; exit }
# Categorise user-writable locations.
$content = get-content $input
$untrusted = @{
'C:\Program Files' = @($content | ? { $_ -like 'C:\Program Files\*' });
'C:\Windows' = @($content | ? { $_ -like 'C:\Windows\*' });
}
# Load the default AppLocker policy.
[xml]$policy = get-content "$PSScriptRoot\Default.xml"
# Add path exceptions to the AppLocker policy.
$trusted.keys | %{
$folder = $_
$trusted.item($folder) | %{
$rule = $policy.selectsinglenode("//FilePathRule[@Name='$_']")
if (! $rule) { return }
$exceptions = $policy.createelement("Exceptions")
[void]$rule.appendchild($exceptions)
$untrusted.item($folder) | %{
$exception = $policy.createelement("FilePathCondition")
$exception.setattribute("Path", "$_\*")
[void]$exceptions.appendchild($exception)
}
}
}
# Save the modified AppLocker policy.
$policy.save("$PSScriptRoot\default.xml")
And now, to insert the user-writable locations into the default policy:
powershell.exe -file \\server\applocker\insertuserwritablelocation.ps1
Note: I have intentionally configured this script to process exceptions for c:\windows
and c:\program files
. We only need support for c:\windows
, but if you decided to keep the default rules for c:\program files
, and you also scanned this folder for user-writable locations, the above script has you covered.
The remaining steps in our process will be managed via a single script named AppLocker.ps1. Add this script to the repository with the following content:
<# examples:
# generate a baseline policy.
powershell.exe -file applocker.ps1 -baseline -output c:\policies\baseline.xml
# generate an application-specific policy.
powershell.exe -file applocker.ps1 -application -in c:\policies\baseline.xml -out c:\policies\application.xml
# generate an adhoc policy.
powershell.exe -file applocker.ps1 -adhoc -in c:\path -filter *.* -out c:\policies\adhoc.xml
# merge policies.
powershell.exe -file applocker.ps1 -merge -in c:\policies -out c:\policies\merged.xml
#>
<# expected usage:
1. run the script to generate a baseline. e.g.
powershell.exe -file applocker.ps1 -baseline -out c:\policies\baseline.xml
2. install an application.
3. rerun the script to generate an application-specific policy. e.g.
powershell.exe -file applocker.ps1 -application -in c:\policies\baseline.xml -out c:\policies\someapp.xml
4. rerun the script to merge applocker policies under c:\policies. e.g.
powershell.exe -file applocker.ps1 -merge -in c:\policies -out c:\policies\soe.xml
#>
param(
[switch]$baseline,
[switch]$application,
[switch]$adhoc,
[switch]$merge,
[string]$in,
[string]$filter,
[string]$out
)
function log($message) { write-host -foregroundcolor green $message }
function error($message) { write-host -foregroundcolor magenta $message; exit }
# check baseline switches.
if ($baseline) {
if ($out -eq $null) { error 'Usage: powershell.exe -file applocker.ps1 -baseline -out c:\policies\baseline.xml' }
if (! (test-path (split-path $out -parent))) { error 'The output folder does not exist.' }
}
# check application switches.
if ($application) {
if ($in -eq $null -or $out -eq $null) { error 'powershell.exe -file applocker.ps1 -application -in c:\policies\baseline.xml -out c:\policies\application.xml' }
if (! (test-path $in)) { error 'The baseline policy does not exist.' }
if (! (test-path (split-path $out -parent))) { error 'The output folder does not exist.' }
}
# check adhoc switches.
if ($adhoc) {
if ($in -eq $null -or $filter -eq $null -or $out -eq $null) { error 'powershell.exe -file applocker.ps1 -adhoc -in c:\path -filter *.* -out c:\policies\adhoc.xml' }
if (! (test-path $in)) { error 'The input folder does not exist.' }
if (! (test-path (split-path $out -parent))) { error 'The output folder does not exist.' }
}
# check merge switches.
if ($merge) {
if ($in -eq $null -or $out -eq $null) { error 'powershell.exe -file applocker.ps1 -merge -in c:\policies -out c:\policies\merged.xml' }
if (! (test-path $in)) { error 'The input folder does not exist.' }
if (! (test-path (split-path $out -parent))) { error 'The output folder does not exist.' }
}
# we will trust all products published by the following vendors.
$vendors = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US", "O=CITRIX SYSTEMS, INC., L=SANTA CLARA, S=CALIFORNIA, C=US"
# directories to scan.
$paths = @('C:\Program Files', 'C:\Program Files (x86)')
# rule collection types.
$types = 'Appx', 'Dll', 'Exe', 'Msi', 'Script'
# empty applocker policy.
$xml = [xml]@'
<AppLockerPolicy Version="1">
<RuleCollection Type="Appx" EnforcementMode="Enabled" />
<RuleCollection Type="Dll" EnforcementMode="Enabled" />
<RuleCollection Type="Exe" EnforcementMode="Enabled" />
<RuleCollection Type="Msi" EnforcementMode="Enabled" />
<RuleCollection Type="Script" EnforcementMode="Enabled" />
</AppLockerPolicy>
'@
# empty publisher rule.
$publisher = [xml]@'
<FilePublisherRule Id="" Name="" Description="" UserOrGroupSid="S-1-1-0" Action="Allow">
<Conditions>
<FilePublisherCondition PublisherName="" ProductName="*" BinaryName="*">
<BinaryVersionRange LowSection="*" HighSection="*" />
</FilePublisherCondition>
</Conditions>
</FilePublisherRule>
'@
# empty file hash rule.
$hash = [xml]@'
<FileHashRule Id="" Name="Windows 10 Hash Rules" Description="" UserOrGroupSid="S-1-1-0" Action="Allow">
<Conditions>
<FileHashCondition />
</Conditions>
</FileHashRule>
'@
# return the "real" extension of a file.
function get-filetype($path) {
$extension = [io.path]::getextension($path)
$extensions = @('.dll', '.exe', '.js', '.msi', '.ps1', '.vbs')
# extensions of file types that are actually dlls.
$imposterdlls = @('.api', '.8bx')
if ($extensions -contains $extension) { return $extension }
# a hack for files which are dll's without a pe header.
if ($imposterdlls -contains $extension) { return '.dll' }
$bytes = get-content $path -readcount 0 -encoding byte
$offset = $bytes[0x3c]
$signature = [char[]] $bytes[$offset..($offset + 3)]
if ([string]::join('', $signature) -eq "PE`0`0") {
$header = $offset + 4
$data = [bitconverter]::toint32($bytes, $header + 18)
if ($data -band 0x2000) { return '.dll' } else { return '.exe' }
}
return $extension
}
# insert a rule based on $template, with a rule type of $class, under $parent, into $policy.
function insert-rule($policy, $parent, $template, $class) {
$rule = $template.clone()
$rule.$class.id = [string]([guid]::newguid().guid)
return $parent.appendchild($policy.importnode($rule.$class, $true))
}
# insert $hashes into $policy under $parent.
function insert-hashes($policy, $hashes, $parent) {
$hashes | %{ [void]$parent.conditions.firstchild.appendchild($policy.importnode($_, $true)) }
}
# insert $publishers into $policy under $parent.
function insert-publishers($policy, $publishers, $parent) {
$publishers | %{
$_.conditions.filepublishercondition.binaryname = '*'
$_.conditions.filepublishercondition.binaryversionrange.lowsection = '*'
$_.conditions.filepublishercondition.binaryversionrange.highsection = '*'
[void]$parent.appendchild($policy.importnode($_, $true))
}
}
# insert $paths into $policy under $parent.
function insert-paths($policy, $paths, $parent) {
$paths | %{ [void]$parent.appendchild($policy.importnode($_, $true)) }
}
# insert conditions ($hashes and $publishers) into $policy under $parent.
function insert-conditions($policy, $parent, $hashes, $publishers) {
if ($hashes.count) {
$node = insert-rule $policy $parent $hash 'filehashrule'
insert-hashes $policy $hashes $node
}
if ($publishers.count) { insert-publishers $policy $publishers $parent }
}
# merge policy $one with $two and return union.
function merge-policies($one, $two) {
$target = $xml.clone()
foreach ($type in $types) {
$hashes = @($one.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']//FileHash") +
$two.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']//FileHash") | sort-object -property data -unique)
$publishers = @($one.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']/FilePublisherRule") +
$two.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']/FilePublisherRule") | sort-object -property name -unique)
# this script will never generate path rules, but we need to cater for merging with a policy that does.
$paths = @($one.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']/FilePathRule") +
$two.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']/FilePathRule") | sort-object -property name -unique)
$parent = $target.applockerpolicy.selectsinglenode("//RuleCollection[@Type='$type']")
insert-conditions $target $parent $hashes $publishers
insert-paths $target $paths $parent
}
return $target
}
# remove duplicate conditions between $one and $two and return result.
function remove-duplicates($one, $two) {
$target = $xml.clone()
foreach ($type in $types) {
$h1 = @($one.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']//FileHash") | sort-object -property data)
$h2 = @($two.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']//FileHash") | sort-object -property data)
$hashes = @(); compare-object -referenceobject $h1 -differenceobject $h2 -property data -passthru | ? { $_.sideindicator -eq '<=' } | %{ $hashes += $_ }
$p1 = @($one.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']/FilePublisherRule") | sort-object -property name)
$p2 = @($two.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']/FilePublisherRule") | sort-object -property name)
$publishers = @(); compare-object -referenceobject $p1 -differenceobject $p2 -property name -passthru | ? { $_.sideindicator -eq '<=' } | %{ $publishers += $_ }
$parent = $target.applockerpolicy.selectsinglenode("//RuleCollection[@Type='$type']")
insert-conditions $target $parent $hashes $publishers
}
return $target
}
# applocker cannot handle more than 1,500 or so hashes in a single rule. let's break into blocks of 1000.
function split-hashes($applocker) {
foreach ($type in $types) {
$collection = $applocker.applockerpolicy.selectsinglenode("//RuleCollection[@Type='$type']")
$hashes = @($applocker.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']//FileHash") | sort-object -property data)
if ([math]::floor($hashes.count) -gt 0) {
[void]$collection.removechild($collection.filehashrule)
}
$min = 0; while ($min -lt $hashes.count) {
$max = $min + 999
insert-conditions $applocker $collection $hashes[$min..$max] $null
$min = $max + 1
}
}
}
# initialise policy.
$applocker = $xml.clone()
# initialise rules array.
$rules = @()
# if merging policies.
if ($merge) {
$applocker = $xml.clone()
get-childitem $in\*.xml -exclude (split-path $out -leaf) | %{
log "Merging AppLocker policy $($_.name)."
$applocker = merge-policies $applocker ([xml](get-content $_.fullname -encoding utf8))
}
log "Chunking hashes into blocks of 1,000."
split-hashes $applocker
log "Saving the merged policy."
$applocker.save($out)
return
}
# if generating an adhoc policy.
if ($adhoc) {
log "Creating adhoc AppLocker rules for $filter in folder $in."
$rules += get-childitem -path $in -filter $filter -recurse | select -expandproperty fullname | get-applockerfileinformation -ea silentlycontinue
$rules | %{
$_.path = $_.path -replace '%OSDRIVE%', 'C:'
$extension = [io.path]::getextension($_.path)
$_.path = $_.path -replace $extension, (get-filetype $_.path)
}
$policy = [xml]($rules | new-applockerpolicy -ruletype publisher, hash -user everyone -xml -ignoremissingfileinformation)
} else {
log "Creating AppLocker rules for the requested paths."
$paths | %{ $rules += get-applockerfileinformation -directory $_ -recurse -ea silentlycontinue }
$policy = [xml]($rules | new-applockerpolicy -ruletype publisher, hash -user everyone -xml -ignoremissingfileinformation)
}
log "Optimising rules."
foreach ($type in $types) {
$parent = $applocker.applockerpolicy.selectsinglenode("//RuleCollection[@Type='$type']")
$hashes = @($policy.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']//FileHash") | sort-object -property data -unique)
$publishers = @($policy.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']/FilePublisherRule") | %{
$_.name = "{0} - {1}" -f $_.conditions.filepublishercondition.publishername, $_.conditions.filepublishercondition.productname; $_
} | sort-object -property name -unique)
insert-conditions $applocker $parent $hashes $publishers
}
log "Consolidating file publisher conditions for trusted vendors."
foreach ($vendor in $vendors) {
$node = $publisher.clone()
$node.filepublisherrule.name = [string]"$vendor - *"
$node.filepublisherrule.conditions.filepublishercondition.publishername = [string]$vendor
foreach ($type in $types) {
$parent = $false
$applocker.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']/FilePublisherRule[contains(@Name, '$vendor')]") | %{
$parent = $_.parentnode
[void]($parent.removechild($_))
}
if ($parent -ne $false) {
$node.filepublisherrule.id = [string]([guid]::newguid().guid)
[void]$parent.appendchild($applocker.importnode($node.filepublisherrule, $true))
}
}
}
if ($application) {
log "Comparing the new policy with the baseline and removing duplicate conditions."
$applocker = remove-duplicates $applocker ([xml](get-content $in -encoding utf8))
}
log "Removing publisher rule wildcards."
foreach ($type in $types) {
$applocker.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']/FilePublisherRule/Conditions/FilePublisherCondition/BinaryVersionRange") | %{
$_.LowSection = "*"
$_.HighSection = "*"
}
}
log "Saving the policy."
$applocker.save($out)
In this step, we will use the above script to scan c:\program files
and c:\program files (x86)
and generate a "baseline" policy based first on publisher certificate (qualified to product), falling back to hash when a certificate cannot be used.
As a reminder, this step should be performed on a system immediately after the deployment of the SOE (i.e. before any "optional" software is deployed).
To generate the baseline, we will use the baseline switch, specifying the output file path of \\server\applocker\baseline.xml
using the out switch:
powershell.exe -file \\server\applocker\applocker.ps1 -baseline -out \\server\applocker\baseline.xml
Having generated a baseline policy for the SOE, it is now time to create a separate policy for each corporate application. If you are using a virtual machine, at this point you would take a snapshot, and then install an approved application that is not already in the SOE (e.g. Microsoft Visio). To generate a policy specific to the application, use the command shown below. This will effectively generate a delta policy that includes rules for all files installed by the application (it does this by generating a new baseline, comparing this with the previous baseline, and exporting the differences). In this instance we use the application switch to indicate we are generating an application-specific policy, the in switch to identify the path to the previously created baseline, and the out switch to specify the name of the resultant application policy.
powershell.exe -file \\server\applocker\applocker.ps1 -application -in \\server\applocker\baseline.xml -out c:\temp\viso.xml
At this point, you would restore the virtual machine snapshot, and then repeat the process for another application.
In some cases you might need to create AppLocker rules for files that aren't deployed via an installer. As an example, there may be a small collection of files dynamically created within a temporary directory when a program is executed (and which aren't present in the Program Files directory when the application is installed). In this scenario, you should collect the relevant files, copy them into a temporary location, and then use the AppLocker script to generate an "adhoc" policy. For example, assuming you have copied a collection of DLL files into a folder named c:\adhoc
, you can call the script with the adhoc switch, with the in parameter set to the directory containing the files, the out parameter set to the path of the resultant policy, and filter set to a mask that identifies the files to be scanned (this could be *.* if multiple files types are to be included):
powershell.exe -file \\server\applocker\applocker.ps1 -adhoc -in c:\adhoc -filter *.dll -out \\server\applocker\adhoc.xml
The "adhoc" switch is also useful when generating an AppLocker policy for an application that installs executable files with non-standard file extensions. Adobe Acrobat, for example, uses DLLs with an extension of .8bx. These file types will not be processed by the native AppLocker PowerShell cmdlets which are used when the script is called with the application switch, and hence won't be picked up in an application-specific policy. We can circumvent this limitation by using he adhoc switch to generate a policy that specifically targets this file type.
If you review the source code of the AppLocker script, you will notice the function get-filetype is used to determine the "real" file type of a file with a non-standard extension. The function attempts to do this by inspecting the file's PE header, but if the file doesn't actually conform to the portable executable file format, you may have to resort to explicitly forcing a specific return type. This is exactly what I have done for files with an extension of .api and .8bx.
The final step in the process is to merge the various policies together into a single consolidated policy. Assuming you have collated all the policies into a common repository accessed via \\server\applocker
, you can call the script with the merge switch, specifying the source folder using the in parameter, and the name of the final policy using the out parameter:
powershell.exe -file \\server\applocker\applocker.ps1 -merge -in \\server\applocker -out \\server\applocker\soe.xml
The script will optimise the merged polices to produce the smallest possible output. Note: The merge process assumes that all of the source policies were generated with the AppLocker script. Do not attempt to merge an AppLocker policy generated via another method.
You can now import the consolidated AppLocker policy (soe.xml) into group policy, and link it to the appropriate organisational unit.
A couple of additional notes on this process:
- The native AppLocker PowerShell cmdlets will seek to optimise an AppLocker policy by consolidating the conditions for files within the same folder into a single rule. This practice is helpful, but it still yields an unnecessarily verbose policy, and it doesn't guarantee the removal of redundant conditions (e.g. the same hash can be included more than once in the same policy). The script presented in this article takes optimisation to the extreme; removing all duplicate conditions, and consolidating as many conditions into a single rule as technically possible. In practice, I've encountered errors when importing an AppLocker policy into Group Policy when a single rule contains more than around 1,300 conditions (I haven't attempted to determine the exact limit). For this reason, the script chunks hashes into rules with a maximum of 1,000 conditions.
- If you adopt the process outlined in this article, make sure you generate a separate baseline policy for each hardware model supported by the SOE (to cater for the presence of drivers and support software specific to each platform). Even though the baseline policies of different systems will be predominantly identical, the merge process will ensure that all redundant conditions are removed.
- The approach I've described allows you to easily retire an application from your environment. If you no longer need to support a previously approved application, just delete the application-specific policy from your "repository", and re-run the merge procedure.
- After generating an updated baseline or application-specific policy, do not immediately remove the superseded policy from your repository, as the resultant merged policy may no longer include rules required by existing computers in the field. You will need to devise an acceptable "overlap" period, during which both new and old rules are retained. Only after you are confident that any software targeted by the older rules has been fully removed from your environment, should you remove the old policy from the repository (so it won't be processed during the next merge operation).
- The AppLocker script (in its current form) will attempt to consolidate publisher rules for all products from Microsoft and Citrix. You can choose to add or remove additional publisher names based on what is appropriate for your environment. To add a new vendor, add the qualified publisher name to the $vendors array on line 66 of the script.