mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-11-15 17:34:11 +01:00
Add fixed native AppLocker Script probe and disposable evidence matrix
This commit is contained in:
1 parent
203fdfc942
commit
8ac4c45653
8 files changed
+577
No files matched your search
@@ -0,0 +1,79 @@
|
||||
// Read-only process-token observations. No privilege or authorization changes.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Principal;
|
||||
namespace Wela.AppLockerScript {
|
||||
public sealed class Group { public string Sid; public uint Attributes; }
|
||||
public sealed class Privilege { public string Luid; public uint Attributes; }
|
||||
public sealed class Token {
|
||||
public string Sid, Name, TokenId, ModifiedId, AuthenticationId, AuthenticationType, ImpersonationLevel, TokenSource;
|
||||
public Group[] Groups; public Privilege[] Privileges;
|
||||
}
|
||||
public static class Native {
|
||||
public const string SourceSha256="__WELA_SOURCE_SHA256__";
|
||||
[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.");Statistics stats=(Statistics)Marshal.PtrToStructure(p,typeof(Statistics));if(stats.Type!=1)throw new InvalidOperationException("A primary token is required.");result.AuthenticationId=Hex(stats.AuthenticationId);result.TokenId=Hex(stats.TokenId);result.ModifiedId=Hex(stats.Modified);}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<Privilege> privileges=new List<Privilege>();for(int i=0;i<count;i++){LuidAndAttributes v=(LuidAndAttributes)Marshal.PtrToStructure(IntPtr.Add(p,4+i*size),typeof(LuidAndAttributes));privileges.Add(new Privilege{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;
|
||||
}
|
||||
public static Token Child(IntPtr handle) {
|
||||
IntPtr token=IntPtr.Zero;
|
||||
if(!OpenProcessToken(handle,8,out token))throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
try {if(IsTokenRestricted(token))throw new InvalidOperationException("Restricted child token unsupported.");return ReadToken(token,"ChildProcess");}finally{CloseHandle(token);}
|
||||
}
|
||||
[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)){CloseHandle(thread);throw new InvalidOperationException("Impersonated callers are unsupported.");}
|
||||
int error=Marshal.GetLastWin32Error();if(error!=1008)throw new Win32Exception(error);
|
||||
if(!OpenProcessToken(GetCurrentProcess(),8,out process))throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
try {if(IsTokenRestricted(process))throw new InvalidOperationException("Restricted caller unsupported.");return ReadToken(process,"Process");}finally{CloseHandle(process);}
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)] struct FileInformation {
|
||||
public uint Attributes;public System.Runtime.InteropServices.ComTypes.FILETIME Creation,Access,Write;
|
||||
public uint Volume,SizeHigh,SizeLow,Links,IndexHigh,IndexLow;
|
||||
}
|
||||
[DllImport("kernel32.dll",SetLastError=true)] static extern bool GetFileInformationByHandle(IntPtr file,out FileInformation info);
|
||||
public static string FileId(IntPtr handle) {
|
||||
FileInformation info;if(!GetFileInformationByHandle(handle,out info))throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
if((info.Attributes&0x410)!=0)throw new InvalidOperationException("One ordinary non-reparse file identity is required.");
|
||||
return info.Volume.ToString("x8")+":"+info.IndexHigh.ToString("x8")+info.IndexLow.ToString("x8");
|
||||
}
|
||||
public static async System.Threading.Tasks.Task<string> ReadLineBoundedAsync(System.IO.StreamReader reader,int limit) {
|
||||
char[] buffer=new char[1];System.Text.StringBuilder text=new System.Text.StringBuilder();
|
||||
while(true){int count=await reader.ReadAsync(buffer,0,1).ConfigureAwait(false);if(count==0)throw new InvalidOperationException("Owned child ended before its ready marker.");if(buffer[0]=='\n')return text.ToString().TrimEnd('\r');if(text.Length>=limit)throw new InvalidOperationException("Owned child line exceeds the bound.");text.Append(buffer[0]);}
|
||||
}
|
||||
public static async System.Threading.Tasks.Task<string> ReadBoundedAsync(System.IO.StreamReader reader,int limit) {
|
||||
char[] buffer=new char[256];System.Text.StringBuilder text=new System.Text.StringBuilder();
|
||||
while(true){int count=await reader.ReadAsync(buffer,0,buffer.Length).ConfigureAwait(false);if(count==0)return text.ToString();if(text.Length+count>limit)throw new InvalidOperationException("Owned child output exceeds the bound.");text.Append(buffer,0,count);}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
# One fixed native Windows PowerShell script. Never prepares policy/services/channels.
|
||||
function Get-WelaAppLockerScriptKey { param($Value) ConvertTo-Json -InputObject $Value -Depth 30 -Compress }
|
||||
function Get-WelaAppLockerScriptSources {
|
||||
$sources=[ordered]@{}
|
||||
foreach($path in @('WELA.ps1','scripts/AppLockerScriptProbe.ps1','scripts/AppLockerScriptNative.cs','scripts/AppLockerScriptWorker.ps1','scripts/AppLockerReadiness.ps1','scripts/WefArrival.ps1')) {
|
||||
$sources[$path]=(Get-FileHash -LiteralPath (Join-Path $script:ScriptRoot $path) -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()
|
||||
}
|
||||
[pscustomobject]$sources
|
||||
}
|
||||
function Initialize-WelaAppLockerScriptNative {
|
||||
if([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or -not [Environment]::Is64BitProcess){throw 'A native 64-bit Windows process is required.'}
|
||||
$bytes=[IO.File]::ReadAllBytes((Join-Path $script:ScriptRoot 'scripts/AppLockerScriptNative.cs'))
|
||||
$hash=Get-WelaArrivalHash $bytes
|
||||
if(-not ('Wela.AppLockerScript.Native' -as [type])) {
|
||||
$text=[Text.UTF8Encoding]::new($false,$true).GetString($bytes).TrimStart([char]0xfeff)
|
||||
if(([regex]::Matches($text,'__WELA_SOURCE_SHA256__')).Count -ne 1){throw 'Unexpected native source fingerprint placeholder.'}
|
||||
Add-Type -TypeDefinition $text.Replace('__WELA_SOURCE_SHA256__',$hash) -ErrorAction Stop
|
||||
}
|
||||
if([Wela.AppLockerScript.Native]::SourceSha256 -cne $hash){throw 'Loaded helper differs from current source; start a fresh PowerShell process.'}
|
||||
}
|
||||
function Get-WelaAppLockerScriptReader { [Wela.AppLockerScript.Native]::Snapshot() }
|
||||
function Get-WelaAppLockerScriptUtcNow { [Wela.AppLockerScript.Native]::UtcNow() }
|
||||
function Get-WelaAppLockerScriptExecutionPolicy {
|
||||
$values=[ordered]@{InheritedProcessValue=$env:PSExecutionPolicyPreference;Machine=@();User=@()}
|
||||
foreach($scope in @('Machine','User')) {
|
||||
$base=if($scope -eq 'Machine'){[Microsoft.Win32.Registry]::LocalMachine}else{[Microsoft.Win32.Registry]::CurrentUser}
|
||||
foreach($path in @('SOFTWARE\Policies\Microsoft\Windows\PowerShell','SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell')) {
|
||||
$key=$base.OpenSubKey($path,$false)
|
||||
try {
|
||||
foreach($name in @('EnableScripts','ExecutionPolicy')) {
|
||||
$present=$null -ne $key -and $name -cin @($key.GetValueNames())
|
||||
$values[$scope]+=[pscustomobject]@{Path=$path;Name=$name;Present=[bool]$present;Kind=$(if($present){[string]$key.GetValueKind($name)}else{$null});Value=$(if($present){$key.GetValue($name,$null,[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)}else{$null})}
|
||||
}
|
||||
}finally{if($key){$key.Dispose()}}
|
||||
}
|
||||
}
|
||||
[pscustomobject]$values
|
||||
}
|
||||
function Get-WelaAppLockerScriptState {
|
||||
$hostState=Get-WelaAppLockerHost
|
||||
$computer=Get-CimInstance Win32_ComputerSystem -ErrorAction Stop
|
||||
$version=Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction Stop
|
||||
$machine=Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Cryptography' -Name MachineGuid -ErrorAction Stop
|
||||
$channel=[Diagnostics.Eventing.Reader.EventLogConfiguration]::new('Microsoft-Windows-AppLocker/MSI and Script')
|
||||
try {$log=[ordered]@{Name=$channel.LogName;Enabled=$channel.IsEnabled;SecurityDescriptor=$channel.SecurityDescriptor;MaximumSize=$channel.MaximumSizeInBytes;Mode=[string]$channel.LogMode;LogFilePath=$channel.LogFilePath}}finally{$channel.Dispose()}
|
||||
$source=Resolve-WelaArrivalPath (Join-Path ([Environment]::SystemDirectory) 'WindowsPowerShell\v1.0\powershell.exe')
|
||||
[pscustomobject][ordered]@{Host=$hostState;MachineGuid=$machine.MachineGuid;UBR=$version.UBR;Computer=[Environment]::MachineName;Domain=[string]$computer.Domain;LocalPolicy=(Get-WelaAppLockerPolicySnapshot Local);EffectivePolicy=(Get-WelaAppLockerPolicySnapshot Effective);Management=(Get-WelaAppLockerManagement);Service=(Get-WelaAppLockerService);Channel=$log;ExecutionPolicy=(Get-WelaAppLockerScriptExecutionPolicy);Source=$source;SourceHash=(Get-FileHash -LiteralPath $source -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()}
|
||||
}
|
||||
function Get-WelaAppLockerScriptStateKey {
|
||||
param($State)
|
||||
if($State.Host.Status -cne 'Candidate' -or $State.Host.Is64BitProcess -isnot [bool] -or -not $State.Host.Is64BitProcess -or $State.Host.ProductType -notin @(1,3) -or ($State.Host.ProductType -eq 1 -and $State.Host.Build -notin @(22000,22621,22631,26100,26200)) -or ($State.Host.ProductType -eq 3 -and $State.Host.Build -notin @(20348,26100))){throw 'A reviewed native Windows 11 or Server 2022/2025 member host is required; DCs are excluded.'}
|
||||
foreach($policy in @($State.LocalPolicy,$State.EffectivePolicy)){if($policy.Status -cne 'Observed' -or $policy.Policy.HasUnknownPolicyData -isnot [bool] -or $policy.Policy.HasUnknownPolicyData){throw 'Local and effective GP policy must be readable and understood.'}}
|
||||
$collection=@($State.EffectivePolicy.Policy.Collections|Where-Object Type -CEQ 'Script')
|
||||
if($collection.Count -ne 1 -or $collection[0].EnforcementMode -cne 'AuditOnly' -or $collection[0].RuleCount -lt 1){throw 'An existing nonempty effective Script AuditOnly collection is required.'}
|
||||
if($State.Service.Status -cne 'Observed' -or $State.Service.State -cne 'Running'){throw 'AppIDSvc must already be running.'}
|
||||
if($State.Channel.Enabled -isnot [bool] -or -not $State.Channel.Enabled -or $State.Channel.Name -cne 'Microsoft-Windows-AppLocker/MSI and Script' -or -not $State.Channel.SecurityDescriptor){throw 'The native MSI and Script channel must already be enabled and readable.'}
|
||||
if($State.Management.Status -cne 'Observed' -or $State.SourceHash -cnotmatch '^[a-f0-9]{64}$' -or -not $State.MachineGuid -or -not $State.Computer){throw 'Incomplete management, machine or source observation.'}
|
||||
Get-WelaAppLockerScriptKey $State
|
||||
}
|
||||
function Get-WelaAppLockerScriptAuthorizationKey {
|
||||
param($Token)
|
||||
if($Token.Sid -cnotmatch '^S-1-\d+(-\d+)+$' -or $Token.AuthenticationId -cnotmatch '^0x[0-9a-f]+$' -or $null -eq $Token.Groups -or $null -eq $Token.Privileges){throw 'Incomplete actual token evidence.'}
|
||||
Get-WelaAppLockerScriptKey ([ordered]@{Sid=$Token.Sid;AuthenticationId=$Token.AuthenticationId;Groups=$Token.Groups;Privileges=$Token.Privileges})
|
||||
}
|
||||
function New-WelaAppLockerScriptText {
|
||||
param([string]$Template,[string]$Nonce)
|
||||
if($Nonce -cnotmatch '^[a-f0-9]{32}$' -or ([regex]::Matches($Template,'__WELA_SCRIPT_NONCE__')).Count -ne 3){throw 'Unexpected fixed worker template or nonce.'}
|
||||
$Template.Replace('__WELA_SCRIPT_NONCE__',$Nonce)
|
||||
}
|
||||
function Read-WelaAppLockerScriptBoundary {
|
||||
$reader=$null;$record=$null
|
||||
try {
|
||||
$query=[Diagnostics.Eventing.Reader.EventLogQuery]::new('Microsoft-Windows-AppLocker/MSI and Script',[Diagnostics.Eventing.Reader.PathType]::LogName,'*');$query.ReverseDirection=$true;$query.TolerateQueryErrors=$false
|
||||
$reader=[Diagnostics.Eventing.Reader.EventLogReader]::new($query);$reader.BatchSize=1
|
||||
$record=$reader.ReadEvent([TimeSpan]::FromSeconds(5))
|
||||
$status=@($reader.LogStatus)
|
||||
if($status.Count -ne 1 -or $status[0].LogName -cne 'Microsoft-Windows-AppLocker/MSI and Script' -or $status[0].StatusCode -ne 0){throw 'Incomplete native channel query status.'}
|
||||
if($null -eq $record){return [long]0}
|
||||
if($record.LogName -cne $status[0].LogName -or $record.RecordId -le 0){throw 'Invalid native record boundary.'}
|
||||
[long]$record.RecordId
|
||||
}finally{if($record){$record.Dispose()};if($reader){$reader.Dispose()}}
|
||||
}
|
||||
function Start-WelaAppLockerScriptProcess {
|
||||
param([string]$Root,$State,$Reader,[string]$SourcesKey)
|
||||
$nonce=[guid]::NewGuid().ToString('N');$path=Join-Path $Root ('wela-script-'+$nonce+'.ps1')
|
||||
$template=[IO.File]::ReadAllText((Join-Path $script:ScriptRoot 'scripts/AppLockerScriptWorker.ps1'))
|
||||
$scriptBytes=[Text.UTF8Encoding]::new($false).GetBytes((New-WelaAppLockerScriptText $template $nonce))
|
||||
$artifact=Write-WelaArrivalArtifact $Root ([IO.Path]::GetFileName($path)) ([Text.UTF8Encoding]::new($false).GetString($scriptBytes))
|
||||
$source=$null;$scriptFile=$null;$process=$null;$started=$false
|
||||
try {
|
||||
$source=[IO.File]::Open($State.Source,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::Read)
|
||||
$scriptFile=[IO.File]::Open($path,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::Read)
|
||||
$sourceId=[Wela.AppLockerScript.Native]::FileId($source.SafeFileHandle.DangerousGetHandle());$scriptId=[Wela.AppLockerScript.Native]::FileId($scriptFile.SafeFileHandle.DangerousGetHandle())
|
||||
if((Get-FileHash -LiteralPath $State.Source -Algorithm SHA256).Hash.ToLowerInvariant() -cne $State.SourceHash -or (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() -cne $artifact.Sha256){throw 'The held native source or generated script differs before launch.'}
|
||||
if((Get-WelaAppLockerScriptKey (Get-WelaAppLockerScriptSources)) -cne $SourcesKey){throw 'Source changed before script launch.'}
|
||||
$readerKey=Get-WelaAppLockerScriptKey $Reader
|
||||
if((Get-WelaAppLockerScriptKey ((Get-WelaAppLockerScriptReader))) -cne $readerKey){throw 'Caller token changed before launch.'}
|
||||
$info=[Diagnostics.ProcessStartInfo]::new();$info.FileName=$State.Source;$info.Arguments='-NoLogo -NoProfile -NonInteractive -File "'+$path+'"';$info.UseShellExecute=$false;$info.CreateNoWindow=$true;$info.RedirectStandardInput=$true;$info.RedirectStandardOutput=$true;$info.RedirectStandardError=$true;$info.WorkingDirectory=$Root
|
||||
$process=[Diagnostics.Process]::new();$process.StartInfo=$info
|
||||
$start=(Get-WelaAppLockerScriptUtcNow)
|
||||
$started=$process.Start();if(-not $started){throw 'The fixed script process did not start.'}
|
||||
$stderr=[Wela.AppLockerScript.Native]::ReadBoundedAsync($process.StandardError,4096)
|
||||
$ready=[Wela.AppLockerScript.Native]::ReadLineBoundedAsync($process.StandardOutput,256)
|
||||
if(-not $ready.Wait(30000)){throw 'The fixed script did not reach its ready marker within thirty seconds.'}
|
||||
$line=$ready.GetAwaiter().GetResult()
|
||||
if($line -cnotmatch ('^WELA_SCRIPT_READY_'+$nonce+'\|(FullLanguage|ConstrainedLanguage)\|5\.1\.[0-9.]+$')){throw ('Unexpected fixed script ready marker: '+$line)}
|
||||
$child=[Wela.AppLockerScript.Native]::Child($process.Handle)
|
||||
if((Get-WelaAppLockerScriptAuthorizationKey $child) -cne (Get-WelaAppLockerScriptAuthorizationKey $Reader)){throw 'The actual child primary/logon authorization differs from the caller.'}
|
||||
if((Get-WelaAppLockerScriptKey ((Get-WelaAppLockerScriptReader))) -cne $readerKey){throw 'Caller token changed while the fixed child started.'}
|
||||
$stdout=[Wela.AppLockerScript.Native]::ReadBoundedAsync($process.StandardOutput,4096)
|
||||
$process.StandardInput.WriteLine('WELA_SCRIPT_GO_'+$nonce);$process.StandardInput.Close()
|
||||
if(-not $process.WaitForExit(10000)){throw 'The fixed child did not complete within ten seconds of release.'}
|
||||
if(-not [Threading.Tasks.Task]::WaitAll([Threading.Tasks.Task[]]@($stdout,$stderr),5000)){throw 'The fixed child output did not complete within five seconds.'}
|
||||
$out=$stdout.GetAwaiter().GetResult();$err=$stderr.GetAwaiter().GetResult();$end=(Get-WelaAppLockerScriptUtcNow)
|
||||
if($process.ExitCode -ne 0 -or $out.TrimEnd("`r","`n") -cne ('WELA_SCRIPT_COMPLETE_'+$nonce) -or $err){throw ('The fixed script did not complete correctly. Exit='+$process.ExitCode+' Error='+$err)}
|
||||
if((Get-WelaAppLockerScriptKey ((Get-WelaAppLockerScriptReader))) -cne $readerKey -or [Wela.AppLockerScript.Native]::FileId($source.SafeFileHandle.DangerousGetHandle()) -cne $sourceId -or [Wela.AppLockerScript.Native]::FileId($scriptFile.SafeFileHandle.DangerousGetHandle()) -cne $scriptId){throw 'Reader or held file identity changed during script execution.'}
|
||||
[pscustomobject][ordered]@{ProcessId=$process.Id;UserSid=$Reader.Sid;ChildToken=$child;NativePowerShell=$State.Source;NativePowerShellSha256=$State.SourceHash;NativePowerShellFileId=$sourceId;ScriptPath=$path;ScriptSha256=$artifact.Sha256;ScriptFileId=$scriptId;ScriptArtifact=$artifact;Nonce=$nonce;Arguments=$info.Arguments;StartedUtc=$start.ToString('o');CompletedUtc=$end.ToString('o');Clock='GetSystemTimePreciseAsFileTime';Ready=$line;Marker=$out.TrimEnd("`r","`n");ExitCode=$process.ExitCode}
|
||||
}finally{
|
||||
if($process){try{if($started -and -not $process.HasExited){$process.Kill();if(-not $process.WaitForExit(5000)){throw 'Owned script process termination is unconfirmed.'}}}finally{$process.Dispose()}}
|
||||
if($scriptFile){$scriptFile.Dispose()};if($source){$source.Dispose()}
|
||||
}
|
||||
}
|
||||
function Read-WelaAppLockerScriptEvents {
|
||||
param([long]$Boundary)
|
||||
$query="*[System[Provider[@Name='Microsoft-Windows-AppLocker'] and (EventID=8005 or EventID=8006) and EventRecordID>$Boundary]]"
|
||||
$reader=$null;$record=$null;$xml=@();$bytes=0
|
||||
try {
|
||||
$nativeQuery=[Diagnostics.Eventing.Reader.EventLogQuery]::new('Microsoft-Windows-AppLocker/MSI and Script',[Diagnostics.Eventing.Reader.PathType]::LogName,$query);$nativeQuery.ReverseDirection=$false;$nativeQuery.TolerateQueryErrors=$false
|
||||
$reader=[Diagnostics.Eventing.Reader.EventLogReader]::new($nativeQuery);$reader.BatchSize=16
|
||||
while($null -ne ($record=$reader.ReadEvent([TimeSpan]::FromSeconds(2)))) {
|
||||
try{$text=$record.ToXml();$bytes+=[Text.Encoding]::UTF8.GetByteCount($text);if($xml.Count -ge 255 -or $bytes -gt 1048576){throw 'Native script query reached its 256-event/one-MiB cap.'};$xml+=$text}finally{$record.Dispose();$record=$null}
|
||||
}
|
||||
$status=@($reader.LogStatus);if($status.Count -ne 1 -or $status[0].LogName -cne 'Microsoft-Windows-AppLocker/MSI and Script' -or $status[0].StatusCode -ne 0){throw 'Incomplete native script query status.'}
|
||||
[pscustomobject]@{Xml=$xml;Query=$query;Bytes=$bytes;Complete=$true}
|
||||
}finally{if($record){$record.Dispose()};if($reader){$reader.Dispose()}}
|
||||
}
|
||||
|
||||
function Test-WelaAppLockerScriptEvent {
|
||||
param([string]$Xml,$Process,$State,[long]$Boundary)
|
||||
$reader=$null
|
||||
try {
|
||||
$settings=New-Object Xml.XmlReaderSettings;$settings.DtdProcessing=[Xml.DtdProcessing]::Prohibit;$settings.XmlResolver=$null;$settings.MaxCharactersInDocument=4194304
|
||||
$reader=[Xml.XmlReader]::Create([IO.StringReader]::new($Xml),$settings);$doc=New-Object Xml.XmlDocument;$doc.XmlResolver=$null;$doc.Load($reader)
|
||||
$ns=New-Object Xml.XmlNamespaceManager($doc.NameTable);$ns.AddNamespace('e','http://schemas.microsoft.com/win/2004/08/events/event');$ns.AddNamespace('a','http://schemas.microsoft.com/schemas/event/Microsoft.Windows/1.0.0.0')
|
||||
if ($doc.DocumentElement.LocalName -cne 'Event' -or $doc.DocumentElement.NamespaceURI -cne $ns.LookupNamespace('e') -or $doc.SelectNodes('/e:Event/e:System',$ns).Count -ne 1 -or $doc.SelectNodes('/e:Event/e:UserData/a:RuleAndFileData',$ns).Count -ne 1 -or $doc.SelectNodes('/e:Event/e:EventData',$ns).Count) {return $false}
|
||||
$system=@{};foreach ($name in @('Provider','EventID','Version','EventRecordID','Channel','Computer','TimeCreated')) {$nodes=$doc.SelectNodes("/e:Event/e:System/e:$name",$ns);if($nodes.Count -ne 1){return $false};$system[$name]=$nodes[0]}
|
||||
if ($system.Provider.GetAttribute('Name') -cne 'Microsoft-Windows-AppLocker' -or $system.Provider.GetAttribute('Guid').Trim('{}') -ine 'cbda4dbf-8d5d-4f69-9578-be14aa540d22' -or $system.EventID.InnerText -cnotin @('8005','8006') -or $system.Version.InnerText -cne '0' -or $system.EventRecordID.InnerText -notmatch '^[1-9][0-9]*$' -or $system.Channel.InnerText -cne 'Microsoft-Windows-AppLocker/MSI and Script') {return $false}
|
||||
$computers=@($State.Computer);if($State.Host.PartOfDomain){$computers+=$State.Computer+'.'+$State.Domain};if($system.Computer.InnerText -notin $computers){return $false}
|
||||
$time=ConvertTo-WelaArrivalUtc $system.TimeCreated.GetAttribute('SystemTime')
|
||||
if($time.UtcDateTime -lt ([DateTimeOffset]::Parse($Process.StartedUtc)).UtcDateTime -or $time.UtcDateTime -gt (ConvertTo-WelaArrivalUtc $Process.CompletedUtc).UtcDateTime -or [long]$system.EventRecordID.InnerText -le $Boundary){return $false}
|
||||
$data=@{};foreach($node in $doc.SelectSingleNode('/e:Event/e:UserData/a:RuleAndFileData',$ns).ChildNodes){if($node.NodeType -eq 'Whitespace'){continue};if($node.NodeType -ne 'Element' -or $node.NamespaceURI -cne $ns.LookupNamespace('a') -or $data.ContainsKey($node.LocalName) -or @($node.ChildNodes|Where-Object NodeType -eq Element).Count){return $false};$data[$node.LocalName]=$node.InnerText}
|
||||
if($data.PolicyName -cne 'SCRIPT' -or $data.TargetUser -cne $Process.UserSid -or $data.TargetProcessId -notmatch '^[1-9][0-9]*$' -or [long]$data.TargetProcessId -ne $Process.ProcessId){return $false}
|
||||
# AppLocker may render the exact Windows directory through this documented path variable.
|
||||
$eventPath=$data.FilePath
|
||||
if($eventPath -imatch '^%OSDRIVE%\\'){$eventPath=[IO.Path]::GetPathRoot($State.Source).TrimEnd('\')+$eventPath.Substring(9)}
|
||||
return $eventPath -ieq $Process.ScriptPath
|
||||
} catch {return $false} finally {if($reader){$reader.Dispose()}}
|
||||
}
|
||||
function Invoke-WelaAppLockerScriptProbe {
|
||||
param([ValidateSet('Plan','Run')][string]$Action='Plan',[string]$OutputPath,[ValidateRange(1,30)][int]$TimeoutSeconds=15)
|
||||
if(($Action -eq 'Run') -ne (-not [string]::IsNullOrWhiteSpace($OutputPath))){throw 'Run requires a new AppLockerScriptOutputPath; Plan does not write files.'}
|
||||
Initialize-WelaAppLockerScriptNative
|
||||
$sources=Get-WelaAppLockerScriptSources;$sourceKey=Get-WelaAppLockerScriptKey $sources
|
||||
$report=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaAppLockerScriptProbe';Action=$Action;Status='Unverified';ExitCode=1;RecordedUtc=(Get-WelaAppLockerScriptUtcNow).ToString('o');Sources=$sources;Before=$null;After=$null;ReaderBefore=$null;ReaderAfter=$null;ReaderInterval='After output/context preparation, through child execution and actual event queries; final metadata is checked separately';Boundary=$null;Process=$null;EventId=$null;Decision=$null;Artifacts=@();Diagnostic='';OutputPath=$null;PolicyChanges=0;ReadyRuleCredit=0;CspPolicyState='Unknown';Scope='One fixed native Windows PowerShell5.1 Script-collection event. Other engines, collections, forwarding and Sigma/backend validation are not tested. Sysmon excluded.'}
|
||||
try {
|
||||
$before=Get-WelaAppLockerScriptState;$report.Before=$before;$key=Get-WelaAppLockerScriptStateKey $before
|
||||
if($Action -eq 'Plan'){$report.ReaderBefore=(Get-WelaAppLockerScriptReader);$report.Status='PrerequisitesObserved';$report.ExitCode=0;return $report}
|
||||
$report.OutputPath=New-WelaArrivalOutput -Path $OutputPath -SourcePath $script:ScriptRoot
|
||||
if((Get-WelaAppLockerScriptStateKey (Get-WelaAppLockerScriptState)) -cne $key){throw 'Context changed during output preparation.'}
|
||||
$report.Artifacts+=Write-WelaArrivalArtifact $report.OutputPath 'before.json' ($before|ConvertTo-Json -Depth 24)
|
||||
$reader=(Get-WelaAppLockerScriptReader);$report.ReaderBefore=$reader;$readerKey=Get-WelaAppLockerScriptKey $reader
|
||||
$report.Boundary=Read-WelaAppLockerScriptBoundary
|
||||
if((Get-WelaAppLockerScriptKey ((Get-WelaAppLockerScriptReader))) -cne $readerKey){throw 'Reader changed during the actual boundary query.'}
|
||||
$process=Start-WelaAppLockerScriptProcess -Root $report.OutputPath -State $before -Reader $reader -SourcesKey $sourceKey;$report.Process=$process
|
||||
$report.Artifacts+= $process.ScriptArtifact
|
||||
$report.Artifacts+=Write-WelaArrivalArtifact $report.OutputPath 'process.json' ($process|ConvertTo-Json -Depth 16)
|
||||
$timer=[Diagnostics.Stopwatch]::StartNew();$matches=@();$batch=$null
|
||||
do {
|
||||
if((Get-WelaAppLockerScriptKey ((Get-WelaAppLockerScriptReader))) -cne $readerKey){throw 'Reader changed before script event query.'}
|
||||
$batch=Read-WelaAppLockerScriptEvents $report.Boundary
|
||||
if($batch.Complete -isnot [bool] -or -not $batch.Complete){throw 'Script query completeness is unknown.'}
|
||||
if((Get-WelaAppLockerScriptKey ((Get-WelaAppLockerScriptReader))) -cne $readerKey){throw 'Reader changed during script event query.'}
|
||||
$matches=@($batch.Xml|Where-Object{Test-WelaAppLockerScriptEvent $_ $process $before $report.Boundary})
|
||||
if($matches.Count -gt 1){throw 'Multiple exact script records make the result ambiguous.'}
|
||||
if($matches.Count -eq 1){break}
|
||||
Start-Sleep -Milliseconds 250
|
||||
}while($timer.Elapsed.TotalSeconds -lt $TimeoutSeconds)
|
||||
$report.ReaderAfter=(Get-WelaAppLockerScriptReader)
|
||||
if((Get-WelaAppLockerScriptKey $report.ReaderAfter) -cne $readerKey){throw 'Reader changed before completion of the actual query interval.'}
|
||||
if($matches.Count -ne 1){
|
||||
# Retain only bounded candidates bearing the owned unique script name.
|
||||
$owned=@($batch.Xml|Where-Object{$_ -like ('*wela-script-'+$process.Nonce+'.ps1*')}|Select-Object -First 4)
|
||||
for($i=0;$i -lt $owned.Count;$i++){$report.Artifacts+=Write-WelaArrivalArtifact $report.OutputPath ('candidate-'+$i+'.xml') $owned[$i]}
|
||||
throw 'No exact native Script8005/8006 event arrived within the timeout.'
|
||||
}
|
||||
$report.After=Get-WelaAppLockerScriptState
|
||||
if((Get-WelaAppLockerScriptStateKey $report.After) -cne $key -or (Get-WelaAppLockerScriptKey (Get-WelaAppLockerScriptSources)) -cne $sourceKey -or (Get-FileHash -LiteralPath $process.ScriptPath -Algorithm SHA256).Hash.ToLowerInvariant() -cne $process.ScriptSha256){throw 'Host, policy, service, channel, execution-policy observation or implementation changed.'}
|
||||
$report.Artifacts+=Write-WelaArrivalArtifact $report.OutputPath 'after.json' ($report.After|ConvertTo-Json -Depth 24)
|
||||
$report.Artifacts+=Write-WelaArrivalArtifact $report.OutputPath 'event.xml' $matches[0]
|
||||
$event=[xml]$matches[0];$report.EventId=[int]$event.Event.System.EventID
|
||||
$report.Decision=if($report.EventId -eq 8005){'Allowed'}else{'AllowedWouldBlockIfEnforced'}
|
||||
$report.Status='NativeScriptEventObserved';$report.ExitCode=0
|
||||
}catch{$report.Diagnostic=$_.Exception.Message}
|
||||
if($report.OutputPath){$json=$report|ConvertTo-Json -Depth 32;if([Text.Encoding]::UTF8.GetByteCount($json) -gt 2097152){throw 'The script probe report exceeded its two-MiB bound.'};$null=Write-WelaArrivalArtifact $report.OutputPath 'manifest.json' $json}
|
||||
$report
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# Fixed locally generated script; no external inputs or configuration writes.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
[Console]::Out.WriteLine(('WELA_SCRIPT_READY___WELA_SCRIPT_NONCE__|' + $ExecutionContext.SessionState.LanguageMode + '|' + $PSVersionTable.PSVersion))
|
||||
$release = [Console]::In.ReadLine()
|
||||
if ($release -cne 'WELA_SCRIPT_GO___WELA_SCRIPT_NONCE__') { exit 17 }
|
||||
[Console]::Out.WriteLine('WELA_SCRIPT_COMPLETE___WELA_SCRIPT_NONCE__')
|
||||
exit 0
|
||||
Reference in new issue
Block a user