Creating Applocker GPO's from Powershell

Applocker's basic setup was covered here, it was an out of the box configuration for the sole purposes of trying to exploit Windows with an RCE and seeing how Applocker would fair. it didn't. Manually configuring Applocker is no fun compared to the alternative, Powershell. Auditing the system, creating custom settings and finally creating Domain Group Policies. I'll cover some of the arrgghhh, Why Microsoft just Why, considerations as well.

Applocker is available with Windows 7 Ultimate and Enterprise, Windows 8 and 10 Pro editions and is a application whitelisting service supporting the following file types: 

Executables (.exe, .com)

Dll's (.ocx, . dll)

Scripts (.vbs, .js, .ps1, .cmd, .bat)

Windows Installers (.msi, .mst, .msp)

Packaged App (.appx)

There is an undocumented feature of blocking API's when DLL enforcement is enabled, requiring path rules. There are others that will be covered later on.

Before we start looking at Applocker there's a decision to make. Install RSAT for Windows 10 or not, no RSAT and it will be a 2 stage process of generating the rules on Windows client and then copying the output to a something with the AD\GPO Powershell modules installed for importing into Group Policy.

So the first hurdle, thanks Microsoft.... All my demo's are created in an offline Hyper-V system with no Internet. With Windows 1803 and above, I'm using 1909, RSAT is no longer available as a separate download...why not. If your planning on installing RSAT run the following commands whilst online:

DISM.exe /Online /add-capability /CapabilityName:Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0
DISM.exe /Online /add-capability /CapabilityName:Rsat.GroupPolicy.Management.Tools~~~~0.0.1.0

Reboot

Create folder 'C:\logs\Applocker' as the working area for scripts and .xml files. 

Next complaint, why doesn't the Applocker Module for Powershell provide full support for path rules? The rule will only work for a folder based on the existing content. No .exe file in the folder, no rules are created. If anyone has a fix, please let me know, I'll update this page with you being credited.

 

To work around the lack of path support, open gpedit.msc and manually create folder rules for any location where the user can Write and\or Execute. Repeat this process for each Applocker Rule type. So you don't have to, click on the image below to download the rules to 'C:\Logs\Applocker\', rename from .txt to .xml.

 

Include any network shares, I've H\: for Home Drives and G:\ for Groups and don't forget DVD's and USB mappings.

It's possible to create an Applocker Deny path rule for 'C:\' with exclusions for 'C:\Program Files\*' and 'C:\Windows\*' etc this then effectively whitelists those entire Directories, may as well deploy the default rules and go home. Instead deny rules will protect individual folders that are writeable.

To protect the root of C:\ from users being able to Write and Create folders, deploy run this command: 

icacls.exe c:\ /remove:g "Authenticated Users" 

Enabling enforcement of .dll's does cause performance issues when the policy is created with PowerShell, grouping is not particularly effective. The number of rules created and processed results in poor performance and with excessive rules approved programs are denied randomly. Hence why I tend to use path rules for .dll's constraining the calling from system defaults of Windows and Program Files. Alternatively, if its a requirement for whitelisting, manually group .dll's and approve by Publisher, until no further 8003 errors are generated in the Applocker Eventlogs. 

Now for the fun bit, Powershell. 

#Prompt for GPO Name

$GPOName = Read-Host "Name of the Applocker GPO....."

#Working Folder on local client

$path = "C:\logs\Applocker"

#Output files
$xmlExe = "$path\Exe.xml"
$xmlScript = "$path\Script.xml"
$xmlMSI = "$path\Msi.xml"
$xmlPack = "$path\Appx.xml"
$xmlPath = "$path\PathRules.xml"
$xmldnyWin32 = "$path\dnyWin32.xml"
$xmldnyWin64 = "$path\dnyWin64.xml"
$xmldnyProg32 = "$path\dnyProg32.xml"
$xmldnyProg64 = "$path\dnyProg64.xml"
$xmldnyWinSxs = "$path\dnyWinSxs.xml"
$xmldnyPS = "$path\dnyPS.xml"

#The following list of files are commonly abused to bypass Applocker or attack the system.
$dny =            

"gathernetworkInf.vbs", 
"installutil.Exe",
#Applocker Bypass
"scrobj.dll",
"sos.dll",
"cipher.Exe",
#Used by malware to encrypt disks (WannaCry)
"certutil.exe",  #Used to download files and encode text to programs 
<#Microsoft recommended files to block

https://docs.microsoft.com/en-us/windows/security/threat-protection/windows-defender-application-control/microsoft-recommended-block-rules#>
"addinprocess.exe",
"addinprocess32.exe",
"addinutil.exe",
"aspnet_compiler.exe",
"bash.exe",
"bginfo.exe"
"cdb.exe",
"csi.exe",
"dbghost.exe",
"dbgsvc.exe",
"dnx.exe",
"dotnet.exe",
"fsi.exe",
"fsiAnyCpu.exe",
"infdefaultinstall.exe",
"kd.exe",
"kill.exe",
"lxssmanager.dll",
"lxrun.exe",
"Microsoft.Build.dll",
"Microsoft.Build.Framework.dll",
"Microsoft.Workflow.Compiler.exe",
"msbuild.exe",
"msbuild.dll",
"mshta.exe",
"ntkd.exe",
"ntsd.exe",
"powershellcustomhost.exe",
"rcsi.exe",
"runscripthelper.exe",
"texttransform.exe",
"visualuiaverifynative.exe",
"wfc.exe",
"windbg.exe",
"wmic.exe"

#Block Powershell and Powershell_ISE, different variable for a separate GPO is created for targeted deny.

$dnyPSExe =
'Powershell.exe',
'Powershell_ise.exe',

'system.management.automation.dll'

#Create policy for all Exe's
Get-ChildItem C:\ -Force -Recurse -ErrorAction SilentlyContinue | 
where {$_.Extension -eq ".exe"} | 
ForEach-Object {$_.FullName   } | 
Get-AppLockerFileInformation -ErrorAction SilentlyContinue | 
New-AppLockerPolicy -RuleType Publisher,Hash -Optimize -Xml |
Out-File $xmlExe

#Create policy for all Scripts
Get-ChildItem C:\ -Force -Recurse -ErrorAction SilentlyContinue | 
where {$_.Extension -eq ".ps1" `
-or $_.Extension -eq ".bat" `
-or $_.Extension -eq ".cmd" `
-or $_.Extension -eq ".vbs" `
-or $_.Extension -eq ".js" } | 
ForEach-Object {$_.FullName} | 
Get-AppLockerFileInformation -ErrorAction SilentlyContinue | 
New-AppLockerPolicy -RuleType Publisher,Hash -Optimize -Xml |
Out-File $xmlScript

#Create policy for all MSI's
Get-ChildItem C:\ -Force -Recurse -ErrorAction SilentlyContinue | 
Where {$_.Extension -eq ".msi" -or $_.Extension -eq ".msp*"} | 
ForEach-Object {$_.FullName} | 
Get-AppLockerFileInformation -ErrorAction SilentlyContinue | 
New-AppLockerPolicy -RuleType Publisher,Hash -Optimize -Xml |
Out-File $xmlMsi

#Create policy for all Users Appx Packages

Get-AppxPackage -allusers | 
Get-AppLockerFileInformation | 
New-AppLockerPolicy -RuleType publisher -Optimize -xml -User Users |
Out-File $xmlAppX 

#Create separate outputs for each Deny list. Once the list of files that are being denied increases the xml files did not contain all repeat instances of the file and hash. 

$ardnyWin32=@()
foreach ($dnyWin32 in $dny)
    {
    $dnyWin32FileInf = Get-ChildItem -Path C:\Windows\system32 -Recurse -Force -ErrorAction SilentlyContinue | 
    Where {$_.name -eq "$dnyWin32"} 
    $dnyWin32Fullname = $dnyWin32FileInf.FullName
    $ardnyWin32 += $dnyWin32Fullname
    }
sleep 5

ForEach-Object {$ardnyWin32} |
Get-AppLockerFileInformation -ErrorAction SilentlyContinue | 
New-AppLockerPolicy -RuleType Hash -Xml | 
Out-File $xmldnyWin32

 

$ardnyWin64=@()
foreach ($dnyWin64 in $dny)
    {
    $dnyWin64FileInf = Get-ChildItem -Path C:\Windows\sysWOW64 -Recurse -Force -ErrorAction SilentlyContinue | 
    Where {$_.name -eq "$dnyWin64"}
    $dnyWin64Fullname = $dnyWin64FileInf.FullName
    $ardnyWin64 += $dnyWin64Fullname
    }
sleep 5

ForEach-Object {$ardnyWin64} |Get-AppLockerFileInformation -ErrorAction SilentlyContinue | 
New-AppLockerPolicy -RuleType Hash -Xml | 
Out-File $xmldnyWin64

$ardnyProg64=@()
foreach ($dnyProg64 in $dny)
    {
    $dnyProg64FileInf = Get-ChildItem -Path "C:\Program Files\" -Recurse -Force -ErrorAction SilentlyContinue | 
    Where {$_.name -eq "$dnyProg64"}
    $dnyProg64Fullname = $dnyProg64FileInf.FullName
    $ardnyProg64 += $dnyProg64Fullname
    }
sleep 5

ForEach-Object {$ardnyProg64} |Get-AppLockerFileInformation -ErrorAction SilentlyContinue | 
New-AppLockerPolicy -RuleType Hash -Xml | 
Out-File $xmldnyProg64

$ardnyProg32=@()
foreach ($dnyProg32 in $dny)
    {
    $dnyProg32FileInf = Get-ChildItem -Path "C:\Program Files (x86)\" -Recurse -Force -ErrorAction SilentlyContinue | 
    Where {$_.name -eq "$dnyProg32"}
    $dnyProg32Fullname = $dnyProg32FileInf.FullName
    $ardnyProg32 += $dnyProg32Fullname
    }
sleep 5

ForEach-Object {$ardnyProg32} |Get-AppLockerFileInformation -ErrorAction SilentlyContinue | 
New-AppLockerPolicy -RuleType Hash -Xml | 
Out-File $xmldnyProg32

$ardnyWinSxs=@()
foreach ($dnyWinSxs in $dny)
    {
    $dnyWinSxsFileInf = Get-ChildItem -Path C:\Windows\winSxs -Recurse -Force -ErrorAction SilentlyContinue | 
    Where {$_.name -eq "$dnyWinSxs"}
    $dnyWinSxsFullname = $dnyWinSxsFileInf.FullName

    $ardnyWinSxs += $dnyWinSxsFullname
    }
sleep 5

ForEach-Object {$ardnyWinSxs} |Get-AppLockerFileInformation -ErrorAction SilentlyContinue | 
New-AppLockerPolicy -RuleType Hash -Xml | 
Out-File $xmldnyWinSxs

$ardnyPS=@()
foreach ($dnyPS in $dnyPSExe)
    {
    $dnyPSFileInf = Get-ChildItem -Path C:\ -Recurse -Force -ErrorAction SilentlyContinue | 
    Where {$_.name -eq "$dnyPS"} 
    $dnyPSFullname = $dnyPSFileInf.FullName
    $ardnyPS += $dnyPSFullname
    }
sleep 5

ForEach-Object {$ardnyPS} |
Get-AppLockerFileInformation -ErrorAction SilentlyContinue | 
New-AppLockerPolicy -RuleType Hash -Xml  | 
Out-File $xmldnyPS

 

#Update XML Output from Not Configured to Enabled and Allow to Deny for block list
$gcXmlExe = get-content $xmlExe
$gcXmlExe.Replace("NotConfigured","Enabled") | 
Out-File $xmlExe

$gcXmlMSI = get-content $xmlMSI
$gcXmlMSI.Replace("NotConfigured","Enabled") | 
Out-File $xmlMSI

$gcXmlScript = get-content $xmlScript
$gcXmlScript.Replace("NotConfigured","Enabled") | 
Out-File $xmlScript

$gcPath = get-content $xmlPath

$gcPath.Replace("NotConfigured","Enabled") | 

Out-File $xmlPath

$ccPack = get-content $xmlPack
$ccPack.Replace("NotConfigured","Enabled") | 
Out-File $xmlPack

$gcdnyWin32 = get-content $xmldnyWin32
$gcdnyWin32.Replace("Allow","Deny").Replace("NotConfigured","Enabled") | 
Out-File $xmldnyWin32

$gcdnyWin64 = get-content $xmldnyWin64
$gcdnyWin64.Replace("Allow","Deny").Replace("NotConfigured","Enabled") | 
Out-File $xmldnyWin64

$gcdnyWinSxs = get-content $xmldnyWinSxs
$gcdnyWinSxs.Replace("Allow","Deny").Replace("NotConfigured","Enabled") | 
Out-File $xmldnyWinSxs

$gcdnyProg64 = get-content $xmldnyProg64
$gcdnyProg64.Replace("Allow","Deny").Replace("NotConfigured","Enabled") | 
Out-File $xmldnyProg64

$gcdnyProg32 = get-content $xmldnyProg32
$gcdnyProg32.Replace("Allow","Deny").Replace("NotConfigured","Enabled") | 
Out-File $xmldnyProg32

$gcdnyPS = get-content $xmldnyPS
$gcdnyPS.Replace("Allow","Deny").Replace("NotConfigured","Enabled") | 
Out-File $xmldnyPS

#GPO Name based on input 

$GPOAdmin = $GPOName + "_PS"
$GPOPath = $GPOName + "_Path"

#Create new GPO's

new-GPO -Name $GPOName
new-GPO -Name $GPOAdmin
New-GPO -Name $GPOPath

#Retrieve GPO Names for CN

$cn = (Get-GPO -Name $GPOName).path
$cnAdmin = (Get-GPO -Name $GPOAdmin).path
$cnPath = (Get-GPO -Name $GPOPath).path

#Retrieve a Domain Controller

$dom=(Get-ADDomainController).hostname

 

#Create Domain GPO's. Merge will append rules. Without merge any additional rules will delete the previous rules.
Set-AppLockerPolicy -XmlPolicy $xmlExe -Merge -Ldap "LDAP://$dom/$cn"
Set-AppLockerPolicy -XmlPolicy $xmlScript -Merge -Ldap "LDAP://$dom/$cn"

Set-AppLockerPolicy -XmlPolicy $xmlMSI -Merge -Ldap "LDAP://$dom/$cn"
Set-AppLockerPolicy -XmlPolicy $xmlPath -Merge -Ldap "LDAP://$dom/$cnPath" 

Set-AppLockerPolicy -XmlPolicy $xmlPack -Merge -Ldap "LDAP://$dom/$cn"

Set-AppLockerPolicy -XmlPolicy $xmldnyWin32 -Merge -Ldap "LDAP://$dom/$cn"
Set-AppLockerPolicy -XmlPolicy $xmldnyWin64 -Merge -Ldap "LDAP://$dom/$cn"

Set-AppLockerPolicy -XmlPolicy $xmldnyWinSxs -Merge -Ldap "LDAP://$dom/$cn"
Set-AppLockerPolicy -XmlPolicy $xmldnyProg64 -Merge -Ldap "LDAP://$dom/$cn"

Set-AppLockerPolicy -XmlPolicy $xmldnyProg32 -Merge -Ldap "LDAP://$dom/$cn"

Set-AppLockerPolicy -XmlPolicy $xmldnyPS -merge -Ldap "LDAP://$dom/$cnAdmin"

 

The script and xml files can be downloaded from Github (here).

Note: The 'PacakgeRules.xml' is no longer required and will be ignored. Windows 10 Feb 21 CU means it needs to be generated on the fly and not pre-provisioned.

Either run the script from Powershell or ISE and enter the name of the GPO when prompted.

The following output will be created.

The following 3 GPO's will be created.

'GPO_Applocker_2020_July' Audit of files

'GPO_Applocker_2020_July_PS' Audit of Powershell instances.

'GPO_Applocker_2020_July_Path' Manually created path rules.

Assign the 3 GPO's to an OU with a representative Windows 10 1909.

 

Create an additional GPO for setting the 'Application Identity' service to Automatic.

Logon to Windows 10 client as a user and run 'gpupdate /force', 

Open Eventvwr and browse to the Applocker logs, check for 8001 event, at this point Applocker is enforcing policy.

Open Office and other applications, reboot and re-logon to the client. Open Eventvwr and review the Applocker events for errors.

Despite the entire system being audited looks like some more file types that aren't supported, but Applocker will block. These need to be whitelisted by path. These have been added to the 'pathRules.xml'

  

Execute 'psr.exe' to test enforcement, an 8004 event is raised.

The next test is to create a Powershell script with the following line 'gwmi Win32_bios | outfile c:\logs\wmi.txt', right click on the script and 'Run with Powershell'

Initially this worked as Powershell is approved. So much for protecting against un-approved Empire type scripts.

Applocker logs did not show any MSI and Script logs.

In fact the only evidence as that Powershell.exe was allowed to run, no mention of the script.

However attempting to run the script from Powershell failed. 

How to tackle the script issue. If your running Powershell logon scripts or App-V it maybe challenging. There is also the issue that some Users or Admins may have a business requirement for Powershell. 

Lets say that Powershell must be blocked. Create an AD group named 'GP_Applocker_Block', that's easy.

Now... add every single user that does not require access to Powershell.....if your in a large organisation that's a lot of users to add and manage going forward, any new user will have to be actively added to the group. This is my biggest gripe with Applocker.

Now update each rule, changing the 'Everyone' group to the 'GP_Applocker_Block' group. 

The script can be amended to add the 'GP_Applocker_Block' to Applocker rules automatically.

ForEach-Object {$ardnyPS} |

Get-AppLockerFileInformation -ErrorAction SilentlyContinue | 

New-AppLockerPolicy -RuleType Hash -Xml -user GP_Applocker_Block |

 Out-File $xmldnyPS

There is a fair amount of information regarding bypassing Applocker and 'Living off the Land' with the solutions normally involving deny lists. Deny lists based on hashes should be considered a moment in time snapshot. Patching, updates the files and the hashes change, at that point the deny list is no longer effective. It is advisable to run just the deny sections of the Powershell script monthly just after patch Tuesday creating new GPO's.

 

I've not covered why all those path rules in any great detail, denying execution from those writable folders is a must, as an example, it prevents older and bug prone and digitally signed files from being downloaded and run from the desktop.  

Any comments or observations please use the contact form and let know. Thanks for your time.