feat: checkpoint reviewed native registry SACL recovery

This commit is contained in:
Shirofune-Security committed 2026-09-22 10:11:02 +09:00
1 parent 03039cb1c6
commit 530b49f26b
7 files changed
+592

No files matched your search

+183
View File
@@ -0,0 +1,183 @@
# One proven selected-root audit ACE, only with empty historical/current descendants.
function Get-WelaRegistryRecoveryKey {param($Value) ConvertTo-Json -InputObject $Value -Depth 32 -Compress}
function Assert-WelaRegistryRecoveryText {param($Value,[string[]]$Names) foreach($name in $Names){if($Value.$name -isnot [string]){throw ('Missing or mistyped registry recovery text: '+$name)}}}
function Assert-WelaRegistryRecoveryNumber {param($Value) if($Value -isnot [int] -and $Value -isnot [long]){throw 'Registry recovery requires an integer.'}}
function Initialize-WelaRegistryRecoveryNative {
$bytes=[IO.File]::ReadAllBytes((Join-Path $PSScriptRoot 'RegistrySaclRecoveryNative.cs'));$hash=Get-WelaArrivalHash $bytes
if(-not ('Wela.RegistrySaclRecovery.Descriptor' -as [type])){
$source=[Text.UTF8Encoding]::new($false,$true).GetString($bytes).TrimStart([char]0xfeff);$marker='__WELA_REGISTRY_SACL_RECOVERY_SOURCE_SHA256__'
if(($source.Split(@($marker),[StringSplitOptions]::None)).Count -ne 2){throw 'Unexpected native registry recovery source binding.'}
Add-Type -TypeDefinition $source.Replace($marker,$hash) -ErrorAction Stop
}
if([Wela.RegistrySaclRecovery.Descriptor]::SourceSha256 -cne $hash){throw 'Loaded registry recovery code differs; start a fresh PowerShell process.'}
}
function Get-WelaRegistryRecoverySources {
$sources=[ordered]@{}
foreach($name in @('WELA.ps1','scripts/RegistrySaclRecovery.ps1','scripts/RegistrySaclRecoveryNative.cs','scripts/SelectedSaclConfiguration.ps1','scripts/SelectedSaclNative.cs','scripts/SelectedSaclDescendants.ps1','scripts/TargetedSaclPlanning.ps1','scripts/Configuration.ps1','scripts/ControlApplicability.ps1','scripts/CustomAuditProfiles.ps1','scripts/AuditRecovery.ps1','scripts/EvtxRecovery.ps1','scripts/WefArrival.ps1','scripts/WecUpdate.ps1','modules/AuditProfiles.psm1','modules/AuditCatalog.psm1','modules/NativeProviders.psm1','config/audit_profiles.json','config/audit_sacl_targets.json','config/control_applicability.json','config/baselines.json')){
$sources[$name]=(Get-FileHash -LiteralPath (Join-Path $script:ScriptRoot $name) -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()
}
[pscustomobject]$sources
}
function Get-WelaRegistryRecoveryContext {
if([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or -not [Environment]::Is64BitProcess){throw 'Registry SACL recovery requires native 64-bit Windows.'}
foreach($name in @('Winmgmt','EventLog')){if((Get-Service -Name $name -ErrorAction Stop).Status -ne 'Running'){throw 'Native observation services must already be running.'}}
Initialize-WelaRegistryRecoveryNative
$token=[Wela.RegistrySaclRecovery.TokenReader]::Snapshot();$identity=[Security.Principal.WindowsIdentity]::GetCurrent()
try{if(-not ([Security.Principal.WindowsPrincipal]::new($identity)).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)){throw 'Registry SACL recovery requires an elevated operator.'}}finally{$identity.Dispose()}
$actualMasks=Get-WelaEffectiveAuditPolicy;$masks=[ordered]@{};foreach($id in @($actualMasks.Keys|Sort-Object)){$masks[$id]=$actualMasks[$id]}
[pscustomobject][ordered]@{Host=(Get-WelaRecoveryHost);Selected=(Get-WelaSelectedSaclContext);Token=$token;AuditMasks=$masks;Precedence=(Get-WelaRegistryState 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' SCENoApplyLegacyAuditPolicy)}
}
function Read-WelaRegistryRecoveryInput {
param([string]$Path)
$file=Read-WelaWecUpdateFile $Path 4194304
if(-not $file.Text){throw 'Original recovery evidence is empty.'}
[pscustomobject]@{Path=$file.Path;Sha256=$file.Hash;Data=(ConvertFrom-WelaEvtxJson $file.Text)}
}
function Assert-WelaRegistryRecoverySnapshot {
param($Snapshot,$Definition)
Assert-WelaEvtxObject $Snapshot @('Path','Kind','Identity','IsDirectory','DescriptorBase64','Owner','Group','DaclBase64','ControlFlags','SecurityInformation','DescriptorScope','Aces')
Assert-WelaRegistryRecoveryText $Snapshot @('Path','Kind','Identity','DescriptorBase64','DescriptorScope')
$path=Resolve-WelaSelectedSaclNativePath $Definition
if($Snapshot.Kind -cne 'Registry' -or $Snapshot.Path -cne $path -or $Snapshot.IsDirectory -isnot [bool] -or $Snapshot.IsDirectory -or $Snapshot.Identity -cnotmatch ('^'+[regex]::Escape($path)+':[0-9]+$') -or $Snapshot.Aces -isnot [array]){throw 'Only exact historical registry snapshots are supported.'}
foreach($name in @('ControlFlags','SecurityInformation')){Assert-WelaRegistryRecoveryNumber $Snapshot.$name}
foreach($ace in $Snapshot.Aces){
Assert-WelaEvtxObject $ace @('Binary','Type','Flags','Mask','Sid','Ordinary');Assert-WelaRegistryRecoveryText $ace @('Binary')
if($ace.Ordinary -isnot [bool] -or ($null -ne $ace.Sid -and $ace.Sid -isnot [string])){throw 'Mistyped historical ACE metadata.'}
foreach($name in @('Type','Flags','Mask')){Assert-WelaRegistryRecoveryNumber $ace.$name}
}
Initialize-WelaRegistryRecoveryNative
$parsed=[Wela.RegistrySaclRecovery.Descriptor]::Observe($Snapshot.Path,$Snapshot.Identity,[Convert]::FromBase64String($Snapshot.DescriptorBase64))
if((Get-WelaSelectedSaclSnapshotKey $parsed) -cne (Get-WelaSelectedSaclSnapshotKey $Snapshot)){throw 'Historical metadata differs from its native descriptor bytes.'}
}
function Assert-WelaRegistryRecoveryEmpty {
param($Inventory,$Root)
Assert-WelaEvtxObject $Inventory @('Status','Maximum','MaximumDepth','StartedUtc','CompletedUtc','Root','Entries','Diagnostics')
Assert-WelaRegistryRecoveryText $Inventory @('Status');Assert-WelaRegistryRecoveryNumber $Inventory.Maximum;Assert-WelaRegistryRecoveryNumber $Inventory.MaximumDepth
if($Inventory.Status -cne 'Complete' -or $Inventory.Maximum -ne 128 -or $Inventory.MaximumDepth -ne 16 -or $Inventory.Entries -isnot [array] -or $Inventory.Entries.Count -ne 0 -or $Inventory.Diagnostics -isnot [array] -or $Inventory.Diagnostics.Count -ne 0 -or (Get-WelaSelectedSaclSnapshotKey $Inventory.Root) -cne (Get-WelaSelectedSaclSnapshotKey $Root)){throw 'Recovery requires complete exact empty historical/current descendants.'}
$start=ConvertTo-WelaArrivalUtc $Inventory.StartedUtc;$end=ConvertTo-WelaArrivalUtc $Inventory.CompletedUtc
if($start -gt $end -or $end -gt [DateTimeOffset]::UtcNow.AddMinutes(1)){throw 'Descendant timestamps are invalid.'}
}
function Assert-WelaRegistryRecoveryDescendantOutcome {
param($Value)
Assert-WelaEvtxObject $Value @('Status','Scope','Ownership','Outcomes','Diagnostics');Assert-WelaRegistryRecoveryText $Value @('Status','Scope','Ownership')
if($Value.Status -cne 'Observed' -or $Value.Outcomes -isnot [array] -or $Value.Outcomes.Count -ne 0 -or $Value.Diagnostics -isnot [array] -or $Value.Diagnostics.Count -ne 0){throw 'Historical descendant verification is incomplete or nonempty.'}
}
function Get-WelaRegistryRecoverySnapshot {
param($Definition)
if($Definition.Kind -isnot [string] -or $Definition.Kind -cne 'Registry'){throw 'Only registry targets are supported.'}
Initialize-WelaRegistryRecoveryNative;$target=[Wela.RegistrySaclRecovery.Target]::new((Resolve-WelaSelectedSaclNativePath $Definition))
try{$target.Read()}finally{$target.Dispose()}
}
function Get-WelaRegistryRecoveryAddition {param($Before,$After,$Ace) Initialize-WelaRegistryRecoveryNative;[Wela.RegistrySaclRecovery.Descriptor]::AddedAce($Before.DescriptorBase64,$After.DescriptorBase64,$Ace.Sid,$Ace.Mask,$Ace.Flags)}
function New-WelaRegistryRecoveryPlan {
param([string]$OriginalPlanPath,[string]$PendingPath,[string]$ConfirmedPath,[string]$ResultsPath)
$context=Get-WelaRegistryRecoveryContext;$sources=Get-WelaRegistryRecoverySources
$files=[ordered]@{};foreach($entry in @(@('OriginalPlan',$OriginalPlanPath),@('Pending',$PendingPath),@('Confirmed',$ConfirmedPath),@('Results',$ResultsPath))){$files[$entry[0]]=Read-WelaRegistryRecoveryInput $entry[1]}
if(@($files.Values.Path|Sort-Object -Unique).Count -ne 4){throw 'Four distinct original evidence files are required.'}
$plan=$files.OriginalPlan.Data;$pending=$files.Pending.Data;$confirmed=$files.Confirmed.Data;$result=$files.Results.Data
$planFields=@('SchemaVersion','Kind','CapturedUtc','Profile','IncludeOptional','IncludeChildren','Context','Sources','Rows','GenerationReadiness','UsableRuleCredit','Catalog','UserInventory')
$rowFields=@('Id','DefinitionKey','Definition','Before','Ace','Status','Diagnostic','After','DescendantsBefore','DescendantsAfter','DescendantVerification')
Assert-WelaEvtxObject $plan $planFields
foreach($value in @($plan,$pending,$confirmed,$result)){Assert-WelaRegistryRecoveryNumber $value.SchemaVersion;if($value.SchemaVersion -ne 1){throw 'Unsupported original schema.'};Assert-WelaRegistryRecoveryText $value @('Kind')}
Assert-WelaRegistryRecoveryText $plan @('Profile','GenerationReadiness')
if($plan.Kind -cne 'WelaSelectedSaclPlan' -or $plan.IncludeChildren -isnot [bool] -or -not $plan.IncludeChildren -or $plan.IncludeOptional -isnot [bool] -or $plan.Rows -isnot [array] -or $plan.Rows.Count -ne 1 -or $plan.Catalog -isnot [array] -or $plan.Catalog.Count -ne 0){throw 'Require one original selected registry target with explicit child consent.'}
$row=$plan.Rows[0];Assert-WelaEvtxObject $row $rowFields;Assert-WelaRegistryRecoveryText $row @('Id','DefinitionKey','Status','Diagnostic')
Assert-WelaRegistryRecoveryText $row.Definition @('Kind','Path','Inheritance','Propagation')
if($row.Status -cne 'ChangeRequired' -or $row.Diagnostic -cne '' -or $null -ne $row.After -or $null -ne $row.DescendantsAfter -or $null -ne $row.DescendantVerification -or $row.Id -cnotmatch '^sacl-[a-f0-9]{24}$' -or $row.Definition.Kind -cne 'Registry' -or $row.Definition.Inheritance -cnotin @('None','ContainerInherit') -or $row.Definition.Propagation -cne 'None'){throw 'Original plan is not one supported registry root audit addition.'}
Assert-WelaSelectedSaclSources $plan.Sources
Assert-WelaRegistryRecoveryText $plan.Context @('Key','Computer')
if((Get-WelaRegistryRecoveryKey $plan.Context) -cne (Get-WelaRegistryRecoveryKey $context.Selected) -or $plan.Context.Computer -cne $context.Host.Computer){throw 'Original host context differs from the actual recovery host.'}
$catalog=Get-WelaSelectedSaclCatalog -Profile $plan.Profile -IncludeOptional:$plan.IncludeOptional -Context $context.Selected
$selection=@($catalog.Rows|Where-Object Id -CEQ $row.Id)
if($selection.Count -ne 1 -or $selection[0].DefinitionKey -cne $row.DefinitionKey -or (Get-WelaSelectedSaclDefinitionKey $row.Definition) -cne $row.DefinitionKey -or (Get-WelaRegistryRecoveryKey $selection[0].Definition) -cne (Get-WelaRegistryRecoveryKey $row.Definition)){throw 'Original target is not the exact currently source-bound catalog selection.'}
Assert-WelaRegistryRecoverySnapshot $row.Before $row.Definition;Assert-WelaRegistryRecoveryEmpty $row.DescendantsBefore $row.Before
$ace=Get-WelaSelectedSaclAce $row.Definition $row.Before -IncludeChildren
Assert-WelaEvtxObject $row.Ace @('Sid','Mask','Flags','RequiredPolicyMask');Assert-WelaRegistryRecoveryText $row.Ace @('Sid');foreach($name in @('Mask','Flags','RequiredPolicyMask')){Assert-WelaRegistryRecoveryNumber $row.Ace.$name}
if((Get-WelaRegistryRecoveryKey $ace) -cne (Get-WelaRegistryRecoveryKey $row.Ace) -or $ace.Flags -notin @(64,128,192,66,130,194) -or (Test-WelaSelectedSaclAce $row.Before $ace)){throw 'Original ACE is mistyped, inherited or already covered.'}
$receiptFields=@('SchemaVersion','Kind','State','RecordedUtc','Computer','ContextKey','Id','Sources','Definition','Before','Ace','After','DescendantsBefore','DescendantsAfter','DescendantVerification','Ownership')
foreach($receipt in @($pending,$confirmed)){
Assert-WelaEvtxObject $receipt $receiptFields;Assert-WelaRegistryRecoveryText $receipt @('Kind','State','Computer','ContextKey','Id','Ownership')
if($receipt.Kind -cne 'WelaSelectedSaclReceipt' -or $receipt.Computer -cne $context.Host.Computer -or $receipt.ContextKey -cne $context.Selected.Key -or $receipt.Id -cne $row.Id -or $receipt.Ownership -cne 'Only the verified explicit selected-root addition; never descendant ACE ownership or bulk rollback authority.'){throw 'Original receipt scope or ownership differs.'}
Assert-WelaSelectedSaclSources $receipt.Sources
foreach($name in @('Definition','Ace')){if((Get-WelaRegistryRecoveryKey $receipt.$name) -cne (Get-WelaRegistryRecoveryKey $row.$name)){throw 'Original receipt differs from selected plan.'}}
Assert-WelaRegistryRecoverySnapshot $receipt.Before $row.Definition
if((Get-WelaSelectedSaclSnapshotKey $receipt.Before) -cne (Get-WelaSelectedSaclSnapshotKey $row.Before)){throw 'Original before-state differs across records.'}
Assert-WelaRegistryRecoveryEmpty $receipt.DescendantsBefore $receipt.Before
}
if($pending.State -cne 'Pending' -or $null -ne $pending.After -or $null -ne $pending.DescendantsAfter -or $null -ne $pending.DescendantVerification -or $confirmed.State -cne 'Confirmed' -or $null -eq $confirmed.After -or (ConvertTo-WelaArrivalUtc $pending.RecordedUtc) -ne (ConvertTo-WelaArrivalUtc $confirmed.RecordedUtc)){throw 'A matching original Pending and Confirmed pair is required.'}
Assert-WelaRegistryRecoverySnapshot $confirmed.After $row.Definition;Assert-WelaRegistryRecoveryEmpty $confirmed.DescendantsAfter $confirmed.After;Assert-WelaRegistryRecoveryDescendantOutcome $confirmed.DescendantVerification
# Last-write metadata can change during the original SACL write; current state must match its exact observed After.
$added=Get-WelaRegistryRecoveryAddition $row.Before $confirmed.After $ace
Assert-WelaEvtxObject $result @('SchemaVersion','Kind','ExitCode','DryRun','BackupPath','Plan','Results','GenerationReadiness','UsableRuleCredit');Assert-WelaRegistryRecoveryNumber $result.ExitCode;Assert-WelaRegistryRecoveryText $result @('BackupPath','GenerationReadiness')
if($result.Kind -cne 'WelaSelectedSaclResult' -or $result.ExitCode -ne 0 -or $result.DryRun -isnot [bool] -or $result.DryRun -or $result.Results -isnot [array] -or $result.Results.Count -ne 1){throw 'Require one completed successful, non-dry-run operation.'}
$applied=$result.Results[0];Assert-WelaEvtxObject $applied $rowFields;Assert-WelaRegistryRecoveryText $applied @('Id','DefinitionKey','Status','Diagnostic')
Assert-WelaEvtxObject $result.Plan $planFields;Assert-WelaRegistryRecoveryText $result.Plan @('Kind')
if($applied.Status -cne 'Applied' -or $applied.Diagnostic -cne '' -or $result.Plan.Kind -cne 'WelaSelectedSaclPlan' -or $result.Plan.Rows -isnot [array] -or $result.Plan.Rows.Count -ne 1 -or (Get-WelaRegistryRecoveryKey $applied) -cne (Get-WelaRegistryRecoveryKey $result.Plan.Rows[0]) -or $applied.Id -cne $row.Id -or $applied.DefinitionKey -cne $row.DefinitionKey){throw 'Completed result status, rows or scope disagree.'}
foreach($name in @('Definition','Ace')){if((Get-WelaRegistryRecoveryKey $applied.$name) -cne (Get-WelaRegistryRecoveryKey $row.$name)){throw 'Completed selection differs from original plan.'}}
foreach($name in @('Before','After')){Assert-WelaRegistryRecoverySnapshot $applied.$name $row.Definition;if((Get-WelaSelectedSaclSnapshotKey $applied.$name) -cne (Get-WelaSelectedSaclSnapshotKey $confirmed.$name)){throw 'Completed descriptor evidence disagrees.'}}
Assert-WelaRegistryRecoveryEmpty $applied.DescendantsBefore $applied.Before;Assert-WelaRegistryRecoveryEmpty $applied.DescendantsAfter $applied.After;Assert-WelaRegistryRecoveryDescendantOutcome $applied.DescendantVerification
foreach($name in @('Profile','IncludeOptional','IncludeChildren','Context','Sources')){if((Get-WelaRegistryRecoveryKey $result.Plan.$name) -cne (Get-WelaRegistryRecoveryKey $plan.$name)){throw 'Completed plan context differs from original selection.'}}
$backup=Resolve-WelaArrivalPath $result.BackupPath
if($files.Pending.Path -ine (Join-Path $backup ($row.Id+'.pending.json')) -or $files.Confirmed.Path -ine (Join-Path $backup ($row.Id+'.confirmed.json'))){throw 'Receipt paths do not match recorded backup directory.'}
$originalTime=ConvertTo-WelaArrivalUtc $plan.CapturedUtc;$configuredTime=ConvertTo-WelaArrivalUtc $result.Plan.CapturedUtc;$receiptTime=ConvertTo-WelaArrivalUtc $pending.RecordedUtc
if($originalTime -gt $configuredTime -or $configuredTime -gt $receiptTime -or $receiptTime -gt [DateTimeOffset]::UtcNow.AddMinutes(1)){throw 'Original timestamps are reversed or in the future.'}
$current=Get-WelaRegistryRecoverySnapshot $row.Definition
if((Get-WelaSelectedSaclSnapshotKey $current) -cne (Get-WelaSelectedSaclSnapshotKey $confirmed.After)){throw 'Current registry last-write identity or full descriptor differs from completed After; value changes also require manual assessment.'}
$inputFiles=[ordered]@{};foreach($name in $files.Keys){$inputFiles[$name]=[pscustomobject]@{Path=$files[$name].Path;Sha256=$files[$name].Sha256}}
$recovery=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaRegistrySaclRecoveryPlan';Id=$row.Id;Context=$context;Sources=$sources;OriginalFiles=[pscustomobject]$inputFiles;Definition=$row.Definition;BeforeAddition=$row.Before;Expected=$current;AddedAce=$added;RequiresAuditReductionConsent=$true;RequiresInheritanceConsent=$true;HistoricalBinding='Original version1 records contain host context/source hashes, not authenticated historical operator identity. Registry path/last-write metadata cannot prove durable key identity.';Outcome='Remove one proven explicit registry-root audit ACE; preserve all other descriptor components and ACE order. Empty or null present SACL can remain. Empty-child observations are not an atomic tree guarantee.';ReadyRuleCredit=0}
Assert-WelaRegistryRecoveryFresh $recovery
$recovery
}
function Assert-WelaRegistryRecoveryBindings {
param($Plan)
if((Get-WelaRegistryRecoveryKey (Get-WelaRegistryRecoverySources)) -cne (Get-WelaRegistryRecoveryKey $Plan.Sources) -or (Get-WelaRegistryRecoveryKey (Get-WelaRegistryRecoveryContext)) -cne (Get-WelaRegistryRecoveryKey $Plan.Context)){throw 'Recovery source, host, logon, token or audit policy changed.'}
foreach($entry in $Plan.OriginalFiles.PSObject.Properties){if((Read-WelaRegistryRecoveryInput $entry.Value.Path).Sha256 -cne $entry.Value.Sha256){throw 'Original recovery evidence changed.'}}
}
function Assert-WelaRegistryRecoveryFresh {param($Plan) Assert-WelaRegistryRecoveryBindings $Plan;if((Get-WelaSelectedSaclSnapshotKey (Get-WelaRegistryRecoverySnapshot $Plan.Definition)) -cne (Get-WelaSelectedSaclSnapshotKey $Plan.Expected)){throw 'Reviewed registry identity/descriptor changed before removal.'}}
function Open-WelaRegistryRecoveryTarget {param($Definition) Initialize-WelaRegistryRecoveryNative;[Wela.RegistrySaclRecovery.Target]::new((Resolve-WelaSelectedSaclNativePath $Definition))}
function Invoke-WelaRegistrySaclRecovery {
param([ValidateSet('Plan','Restore')][string]$Action='Plan',[string]$OriginalPlanPath,[string]$PendingPath,[string]$ConfirmedPath,[string]$OriginalResultsPath,[string]$PlanPath,[string]$PlanHash,[string]$OutputPath,[switch]$AllowAuditReduction,[switch]$AllowInheritance)
$ErrorActionPreference='Stop'
if($Action -eq 'Plan'){
if(-not $OriginalPlanPath -or -not $PendingPath -or -not $ConfirmedPath -or -not $OriginalResultsPath -or -not $OutputPath -or $PlanPath -or $PlanHash -or $AllowAuditReduction -or $AllowInheritance){throw 'Plan requires four original evidence paths and a new output directory only.'}
}elseif(-not $PlanPath -or $PlanHash -cnotmatch '^[a-f0-9]{64}$' -or -not $OutputPath -or $OriginalPlanPath -or $PendingPath -or $ConfirmedPath -or $OriginalResultsPath){throw 'Restore requires only reviewed plan path/hash, new output and both explicit consents.'}
$output=New-WelaArrivalOutput $OutputPath $script:ScriptRoot
$report=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaRegistrySaclRecovery';Action=$Action;Status='Refused';ExitCode=1;PlanHash=$null;WriteAttempted=$false;Before=$null;After=$null;OriginalDescriptorBytesMatch=$false;Artifacts=@();OutputPath=$output;Diagnostic='';ReadyRuleCredit=0;PolicyChanges=0;Scope='One proven registry-root audit ACE; complete empty historical/current descendants only. No full descriptor rollback, historical key identity, atomic-tree, event or Sigma claim.'}
$target=$null
try {
if($Action -eq 'Plan'){
$plan=New-WelaRegistryRecoveryPlan $OriginalPlanPath $PendingPath $ConfirmedPath $OriginalResultsPath
$artifact=Write-WelaWecUpdateArtifact $output 'plan.json' (Get-WelaRegistryRecoveryKey $plan);$report.Artifacts+=$artifact;$report.PlanHash=$artifact.Sha256
Assert-WelaRegistryRecoveryFresh $plan
$report.Status='ReviewRequired';$report.ExitCode=0
}else{
$inputFile=Read-WelaRegistryRecoveryInput $PlanPath
if($inputFile.Sha256 -cne $PlanHash){throw 'Reviewed recovery plan hash differs.'}
$plan=$inputFile.Data;Assert-WelaRegistryRecoveryText $plan @('Kind')
if($plan.Kind -cne 'WelaRegistrySaclRecoveryPlan'){throw 'Unsupported recovery plan kind.'}
$inputs=$plan.OriginalFiles;$rebuilt=New-WelaRegistryRecoveryPlan $inputs.OriginalPlan.Path $inputs.Pending.Path $inputs.Confirmed.Path $inputs.Results.Path
if((Get-WelaRegistryRecoveryKey $plan) -cne (Get-WelaRegistryRecoveryKey $rebuilt)){throw 'Reviewed recovery plan is stale or modified.'}
if(-not $AllowAuditReduction -or -not $AllowInheritance){throw 'Restore requires explicit AllowAuditReduction and AllowInheritance: selected auditing is reduced and concurrent children can be affected.'}
$report.PlanHash=$PlanHash;$report.Before=$plan.Expected
$report.Artifacts+=Write-WelaWecUpdateArtifact $output 'reviewed-plan.json' (Get-WelaRegistryRecoveryKey $plan)
$report.Artifacts+=Write-WelaWecUpdateArtifact $output 'pending.json' (Get-WelaRegistryRecoveryKey ([pscustomobject]@{Kind='WelaRegistrySaclRecoveryIntent';State='Pending';PlanHash=$PlanHash;Before=$plan.Expected;RemoveAce=$plan.AddedAce;AllowAuditReduction=[bool]$AllowAuditReduction;AllowInheritance=[bool]$AllowInheritance;RecordedUtc=[DateTime]::UtcNow.ToString('o')}))
Assert-WelaRegistryRecoveryFresh $plan
if((Read-WelaRegistryRecoveryInput $PlanPath).Sha256 -cne $PlanHash){throw 'Reviewed recovery plan changed immediately before write.'}
foreach($artifact in $report.Artifacts){if((Get-FileHash -LiteralPath (Join-Path $output $artifact.Name) -Algorithm SHA256).Hash.ToLowerInvariant() -cne $artifact.Sha256){throw 'Saved recovery receipt changed before write.'}}
$target=Open-WelaRegistryRecoveryTarget $plan.Definition
try{$report.After=$target.Remove($plan.Expected.Identity,$plan.Expected.DescriptorBase64,$plan.AddedAce)}finally{$report.WriteAttempted=$target.WriteAttempted;if($target.AfterObservation){$report.After=$target.AfterObservation}}
$target.Dispose();$target=$null
if($null -ne $report.After){$report.Artifacts+=Write-WelaWecUpdateArtifact $output 'after.json' (Get-WelaRegistryRecoveryKey $report.After)}
Assert-WelaRegistryRecoveryBindings $plan
if((Get-WelaSelectedSaclSnapshotKey (Get-WelaRegistryRecoverySnapshot $plan.Definition)) -cne (Get-WelaSelectedSaclSnapshotKey $report.After)){throw 'Final registry identity/descriptor or empty-child state differs after removal.'}
if((Read-WelaRegistryRecoveryInput $PlanPath).Sha256 -cne $PlanHash){throw 'Reviewed plan changed after write.'}
foreach($artifact in $report.Artifacts){if((Get-FileHash -LiteralPath (Join-Path $output $artifact.Name) -Algorithm SHA256).Hash.ToLowerInvariant() -cne $artifact.Sha256){throw 'Saved recovery receipt changed after write.'}}
$report.OriginalDescriptorBytesMatch=$report.After.DescriptorBase64 -ceq $plan.BeforeAddition.DescriptorBase64
$report.Artifacts+=Write-WelaWecUpdateArtifact $output 'confirmed.json' (Get-WelaRegistryRecoveryKey ([pscustomobject]@{Kind='WelaRegistrySaclRecoveryConfirmation';State='Confirmed';PlanHash=$PlanHash;Before=$plan.Expected;After=$report.After;RemoveAce=$plan.AddedAce;RecordedUtc=[DateTime]::UtcNow.ToString('o')}))
$report.Status='AddedAceRemoved';$report.ExitCode=0
}
}catch{$report.Status=if($report.WriteAttempted){'WriteAttemptedUnverified'}else{'Refused'};$report.Diagnostic=$_.Exception.Message}
finally{if($target){try{$target.Dispose()}catch{$report.Status='WriteAttemptedUnverified';$report.ExitCode=1;$report.Diagnostic+=' Native cleanup failed: '+$_.Exception.Message}}}
$null=Write-WelaWecUpdateArtifact $output 'manifest.json' (Get-WelaRegistryRecoveryKey $report)
$report
}
+206
View File
@@ -0,0 +1,206 @@
// Empty-key registry recovery: remove one proven explicit selected-root audit ACE.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
namespace Wela.RegistrySaclRecovery {
public sealed class Ace { public string Binary; public int Type,Flags,Mask; public string Sid; public bool Ordinary; }
public sealed class Snapshot {
public string Path,Kind,Identity; public bool IsDirectory;
public string DescriptorBase64,Owner,Group,DaclBase64; public int ControlFlags,SecurityInformation;
public string DescriptorScope; public Ace[] Aces;
}
public static class Descriptor {
public const string SourceSha256="__WELA_REGISTRY_SACL_RECOVERY_SOURCE_SHA256__";
public static string Bytes(GenericAcl value) { if(value==null)return null;byte[] b=new byte[value.BinaryLength];value.GetBinaryForm(b,0);return Convert.ToBase64String(b); }
public static string Bytes(GenericAce value) { byte[] b=new byte[value.BinaryLength];value.GetBinaryForm(b,0);return Convert.ToBase64String(b); }
static string Sid(SecurityIdentifier value) {return value==null?null:value.Value;}
public static RawSecurityDescriptor Parse(string value) {
byte[] b=Convert.FromBase64String(value);
if(b.Length<20||b.Length>1048576||Convert.ToBase64String(b)!=value)throw new InvalidOperationException("Invalid or noncanonical descriptor bytes.");
RawSecurityDescriptor sd=new RawSecurityDescriptor(b,0);
return sd;
}
static Dictionary<string,int> Counts(RawAcl acl) {
Dictionary<string,int> counts=new Dictionary<string,int>(StringComparer.Ordinal);
if(acl!=null)foreach(GenericAce ace in acl){string b=Bytes(ace);if(!counts.ContainsKey(b))counts[b]=0;counts[b]++;}
return counts;
}
static void Outside(RawSecurityDescriptor before,RawSecurityDescriptor after,bool allowPresence) {
int mask=allowPresence?~16:~0;
if(Sid(before.Owner)!=Sid(after.Owner)||Sid(before.Group)!=Sid(after.Group)||Bytes(before.DiscretionaryAcl)!=Bytes(after.DiscretionaryAcl)||before.ResourceManagerControl!=after.ResourceManagerControl||(((int)before.ControlFlags)&mask)!=(((int)after.ControlFlags)&mask))throw new InvalidOperationException("Owner, group, DACL or preserved control/header fields differ.");
}
public static string AddedAce(string beforeBytes,string afterBytes,string sid,int mask,int flags) {
if((sid!="S-1-1-0"&&sid!="S-1-5-11")||mask<=0||(flags!=64&&flags!=128&&flags!=192&&flags!=66&&flags!=130&&flags!=194))throw new InvalidOperationException("Only an explicit ordinary selected-root audit ACE is supported.");
RawSecurityDescriptor before=Parse(beforeBytes),after=Parse(afterBytes);Outside(before,after,true);
if(after.SystemAcl==null||after.SystemAcl.Revision!=(before.SystemAcl==null?2:before.SystemAcl.Revision))throw new InvalidOperationException("SACL revision changed during the claimed addition.");
if(before.SystemAcl!=null)foreach(GenericAce entry in before.SystemAcl){CommonAce common=entry as CommonAce;if(common!=null&&!common.IsCallback&&common.AceType==AceType.SystemAudit&&common.SecurityIdentifier.Value==sid&&(int)common.AceFlags==flags&&(common.AccessMask&mask)==mask)throw new InvalidOperationException("Original descriptor already covered the requested audit ACE.");}
string added=Bytes(new CommonAce((AceFlags)flags,AceQualifier.SystemAudit,mask,new SecurityIdentifier(sid),false,null));
Dictionary<string,int> remaining=Counts(after.SystemAcl);
if(!remaining.ContainsKey(added)||remaining[added]!=1)throw new InvalidOperationException("Expected exactly one new matching audit ACE.");
remaining[added]--;
if(before.SystemAcl!=null)foreach(GenericAce entry in before.SystemAcl){string b=Bytes(entry);if(!remaining.ContainsKey(b)||remaining[b]<1)throw new InvalidOperationException("An original ACE was changed or removed.");remaining[b]--;}
foreach(int count in remaining.Values)if(count!=0)throw new InvalidOperationException("The completed operation changed more than one audit ACE.");
int oldCount=before.SystemAcl==null?0:before.SystemAcl.Count;for(int i=0;i<oldCount;i++)if(Bytes(before.SystemAcl[i])!=Bytes(after.SystemAcl[i]))throw new InvalidOperationException("Original ACE ordering changed.");if(Bytes(after.SystemAcl[oldCount])!=added)throw new InvalidOperationException("The selected audit ACE was not the single append.");
return added;
}
public static void Removed(string beforeBytes,string afterBytes,string added) {
RawSecurityDescriptor before=Parse(beforeBytes),after=Parse(afterBytes);Outside(before,after,false);
if(before.SystemAcl==null)throw new InvalidOperationException("Original SACL is absent.");
if(after.SystemAcl==null){if(before.SystemAcl.Count!=1)throw new InvalidOperationException("A null SACL would lose unrelated audit ACEs.");}
else if(before.SystemAcl.Revision!=after.SystemAcl.Revision)throw new InvalidOperationException("SACL revision changed during removal: "+before.SystemAcl.Revision+" to "+after.SystemAcl.Revision+" (after count "+after.SystemAcl.Count+").");
Dictionary<string,int> expected=Counts(before.SystemAcl),actual=Counts(after.SystemAcl);
if(!expected.ContainsKey(added)||expected[added]!=1)throw new InvalidOperationException("The selected audit ACE is no longer unique.");
expected[added]--;
foreach(KeyValuePair<string,int> entry in expected){int count=actual.ContainsKey(entry.Key)?actual[entry.Key]:0;if(count!=entry.Value)throw new InvalidOperationException("Unrelated audit ACEs changed during removal.");actual.Remove(entry.Key);}
if(actual.Count!=0)throw new InvalidOperationException("Unexpected ACE appeared during removal.");
List<string> ordered=new List<string>();foreach(GenericAce entry in before.SystemAcl)if(Bytes(entry)!=added)ordered.Add(Bytes(entry));if(after.SystemAcl!=null){for(int i=0;i<ordered.Count;i++)if(Bytes(after.SystemAcl[i])!=ordered[i])throw new InvalidOperationException("Unrelated audit ACE order changed during removal.");}
}
public static string SaclRepresentation(string value) {
RawSecurityDescriptor sd=Parse(value);bool present=(sd.ControlFlags&ControlFlags.SystemAclPresent)!=0;
if(!present)return "Absent";
if(sd.SystemAcl==null)return "PresentNull";
return (sd.SystemAcl.Count==0?"PresentEmpty":"PresentWithAces")+";Revision="+sd.SystemAcl.Revision;
}
public static Snapshot Observe(string path,string identity,byte[] bytes) {
string encoded=Convert.ToBase64String(bytes);RawSecurityDescriptor sd=Parse(encoded);List<Ace> entries=new List<Ace>();
if(sd.SystemAcl!=null)foreach(GenericAce ace in sd.SystemAcl){CommonAce common=ace as CommonAce;bool ordinary=common!=null&&!common.IsCallback&&common.AceType==AceType.SystemAudit;entries.Add(new Ace {Binary=Bytes(ace),Type=(int)ace.AceType,Flags=(int)ace.AceFlags,Mask=ordinary?common.AccessMask:0,Sid=ordinary?common.SecurityIdentifier.Value:null,Ordinary=ordinary});}
return new Snapshot {Path=path,Kind="Registry",Identity=identity,IsDirectory=false,DescriptorBase64=encoded,Owner=Sid(sd.Owner),Group=Sid(sd.Group),DaclBase64=Bytes(sd.DiscretionaryAcl),ControlFlags=(int)sd.ControlFlags,SecurityInformation=511,DescriptorScope="WinSDK-defined sections 0x1ff; future sections unobserved",Aces=entries.ToArray()};
}
}
sealed class Privilege : IDisposable {
[StructLayout(LayoutKind.Sequential)] struct Luid {public uint Low;public int High;}
[StructLayout(LayoutKind.Sequential)] struct TokenPrivileges {public uint Count;public Luid Luid;public uint Attributes;}
[DllImport("kernel32.dll")] static extern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll")] static extern IntPtr GetCurrentThread();
[DllImport("kernel32.dll",SetLastError=true)] static extern bool CloseHandle(IntPtr value);
[DllImport("advapi32.dll",SetLastError=true)] static extern bool OpenProcessToken(IntPtr process,uint access,out IntPtr token);
[DllImport("advapi32.dll",SetLastError=true)] static extern bool OpenThreadToken(IntPtr thread,uint access,bool self,out IntPtr token);
[DllImport("advapi32.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern bool LookupPrivilegeValue(string system,string name,out Luid luid);
[DllImport("advapi32.dll",SetLastError=true)] static extern bool AdjustTokenPrivileges(IntPtr token,bool disable,ref TokenPrivileges value,uint size,out TokenPrivileges previous,out uint required);
IntPtr token;TokenPrivileges previous;
public Privilege(){IntPtr thread;
if(OpenThreadToken(GetCurrentThread(),8,true,out thread)){CloseHandle(thread);throw new InvalidOperationException("Impersonated recovery is unsupported.");}
int error=Marshal.GetLastWin32Error();if(error!=1008)throw new Win32Exception(error);
if(!OpenProcessToken(GetCurrentProcess(),0x28,out token))throw new Win32Exception(Marshal.GetLastWin32Error());
try{Luid luid;if(!LookupPrivilegeValue(null,"SeSecurityPrivilege",out luid))throw new Win32Exception(Marshal.GetLastWin32Error());TokenPrivileges request=new TokenPrivileges {Count=1,Luid=luid,Attributes=2};uint required;bool ok=AdjustTokenPrivileges(token,false,ref request,(uint)Marshal.SizeOf(typeof(TokenPrivileges)),out previous,out required);error=Marshal.GetLastWin32Error();if(!ok||error!=0)throw new Win32Exception(error,"SeSecurityPrivilege is unavailable.");}
catch{CloseHandle(token);token=IntPtr.Zero;throw;}
}
public void Dispose(){if(token==IntPtr.Zero)return;try{TokenPrivileges ignored;uint required;bool ok=AdjustTokenPrivileges(token,false,ref previous,(uint)Marshal.SizeOf(typeof(TokenPrivileges)),out ignored,out required);int error=Marshal.GetLastWin32Error();if(!ok||error!=0)throw new Win32Exception(error,"SeSecurityPrivilege restoration failed.");}finally{CloseHandle(token);token=IntPtr.Zero;}}
}
public sealed class Target : IDisposable {
[DllImport("kernel32.dll")] static extern IntPtr LocalFree(IntPtr value);
[DllImport("advapi32.dll",CharSet=CharSet.Unicode,ExactSpelling=true)] static extern int RegOpenKeyExW(IntPtr root,string name,uint options,uint access,out IntPtr key);
[DllImport("advapi32.dll",CharSet=CharSet.Unicode,ExactSpelling=true)] static extern int RegQueryValueExW(IntPtr key,string name,IntPtr reserved,out uint type,IntPtr data,ref uint size);
[DllImport("advapi32.dll",CharSet=CharSet.Unicode,ExactSpelling=true)] static extern int RegQueryInfoKeyW(IntPtr key,IntPtr cls,IntPtr clsSize,IntPtr reserved,IntPtr subKeys,IntPtr maxSubKey,IntPtr maxClass,IntPtr values,IntPtr maxValueName,IntPtr maxValue,IntPtr securitySize,out long written);
[DllImport("advapi32.dll",CharSet=CharSet.Unicode,ExactSpelling=true)] static extern int RegEnumKeyExW(IntPtr key,uint index,StringBuilder name,ref uint length,IntPtr reserved,IntPtr cls,IntPtr clsLength,IntPtr written);
[DllImport("advapi32.dll")] static extern int RegCloseKey(IntPtr key);
[DllImport("ntdll.dll")] static extern int NtQueryKey(IntPtr key,int cls,IntPtr information,uint length,out uint resultLength);
[DllImport("advapi32.dll")] static extern uint GetSecurityInfo(IntPtr handle,uint kind,uint flags,out IntPtr owner,out IntPtr group,out IntPtr dacl,out IntPtr sacl,out IntPtr descriptor);
[DllImport("advapi32.dll")] static extern uint GetSecurityDescriptorLength(IntPtr descriptor);
[DllImport("advapi32.dll")] static extern uint SetSecurityInfo(IntPtr handle,uint kind,uint flags,IntPtr owner,IntPtr group,IntPtr dacl,IntPtr sacl);
readonly string path,nativePath;readonly List<IntPtr> keys=new List<IntPtr>();IntPtr handle;Privilege privilege;
public bool WriteAttempted {get;private set;}public Snapshot AfterObservation {get;private set;}
public Target(string path){
this.path=path;
if(String.IsNullOrEmpty(path)||path.IndexOfAny(new char[]{'/','*','?','%','\0'})>=0)throw new InvalidOperationException("Exact local registry path required.");
string[] parts=path.Split('\\');IntPtr root;
if(parts[0]=="HKEY_LOCAL_MACHINE"){root=new IntPtr(unchecked((int)0x80000002));nativePath="\\REGISTRY\\MACHINE";}
else if(parts[0]=="HKEY_USERS"){root=new IntPtr(unchecked((int)0x80000003));nativePath="\\REGISTRY\\USER";}
else throw new InvalidOperationException("Only selected HKLM/HKU keys are supported.");
if(parts.Length<2)throw new InvalidOperationException("Hive roots cannot be recovered.");
for(int i=1;i<parts.Length;i++){if(parts[i].Length==0||parts[i]=="."||parts[i]=="..")throw new InvalidOperationException("Ambiguous registry path.");nativePath+="\\"+parts[i];}
try{
privilege=new Privilege();IntPtr current=root;
for(int i=1;i<parts.Length;i++){
IntPtr opened;int error=RegOpenKeyExW(current,parts[i],8,0x01020101U|(i==parts.Length-1?8U:0U),out opened);
if(error!=0)throw new Win32Exception(error,"Selected registry component open failed.");keys.Add(opened);current=opened;
uint type,size=0;error=RegQueryValueExW(current,"SymbolicLinkValue",IntPtr.Zero,out type,IntPtr.Zero,ref size);
if(error==0&&type==6)throw new InvalidOperationException("Registry symbolic-link component refused.");if(error!=0&&error!=2)throw new Win32Exception(error,"Registry link state is unreadable.");
}
handle=current;Check();AssertEmpty();
}catch{Dispose();throw;}
}
string Check(){
if(handle==IntPtr.Zero)throw new ObjectDisposedException("Target");uint required;int status=NtQueryKey(handle,3,IntPtr.Zero,0,out required);
if((status!=unchecked((int)0xC0000023)&&status!=unchecked((int)0x80000005))||required<6||required>65536)throw new InvalidOperationException("Native registry name query is unavailable.");
IntPtr buffer=Marshal.AllocHGlobal((int)required);
try{uint actual;status=NtQueryKey(handle,3,buffer,required,out actual);if(status!=0||actual>required)throw new InvalidOperationException("Native registry name query failed.");int size=Marshal.ReadInt32(buffer);if(size<2||size%2!=0||size>required-4)throw new InvalidOperationException("Native registry name is malformed.");string name=Marshal.PtrToStringUni(IntPtr.Add(buffer,4),size/2);if(!String.Equals(name,nativePath,StringComparison.OrdinalIgnoreCase))throw new InvalidOperationException("Held registry name differs from the selected path.");}finally{Marshal.FreeHGlobal(buffer);}
long written;int error=RegQueryInfoKeyW(handle,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,out written);if(error!=0)throw new Win32Exception(error,"Registry last-write observation failed.");return path+":"+written;
}
public void AssertEmpty(){if(handle==IntPtr.Zero)throw new ObjectDisposedException("Target");StringBuilder name=new StringBuilder(256);uint size=256;int error=RegEnumKeyExW(handle,0,name,ref size,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero);if(error==0||error==234)throw new InvalidOperationException("Recovery requires an observed empty registry descendant inventory.");if(error!=259)throw new Win32Exception(error,"Registry child enumeration is unknown.");}
public Snapshot Read(){
string identity=Check();AssertEmpty();IntPtr owner,group,dacl,sacl,descriptor;uint error=GetSecurityInfo(handle,4,511,out owner,out group,out dacl,out sacl,out descriptor);if(error!=0)throw new Win32Exception((int)error,"Full SDK-defined registry descriptor read failed.");byte[] bytes;
try{uint size=GetSecurityDescriptorLength(descriptor);if(size<20||size>1048576)throw new InvalidOperationException("Invalid native descriptor size.");bytes=new byte[size];Marshal.Copy(descriptor,bytes,0,(int)size);}finally{LocalFree(descriptor);}
AssertEmpty();if(Check()!=identity)throw new InvalidOperationException("Registry last-write identity changed during observation.");return Descriptor.Observe(path,identity,bytes);
}
public Snapshot Remove(string expectedIdentity,string expectedDescriptor,string added){
Snapshot before=Read();if(before.Identity!=expectedIdentity||before.DescriptorBase64!=expectedDescriptor)throw new InvalidOperationException("Reviewed registry identity or descriptor changed before removal.");
RawSecurityDescriptor sd=Descriptor.Parse(before.DescriptorBase64);int index=-1;
if(sd.SystemAcl!=null)for(int i=0;i<sd.SystemAcl.Count;i++)if(Descriptor.Bytes(sd.SystemAcl[i])==added){if(index!=-1)throw new InvalidOperationException("Audit ACE is not unique.");index=i;}
if(index<0)throw new InvalidOperationException("Audit ACE is absent.");CommonAce ace=sd.SystemAcl[index] as CommonAce;
if(ace==null||ace.IsCallback||ace.AceType!=AceType.SystemAudit||((int)ace.AceFlags!=64&&(int)ace.AceFlags!=128&&(int)ace.AceFlags!=192&&(int)ace.AceFlags!=66&&(int)ace.AceFlags!=130&&(int)ace.AceFlags!=194))throw new InvalidOperationException("Only the proven explicit selected-root audit ACE can be removed.");
sd.SystemAcl.RemoveAce(index);byte[] bytes=new byte[sd.SystemAcl.BinaryLength];sd.SystemAcl.GetBinaryForm(bytes,0);IntPtr buffer=Marshal.AllocHGlobal(bytes.Length);
try{
Marshal.Copy(bytes,0,buffer,bytes.Length);Snapshot last=Read();if(last.Identity!=expectedIdentity||last.DescriptorBase64!=expectedDescriptor)throw new InvalidOperationException("Registry changed immediately before removal.");
WriteAttempted=true;uint error=SetSecurityInfo(handle,4,8,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,buffer);if(error!=0)throw new Win32Exception((int)error,"SACL-only registry removal failed.");
}finally{Marshal.FreeHGlobal(buffer);}
Snapshot after=Read();AfterObservation=after;Descriptor.Removed(before.DescriptorBase64,after.DescriptorBase64,added);return after;
}
public void Dispose(){try{for(int i=keys.Count-1;i>=0;i--)RegCloseKey(keys[i]);keys.Clear();handle=IntPtr.Zero;}finally{if(privilege!=null){privilege.Dispose();privilege=null;}}}
}
public sealed class Group { public string Sid; public uint Attributes; }
public sealed class TokenPrivilege { public string Luid; public uint Attributes; }
public sealed class Token {
public string Sid, Name, AuthenticationId, AuthenticationType, ImpersonationLevel, TokenSource;
public Group[] Groups; public TokenPrivilege[] Privileges;
}
public static class TokenReader {
[DllImport("kernel32.dll",ExactSpelling=true)] static extern void GetSystemTimePreciseAsFileTime(out long value);
public static DateTime UtcNow() {long value;GetSystemTimePreciseAsFileTime(out value);return DateTime.FromFileTimeUtc(value);}
[StructLayout(LayoutKind.Sequential)] struct Luid {public uint Low; public int High;}
[StructLayout(LayoutKind.Sequential)] struct Statistics {public Luid TokenId,AuthenticationId;public long Expiration;public int Type,Level;public uint Charged,Available,Groups,Privileges;public Luid Modified;}
[StructLayout(LayoutKind.Sequential)] struct SidAndAttributes {public IntPtr Sid;public uint Attributes;}
[StructLayout(LayoutKind.Sequential)] struct TokenGroups {public uint Count;public SidAndAttributes First;}
[StructLayout(LayoutKind.Sequential)] struct LuidAndAttributes {public Luid Luid;public uint Attributes;}
[DllImport("kernel32.dll")] static extern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll")] static extern IntPtr GetCurrentThread();
[DllImport("kernel32.dll",SetLastError=true)] static extern bool CloseHandle(IntPtr h);
[DllImport("advapi32.dll",SetLastError=true)] static extern bool OpenProcessToken(IntPtr p,uint access,out IntPtr t);
[DllImport("advapi32.dll",SetLastError=true)] static extern bool OpenThreadToken(IntPtr p,uint access,bool self,out IntPtr t);
[DllImport("advapi32.dll",SetLastError=true)] static extern bool GetTokenInformation(IntPtr t,int cls,IntPtr data,int length,out int needed);
static string Hex(Luid id) {return "0x"+(((ulong)(uint)id.High<<32)|id.Low).ToString("x");}
static IntPtr Read(IntPtr token,int cls,out int length) {
GetTokenInformation(token,cls,IntPtr.Zero,0,out length);
if(Marshal.GetLastWin32Error()!=122||length<4||length>65536)throw new InvalidOperationException("Unknown or oversized token information.");
IntPtr data=Marshal.AllocHGlobal(length);
if(!GetTokenInformation(token,cls,data,length,out length)){int error=Marshal.GetLastWin32Error();Marshal.FreeHGlobal(data);throw new Win32Exception(error);}
return data;
}
static Token ReadToken(IntPtr token,string source) {
Token result=new Token();result.TokenSource=source;using(WindowsIdentity identity=new WindowsIdentity(token)){result.Sid=identity.User.Value;result.Name=identity.Name;result.AuthenticationType=identity.AuthenticationType;result.ImpersonationLevel=identity.ImpersonationLevel.ToString();}
int length;IntPtr p=Read(token,10,out length);
try {if(length<Marshal.SizeOf(typeof(Statistics)))throw new InvalidOperationException("Truncated token statistics.");result.AuthenticationId=Hex(((Statistics)Marshal.PtrToStructure(p,typeof(Statistics))).AuthenticationId);}finally{Marshal.FreeHGlobal(p);}
p=Read(token,2,out length);
try {int count=Marshal.ReadInt32(p),offset=(int)Marshal.OffsetOf(typeof(TokenGroups),"First"),size=Marshal.SizeOf(typeof(SidAndAttributes));if(count<0||count>4096||offset+(long)count*size>length)throw new InvalidOperationException("Invalid token groups.");List<Group> groups=new List<Group>();for(int i=0;i<count;i++){SidAndAttributes g=(SidAndAttributes)Marshal.PtrToStructure(IntPtr.Add(p,offset+i*size),typeof(SidAndAttributes));groups.Add(new Group{Sid=new SecurityIdentifier(g.Sid).Value,Attributes=g.Attributes});}groups.Sort((a,b)=>String.CompareOrdinal(a.Sid,b.Sid));result.Groups=groups.ToArray();}finally{Marshal.FreeHGlobal(p);}
p=Read(token,3,out length);
try {int count=Marshal.ReadInt32(p),size=Marshal.SizeOf(typeof(LuidAndAttributes));if(count<0||count>4096||4+(long)count*size>length)throw new InvalidOperationException("Invalid token privileges.");List<TokenPrivilege> privileges=new List<TokenPrivilege>();for(int i=0;i<count;i++){LuidAndAttributes v=(LuidAndAttributes)Marshal.PtrToStructure(IntPtr.Add(p,4+i*size),typeof(LuidAndAttributes));privileges.Add(new TokenPrivilege{Luid=Hex(v.Luid),Attributes=v.Attributes});}privileges.Sort((a,b)=>String.CompareOrdinal(a.Luid,b.Luid));result.Privileges=privileges.ToArray();}finally{Marshal.FreeHGlobal(p);}
return result;
}
[DllImport("advapi32.dll")] static extern bool IsTokenRestricted(IntPtr token);
public static Token Snapshot() {
IntPtr thread=IntPtr.Zero,process=IntPtr.Zero;
if(!OpenThreadToken(GetCurrentThread(),8,true,out thread)){int error=Marshal.GetLastWin32Error();if(error!=1008)throw new Win32Exception(error);}
try {
if(!OpenProcessToken(GetCurrentProcess(),8,out process))throw new Win32Exception(Marshal.GetLastWin32Error());
if(IsTokenRestricted(process)||(thread!=IntPtr.Zero&&IsTokenRestricted(thread)))throw new InvalidOperationException("Restricted tokens are unsupported.");
Token primary=ReadToken(process,"Process");
if(thread!=IntPtr.Zero)throw new InvalidOperationException("Impersonated recovery is unsupported.");
return primary;
} finally {if(process!=IntPtr.Zero)CloseHandle(process);if(thread!=IntPtr.Zero)CloseHandle(thread);}
}
}
}