mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-08 23:35:28 +02:00
test: corroborate runtime token task and measure full native verification
This commit is contained in:
1 parent
b5e244bb68
commit
10c50b9a74
5 files changed
+32
-8
No files matched your search
@@ -6,9 +6,11 @@ Microsoft's [Token Right Adjusted guidance](https://learn.microsoft.com/en-us/pr
|
||||
|
||||
The opted-in test changes exactly two audit masks and the advanced-audit precedence DWORD on an isolated GitHub-hosted runner. First it sets Token Right Adjusted Events (`0CCE924A-69AE-11D9-BED3-505054503030`) to Success and Authorization Policy Change (`0CCE9231-69AE-11D9-BED3-505054503030`) to None. Then it reverses those two masks. The other 57 audit masks remain at their observed original values. This comparison establishes the selected two-mask behavior under that retained context; it is not an experiment with all other audit sources disabled.
|
||||
|
||||
The installed provider task definitions and independently read `wevtutil gp` XML must both name `SE_ADT_DETAILEDTRACKING_TOKENRIGHTADJ` with value 13317. The emitted header is checked against that reviewed runtime task; it is not inferred from a candidate event. The observed generic 4703 declaration reports task 0, and that declaration remains in the receipt alongside the runtime definition. The older Microsoft event example uses task 13570; this discrepancy is retained rather than presented as a universal mapping correction.
|
||||
|
||||
Each phase starts a fresh owned child. A test-only native helper requires an already-enabled `SeDebugPrivilege` in that child's primary token, disables it, reads the complete privilege inventory, restores its original attributes and verifies the complete inventory again. It refuses impersonation, missing or disabled privileges. It never grants a new right, removes a privilege, opens another process, performs a debug operation or changes an account's assigned rights. The executable path is read through `QueryFullProcessImageName` before the operation so `Get-Process` cannot introduce an extra privilege adjustment inside the measured operation.
|
||||
|
||||
Acceptance requires two distinct actual Security 4703 records in the TokenRight-only phase: exactly the fixed disable and restoration. The inverse phase must have no matching records during its bounded observation. A match requires the installed provider GUID/name, eventID/version/task, Security channel, success keyword, observed computer identity, fresh record boundary, owned PID/executable, subject and target SID/logon ID, and exact privilege direction/sentinel. The precise UTC envelope starts before the native adjustment and ends after native final inventory equality. Individual syscall-return times are also retained. Security logging can timestamp a record just after the adjustment call returns; the measured verification interval is part of the operation, with no artificial delay or padded interval accepted as evidence. A wider query only collects diagnostic candidates; the strict matcher determines attribution.
|
||||
Acceptance requires two distinct actual Security 4703 records in the TokenRight-only phase: exactly the fixed disable and restoration. The inverse phase must have no matching records during its bounded observation. A match requires the installed provider GUID/name, eventID/version/task, Security channel, success keyword, observed computer identity, fresh record boundary, owned PID/executable, subject and target SID/logon ID, and exact privilege direction/sentinel. The precise UTC envelope starts before the native adjustment and ends after the required native full-token after-snapshot. Individual syscall-return times and the inner privilege-inventory verification endpoint are also retained. Security logging can timestamp a record just after the adjustment call returns; the measured verification interval is part of the operation, with no artificial delay or padded interval accepted as evidence. A wider query only collects diagnostic candidates; the strict matcher determines attribution.
|
||||
|
||||
The child has a 90-second limit, bounded asynchronous output, a bounded drain and confirmed termination before fixture cleanup. Native event reads have a timeout and require one successful Security-channel status. Query errors, schema differences, extra attributable records, caps, missing events, policy drift or failed cleanup fail the fixture; they are never reported as an empty successful observation. Native events and diagnostic XML remain in the short-lived CI artifacts.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ $ErrorActionPreference='Stop'
|
||||
$count=0
|
||||
function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++}
|
||||
$start=[DateTime]::Parse('2026-09-22T00:00:00Z').ToFileTimeUtc()
|
||||
$context=[pscustomobject]@{Task=13570;Computers=@('HOST','HOST.example.test');Watermark=100;ProcessId=1234;ProcessName='C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe';Sid='S-1-5-21-1-2-3-500';AuthenticationId='0x123';DisableStartedFileTime=$start;DisableReturnedFileTime=$start+100000;RestoreStartedFileTime=$start+200000;RestoreReturnedFileTime=$start+300000;OperationCompletedFileTime=$start+300000}
|
||||
$context=[pscustomobject]@{Task=13570;Computers=@('HOST','HOST.example.test');Watermark=100;ProcessId=1234;ProcessName='C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe';Sid='S-1-5-21-1-2-3-500';AuthenticationId='0x123';DisableStartedFileTime=$start;DisableReturnedFileTime=$start+100000;RestoreStartedFileTime=$start+200000;RestoreReturnedFileTime=$start+300000;PrivilegeVerificationCompletedFileTime=$start+300000;OperationCompletedFileTime=$start+300000}
|
||||
$xml=@'
|
||||
<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event"><System><Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-5478-4994-a5ba-3e3b0328c30d}"/><EventID>4703</EventID><Version>0</Version><Level>0</Level><Task>13570</Task><Opcode>0</Opcode><Keywords>0x8020000000000000</Keywords><TimeCreated SystemTime="2026-09-22T00:00:00.0050000Z"/><EventRecordID>101</EventRecordID><Channel>Security</Channel><Computer>HOST.example.test</Computer></System><EventData><Data Name="SubjectUserSid">S-1-5-21-1-2-3-500</Data><Data Name="SubjectLogonId">0x123</Data><Data Name="TargetUserSid">S-1-5-21-1-2-3-500</Data><Data Name="TargetLogonId">0x123</Data><Data Name="ProcessName">C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe</Data><Data Name="ProcessId">0x4d2</Data><Data Name="EnabledPrivilegeList">-</Data><Data Name="DisabledPrivilegeList">SeDebugPrivilege</Data></EventData></Event>
|
||||
'@
|
||||
@@ -34,7 +34,7 @@ $review=Get-WelaEventMappingReview @(Import-Csv "$PSScriptRoot/../config/eid_sub
|
||||
Assert ($review.State -ceq 'Conditional' -and $review.Candidates.Count -eq 2 -and -not $review.DetectionReady) 'Build-specific generation never erases historical candidates or grants Sigma credit.'
|
||||
Assert-WelaTokenAttributionTimes $context ($start-100) ($start+400000)
|
||||
Assert $true 'Typed monotonic native timing accepted.'
|
||||
foreach($field in @('DisableStartedFileTime','DisableReturnedFileTime','RestoreStartedFileTime','RestoreReturnedFileTime','OperationCompletedFileTime')){
|
||||
foreach($field in @('DisableStartedFileTime','DisableReturnedFileTime','RestoreStartedFileTime','RestoreReturnedFileTime','PrivilegeVerificationCompletedFileTime','OperationCompletedFileTime')){
|
||||
foreach($bad in @($true,'134345000000000000',0L,($start-200),($start+500000))){
|
||||
$copy=$context|ConvertTo-Json -Depth 8|ConvertFrom-Json;$copy.$field=$bad;$rejected=$false
|
||||
try{Assert-WelaTokenAttributionTimes $copy ($start-100) ($start+400000)}catch{$rejected=$true}
|
||||
@@ -42,4 +42,9 @@ foreach($field in @('DisableStartedFileTime','DisableReturnedFileTime','RestoreS
|
||||
}
|
||||
}
|
||||
$copy=$context|ConvertTo-Json -Depth 8|ConvertFrom-Json;$copy.RestoreStartedFileTime=$start+50000;$rejected=$false;try{Assert-WelaTokenAttributionTimes $copy ($start-100) ($start+400000)}catch{$rejected=$true};Assert $rejected 'Nonmonotonic in-envelope timestamps refused.'
|
||||
$definitions=@([pscustomobject]@{Name='SE_ADT_DETAILEDTRACKING_TOKENRIGHTADJ';Value=13317})
|
||||
$publisher='<provider name="Microsoft-Windows-Security-Auditing" guid="54849625-5478-4994-a5ba-3e3b0328c30d"><tasks><task name="SE_ADT_DETAILEDTRACKING_TOKENRIGHTADJ" value="13317"/></tasks><events><event value="4703" version="0" task="0"/></events></provider>'
|
||||
Assert ((Get-WelaTokenAttributionTask $definitions $publisher) -eq 13317) 'Independent native task definition and publisher XML bind runtime task despite generic event declaration0.'
|
||||
foreach($bad in @(@(),@($definitions[0],$definitions[0]),@([pscustomobject]@{Name=$definitions[0].Name;Value=$true}),@([pscustomobject]@{Name=$definitions[0].Name;Value='13317'}),@([pscustomobject]@{Name=$definitions[0].Name;Value=13570}),@([pscustomobject]@{Name='Wrong';Value=13317}))){$rejected=$false;try{$null=Get-WelaTokenAttributionTask $bad $publisher}catch{$rejected=$true};Assert $rejected 'Missing/duplicate/mistyped/wrong native task definition refuses.'}
|
||||
foreach($text in @($publisher.Replace('13317','13570'),$publisher.Replace('TOKENRIGHTADJ','OTHER'),$publisher.Replace('54849625','54849626'),$publisher.Replace('<tasks>','<other>').Replace('</tasks>','</other>'))){$rejected=$false;try{$null=Get-WelaTokenAttributionTask $definitions $text}catch{$rejected=$true};Assert $rejected 'Native publisher task/identity mismatch refuses.'}
|
||||
Write-Host "PASS: $count strict token attribution and catalog checks."
|
||||
@@ -22,10 +22,11 @@ if(-not(Test-WelaDefaultContextComplete $hostContext) -or $hostContext.ProductTy
|
||||
$provider=Get-WinEvent -ListProvider 'Microsoft-Windows-Security-Auditing'
|
||||
$schema=@($provider.Events|Where-Object{$_.Id -eq 4703 -and $_.Version -eq 0})
|
||||
if($schema.Count -ne 1 -or $provider.Id -ne [guid]'54849625-5478-4994-a5ba-3e3b0328c30d'){throw 'Exactly one installed version0 Security4703 schema is required.'}
|
||||
$eventTask=[int]$schema[0].Task.Value
|
||||
$declaredTask=[int]$schema[0].Task.Value
|
||||
Save 'provider-diagnostic.json' @{TaskType=$schema[0].Task.GetType().FullName;TaskValue=$schema[0].Task.Value;TaskName=$schema[0].Task.Name;TaskDisplay=$schema[0].Task.DisplayName;Tasks=@($provider.Tasks|ForEach-Object{@{Value=$_.Value;Name=$_.Name;Display=$_.DisplayName;Guid=[string]$_.EventGuid}})}
|
||||
$publisher=Invoke-WelaNative wevtutil.exe @('gp','Microsoft-Windows-Security-Auditing','/ge:true','/gm:false','/f:xml')
|
||||
$publisherText=$publisher.Output -join "`n";if($publisherText.Length -gt 4194304){throw 'Native publisher metadata exceeds fixture bound.'};[IO.File]::WriteAllText((Join-Path $root 'publisher.xml'),$publisherText)
|
||||
$eventTask=Get-WelaTokenAttributionTask @($provider.Tasks) $publisherText
|
||||
$computerProperties=[Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties();$computers=@([Environment]::MachineName,$computerProperties.HostName);if($computerProperties.DomainName){$computers+=$computerProperties.HostName+'.'+$computerProperties.DomainName};$computers=@($computers|Sort-Object -Unique)
|
||||
function Channel{(Invoke-WelaNative wevtutil.exe @('gl','Security','/f:xml')).Output -join "`n"}
|
||||
function Services{@(Get-Service Winmgmt,EventLog|Sort-Object Name|ForEach-Object{[pscustomobject]@{Name=$_.Name;Status=[string]$_.Status;StartType=[string]$_.StartType}})}
|
||||
@@ -44,7 +45,7 @@ Save 'mapping-review.json' $mapping
|
||||
$sources=[ordered]@{}
|
||||
foreach($file in @('tests/TokenRightAttribution.Windows.Tests.ps1','tests/TokenRightAttributionNative.cs','tests/TokenRightAttributionEvidence.ps1','tests/TokenRightAttribution.Tests.ps1','modules/AuditProfiles.psm1','modules/AuditCatalog.psm1','scripts/Configuration.ps1','scripts/WefArrival.ps1','scripts/WmiProbe.ps1','scripts/WmiProbeNative.cs','scripts/ControlApplicability.ps1','config/audit_profiles.json','config/baselines.json','config/eid_subcategory_mapping.csv')){$sources[$file]=(Get-FileHash -LiteralPath "$repo/$file" -Algorithm SHA256).Hash.ToLowerInvariant()}
|
||||
$beforeMasks=Get-WelaEffectiveAuditPolicy;$masks=Masks;$beforePrecedence=Get-WelaRegistryState $path $name;$beforeToken=[Wela.WmiProbe.Native]::Snapshot();$failure=$null;$errors=@()
|
||||
Save 'original.json' @{Masks=$beforeMasks;Precedence=$beforePrecedence;Token=$beforeToken;Head=$env:GITHUB_SHA;Engine=$PSVersionTable.PSVersion.ToString();Host=$hostContext;Channel=$originalChannel;Services=$originalServices;Computers=$computers;Provider=[string]$provider.Id;Schema=@{Id=4703;Version=0;Task=$eventTask;Template=$schema[0].Template}}
|
||||
Save 'original.json' @{Masks=$beforeMasks;Precedence=$beforePrecedence;Token=$beforeToken;Head=$env:GITHUB_SHA;Engine=$PSVersionTable.PSVersion.ToString();Host=$hostContext;Channel=$originalChannel;Services=$originalServices;Computers=$computers;Provider=[string]$provider.Id;Schema=@{Id=4703;Version=0;Task=$declaredTask;RuntimeTask=$eventTask;RuntimeTaskName='SE_ADT_DETAILEDTRACKING_TOKENRIGHTADJ';Template=$schema[0].Template}}
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;using System.IO;using System.Text;using System.Threading.Tasks;
|
||||
public static class WelaTokenFixturePipe {
|
||||
@@ -81,6 +82,7 @@ $executable=[Wela.TokenRightProbe.Native]::Executable()
|
||||
$before=[Wela.WmiProbe.Native]::Snapshot()
|
||||
$outcome=[Wela.TokenRightProbe.Native]::Run()
|
||||
$after=[Wela.WmiProbe.Native]::Snapshot()
|
||||
$outcome.OperationCompletedFileTime=[Wela.WmiProbe.Native]::UtcNow().ToFileTimeUtc()
|
||||
[pscustomobject]@{ProcessId=$PID;ProcessName=$executable;Before=$before;After=$after;Outcome=$outcome}|ConvertTo-Json -Depth 24|Set-Content -LiteralPath $Result -Encoding UTF8
|
||||
if($outcome.Status -ne 'Adjusted' -or -not $outcome.Restored -or (($before|ConvertTo-Json -Depth 24 -Compress) -cne ($after|ConvertTo-Json -Depth 24 -Compress))){exit 1}
|
||||
exit 0
|
||||
|
||||
@@ -31,10 +31,25 @@ function Get-WelaTokenAttributionMatch {
|
||||
function Assert-WelaTokenAttributionTimes {
|
||||
param($Operation,[long]$Launched,[long]$Observed)
|
||||
$previous=$Launched
|
||||
foreach($name in @('DisableStartedFileTime','DisableReturnedFileTime','RestoreStartedFileTime','RestoreReturnedFileTime','OperationCompletedFileTime')){
|
||||
foreach($name in @('DisableStartedFileTime','DisableReturnedFileTime','RestoreStartedFileTime','RestoreReturnedFileTime','PrivilegeVerificationCompletedFileTime','OperationCompletedFileTime')){
|
||||
$value=$Operation.$name
|
||||
if(($value -isnot [long] -and $value -isnot [int]) -or $value -le 0 -or $value -lt $previous -or $value -gt $Observed){throw 'Native operation timestamps must be typed, monotonic and within parent observations.'}
|
||||
$previous=$value
|
||||
}
|
||||
if($Launched -gt $Observed -or ($Observed-$Launched) -gt 950000000){throw 'Parent operation envelope exceeds its bounded worker lifetime.'}
|
||||
}
|
||||
function Get-WelaTokenAttributionTask {
|
||||
param($Definitions,[string]$PublisherXml)
|
||||
$name='SE_ADT_DETAILEDTRACKING_TOKENRIGHTADJ'
|
||||
$rows=@($Definitions|Where-Object{$_.Name -is [string] -and $_.Name -ceq $name})
|
||||
if($rows.Count -ne 1 -or $rows[0].Value -isnot [int] -or $rows[0].Value -ne 13317){throw 'The independently installed token-right task definition is absent or differs.'}
|
||||
if($PublisherXml.Length -gt 4194304){throw 'Publisher XML exceeds its fixture bound.'}
|
||||
$settings=[Xml.XmlReaderSettings]::new();$settings.DtdProcessing=[Xml.DtdProcessing]::Prohibit;$settings.XmlResolver=$null;$settings.MaxCharactersInDocument=4194304
|
||||
$reader=[Xml.XmlReader]::Create([IO.StringReader]::new($PublisherXml),$settings);$xml=[Xml.XmlDocument]::new();$xml.XmlResolver=$null
|
||||
try{$xml.Load($reader)}finally{$reader.Dispose()}
|
||||
$root=$xml.DocumentElement
|
||||
if($root.LocalName -cne 'provider' -or $root.GetAttribute('name') -cne 'Microsoft-Windows-Security-Auditing' -or $root.GetAttribute('guid') -ine '54849625-5478-4994-a5ba-3e3b0328c30d'){throw 'Native publisher identity differs.'}
|
||||
$tasks=@($root.SelectNodes('tasks/task')|Where-Object{$_.GetAttribute('name') -ceq $name})
|
||||
if($tasks.Count -ne 1 -or $tasks[0].GetAttribute('value') -cne '13317'){throw 'Native publisher XML does not corroborate the fixed token-right task.'}
|
||||
13317
|
||||
}
|
||||
@@ -10,7 +10,7 @@ namespace Wela.TokenRightProbe {
|
||||
public string Status, Diagnostic, Luid;
|
||||
public bool AdjustmentAttempted, Restored;
|
||||
public uint OriginalAttributes;
|
||||
public long DisableStartedFileTime, DisableReturnedFileTime, RestoreStartedFileTime, RestoreReturnedFileTime, OperationCompletedFileTime;
|
||||
public long DisableStartedFileTime, DisableReturnedFileTime, RestoreStartedFileTime, RestoreReturnedFileTime, PrivilegeVerificationCompletedFileTime, OperationCompletedFileTime;
|
||||
public Privilege[] Before, Disabled, After;
|
||||
}
|
||||
public static class Native {
|
||||
@@ -86,7 +86,7 @@ namespace Wela.TokenRightProbe {
|
||||
} finally {
|
||||
if(result.AdjustmentAttempted) {
|
||||
result.RestoreStartedFileTime=Now();Change(token,target,result.OriginalAttributes);result.RestoreReturnedFileTime=Now();
|
||||
result.After=Read(token);Equal(result.Before,result.After,result.Luid,true);result.Restored=true;result.OperationCompletedFileTime=Now();
|
||||
result.After=Read(token);Equal(result.Before,result.After,result.Luid,true);result.Restored=true;result.PrivilegeVerificationCompletedFileTime=Now();result.OperationCompletedFileTime=result.PrivilegeVerificationCompletedFileTime;
|
||||
}
|
||||
}
|
||||
} catch(Exception error) {result.Status=result.AdjustmentAttempted?"Unverified":"Refused";result.Diagnostic=error.ToString();}
|
||||
|
||||
Reference in new issue
Block a user