Im September 2025 hatte ich die Gelegenheit, am AVD Techfest teilzunehmen. Dort hörte ich zum ersten Mal von AaronLocker. Und ja, das gibt es schon sehr lange.
Schon seit vielen Jahren bin ich im Bereich EUC (End User Computing) unterwegs und kannte schon Software Restrictions und später AppLocker. Beides habe ich eingesetzt. Immer mehr haben wir die Situation, dass nicht mehr GPOs für die Konfiguration und Härtung der Session Hosts verwendet werden. So war der Einsatz von AppLocker eher nicht mehr praktikabel.
In der Session „Securing Your Session Hosts: Easy to implement security tips for Citrix, AVD, Windows365 and Omnissa“ von meinen MVP-Kollegen Patrick van den Born und Stefan Dingemanse haben sie den Einsatz von AaronLocker für das Härten von Images vorgestellt. Sogleich war mir klar, dass ich dies für Nerdio Manager for Enterprise implementieren wollte.
Obwohl heute mit Windows Defender Application Control (WDAC) eine Lösung existiert, welche auch kontinuierlich erweitert und verbessert wird, hat mich die Einfachheit von AaronLocker überzeugt. Gerade für Kunden, die WDAC noch nicht eingeführt haben, kann dies eine Alternative sein, um die Sicherheit auf den Session Hosts zu erhöhen.
Ich habe also eine Lösung für Nerdio erstellt, welche sich in den Erstellungsprozess von Images leicht integrieren lässt. Es gibt genug Blogs und Beiträge im Internet, die AaronLocker selbst vorstellen, weshalb ich mich auf die Implementierung in Nerdio konzentriere.
Implementierung
Der einfachste Weg, AaronLocker bereitzustellen, sind Shell Apps. Mit Shell Apps kann man mit drei Skripten und weiteren Daten in einer Zip-Datei ein Deployment erstellen.
In einer Shell App gibt es ein Detect Script, welches überprüft, ob die Anwendung schon installiert wurde, und am Ende der Installation überprüft, ob die Installation erfolgreich war. Mit dem Install Script wird die eigentliche Installation durchgeführt. Das Uninstall Script ist in unserem Fall praktisch irrelevant, da wir AaronLocker nur bei der Erstellung des Images installieren/ausführen.
Da AaronLocker direkt von der Quelle heruntergeladen wird, müssen wir Anpassungen vornehmen können. Dazu gibt es Dateien im Ordner CustomizationInputs, welche den Bedürfnissen angepasst werden können. Diese Dateien kopieren wir in die Zip-Datei und können so bei Bedarf Anpassungen vornehmen und die Zip-Datei wieder aktualisieren.
General

Detect Script
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
$basePath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" $productCode = 'AaronLocker' $programRegistryPath = Join-Path $basePath $productCode # Check if the application is installed if (!(Test-Path $programRegistryPath)) { $Context.Log("RegistryKey does not exist: " + $programRegistryPath) return $false } # Check if the instaled version has the correct value $installedVersion = (Get-ItemProperty -Path $programRegistryPath -Name 'InstalledVersion').InstalledVersion $Context.Log("InstalledVersion: " + $Context.TargetVersion) $Context.Log("TargetVersion: " + $Context.TargetVersion) if ($installedVersion -eq $Context.TargetVersion) { $Context.Log("Installed version is identical to the target version") return $true } else { $Context.Log("Installed version is not the same as the target version") return $false } |
Install Script
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
#name: AaronLocker #description: Execute AaronLocker and apply configuration files #execution mode: Combined #tags: beckmann.ch, Preview <# Notes: Use this script to execute AaronLocker and apply configuration files. #> <# .SYNOPSIS Installs AaronLocker, creates AppLocker policies and applies them as a local GPO (AppLocker). .PARAMETER Mode 'Audit' or 'Enforce' (Default: 'Audit'). .PARAMETER AaronLockerPath Target path (Default: 'C:\Program Files\AaronLocker'). .NOTES - Requires AppLocker-capable edition (Enterprise/Education/Server). - Backup existing local AppLocker policy as XML backup. #> $ErrorActionPreference = 'Stop' [string]$Mode = 'Enforce' # -- Functions --------------------------------------------------------------- function Start-Unzip { param([string]$zipfile, [string]$outpath) $Null = Add-Type -AssemblyName System.IO.Compression.FileSystem $Null = [System.IO.Compression.ZipFile]::ExtractToDirectory($zipfile, $outpath) } # -- Guardrails --------------------------------------------------------------- # AppLocker Cmdlets available? if (-not (Get-Command Set-AppLockerPolicy -ErrorAction SilentlyContinue)) { throw "AppLocker Cmdlets not found. OS edition must support AppLocker." } # -- Constants ---------------------------------------------------------------- $scriptRoot = $pwd [string]$AaronLockerPath = "$env:ProgramFiles\AaronLocker" $Context.Log("Mode: " + $Mode) $Context.Log("AaronLockerPath: " + $AaronLockerPath) $Context.Log("ScriptRoot: " + $scriptRoot.Path) [string]$aaronLockerUrl = 'https://github.com/microsoft/AaronLocker/archive/refs/heads/main.zip' [string]$aaronLockerZip = Join-Path $scriptRoot 'AaronLocker-main.zip' [string]$srcAaronLockerRoot = Join-Path $scriptRoot 'AaronLocker-main' [string]$srcAaronLockerTool = Join-Path $srcAaronLockerRoot 'AaronLocker' $Context.Log("AaronLockerUrl: " + $aaronLockerUrl) $Context.Log("AaronLockerZip: " + $aaronLockerZip) $Context.Log("SrcAaronLockerRoot: " + $srcAaronLockerRoot) $Context.Log("SrcAaronLockerTool: " + $srcAaronLockerTool) [string]$accessChkUrl = 'https://download.sysinternals.com/files/AccessChk.zip' [string]$accessChkZip = Join-Path $scriptRoot 'AccessChk.zip' [string]$srcAccessChkRoot = Join-Path $scriptRoot 'AccessChk' $Context.Log("AccessChkUrl: " + $accessChkUrl) $Context.Log("AccessChkZip: " + $accessChkZip) $Context.Log("SrcAccessChkRoot: " + $srcAccessChkRoot) [string]$customizationFolder = 'CustomizationInputs' [string]$customizationInputSource = Join-Path $scriptRoot $customizationFolder [string]$customizationInputTarget = Join-Path $AaronLockerPath $customizationFolder $Context.Log("CustomizationInputSource: " + $customizationInputSource) $Context.Log("CustomizationInputTarget: " + $customizationInputTarget) # -- Prepare directories ------------------------------------------------------ $Context.Log("Creating target directory: " + $AaronLockerPath) New-Item -Path $AaronLockerPath -ItemType Directory -Force | Out-Null # -- Download AaronLocker ----------------------------------------------------- $Context.Log("Downloading AaronLocker...") Invoke-WebRequest -Uri $aaronLockerUrl -OutFile $aaronLockerZip if (Test-Path $srcAaronLockerRoot) { Remove-Item -Path $srcAaronLockerRoot -Recurse -Force } # Unpack $Context.Log("Unpacking AaronLocker...") Start-Unzip "$aaronLockerZip" "$scriptRoot" # Copy the relevant folder to destination if (-not (Test-Path $srcAaronLockerTool)) { throw "AaronLocker content not found in the archive." } robocopy $srcAaronLockerTool $AaronLockerPath /E /NFL /NDL /NJH /NJS /NP | Out-Null # -- Download AccessChk (used by AaronLocker) ------------------- $Context.Log("Downloading Sysinternals AccessChk...") Invoke-WebRequest -Uri $accessChkUrl -OutFile $accessChkZip if (Test-Path $srcAccessChkRoot) { Remove-Item -Path $srcAccessChkRoot -Recurse -Force } # Unpack $Context.Log("Unpacking AccessChk...") Start-Unzip "$accessChkZip" "$srcAccessChkRoot" $Context.Log("Copying AccessChk to AaronLocker folder...") $accessExes = Get-ChildItem -Path $srcAccessChkRoot -Include 'AccessChk64a.exe', 'AccessChk64.exe', 'AccessChk.exe' -Recurse -ErrorAction SilentlyContinue if ($accessExes) { foreach ($accessExe in $accessExes) { Copy-Item $accessExe.FullName (Join-Path $AaronLockerPath $accessExe.Name) -Force } } else { $Context.Log("ERROR: AccessChk could not be extracted. AaronLocker may work less accurately without it.") } # -- Copy CustomizationInput files ------------------------------------------ $Context.Log("Copying CustomizationInput files...") $customizationFiles = Get-ChildItem -Path $customizationInputSource -Recurse -File foreach ($file in $customizationFiles) { Copy-Item -Path $file.FullName -Destination $customizationInputTarget -Force } # -- Policies generate -------------------------------------------------------- $policyOutDir = Join-Path $AaronLockerPath 'Outputs' New-Item -Path $policyOutDir -ItemType Directory -Force | Out-Null # Ermittel das Create-Policies-Skript (Namensvariante je nach Repo-Stand) $createScript = Get-ChildItem -Path $AaronLockerPath -Filter '*Create*Policies*.ps1' -Recurse | Select-Object -First 1 if (-not $createScript) { $Context.Log("ERROR: Create-Policies-Skript in $AaronLockerPath not found."); throw "Create-Policies-Skript in $AaronLockerPath not found." } $Context.Log("Generating AppLocker-Policies with $($createScript.Name)...") # Many AaronLocker scripts operate from the working directory - switch to it Push-Location $AaronLockerPath try { # Execute in the current process context & $createScript.FullName -ErrorAction Stop } finally { Pop-Location } # -- Output files detection ------------------------------------------------ # AaronLocker typically creates: AppLockerRules-<Date>-Audit.xml / -Enforce.xml $policyAudit = Get-ChildItem -Path $AaronLockerPath -Filter '*AppLockerRules*-Audit.xml' -Recurse | Sort-Object LastWriteTime -Descending | Select-Object -First 1 $policyEnf = Get-ChildItem -Path $AaronLockerPath -Filter '*AppLockerRules*-Enforce.xml' -Recurse | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if (-not $policyAudit -or -not $policyEnf) { # Fallback: any audit/enforce file in the outputs $policyAudit = Get-ChildItem $policyOutDir -Filter '*Audit*.xml' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 $policyEnf = Get-ChildItem $policyOutDir -Filter '*Enforce*.xml' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 } if (($Mode -eq 'Audit' -and -not $policyAudit) -or ($Mode -eq 'Enforce' -and -not $policyEnf)) { throw "Could not find the generated ${Mode}-Policy XML." } $targetPolicy = if ($Mode -eq 'Audit') { $policyAudit.FullName } else { $policyEnf.FullName } $Context.Log("Using Policy: $targetPolicy") # -- Backup of the existing local policy ----------------------------------- $backupDir = Join-Path $AaronLockerPath 'Backups' New-Item -Path $backupDir -ItemType Directory -Force | Out-Null $backupFile = Join-Path $backupDir ("Local-AppLocker-Backup-{0:yyyyMMdd-HHmmss}.xml" -f (Get-Date)) try { (Get-AppLockerPolicy -Local -Xml) | Out-File -FilePath $backupFile -Encoding utf8 -Force $Context.Log("Backup of the local AppLocker policy: $backupFile") } catch { $Context.Log("ERROR: Could not backup existing local policy: $($_.Exception.Message)") } # -- Application Identity Service activation ---------------------------------- try { sc.exe config AppIDSvc start= auto if ((Get-Service AppIDSvc).Status -ne 'Running') { Start-Service AppIDSvc } } catch { $Context.Log("WARNING: Could not start 'Application Identity' service: $($_.Exception.Message)") } # -- Apply policy (local) -------------------------------------------------- # Hinweis: Without -LDAP, Set-AppLockerPolicy acts on the local policy. $Context.Log("Applying AppLocker-${Mode}-Policy locally...") Set-AppLockerPolicy -XmlPolicy $targetPolicy -ErrorAction Stop # Optional: Trigger gpupdate (not mandatory) try { gpupdate.exe /target:computer /force | Out-Null } catch {} $Context.Log("Done. AppLocker ($Mode) is set. Restart recommended.") # Create the registry key and set the version $basePath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" $productCode = 'AaronLocker' $programRegistryPath = Join-Path $basePath $productCode New-Item -Path $programRegistryPath -Force -ErrorAction SilentlyContinue New-ItemProperty -Path $programRegistryPath -Name 'InstalledVersion' -Value $Context.TargetVersion -PropertyType String -Force |
Uninstall Script
|
1 |
Write-Host "Uninstall is not required" |
Versions mit Zip Datei


Abschluss
Auch wenn AppLocker nicht mehr topaktuell ist, so gibt es im Moment noch Sicherheitsupdates. Und um ein Image und damit Session Hosts besser zu schützen, kann AppLocker eine Alternative sein. In diesem Fall kann man mit AaronLocker den Prozess für das Erstellen und Anwenden der Regeln deutlich vereinfachen.
Ich hoffe, dass dir diesen Artikel hilft, AaronLocker bei dir zu implementieren.