configure-sacl: use .NET RegistryKey API for SACLs (Get-Acl -Audit is unreliable on the registry)

Live-tested on Windows Server 2019; three bugs fixed found during testing:
- TOKEN_PRIVILEGES had a `long Luid` after a `uint Count`, which is 8-byte aligned on x64
  and inserted padding, so AdjustTokenPrivileges failed with ERROR_NOT_ALL_ASSIGNED and the
  new privilege guard aborted. Split the LUID into LuidLow(uint)+LuidHigh(int) to match the
  native layout.
- Get-Acl/Set-Acl -Audit is unreliable on the registry provider (returns/throws "path does
  not exist" and null). Registry SACLs now use the .NET RegistryKey API
  (OpenSubKey with ReadPermissions,ChangePermissions -> GetAccessControl(Audit) ->
  AddAuditRule -> SetAccessControl), which honors the enabled SeSecurityPrivilege. Absent
  ASEP keys are provisioned via CreateSubKey then reopened.
- Tamper-protected keys (e.g. Defender Exclusions) that deny even admin are reported as
  SKIPPED, not ERROR.

Verified: File System/Registry/Handle subcategories enabled; HKLM Run carries the Everyone
Success+Failure ContainerInherit SACL; a test autorun write produced EventID 4657 - with no
global registry auditing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MVUmXZBkr5FnZ2hwFkDhx7
This commit is contained in:
Shirofune-SecurityandClaude Opus 4.8 committed 2026-09-17 22:24:27 +09:00
1 parent 1308bc003d
commit d9bef96e0d
1 file changed
+43 -27
+43 -27
View File
@@ -1299,12 +1299,15 @@ function Enable-WelaPrivilege {
[DllImport("advapi32.dll", SetLastError=true)] public static extern bool LookupPrivilegeValue(string host, string name, out long luid);
[DllImport("advapi32.dll", SetLastError=true)] public static extern bool AdjustTokenPrivileges(IntPtr tok, bool dis, ref TOKEN_PRIVILEGES np, uint len, IntPtr prev, IntPtr rl);
[DllImport("kernel32.dll")] public static extern IntPtr GetCurrentProcess();
// Layout must match native TOKEN_PRIVILEGES exactly: DWORD Count; LUID(LowPart DWORD, HighPart LONG); DWORD Attr.
// (A single 8-byte 'long' Luid would be 8-byte aligned on x64 and insert padding, misaligning the struct.)
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct TOKEN_PRIVILEGES { public uint Count; public long Luid; public uint Attr; }
public struct TOKEN_PRIVILEGES { public uint Count; public uint LuidLow; public int LuidHigh; public uint Attr; }
public static bool Enable(string priv) {
IntPtr tok; if(!OpenProcessToken(GetCurrentProcess(), 0x28, out tok)) return false;
long luid; if(!LookupPrivilegeValue(null, priv, out luid)) return false;
TOKEN_PRIVILEGES tp = new TOKEN_PRIVILEGES(); tp.Count=1; tp.Luid=luid; tp.Attr=0x2;
TOKEN_PRIVILEGES tp = new TOKEN_PRIVILEGES();
tp.Count=1; tp.LuidLow=(uint)(luid & 0xFFFFFFFF); tp.LuidHigh=(int)(luid >> 32); tp.Attr=0x2;
bool ok = AdjustTokenPrivileges(tok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
return ok && System.Runtime.InteropServices.Marshal.GetLastWin32Error() == 0;
}
@@ -1329,20 +1332,37 @@ function Test-WelaAuditRulePresent {
}
function Set-RegistryAuditSacl {
# Provision the key if absent (ASEPs are often created only when first used - a missing key must
# still carry an inheritable SACL so a later attacker write is audited), then add the audit ACE.
param([string]$Path, $Rights, $Inh, $Sid, $AuditFlags, [string]$Note)
if (-not (Test-Path -LiteralPath $Path)) {
New-Item -Path $Path -Force | Out-Null
Write-Host "[NEW] provisioned absent key $Path" -ForegroundColor DarkGreen
# Add an audit SACL to a registry key using the .NET RegistryKey API. Get-Acl/Set-Acl -Audit is
# unreliable on the registry provider, so we open the key with the .NET API (which honors the
# enabled SeSecurityPrivilege for SACL read/write). CreateSubKey is idempotent - it opens the key
# if it exists and provisions it if absent, so an ASEP created later still inherits the audit ACE.
param($BaseKey, [string]$SubPath, $Rights, $Inh, [string]$Sid, $AuditFlags, [string]$Label, [string]$Note)
$rk = $null
try {
# OpenSubKey has the RegistryRights overload (CreateSubKey does not); provision absent keys first.
$rights2 = [System.Security.AccessControl.RegistryRights]"ReadPermissions,ChangePermissions"
$rk = $BaseKey.OpenSubKey($SubPath, [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree, $rights2)
if (-not $rk) {
$created = $BaseKey.CreateSubKey($SubPath) # provision absent ASEP so a later write inherits the SACL
if ($created) { $created.Close() }
$rk = $BaseKey.OpenSubKey($SubPath, [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree, $rights2)
}
if (-not $rk) { Write-Host "[ERROR] $Label : cannot open/create key" -ForegroundColor Red; return }
$sec = $rk.GetAccessControl([System.Security.AccessControl.AccessControlSections]::Audit)
$existing = $sec.GetAuditRules($true, $true, [System.Security.Principal.SecurityIdentifier]) | Where-Object {
$_.IdentityReference.Value -eq $Sid -and (([int]$_.RegistryRights -band [int]$Rights) -eq [int]$Rights) -and ($_.InheritanceFlags -eq $Inh) -and (($_.AuditFlags -band $AuditFlags) -eq $AuditFlags) }
if ($existing) { Write-Host "[SKIPPED] $Label : SACL already present ($Note)" -ForegroundColor Yellow; return }
$rule = New-Object System.Security.AccessControl.RegistryAuditRule((New-Object System.Security.Principal.SecurityIdentifier($Sid)), $Rights, $Inh, "None", $AuditFlags)
$sec.AddAuditRule($rule)
$rk.SetAccessControl($sec)
Write-Host "[OK] $Label ($Note)" -ForegroundColor Green
}
$acl = Get-Acl -LiteralPath $Path -Audit
if (Test-WelaAuditRulePresent $acl.Audit $Sid ([int]$Rights) 'RegistryRights' $Inh $AuditFlags) {
Write-Host "[SKIPPED] $Path : SACL already present ($Note)" -ForegroundColor Yellow; return
catch {
if ("$_" -match 'access is not allowed|Requested registry access') {
Write-Host "[SKIPPED] $Label : access denied (tamper-protected key, e.g. Defender Exclusions)" -ForegroundColor DarkYellow
} else { Write-Host "[ERROR] $Label : $_" -ForegroundColor Red }
}
$rule = New-Object System.Security.AccessControl.RegistryAuditRule((New-Object System.Security.Principal.SecurityIdentifier($Sid)), $Rights, $Inh, "None", $AuditFlags)
$acl.AddAuditRule($rule); Set-Acl -LiteralPath $Path -AclObject $acl
Write-Host "[OK] $Path ($Note)" -ForegroundColor Green
finally { if ($rk) { $rk.Close() } }
}
function Set-AuditSacl {
@@ -1394,11 +1414,10 @@ function Set-AuditSacl {
# 2) Machine registry SACLs (absent keys are provisioned so future writes are audited)
Write-Host "Applying targeted machine REGISTRY audit SACLs..."
foreach ($t in $targets.registry) {
try {
$rights = [System.Security.AccessControl.RegistryRights]($t.rights -join ",")
$inh = if ($t.inherit) { [System.Security.AccessControl.InheritanceFlags]"ContainerInherit" } else { [System.Security.AccessControl.InheritanceFlags]"None" }
Set-RegistryAuditSacl -Path $t.path -Rights $rights -Inh $inh -Sid $everyone.Value -AuditFlags $auditFlags -Note $t.note
} catch { Write-Host "[ERROR] $($t.path) : $_" -ForegroundColor Red }
$rights = [System.Security.AccessControl.RegistryRights]($t.rights -join ",")
$inh = if ($t.inherit) { [System.Security.AccessControl.InheritanceFlags]"ContainerInherit" } else { [System.Security.AccessControl.InheritanceFlags]"None" }
$sub = $t.path -replace '^HKLM:\\', ''
Set-RegistryAuditSacl -BaseKey ([Microsoft.Win32.Registry]::LocalMachine) -SubPath $sub -Rights $rights -Inh $inh -Sid $everyone.Value -AuditFlags $auditFlags -Label $t.path -Note $t.note
}
Write-Host ""
@@ -1449,7 +1468,7 @@ function Set-AuditSacl {
foreach ($prof in $profiles) {
$loadedHere = $false; $mount = $null
if ($prof.Loaded) {
$base = "Microsoft.PowerShell.Core\Registry::HKEY_USERS\$($prof.Sid)"
$userRoot = $prof.Sid
} else {
$hive = Join-Path $prof.Path "NTUSER.DAT"
if (-not (Test-Path -LiteralPath $hive)) { continue }
@@ -1457,16 +1476,13 @@ function Set-AuditSacl {
$out = reg load "HKU\$mount" "$hive" 2>&1
if ($LASTEXITCODE -ne 0) { Write-Host "[SKIPPED] hive $($prof.Sid) : cannot load ($out)" -ForegroundColor DarkYellow; continue }
$loadedHere = $true
$base = "Microsoft.PowerShell.Core\Registry::HKEY_USERS\$mount"
$userRoot = $mount
}
try {
foreach ($t in $targets.user_registry) {
$key = "$base\$($t.key)"
try {
$rights = [System.Security.AccessControl.RegistryRights]($t.rights -join ",")
$inh = if ($t.inherit) { [System.Security.AccessControl.InheritanceFlags]"ContainerInherit" } else { [System.Security.AccessControl.InheritanceFlags]"None" }
Set-RegistryAuditSacl -Path $key -Rights $rights -Inh $inh -Sid $everyone.Value -AuditFlags $auditFlags -Note "$($t.note) [$($prof.Sid)]"
} catch { Write-Host "[ERROR] $($t.key) [$($prof.Sid)] : $_" -ForegroundColor Red }
$rights = [System.Security.AccessControl.RegistryRights]($t.rights -join ",")
$inh = if ($t.inherit) { [System.Security.AccessControl.InheritanceFlags]"ContainerInherit" } else { [System.Security.AccessControl.InheritanceFlags]"None" }
Set-RegistryAuditSacl -BaseKey ([Microsoft.Win32.Registry]::Users) -SubPath "$userRoot\$($t.key)" -Rights $rights -Inh $inh -Sid $everyone.Value -AuditFlags $auditFlags -Label "HKU\$userRoot\$($t.key)" -Note "$($t.note) [$($prof.Sid)]"
}
}
finally {