mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-14 18:25:33 +02:00
Measure held-readback phase and refuse implementation targets before hashing
This commit is contained in:
1 parent
03bc63e996
commit
d832b1e090
10 files changed
+93
-29
No files matched your search
@@ -1,4 +1,51 @@
|
||||
# Explicit one-byte existing-file read and exact local Security4663 evidence.
|
||||
# Resolve target scope before reading any external native-helper source or hashing dependencies.
|
||||
# This small literal helper opens metadata only and never reads target bytes.
|
||||
function Initialize-WelaFileProbeScopeNative {
|
||||
if([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or -not [Environment]::Is64BitProcess){throw 'The file probe requires native 64-bit Windows.'}
|
||||
$definition=@'
|
||||
using System;using System.ComponentModel;using System.Runtime.InteropServices;using System.Text;
|
||||
namespace Wela.FileAccessScope {
|
||||
public sealed class Observation {public string Path;public uint Links,Attributes;}
|
||||
public static class Native {
|
||||
public const string SourceSha256="__WELA_FILE_SCOPE_SOURCE_SHA256__";
|
||||
[StructLayout(LayoutKind.Sequential,Pack=4)] struct Info {public uint Attributes;public long Created,Accessed,Written;public uint Volume,SizeHigh,SizeLow,Links,IndexHigh,IndexLow;}
|
||||
[DllImport("kernel32.dll",CharSet=CharSet.Unicode,SetLastError=true,ExactSpelling=true)] static extern IntPtr CreateFileW(string path,uint access,uint share,IntPtr security,uint disposition,uint flags,IntPtr template);
|
||||
[DllImport("kernel32.dll",SetLastError=true)] static extern bool CloseHandle(IntPtr handle);
|
||||
[DllImport("kernel32.dll",SetLastError=true)] static extern bool GetFileInformationByHandle(IntPtr handle,out Info info);
|
||||
[DllImport("kernel32.dll",CharSet=CharSet.Unicode,SetLastError=true,ExactSpelling=true)] static extern uint GetFinalPathNameByHandleW(IntPtr handle,StringBuilder path,uint length,uint flags);
|
||||
public static Observation Observe(string path) {
|
||||
IntPtr handle=CreateFileW(path,0x80,7,IntPtr.Zero,3,0x00200000,IntPtr.Zero);
|
||||
if(handle==new IntPtr(-1))throw new Win32Exception(Marshal.GetLastWin32Error(),"Metadata-only target-scope observation failed.");
|
||||
try{Info info;if(!GetFileInformationByHandle(handle,out info))throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
StringBuilder final=new StringBuilder(32768);uint length=GetFinalPathNameByHandleW(handle,final,(uint)final.Capacity,0);
|
||||
if(length==0||length>=final.Capacity||!final.ToString().StartsWith(@"\\?\",StringComparison.Ordinal))throw new InvalidOperationException("Canonical local target scope is unknown.");
|
||||
return new Observation{Path=final.ToString().Substring(4),Links=info.Links,Attributes=info.Attributes};
|
||||
}finally{CloseHandle(handle);}
|
||||
}
|
||||
}
|
||||
}
|
||||
'@
|
||||
$hash=Get-WelaArrivalHash ([Text.UTF8Encoding]::new($false).GetBytes($definition))
|
||||
if(-not ('Wela.FileAccessScope.Native' -as [type])){Add-Type -TypeDefinition $definition.Replace('__WELA_FILE_SCOPE_SOURCE_SHA256__',$hash) -ErrorAction Stop}
|
||||
if([Wela.FileAccessScope.Native]::SourceSha256 -cne $hash){throw 'Loaded file scope helper differs; start a fresh session.'}
|
||||
}
|
||||
function Assert-WelaFileProbeScopeObservation {
|
||||
param([string]$SelectedPath,$Selected,$SourceFile,$Engine)
|
||||
foreach($item in @($Selected,$SourceFile,$Engine)){if($item.Path -isnot [string] -or -not $item.Path){throw 'Incomplete canonical target scope.'};Assert-WelaFileProbePath $item.Path}
|
||||
$sourceRoot=$SourceFile.Path.Substring(0,$SourceFile.Path.LastIndexOf('\')+1)
|
||||
if($Selected.Path.StartsWith($sourceRoot,[StringComparison]::OrdinalIgnoreCase)){throw 'Select a file outside the WELA source tree; implementation targets are unsupported.'}
|
||||
if($Selected.Path.Equals($Engine.Path,[StringComparison]::OrdinalIgnoreCase)){throw 'The active PowerShell engine cannot be the selected file target.'}
|
||||
if($Selected.Path -ine $SelectedPath -or -not(Test-WelaFileProbeInteger $Selected.Links) -or $Selected.Links -ne 1 -or -not(Test-WelaFileProbeInteger $Selected.Attributes) -or ($Selected.Attributes -band 1040)){throw 'Only ordinary canonical single-link file targets are supported; aliases and reparse targets are refused.'}
|
||||
}
|
||||
function Assert-WelaFileProbeTargetScope {
|
||||
param([string]$Path)
|
||||
Initialize-WelaFileProbeScopeNative
|
||||
$selected=[Wela.FileAccessScope.Native]::Observe($Path)
|
||||
$source=[Wela.FileAccessScope.Native]::Observe((Join-Path $script:ScriptRoot 'WELA.ps1'))
|
||||
$engine=[Wela.FileAccessScope.Native]::Observe((Get-Process -Id $PID -ErrorAction Stop).Path)
|
||||
Assert-WelaFileProbeScopeObservation $Path $selected $source $engine
|
||||
}
|
||||
function Initialize-WelaFileProbeNative {
|
||||
Initialize-WelaWmiProbeNative
|
||||
$source=Join-Path $PSScriptRoot 'FileAccessProbeNative.cs';$bytes=[IO.File]::ReadAllBytes($source);$hash=Get-WelaArrivalHash $bytes
|
||||
@@ -57,7 +104,7 @@ function Get-WelaFileProbeSnapshot {
|
||||
}
|
||||
function Get-WelaFileProbeState {
|
||||
param([string]$Path)
|
||||
Assert-WelaFileProbePath $Path;Initialize-WelaFileProbeNative
|
||||
Assert-WelaFileProbePath $Path;Assert-WelaFileProbeTargetScope $Path;Initialize-WelaFileProbeNative
|
||||
$services=@(Get-Service -Name EventLog,Winmgmt,RpcSs -ErrorAction Stop|Sort-Object Name|ForEach-Object {[pscustomobject]@{Name=$_.Name;Status=[string]$_.Status}})
|
||||
if($services.Count -ne 3 -or @($services|Where-Object Status -ne 'Running').Count){throw 'EventLog, Winmgmt and RpcSs must already be running.'}
|
||||
$null=Get-WelaFileProbeReaderKey (Get-WelaChannelReader)
|
||||
@@ -117,13 +164,13 @@ function Assert-WelaFileProbeOperation {
|
||||
foreach($name in @('Kind','Nonce','Executable','FilePath')){if($Operation.$name -isnot [string]){throw 'Untyped fixed file worker authority.'}}
|
||||
if($Operation.Kind -cne 'WelaOneByteFileRead' -or $Operation.Nonce -cne $Nonce -or -not(Test-WelaFileProbeInteger $Operation.ProcessId) -or $Operation.ProcessId -ne $ProcessId -or $Operation.Executable -ine $State.Engine -or $Operation.FilePath -ine $State.File.Path){throw 'Unexpected fixed file worker identity.'}
|
||||
$read=$Operation.Read
|
||||
foreach($name in @('Clock','HandleId','BeforeKey','AfterKey')){if($read.$name -isnot [string]){throw 'Untyped native read receipt.'}}
|
||||
if($read.Clock -cne 'GetSystemTimePreciseAsFileTime' -or $read.Succeeded -isnot [bool] -or -not $read.Succeeded -or -not(Test-WelaFileProbeInteger $read.ReadCalls) -or $read.ReadCalls -ne 1 -or -not(Test-WelaFileProbeInteger $read.BytesRead) -or $read.BytesRead -ne 1 -or $read.HandleId -cnotmatch '^0x[0-9a-f]+$' -or [Convert]::ToUInt64($read.HandleId.Substring(2),16) -eq 0 -or $read.BeforeKey -cne $State.File.StateKey -or $read.AfterKey -cne $State.File.StateKey){throw 'Expected exactly one successful byte read from the unchanged held file.'}
|
||||
$start=ConvertTo-WelaArrivalUtc $read.StartedUtc;$end=ConvertTo-WelaArrivalUtc $read.CompletedUtc
|
||||
if($LaunchedUtc -gt $ObservedUtc -or $start -lt $LaunchedUtc -or $end -lt $start -or $end -gt $ObservedUtc -or ($end-$start).TotalSeconds -gt 20){throw 'Invalid precise one-byte native read interval.'}
|
||||
foreach($name in @('Clock','Phase','HandleId','BeforeKey','AfterKey')){if($read.$name -isnot [string]){throw 'Untyped native read receipt.'}}
|
||||
if($read.Phase -cne 'OneByteReadAndHeldIdentityReadback' -or $read.Clock -cne 'GetSystemTimePreciseAsFileTime' -or $read.Succeeded -isnot [bool] -or -not $read.Succeeded -or -not(Test-WelaFileProbeInteger $read.ReadCalls) -or $read.ReadCalls -ne 1 -or -not(Test-WelaFileProbeInteger $read.BytesRead) -or $read.BytesRead -ne 1 -or $read.HandleId -cnotmatch '^0x[0-9a-f]+$' -or [Convert]::ToUInt64($read.HandleId.Substring(2),16) -eq 0 -or $read.BeforeKey -cne $State.File.StateKey -or $read.AfterKey -cne $State.File.StateKey){throw 'Expected exactly one successful byte read from the unchanged held file.'}
|
||||
$start=ConvertTo-WelaArrivalUtc $read.StartedUtc;$returned=ConvertTo-WelaArrivalUtc $read.ReadReturnedUtc;$end=ConvertTo-WelaArrivalUtc $read.CompletedUtc
|
||||
if($LaunchedUtc -gt $ObservedUtc -or $start -lt $LaunchedUtc -or $returned -lt $start -or $end -lt $returned -or $end -gt $ObservedUtc -or ($end-$start).TotalSeconds -gt 20){throw 'Invalid precise one-byte/readback operation interval.'}
|
||||
if((Get-WelaFileProbeTokenKey $Operation.BeforeToken) -cne (Get-WelaFileProbeTokenKey $Operation.AfterToken) -or (Get-WelaFileProbeTokenKey $Operation.BeforeToken) -cne (Get-WelaFileProbeTokenKey $State.Token) -or
|
||||
(Get-WelaFileProbeReaderKey $Operation.BeforeReader) -cne (Get-WelaFileProbeReaderKey $Operation.AfterReader) -or (Get-WelaFileProbeReaderKey $Operation.BeforeReader -AuthorizationOnly) -cne (Get-WelaFileProbeKey $State.Reader)){throw 'Worker primary token differs from the caller or changed during the native read.'}
|
||||
$read.StartedUtc=$start.UtcDateTime.ToString('o');$read.CompletedUtc=$end.UtcDateTime.ToString('o')
|
||||
$read.StartedUtc=$start.UtcDateTime.ToString('o');$read.ReadReturnedUtc=$returned.UtcDateTime.ToString('o');$read.CompletedUtc=$end.UtcDateTime.ToString('o')
|
||||
}
|
||||
function Start-WelaFileProbeRead {
|
||||
param($State,[string]$RequestPath,[string]$Nonce)
|
||||
@@ -202,7 +249,7 @@ function Invoke-WelaFileAccessProbe {
|
||||
$matches=@($batch.Xml|Where-Object {Test-WelaFileProbeEvent $_ $operation $before});if($matches.Count){break};Start-Sleep -Milliseconds 250
|
||||
}while($timer.Elapsed.TotalSeconds -lt $TimeoutSeconds)
|
||||
$report.Matches=$matches.Count
|
||||
if($matches.Count -ne 1){$i=0;foreach($xml in @($batch.Xml|Select-Object -First 4)){$i++;$report.Artifacts+=Write-WelaFileProbeArtifact $report.OutputPath $outputKey ('candidate-'+$i+'.xml') $xml};throw 'Exactly one attributable native4663 was not observed in the precise read interval.'}
|
||||
if($matches.Count -ne 1){$i=0;foreach($xml in @($batch.Xml|Select-Object -First 4)){$i++;$report.Artifacts+=Write-WelaFileProbeArtifact $report.OutputPath $outputKey ('candidate-'+$i+'.xml') $xml};throw 'Exactly one attributable native4663 was not observed in the measured one-byte/readback phase.'}
|
||||
$report.Artifacts+=Write-WelaFileProbeArtifact $report.OutputPath $outputKey 'event.xml' $matches[0]
|
||||
if((Get-WelaFileProbeWatermark) -lt $operation.RecordIdBefore){throw 'Security record boundary moved backwards.'}
|
||||
$after=Get-WelaFileProbeState $before.File.Path;$report.After=$after
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Wela.FileAccessProbe {
|
||||
public long Size; public uint Attributes,Links; public int SecurityInformation; public Ace[] Aces;
|
||||
}
|
||||
public sealed class ReadReceipt {
|
||||
public string StartedUtc,CompletedUtc,Clock,HandleId,BeforeKey,AfterKey;
|
||||
public string StartedUtc,ReadReturnedUtc,CompletedUtc,Clock,Phase,HandleId,BeforeKey,AfterKey;
|
||||
public int ReadCalls,BytesRead; public bool Succeeded;
|
||||
}
|
||||
sealed class SecurityPrivilege : IDisposable {
|
||||
@@ -98,11 +98,14 @@ namespace Wela.FileAccessProbe {
|
||||
public ReadReceipt ReadOne(string expectedKey) {
|
||||
if(!canRead||readAttempted)throw new InvalidOperationException("Exactly one explicitly requested data read is permitted.");
|
||||
Observation before=Observe();if(!String.Equals(before.StateKey,expectedKey,StringComparison.Ordinal))throw new InvalidOperationException("Selected file changed before its one-byte read.");
|
||||
byte[] buffer=new byte[1];readAttempted=true;uint count=0;DateTime started=UtcNow(),completed;bool success;int error;
|
||||
try{success=ReadFile(handle,buffer,1,out count,IntPtr.Zero);error=Marshal.GetLastWin32Error();completed=UtcNow();}finally{Array.Clear(buffer,0,buffer.Length);}
|
||||
byte[] buffer=new byte[1];readAttempted=true;uint count=0;DateTime started=UtcNow(),returned;bool success;int error;
|
||||
try{success=ReadFile(handle,buffer,1,out count,IntPtr.Zero);error=Marshal.GetLastWin32Error();returned=UtcNow();}finally{Array.Clear(buffer,0,buffer.Length);}
|
||||
if(!success)throw new Win32Exception(error,"The one-byte read failed.");if(count!=1)throw new InvalidOperationException("The fixed read did not return exactly one byte.");
|
||||
Observation after=Observe();if(after.StateKey!=before.StateKey)throw new InvalidOperationException("Held file identity, data metadata or descriptor changed during the read.");
|
||||
return new ReadReceipt{StartedUtc=started.ToString("o"),CompletedUtc=completed.ToString("o"),Clock="GetSystemTimePreciseAsFileTime",ReadCalls=1,BytesRead=1,Succeeded=true,HandleId="0x"+unchecked((ulong)handle.ToInt64()).ToString("x"),BeforeKey=before.StateKey,AfterKey=after.StateKey};
|
||||
// Measure the real completed phase, including the existing held-handle identity/security readback.
|
||||
// Retain the immediate ReadFile return separately; add no delay or timestamp padding.
|
||||
DateTime completed=UtcNow();
|
||||
return new ReadReceipt{StartedUtc=started.ToString("o"),ReadReturnedUtc=returned.ToString("o"),CompletedUtc=completed.ToString("o"),Phase="OneByteReadAndHeldIdentityReadback",Clock="GetSystemTimePreciseAsFileTime",ReadCalls=1,BytesRead=1,Succeeded=true,HandleId="0x"+unchecked((ulong)handle.ToInt64()).ToString("x"),BeforeKey=before.StateKey,AfterKey=after.StateKey};
|
||||
}
|
||||
public void Dispose(){if(handle!=IntPtr.Zero){CloseHandle(handle);handle=IntPtr.Zero;}}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user