mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-24 23:18:11 +02:00
Verify reviewed descendant SACL propagation and preservation (#429)
* Verify reviewed descendant SACL propagation and preservation * Reference descendant SACL PR429 in release notes * Prepare protected disposable SACL fixtures through native handles * Use read-control handles for disposable native SACL protection
This commit is contained in:
1 parent
b84b97b358
commit
bcd4e9717e
15 files changed
+522
-17
No files matched your search
@@ -0,0 +1,122 @@
|
||||
# Bounded observations only. Descendants are never supplied to the native writer.
|
||||
function Get-WelaSelectedSaclChildNames {
|
||||
param($Definition,$Snapshot,[int]$Maximum)
|
||||
$path=Resolve-WelaSelectedSaclNativePath $Definition
|
||||
Initialize-WelaSelectedSaclNative
|
||||
$privilege=New-Object Wela.SelectedSacl.Privilege;$target=$null
|
||||
try {
|
||||
$target=New-Object Wela.SelectedSacl.Target($Definition.Kind,$path,$true)
|
||||
$before=$target.Read()
|
||||
if((Get-WelaSelectedSaclSnapshotKey $before) -cne (Get-WelaSelectedSaclSnapshotKey $Snapshot)){throw 'Container changed before enumeration.'}
|
||||
$children=$target.Enumerate($Maximum)
|
||||
if((Get-WelaSelectedSaclSnapshotKey ($target.Read())) -cne (Get-WelaSelectedSaclSnapshotKey $before)){throw 'Container changed during enumeration.'}
|
||||
$children
|
||||
} finally {if($target){$target.Dispose()};$privilege.Dispose()}
|
||||
}
|
||||
function New-WelaSelectedSaclChildDefinition {
|
||||
param([string]$Kind,[string]$Path)
|
||||
[pscustomobject]@{Kind=$Kind;Path=$(if($Kind -eq 'Registry'){'Registry::'+$Path}else{$Path});Resolution='Resolved'}
|
||||
}
|
||||
function Get-WelaSelectedSaclDescendantKey {
|
||||
param($Inventory)
|
||||
if($null -eq $Inventory -or $Inventory.Status -cne 'Complete' -or $Inventory.Maximum -ne 128 -or $Inventory.MaximumDepth -ne 16 -or $Inventory.Entries -isnot [array] -or $Inventory.Entries.Count -gt 128){throw 'Descendant capture is incomplete or has unknown limits; review a new plan.'}
|
||||
$fields=@('128','16',(Get-WelaSelectedSaclSnapshotKey $Inventory.Root))
|
||||
foreach($entry in $Inventory.Entries){
|
||||
if($entry.ProtectedBarrier -isnot [bool] -or $entry.Depth -lt 1 -or $entry.Depth -gt 16 -or $entry.Path -cne $entry.Snapshot.Path){throw 'Malformed descendant evidence.'}
|
||||
$fields+=@($entry.Path,$entry.ParentPath,[string]$entry.Depth,[string]$entry.ProtectedBarrier,(Get-WelaSelectedSaclSnapshotKey $entry.Snapshot))
|
||||
}
|
||||
Get-WelaSelectedSaclHash $fields
|
||||
}
|
||||
function Get-WelaSelectedSaclDescendants {
|
||||
param($Definition,$RootSnapshot)
|
||||
$entries=New-Object 'System.Collections.Generic.List[object]'
|
||||
$diagnostics=New-Object 'System.Collections.Generic.List[string]'
|
||||
$queue=New-Object 'System.Collections.Generic.Queue[object]'
|
||||
$queue.Enqueue([pscustomobject]@{Definition=$Definition;Snapshot=$RootSnapshot;Depth=0;ProtectedBarrier=$false})
|
||||
$seen=New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
|
||||
$null=$seen.Add($RootSnapshot.Path)
|
||||
$identities=New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::Ordinal)
|
||||
if($RootSnapshot.Kind -eq 'FileSystem'){$null=$identities.Add($RootSnapshot.Identity)}
|
||||
$started=[DateTime]::UtcNow;$bytes=0
|
||||
try {
|
||||
while($queue.Count){
|
||||
if(([DateTime]::UtcNow-$started).TotalSeconds -gt 30){throw 'Descendant scan time budget exceeded (individual native reads are not cancellable).'}
|
||||
$parent=$queue.Dequeue()
|
||||
if($parent.Snapshot.Kind -ne 'Registry' -and -not $parent.Snapshot.IsDirectory){continue}
|
||||
$remaining=128-$entries.Count
|
||||
$children=Get-WelaSelectedSaclChildNames $parent.Definition $parent.Snapshot ([Math]::Max(1,$remaining))
|
||||
if($children.Truncated -or @($children.Names).Count -gt $remaining){throw 'Descendant count exceeds the reviewed maximum of 128.'}
|
||||
if($parent.Depth -ge 16 -and @($children.Names).Count){throw 'Descendant depth exceeds the reviewed maximum of 16.'}
|
||||
foreach($name in $children.Names){
|
||||
if([string]::IsNullOrEmpty($name) -or $name -in @('.','..') -or $name -match '[\\/\x00-\x1f]' -or ($parent.Snapshot.Kind -eq 'FileSystem' -and $name -match '[:*?<>|]|[ .]$')){throw 'Ambiguous native descendant name.'}
|
||||
$path=$parent.Snapshot.Path.TrimEnd('\')+'\'+$name
|
||||
if(-not $seen.Add($path)){throw 'Duplicate descendant path during enumeration.'}
|
||||
$childDefinition=New-WelaSelectedSaclChildDefinition $Definition.Kind $path
|
||||
$snapshot=Get-WelaSelectedSaclSnapshot $childDefinition
|
||||
if($snapshot.Path -ine $path -or $snapshot.Kind -cne $Definition.Kind){throw 'Descendant snapshot does not identify the enumerated child.'}
|
||||
$null=Get-WelaSelectedSaclSnapshotKey $snapshot
|
||||
if($snapshot.Kind -eq 'FileSystem' -and -not $identities.Add($snapshot.Identity)){throw 'Repeated file identity (hard link/alias) prevents unique descendant attribution.'}
|
||||
$bytes+=[Text.Encoding]::UTF8.GetByteCount(($snapshot|ConvertTo-Json -Depth 12 -Compress))
|
||||
if($bytes -gt 2097152){throw 'Descendant snapshot evidence exceeds 2 MiB.'}
|
||||
$barrier=$parent.ProtectedBarrier -or (($snapshot.ControlFlags -band 8192) -ne 0)
|
||||
$entry=[pscustomobject]@{Path=$path;ParentPath=$parent.Snapshot.Path;Depth=$parent.Depth+1;ProtectedBarrier=[bool]$barrier;Snapshot=$snapshot}
|
||||
$entries.Add($entry)
|
||||
$queue.Enqueue([pscustomobject]@{Definition=$childDefinition;Snapshot=$snapshot;Depth=$entry.Depth;ProtectedBarrier=[bool]$barrier})
|
||||
}
|
||||
}
|
||||
# A second pass by the caller verifies membership and descriptor stability.
|
||||
}catch{$diagnostics.Add($_.Exception.Message)}
|
||||
[pscustomobject]@{Status=$(if($diagnostics.Count){'Incomplete'}else{'Complete'});Maximum=128;MaximumDepth=16;StartedUtc=$started.ToString('o');CompletedUtc=[DateTime]::UtcNow.ToString('o');Root=$RootSnapshot;Entries=@($entries.ToArray());Diagnostics=@($diagnostics.ToArray())}
|
||||
}
|
||||
function Get-WelaSelectedSaclStableDescendants {
|
||||
param($Definition,$Snapshot)
|
||||
$first=Get-WelaSelectedSaclDescendants $Definition $Snapshot
|
||||
if($first.Status -ne 'Complete'){return $first}
|
||||
$fresh=Get-WelaSelectedSaclSnapshot $Definition
|
||||
$second=Get-WelaSelectedSaclDescendants $Definition $fresh
|
||||
if($second.Status -eq 'Complete' -and (Get-WelaSelectedSaclDescendantKey $first) -cne (Get-WelaSelectedSaclDescendantKey $second)){
|
||||
$second.Status='Incomplete';$second.Diagnostics=@('Descendant membership, identity or descriptor changed between captures.')
|
||||
}
|
||||
$second
|
||||
}
|
||||
function Assert-WelaSelectedSaclDescendantPreservation {
|
||||
param($Before,$After,[bool]$Protected)
|
||||
if($Before.Kind -cne $After.Kind -or $Before.Path -cne $After.Path -or $Before.IsDirectory -ne $After.IsDirectory -or $Before.SecurityInformation -ne $After.SecurityInformation -or $Before.DescriptorScope -cne $After.DescriptorScope){throw 'Child identity/type or descriptor scope changed.'}
|
||||
# Registry identity incorporates last-write time and therefore can change as part of an ACL update.
|
||||
if($Before.Kind -eq 'FileSystem' -and $Before.Identity -cne $After.Identity){throw 'Child file identity changed.'}
|
||||
if($Before.Owner -cne $After.Owner -or $Before.Group -cne $After.Group -or $Before.DaclBase64 -cne $After.DaclBase64 -or ($Before.ControlFlags -band (-bnot 2576)) -ne ($After.ControlFlags -band (-bnot 2576))){throw 'Child owner/group/DACL/protection or non-SACL controls changed.'}
|
||||
if($Protected -and $Before.DescriptorBase64 -cne $After.DescriptorBase64){throw 'Protected child or protected subtree descriptor changed.'}
|
||||
$counts=New-Object 'System.Collections.Generic.Dictionary[string,int]' ([StringComparer]::Ordinal)
|
||||
foreach($entry in $After.Aces){if(-not $counts.ContainsKey($entry.Binary)){$counts[$entry.Binary]=0};$counts[$entry.Binary]++}
|
||||
foreach($entry in $Before.Aces){if(-not $counts.ContainsKey($entry.Binary) -or $counts[$entry.Binary] -lt 1){throw 'Original child audit/unknown ACE changed or disappeared.'};$counts[$entry.Binary]--}
|
||||
# Arbitrary new explicit/unknown ACEs cannot be attributed to inheritance.
|
||||
foreach($entry in $After.Aces){if($counts[$entry.Binary] -gt 0 -and (-not $entry.Ordinary -or $entry.Type -ne 2 -or ($entry.Flags -band 16) -eq 0)){throw 'Unexplained explicit or unknown child ACE appeared.'}}
|
||||
}
|
||||
function Test-WelaSelectedSaclDescendantOutcomes {
|
||||
param($Before,$After,$Ace)
|
||||
$outcomes=New-Object 'System.Collections.Generic.List[object]'
|
||||
$diagnostics=New-Object 'System.Collections.Generic.List[string]'
|
||||
if($After.Status -ne 'Complete'){$diagnostics.Add('After-state inventory is incomplete: '+($After.Diagnostics -join '; '))}
|
||||
$map=@{};foreach($entry in $After.Entries){$map[$entry.Path]=$entry}
|
||||
foreach($entry in $Before.Entries){
|
||||
$status='Unverified';$message='';$actual=$null
|
||||
try {
|
||||
if(-not $map.ContainsKey($entry.Path)){throw 'Reviewed descendant disappeared or could not be observed.'}
|
||||
$actual=$map[$entry.Path];$map.Remove($entry.Path)
|
||||
if($actual.ParentPath -cne $entry.ParentPath -or $actual.Depth -ne $entry.Depth -or $actual.ProtectedBarrier -ne $entry.ProtectedBarrier){throw 'Child topology or inheritance protection changed.'}
|
||||
Assert-WelaSelectedSaclDescendantPreservation $entry.Snapshot $actual.Snapshot $entry.ProtectedBarrier
|
||||
if($entry.ProtectedBarrier){$status='ProtectedUnchanged'}
|
||||
elseif(($Ace.Flags -band 3) -eq 0){$status='PreservedWithoutRequestedInheritance'}
|
||||
else {
|
||||
$flags=($Ace.Flags -band 192) -bor 16
|
||||
if($actual.Snapshot.Kind -eq 'Registry' -or $actual.Snapshot.IsDirectory){$flags=$flags -bor ($Ace.Flags -band 3)}
|
||||
$expected=[pscustomobject]@{Sid=$Ace.Sid;Mask=$Ace.Mask;Flags=$flags}
|
||||
if(-not (Test-WelaSelectedSaclAce $actual.Snapshot $expected)){throw 'Requested inherited audit ACE was not observed; propagation may be incomplete or blocked.'}
|
||||
$status='InheritedAceObserved'
|
||||
}
|
||||
}catch{$message=$_.Exception.Message;$diagnostics.Add($entry.Path+': '+$message)}
|
||||
$outcomes.Add([pscustomobject]@{Path=$entry.Path;Status=$status;Diagnostic=$message;Before=$entry.Snapshot;After=$(if($actual){$actual.Snapshot}else{$null})})
|
||||
}
|
||||
foreach($entry in $map.Values){$outcomes.Add([pscustomobject]@{Path=$entry.Path;Status='NewUnreviewedChild';Diagnostic='Child appeared after the reviewed snapshot; no pre-write backup or ownership established.';Before=$null;After=$entry.Snapshot});$diagnostics.Add('New unreviewed descendant: '+$entry.Path)}
|
||||
[pscustomobject]@{Status=$(if($diagnostics.Count){'Unverified'}else{'Observed'});Scope='Reviewed existing descendants at the recorded observations only; propagation is non-atomic. Registry recreation between post-write observations cannot be excluded by last-write metadata.';Ownership='No descendant ACE ownership or automatic rollback authority.';Outcomes=@($outcomes.ToArray());Diagnostics=@($diagnostics.ToArray())}
|
||||
}
|
||||
Reference in new issue
Block a user