Files
WELA/scripts/NamedRegistryRecovery.ps1

75 lines
4.6 KiB
PowerShell

# Value-only recovery for three built-in logging switches. No arbitrary registry replay.
function Get-WelaNamedRecoveryCatalog {
foreach ($item in @(
@('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\Audit','ProcessCreationIncludeCmdLine_Enabled'),
@('HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging','EnableScriptBlockLogging'),
@('HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging','EnableModuleLogging')
)) {[pscustomobject]@{Id=('Registry/'+$item[0]+'/'+$item[1]);Path=$item[0];Name=$item[1]}}
}
function Get-WelaNamedRecoverySources {
foreach ($relative in @('scripts/NamedRegistryRecovery.ps1','scripts/NamedRegistryRecoveryNative.cs','scripts/AuditRecovery.ps1','scripts/Configuration.ps1')) {
[pscustomobject]@{Path=$relative;Sha256=(Get-FileHash -LiteralPath (Join-Path $PSScriptRoot ('../'+$relative)) -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()}
}
}
function Initialize-WelaNamedRecoveryNative {
$path=Join-Path $PSScriptRoot 'NamedRegistryRecoveryNative.cs'
$bytes=[IO.File]::ReadAllBytes($path);$hash=Get-WelaRecoveryHash $bytes
if ('Wela.NamedRegistryRecovery.Key' -as [type]) {
if ([Wela.NamedRegistryRecovery.Key]::SourceSha256 -cne $hash) {throw 'Loaded named-registry native source differs; start a fresh process.'}
return
}
$source=(New-Object Text.UTF8Encoding($false,$true)).GetString($bytes).Replace('__WELA_SOURCE_SHA256__',$hash)
Add-Type -TypeDefinition $source -ErrorAction Stop
}
function Assert-WelaNamedRecoveryValue {
param($State)
if ($State.KeyExists -isnot [bool] -or $State.ValueExists -isnot [bool]) {throw 'Logging registry state requires typed existence flags.'}
if ($State.ValueExists) {
if (-not $State.KeyExists -or $State.Type -cne 'DWord' -or ($State.Value -isnot [int] -and $State.Value -isnot [long]) -or $State.Value -notin @(0,1)) {throw 'Only prior DWORD 0/1 or value absence is supported.'}
} elseif ($null -ne $State.Value -or $null -ne $State.Type) {throw 'Absent logging value has inconsistent state.'}
}
function Get-WelaNamedRecoveryGuard {
param($Observation)
[pscustomobject][ordered]@{ObjectName=$Observation.ObjectName;OtherValues=$Observation.OtherValues;Children=$Observation.Children;Security=$Observation.Security}
}
function Get-WelaNamedRecoveryState {
param($Observation)
[pscustomobject]@{KeyExists=$true;ValueExists=[bool]$Observation.Exists;Value=$(if ($Observation.Exists) {[int]$Observation.Value} else {$null});Type=$(if ($Observation.Exists) {'DWord'} else {$null})}
}
function Open-WelaNamedRecoveryKey {
param($Target,[bool]$Write=$false)
$known=@(Get-WelaNamedRecoveryCatalog | Where-Object {$_.Path -ceq $Target.Path -and $_.Name -ceq $Target.Name})
if ($known.Count -ne 1) {throw 'Unknown logging recovery target.'}
Initialize-WelaNamedRecoveryNative
[Wela.NamedRegistryRecovery.Key]::new($Target.Path,$Write)
}
function Get-WelaNamedRecoveryObservation {
param($Target)
$key=Open-WelaNamedRecoveryKey $Target
try {
$observation=$key.Read($Target.Name)
if ($observation.ObjectName -ine ('\REGISTRY\MACHINE\'+$Target.Path.Substring(6))) {throw 'Native registry name does not match the selected path.'}
$observation
} finally {$key.Dispose()}
}
function Assert-WelaNamedRecoveryGuard {
param($Control,$Observation)
if ((Get-WelaRecoveryKey (Get-WelaNamedRecoveryGuard $Observation)) -cne (Get-WelaRecoveryKey $Control.RegistryGuard)) {throw 'Logging registry path, other values, children or security changed since planning.'}
}
function Set-WelaNamedRecoveryValue {
param($Control)
$key=Open-WelaNamedRecoveryKey $Control.Target $true
try {
$before=$key.Read($Control.Target.Name)
Assert-WelaNamedRecoveryGuard $Control $before
if ((Get-WelaRecoveryKey (Get-WelaNamedRecoveryState $before)) -cne (Get-WelaRecoveryKey $Control.Expected)) {throw 'Logging value changed before recovery.'}
$value=if ($Control.RecoverTo.ValueExists) {[int]$Control.RecoverTo.Value} else {0}
$after=$key.Restore($Control.Target.Name,$before,$Control.RecoverTo.ValueExists,$value)
Assert-WelaNamedRecoveryGuard $Control $after
# Reopen the selected path after the handle-based write to detect visible path drift.
$fresh=Get-WelaNamedRecoveryObservation $Control.Target
Assert-WelaNamedRecoveryGuard $Control $fresh
if ((Get-WelaRecoveryKey (Get-WelaNamedRecoveryState $fresh)) -cne (Get-WelaRecoveryKey $Control.RecoverTo)) {throw 'Reopened logging value differs after recovery.'}
} finally {$key.Dispose()}
}