fix: normalize native WEF reader tokens before strict validation

This commit is contained in:
Shirofune-Security committed 2026-09-22 12:30:51 +09:00
1 parent 1dfb66fb40
commit 9692edcec6
4 files changed
+31 -5

No files matched your search

+4
View File
@@ -96,3 +96,7 @@ tests/NativeProviderConfigure.Windows.Tests.ps1 text eol=lf
/modules/WecSubscriptionInventory.cs text eol=lf
/tests/WecCollectorObservation* text eol=lf
/tests/WecSubscriptionInventory* text eol=lf
# Native query receipts bind the same source bytes on every supported engine.
/scripts/WefQuery* text eol=lf
/tests/WefQuery* text eol=lf
+9 -1
View File
@@ -22,7 +22,15 @@ function Get-WelaWefQuerySources {
}
function Get-WelaWefQueryToken {
Initialize-WelaWefQueryNative
[Wela.WefQueryToken.Native]::Snapshot()
ConvertTo-WelaWefQueryTokenObservation ([Wela.WefQueryToken.Native]::Snapshot())
}
function ConvertTo-WelaWefQueryTokenObservation {
param($Token)
if($Token -isnot [Wela.WefQueryToken.Token]){throw 'Expected the native query token observation.'}
# Normalize native DTOs at the boundary, using the same strict shape as worker receipts.
$observed=ConvertFrom-WelaArrivalJson (Get-WelaWefQueryKey $Token)
$null=Get-WelaWefQueryTokenKey $observed
$observed
}
function Get-WelaWefQueryTokenKey {
param($Token)
+9
View File
@@ -7,6 +7,15 @@ function Assert($value,[string]$message){if(-not $value){throw $message};$script
function Reject([scriptblock]$code,[string]$message){$caught=$false;try{& $code|Out-Null}catch{$caught=$true};Assert $caught $message}
function Clone($value){ConvertFrom-WelaArrivalJson (Get-WelaWefQueryKey $value)}
Initialize-WelaWefQueryNative
$nativeToken=[Wela.WefQueryToken.Token]::new();$nativeToken.Sid='S-1-5-21-1-2-3-1000';$nativeToken.Name='Host\reader';$nativeToken.AuthenticationId='0x123';$nativeToken.AuthenticationType='NTLM';$nativeToken.ImpersonationLevel='None';$nativeToken.TokenSource='Process'
$nativeGroup=[Wela.WefQueryToken.Group]::new();$nativeGroup.Sid='S-1-5-32-545';$nativeGroup.Attributes=[uint32]::MaxValue;$nativeToken.Groups=@($nativeGroup);$nativeToken.Privileges=@()
$observedToken=ConvertTo-WelaWefQueryTokenObservation $nativeToken
Assert ($observedToken -is [pscustomobject] -and $observedToken.Groups -is [array] -and $observedToken.Groups.Count -eq 1 -and $observedToken.Privileges -is [array] -and $observedToken.Privileges.Count -eq 0) 'Actual native DTO normalizes singleton groups and empty privileges for strict receipt validation.'
Assert ($observedToken.Groups[0].Attributes -eq [uint32]::MaxValue -and (Get-WelaWefQueryTokenKey $observedToken) -ceq (Get-WelaWefQueryTokenKey (Clone $nativeToken))) 'Native token normalization preserves every unsigned attribute and token comparison.'
$nativePrivilege=[Wela.WefQueryToken.Privilege]::new();$nativePrivilege.Luid='0x14';$nativePrivilege.Attributes=2;$nativeToken.Privileges=@($nativePrivilege)
Assert ((ConvertTo-WelaWefQueryTokenObservation $nativeToken).Privileges[0].Attributes -eq 2) 'Native privilege DTO normalizes without losing enabled attributes.'
$nativeToken.Sid='invalid';Reject {ConvertTo-WelaWefQueryTokenObservation $nativeToken} 'Invalid native token remains unverified.'
Reject {ConvertTo-WelaWefQueryTokenObservation $observedToken} 'Native boundary rejects a substituted arbitrary object.'
$buffer=[Runtime.InteropServices.Marshal]::AllocHGlobal(128)
try{
function Reset-Buffer([int]$type,[int]$count){for($i=0;$i -lt 128;$i++){[Runtime.InteropServices.Marshal]::WriteByte($buffer,$i,0)};[Runtime.InteropServices.Marshal]::WriteInt32($buffer,12,$type);[Runtime.InteropServices.Marshal]::WriteInt32($buffer,8,$count);[Runtime.InteropServices.Marshal]::WriteIntPtr($buffer,[IntPtr]::Add($buffer,16))}
+9 -4
View File
@@ -102,10 +102,15 @@ finally{
if($aclChanged){try{& wevtutil.exe sl $channel ('/ca:'+$channelBefore.SecurityDescriptor);if($LASTEXITCODE -ne 0){throw 'Original descriptor restore failed.'};$global:LASTEXITCODE=0}catch{$cleanupErrors+=$_.Exception.Message}}
if($ownedSid){try{$current=Get-LocalUser -Name $userName -ErrorAction Stop;if($current.SID.Value -cne $ownedSid){throw 'Owned account changed identity.'};Remove-LocalUser -SID $ownedSid -ErrorAction Stop;if(Get-LocalUser -SID $ownedSid -ErrorAction SilentlyContinue){throw 'Owned account remains.'}}catch{$cleanupErrors+=$_.Exception.Message}}
$restored=$null;try{$restored=[pscustomobject]@{Host=(Get-WelaWefQueryHost);Profiles=@(Profiles);Hives=@(Hives);Services=(Services);Channels=(NativeChannels);Masks=(Get-WelaEffectiveAuditPolicy);Precedence=(Get-WelaRegistryState 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' SCENoApplyLegacyAuditPolicy);Token=(Get-WelaWefQueryToken)};Save 'restored.json' $restored}catch{$cleanupErrors+=$_.Exception.Message}
$channelsRestored=$restored -and (Key $restored.Channels) -ceq (Key $before.Channels);$servicesRestored=$restored -and (Key $restored.Services) -ceq (Key $before.Services);$policyRestored=$restored -and (Key $restored.Masks) -ceq (Key $before.Masks) -and (Key $restored.Precedence) -ceq (Key $before.Precedence);$tokenRestored=$restored -and (Get-WelaWefQueryTokenKey $restored.Token) -ceq (Get-WelaWefQueryTokenKey $before.Token)
$profilesRestored=$restored -and (Key $restored.Profiles) -ceq (Key $before.Profiles) -and (Key $restored.Hives) -ceq (Key $before.Hives)
if(-not $profilesRestored -or -not $channelsRestored -or -not $servicesRestored -or -not $policyRestored -or -not $tokenRestored){$cleanupErrors+='Original profile/hive/channel/services/policy/token differ.'}
Save 'cleanup.json' ([pscustomobject]@{Passed=$passed;Assertions=$script:assertions;ChannelsRestored=[bool]$channelsRestored;ServicesRestored=[bool]$servicesRestored;PolicyRestored=[bool]$policyRestored;TokenRestored=[bool]$tokenRestored;ProfilesAndHivesRestored=[bool]$profilesRestored;AccountRemoved=[bool](-not $ownedSid -or -not(Get-LocalUser -SID $ownedSid -ErrorAction SilentlyContinue));Errors=$cleanupErrors;EventGeneration='Not tested';Forwarding='Not tested';Sources=(Get-WelaWefQuerySources)})
$channelsRestored=$false;$servicesRestored=$false;$policyRestored=$false;$tokenRestored=$false;$profilesRestored=$false;$accountRemoved=$false;$cleanupSources=$null
try{
$channelsRestored=$restored -and (Key $restored.Channels) -ceq (Key $before.Channels);$servicesRestored=$restored -and (Key $restored.Services) -ceq (Key $before.Services);$policyRestored=$restored -and (Key $restored.Masks) -ceq (Key $before.Masks) -and (Key $restored.Precedence) -ceq (Key $before.Precedence);$tokenRestored=$restored -and (Get-WelaWefQueryTokenKey $restored.Token) -ceq (Get-WelaWefQueryTokenKey $before.Token)
$profilesRestored=$restored -and (Key $restored.Profiles) -ceq (Key $before.Profiles) -and (Key $restored.Hives) -ceq (Key $before.Hives)
}catch{$cleanupErrors+='Cleanup comparison: '+$_.Exception.Message}
try{$accountRemoved=-not $ownedSid -or -not(Get-LocalUser -SID $ownedSid -ErrorAction SilentlyContinue)}catch{$cleanupErrors+='Account observation: '+$_.Exception.Message}
try{$cleanupSources=Get-WelaWefQuerySources}catch{$cleanupErrors+='Source observation: '+$_.Exception.Message}
if(-not $profilesRestored -or -not $channelsRestored -or -not $servicesRestored -or -not $policyRestored -or -not $tokenRestored -or -not $accountRemoved){$cleanupErrors+='Original profile/hive/channel/services/policy/token or owned account differ.'}
Save 'cleanup.json' ([pscustomobject]@{Passed=$passed;Assertions=$script:assertions;ChannelsRestored=[bool]$channelsRestored;ServicesRestored=[bool]$servicesRestored;PolicyRestored=[bool]$policyRestored;TokenRestored=[bool]$tokenRestored;ProfilesAndHivesRestored=[bool]$profilesRestored;AccountRemoved=[bool]$accountRemoved;Errors=$cleanupErrors;EventGeneration='Not tested';Forwarding='Not tested';Sources=$cleanupSources})
if($cleanupErrors.Count){throw ('Fixture cleanup incomplete: '+($cleanupErrors -join '; '))}
}
Write-Host "WefQuery.Windows.Tests: $script:assertions actual native assertions passed; complete owned fixture cleanup."