From 5bebb9a7a503c7c1304d2394c4e68cf7a03b2e4e Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:22:21 +0900 Subject: [PATCH] test: isolate native 4703 audit attribution and retain query diagnostics --- tests/TokenRightProbe.Feasibility.ps1 | 71 ++++++++++++++++++--------- 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/tests/TokenRightProbe.Feasibility.ps1 b/tests/TokenRightProbe.Feasibility.ps1 index 4f8d56a1..86a13920 100644 --- a/tests/TokenRightProbe.Feasibility.ps1 +++ b/tests/TokenRightProbe.Feasibility.ps1 @@ -30,28 +30,55 @@ exit 0 try{ if($beforeMasks.Count -ne 59){throw 'All59 masks required.'} Set-ItemProperty -LiteralPath $path -Name $name -Value 1 -Type DWord - Set-WelaEffectiveAuditPolicy -Guid $guid -Mask ($beforeMasks[$guid] -bor 1) -Mode exact - Set-WelaEffectiveAuditPolicy -Guid $auth -Mask 0 -Mode exact - $engine=(Get-Process -Id $PID).Path - & $engine -NoLogo -NoProfile -NonInteractive -File $worker $repo $receipt - if($LASTEXITCODE -ne 0){throw 'Native worker failed.'} - $result=Get-Content -Raw $receipt|ConvertFrom-Json - $start=[DateTime]::FromFileTimeUtc($result.Outcome.DisableStartedFileTime).AddSeconds(-1);$end=[DateTime]::FromFileTimeUtc($result.Outcome.RestoreReturnedFileTime).AddSeconds(1) - $query="*[System[EventID=4703 and TimeCreated[@SystemTime>='$($start.ToString('o'))' and @SystemTime<='$($end.ToString('o'))']]]" - $matches=@();$deadline=[DateTime]::UtcNow.AddSeconds(15) - do{ - $events=@(Get-WinEvent -LogName Security -FilterXPath $query -MaxEvents 256 -ErrorAction SilentlyContinue) - foreach($event in $events){ - $raw=$event.ToXml();[xml]$xml=$raw;$data=@{};foreach($field in $xml.Event.EventData.Data){$data[[string]$field.Name]=[string]$field.'#text'} - if($data.ProcessId -and [Convert]::ToInt64($data.ProcessId,16) -eq $result.ProcessId -and ($data.EnabledPrivilegeList -match 'SeDebugPrivilege' -or $data.DisabledPrivilegeList -match 'SeDebugPrivilege')){$matches+=@([pscustomobject]@{RecordId=$event.RecordId;Xml=$raw;Data=$data})} - $event.Dispose() - } - $matches=@($matches|Sort-Object RecordId -Unique) - if($matches.Count -ge 2){break};Start-Sleep -Milliseconds 250 - }while([DateTime]::UtcNow -lt $deadline) - Save 'events.json' $matches - if($matches.Count -lt 2){throw 'No two attributable actual4703 adjustment events were observed.'} - Write-Host "Native feasibility observed $($matches.Count) attributable4703 events with Token Right Adjusted success enabled and Authorization Policy Change disabled." + $phases=@(@{Name='TokenRightOnly';Token=1;Authorization=0},@{Name='AuthorizationOnly';Token=0;Authorization=1}) + $summaries=@() + foreach($phase in $phases){ + Set-WelaEffectiveAuditPolicy -Guid $guid -Mask $phase.Token -Mode exact + Set-WelaEffectiveAuditPolicy -Guid $auth -Mask $phase.Authorization -Mode exact + $prepared=Get-WelaEffectiveAuditPolicy + foreach($entry in $beforeMasks.Keys){$expected=$beforeMasks[$entry];if($entry -eq $guid){$expected=$phase.Token};if($entry -eq $auth){$expected=$phase.Authorization};if($prepared[$entry] -ne $expected){throw 'Prepared native policy differs from the exact selected two-mask change.'}} + Save ($phase.Name+'-prepared.json') @{Phase=$phase;Masks=$prepared;Precedence=Get-WelaRegistryState $path $name} + $receipt=Join-Path $root ($phase.Name+'-worker.json') + $engine=(Get-Process -Id $PID).Path + & $engine -NoLogo -NoProfile -NonInteractive -File $worker $repo $receipt + if($LASTEXITCODE -ne 0){throw 'Native worker failed.'} + $result=Get-Content -Raw $receipt|ConvertFrom-Json + $exactStart=[DateTime]::FromFileTimeUtc($result.Outcome.DisableStartedFileTime);$exactEnd=[DateTime]::FromFileTimeUtc($result.Outcome.RestoreReturnedFileTime) + $start=$exactStart.AddSeconds(-2);$end=$exactEnd.AddSeconds(2) + $query="*[System[Provider[@Name='Microsoft-Windows-Security-Auditing'] and EventID=4703 and TimeCreated[@SystemTime>='$($start.ToString('o'))' and @SystemTime<='$($end.ToString('o'))']]]" + $candidates=@{};$queryErrors=@();$attributedEvents=@();$deadline=[DateTime]::UtcNow.AddSeconds(15) + do{ + $reader=$null + try{ + $nativeQuery=[System.Diagnostics.Eventing.Reader.EventLogQuery]::new('Security',[System.Diagnostics.Eventing.Reader.PathType]::LogName,$query) + $reader=[System.Diagnostics.Eventing.Reader.EventLogReader]::new($nativeQuery) + $count=0 + while($null -ne ($event=$reader.ReadEvent())){ + try{ + $count++;if($count -gt 512){throw 'Diagnostic candidate count exceeded512.'} + $raw=$event.ToXml();if($raw.Length -gt 65536){throw 'Candidate XML exceeded64Ki characters.'} + [xml]$xml=$raw;$data=@{};foreach($field in $xml.Event.EventData.Data){$data[[string]$field.Name]=[string]$field.'#text'} + $time=[DateTime]::Parse([string]$xml.Event.System.TimeCreated.SystemTime,[Globalization.CultureInfo]::InvariantCulture,[Globalization.DateTimeStyles]::RoundtripKind).ToUniversalTime() + $identity=$data.ProcessId -and [Convert]::ToInt64($data.ProcessId,16) -eq $result.ProcessId -and $data.ProcessName -ieq $result.ProcessName -and $data.SubjectUserSid -ceq $result.Before.Sid -and $data.TargetUserSid -ceq $result.Before.Sid -and $data.SubjectLogonId -ieq $result.Before.AuthenticationId -and $data.TargetLogonId -ieq $result.Before.AuthenticationId + $privilege=$data.EnabledPrivilegeList -ceq 'SeDebugPrivilege' -or $data.DisabledPrivilegeList -ceq 'SeDebugPrivilege' + $exact=$identity -and $privilege -and $time -ge $exactStart -and $time -le $exactEnd + $candidates[[string]$event.RecordId]=[pscustomobject]@{RecordId=$event.RecordId;Xml=$raw;Data=$data;ExactIdentity=[bool]$identity;ExactInterval=($time -ge $exactStart -and $time -le $exactEnd);Attributed=[bool]$exact} + }finally{$event.Dispose()} + } + }catch{$queryErrors+=@($_.ToString());break}finally{if($reader){$reader.Dispose()}} + $attributedEvents=@($candidates.Values|Where-Object{$_.Attributed}|Sort-Object RecordId) + if($attributedEvents.Count -ge 2){break};Start-Sleep -Milliseconds 250 + }while([DateTime]::UtcNow -lt $deadline) + Save ($phase.Name+'-candidates.json') @($candidates.Values|Sort-Object RecordId) + Save ($phase.Name+'-events.json') $attributedEvents + Save ($phase.Name+'-query.json') @{XPath=$query;ExactStart=$exactStart;ExactEnd=$exactEnd;QueryErrors=$queryErrors;CandidateCount=$candidates.Count;AttributedCount=$attributedEvents.Count;NoMatchingEvents=($candidates.Count -eq 0);DiagnosticOnly=$true} + if($queryErrors.Count){throw 'Native4703 observation failed; see retained query errors.'} + if((Key (Get-WelaEffectiveAuditPolicy)) -cne (Key $prepared)){throw 'Prepared audit policy drifted during observation.'} + $summaries+=@([pscustomobject]@{Phase=$phase.Name;CandidateCount=$candidates.Count;AttributedCount=$attributedEvents.Count;ReadComplete=$true;AdjustedAndRestored=($result.Outcome.Status -eq 'Adjusted' -and $result.Outcome.Restored)}) + Save 'summary.json' $summaries + Write-Host "$($phase.Name): $($attributedEvents.Count) exact native4703 records, $($candidates.Count) bounded diagnostic candidates." + } + if(@($summaries|Where-Object{$_.AttributedCount -gt 0}).Count -eq 0){throw 'Neither selected policy phase produced an attributable4703 event.'} }catch{$failure=$_.ToString();throw}finally{ foreach($restoreGuid in @($guid,$auth)){try{Set-WelaEffectiveAuditPolicy -Guid $restoreGuid -Mask $beforeMasks[$restoreGuid] -Mode exact}catch{$errors+=$_.ToString()}} try{if($beforePrecedence.ValueExists){Set-ItemProperty -LiteralPath $path -Name $name -Value $beforePrecedence.Value -Type $beforePrecedence.Type}else{Remove-ItemProperty -LiteralPath $path -Name $name -ErrorAction Stop}}catch{$errors+=$_.ToString()}