diff --git a/scripts/WecAuthorizationNative.cs b/scripts/WecAuthorizationNative.cs index 4b89e79a..d8a3f34b 100644 --- a/scripts/WecAuthorizationNative.cs +++ b/scripts/WecAuthorizationNative.cs @@ -31,6 +31,20 @@ namespace Wela.WecAuthorization { } if(!String.Equals(expected.ToString(),value,StringComparison.Ordinal))throw new ArgumentException("Unsupported authorization descriptor."); } + // Public only for allocated-buffer ABI/type regression tests; performs no native calls. + public static object Decode(IntPtr buffer,uint size,int property) { + if(buffer==IntPtr.Zero||size<16||size>1048576||property!=0&&property!=6&&property!=7&&property!=10&&property!=11&&property!=19&&property!=27&&property!=31)throw new InvalidOperationException("Invalid native property buffer or selection."); + int type=Marshal.ReadInt32(buffer,12); + if(property==27){if(type!=2)throw new InvalidOperationException("Subscription type is not UInt32.");return unchecked((uint)Marshal.ReadInt32(buffer));} + if(property==0){if(type!=1)throw new InvalidOperationException("Enabled is not a scalar Boolean.");int value=Marshal.ReadInt32(buffer);if(value!=0&&value!=1)throw new InvalidOperationException("Invalid native Boolean.");return value==1;} + if(type==0&&property==6)return ""; + if(type!=4)throw new InvalidOperationException("Expected scalar native string."); + IntPtr pointer=Marshal.ReadIntPtr(buffer);long offset=pointer.ToInt64()-buffer.ToInt64(); + if(pointer==IntPtr.Zero||offset<16||offset>size-2)throw new InvalidOperationException("Native string pointer is outside its buffer."); + StringBuilder text=new StringBuilder(); + for(int i=0;i<524288&&offset+2L*i+2<=size;i++){char c=(char)(ushort)Marshal.ReadInt16(pointer,2*i);if(c==0)return text.ToString();text.Append(c);} + throw new InvalidOperationException("Unterminated native string."); + } static object Read(IntPtr h,int property) { uint size=16; for(int attempt=0;attempt<3;attempt++) { @@ -39,16 +53,7 @@ namespace Wela.WecAuthorization { uint used;bool ok=EcGetSubscriptionProperty(h,property,0,size,buffer,out used);int error=Marshal.GetLastWin32Error(); if(!ok){if(error!=122)throw new Win32Exception(error);if(used<=size||used>1048576)throw new InvalidOperationException("Invalid native property buffer size.");size=used;continue;} if(used<16||used>size)throw new InvalidOperationException("Invalid native property length."); - int type=Marshal.ReadInt32(buffer,12); - if(property==27){if(type!=8)throw new InvalidOperationException("Subscription type is not UInt32.");return unchecked((uint)Marshal.ReadInt32(buffer));} - if(property==0){if(type!=1)throw new InvalidOperationException("Enabled is not a scalar Boolean.");int value=Marshal.ReadInt32(buffer);if(value!=0&&value!=1)throw new InvalidOperationException("Invalid native Boolean.");return value==1;} - if(type==0&&property==6)return ""; - if(type!=4)throw new InvalidOperationException("Expected scalar native string."); - IntPtr pointer=Marshal.ReadIntPtr(buffer);long offset=pointer.ToInt64()-buffer.ToInt64(); - if(pointer==IntPtr.Zero||offset<16||offset>used-2)throw new InvalidOperationException("Native string pointer is outside its buffer."); - StringBuilder text=new StringBuilder(); - for(int i=0;i<524288&&offset+2L*i+2<=used;i++){char c=(char)(ushort)Marshal.ReadInt16(pointer,2*i);if(c==0)return text.ToString();text.Append(c);} - throw new InvalidOperationException("Unterminated native string."); + return Decode(buffer,used,property); }finally{Marshal.FreeHGlobal(buffer);} } throw new InvalidOperationException("Native property changed repeatedly."); diff --git a/tests/WecAuthorization.Tests.ps1 b/tests/WecAuthorization.Tests.ps1 index 9d49168f..bdc8198a 100644 --- a/tests/WecAuthorization.Tests.ps1 +++ b/tests/WecAuthorization.Tests.ps1 @@ -18,6 +18,32 @@ Assert ((Get-WelaWecAuthorizationKey @(Get-WelaWecAuthorizationSids @($sidB,$sid foreach($good in @($authA,$authAB)){[Wela.WecAuthorization.Edit]::ValidateAuthorization($good);$count++} foreach($bad in @('',$authA.Replace('GA','GR'),$authA.Replace('NSG:NS','SYG:SY'),($authA+$authA),$authA.Replace('11-22','011-22'),$authA.Replace('11-22','4294967296-22'),$authAB.Replace($sidB,$sidA),('O:NSG:NSD:(A;;GA;;;'+$sidB+')(A;;GA;;;'+$sidA+')'),($authA+'S:(AU;SA;GA;;;WD)'))){Reject {[Wela.WecAuthorization.Edit]::ValidateAuthorization($bad)}} Assert ([Wela.WecAuthorization.Edit]::SourceSha256 -ceq (Get-WelaArrivalHash ([IO.File]::ReadAllBytes("$repo/scripts/WecAuthorizationNative.cs")))) 'Compiled native helper binds the exact source bytes' +# Allocated EC_VARIANT buffers exercise the WEC ABI rather than EVT_VARIANT values. +$buffer=[Runtime.InteropServices.Marshal]::AllocHGlobal(64) +try { + for($i=0;$i -lt 64;$i++){[Runtime.InteropServices.Marshal]::WriteByte($buffer,$i,0)} + [Runtime.InteropServices.Marshal]::WriteInt32($buffer,12,2) + Assert ([Wela.WecAuthorization.Edit]::Decode($buffer,16,27) -eq [uint32]0) 'EcVarTypeUInt32 is 2 and source-initiated value is zero' + [Runtime.InteropServices.Marshal]::WriteInt32($buffer,0,1) + Assert ([Wela.WecAuthorization.Edit]::Decode($buffer,16,27) -eq [uint32]1) 'Collector-initiated scalar remains distinguishable' + foreach($type in @(0,1,4,8,130)){[Runtime.InteropServices.Marshal]::WriteInt32($buffer,12,$type);Reject {[Wela.WecAuthorization.Edit]::Decode($buffer,16,27)} 'UInt32'} + [Runtime.InteropServices.Marshal]::WriteInt32($buffer,12,1) + Assert ([Wela.WecAuthorization.Edit]::Decode($buffer,16,0) -eq $true) 'Native scalar Boolean decodes true' + [Runtime.InteropServices.Marshal]::WriteInt32($buffer,0,0) + Assert ([Wela.WecAuthorization.Edit]::Decode($buffer,16,0) -eq $false) 'Native scalar Boolean decodes false' + [Runtime.InteropServices.Marshal]::WriteInt32($buffer,0,2);Reject {[Wela.WecAuthorization.Edit]::Decode($buffer,16,0)} 'Boolean' + [Runtime.InteropServices.Marshal]::WriteInt32($buffer,12,4) + [Runtime.InteropServices.Marshal]::WriteIntPtr($buffer,[IntPtr]::Add($buffer,16));[Runtime.InteropServices.Marshal]::WriteInt16($buffer,16,65) + Assert ([Wela.WecAuthorization.Edit]::Decode($buffer,20,31) -ceq 'A') 'String data is decoded within returned bounds' + Reject {[Wela.WecAuthorization.Edit]::Decode($buffer,18,31)} 'Unterminated' + [Runtime.InteropServices.Marshal]::WriteIntPtr($buffer,[IntPtr]::Add($buffer,64));Reject {[Wela.WecAuthorization.Edit]::Decode($buffer,20,31)} 'outside' + [Runtime.InteropServices.Marshal]::WriteInt32($buffer,12,132);Reject {[Wela.WecAuthorization.Edit]::Decode($buffer,20,31)} 'scalar' + Reject {[Wela.WecAuthorization.Edit]::Decode($buffer,15,31)} 'buffer';Reject {[Wela.WecAuthorization.Edit]::Decode([IntPtr]::Zero,16,31)} 'buffer' + Reject {[Wela.WecAuthorization.Edit]::Decode($buffer,16,1)} 'selection' + [Runtime.InteropServices.Marshal]::WriteInt32($buffer,12,0) + Assert ([Wela.WecAuthorization.Edit]::Decode($buffer,16,6) -ceq '') 'Absent description normalizes to empty' + Reject {[Wela.WecAuthorization.Edit]::Decode($buffer,16,31)} 'scalar' +}finally{[Runtime.InteropServices.Marshal]::FreeHGlobal($buffer)} $before=Get-WelaWecAuthorizationDefinition $base;$after=Get-WelaWecAuthorizationDefinition ($base.Replace($authA,$authAB)) Assert ($before.SourceSids.Count -eq 1 -and $after.SourceSids.Count -eq 2 -and $before.PreservedKey -ceq $after.PreservedKey -and $before.WholeKey -cne $after.WholeKey) 'Only the explicit allow list is excluded from preserved XML' foreach($bad in @($base.Replace('SourceInitiated','CollectorInitiated'),$base.Replace('>false','>true'),$base.Replace($authA,'D:(A;;GA;;;WD)'),$base.Replace($authA,''),$base.Replace($authA,$authAB.Replace($sidB,$sidA)),$base.Replace('Path="Security"','Path="Microsoft-Windows-Sysmon/Operational"'),$base.Replace('','bad'))){Reject {Get-WelaWecAuthorizationDefinition $bad}} diff --git a/tests/WecAuthorization.Windows.Tests.ps1 b/tests/WecAuthorization.Windows.Tests.ps1 index 1719c989..a774a78b 100644 --- a/tests/WecAuthorization.Windows.Tests.ps1 +++ b/tests/WecAuthorization.Windows.Tests.ps1 @@ -15,12 +15,13 @@ function Key($Value){Get-WelaWecAuthorizationKey $Value} function Services {@(Get-CimInstance Win32_Service -Filter "Name='Wecsvc' OR Name='Winmgmt' OR Name='EventLog' OR Name='WinRM'"|Sort-Object Name|Select-Object Name,State,StartMode)} function Channel {$c=[Diagnostics.Eventing.Reader.EventLogConfiguration]::new('ForwardedEvents');try{[pscustomobject]@{Name=$c.LogName;Enabled=$c.IsEnabled;Mode=[string]$c.LogMode;MaximumBytes=$c.MaximumSizeInBytes;Path=$c.LogFilePath;SecurityDescriptor=$c.SecurityDescriptor}}finally{$c.Dispose()}} function EnableChannel([bool]$Value){$c=[Diagnostics.Eventing.Reader.EventLogConfiguration]::new('ForwardedEvents');try{$c.IsEnabled=$Value;$c.SaveChanges()}finally{$c.Dispose()}} -function Ids {@((Invoke-WelaNative 'wecutil.exe' @('es')).Output|ForEach-Object {$_.ToString().Trim()}|Where-Object {$_}|Sort-Object)} -function Inventory {$ids=@(Ids);if($ids.Count -gt 64){throw 'Disposable inventory exceeds bound.'};@($ids|ForEach-Object {[pscustomobject]@{Id=$_;Xml=Read-WelaWecSubscriptionXml $_}})} +Add-Type -Path (Join-Path $PSScriptRoot 'WecAuthorizationFixtureNative.cs') +function Ids {[Wela.WecAuthorizationFixture.Inventory]::Read()|Sort-Object} +function Inventory {$ids=@(Ids);Save 'last-observed-ids.json' $ids;@($ids|ForEach-Object {[pscustomobject]@{Id=$_;Xml=Read-WelaWecSubscriptionXml $_}})} $nonce=[guid]::NewGuid().ToString('N');$id='WELA-Authorization-'+$nonce;$description='Owned authorization '+$nonce+' '+[char]0x65e5+[char]0x672c $sidA='S-1-5-21-111111111-222222222-333333333-1234';$sidB='S-1-5-21-111111111-222222222-333333333-1235' $root=Join-Path $env:RUNNER_TEMP ('wela-wec-authorization-'+$nonce);$null=New-Item -ItemType Directory $root -function Save($Name,$Value){$Value|ConvertTo-Json -Depth 24|Set-Content -LiteralPath (Join-Path $root $Name) -Encoding UTF8} +function Save($Name,$Value){ConvertTo-Json -InputObject $Value -Depth 24|Set-Content -LiteralPath (Join-Path $root $Name) -Encoding UTF8} # Native -File argument binding cannot portably carry a string[] on both engines. # This fixture wrapper supplies the selected array to the actual public script. $wrapper=Join-Path $root 'invoke-public.ps1' @@ -32,7 +33,9 @@ if($PSBoundParameters.ContainsKey('Id')){$p.WecAuthorizationId=$Id} if($PSBoundParameters.ContainsKey('Sids')){$p.WecAuthorizationSourceSid=@($Sids.Split(';'))} if($PSBoundParameters.ContainsKey('PlanPath')){$p.WecAuthorizationPlanPath=$PlanPath} if($PSBoundParameters.ContainsKey('PlanHash')){$p.WecAuthorizationPlanHash=$PlanHash} +$global:LASTEXITCODE=0 & $WelaPath @p +exit $LASTEXITCODE '@|Set-Content -LiteralPath $wrapper -Encoding UTF8 function Public([string[]]$Arguments,[string]$Output,[bool]$Success=$true){ $prior=$ErrorActionPreference;try{$ErrorActionPreference='Continue';$text=@(&$engine -NoLogo -NoProfile -NonInteractive -File $wrapper -WelaPath "$repo/WELA.ps1" @Arguments -OutputPath $Output 2>&1);$code=$LASTEXITCODE}finally{$ErrorActionPreference=$prior} @@ -48,6 +51,7 @@ try { $wec=@($beforeServices|Where-Object Name -eq Wecsvc);Assert ($wec.Count -eq 1 -and $wec[0].State -in @('Running','Stopped') -and $wec[0].StartMode -in @('Auto','Manual','Disabled')) 'Stable original service state required' if($wec[0].StartMode -eq 'Disabled'){Set-Service Wecsvc -StartupType Manual};if($wec[0].State -eq 'Stopped'){Start-Service Wecsvc} if(-not $beforeChannel.Enabled){EnableChannel $true} + Save 'original-console-enumeration.json' (Invoke-WelaNative 'wecutil.exe' @('es')) $original=@(Inventory);Save 'original-inventory.json' $original;Assert (@(Ids) -notcontains $id) 'Unique owned subscription is initially absent' $query='' $xml=@" diff --git a/tests/WecAuthorizationFixtureNative.cs b/tests/WecAuthorizationFixtureNative.cs new file mode 100644 index 00000000..de6e5881 --- /dev/null +++ b/tests/WecAuthorizationFixtureNative.cs @@ -0,0 +1,25 @@ +// Read-only disposable-fixture inventory; bypasses console/native text decoding. +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Text; +namespace Wela.WecAuthorizationFixture { + public static class Inventory { + [DllImport("wecapi.dll",SetLastError=true)] static extern IntPtr EcOpenSubscriptionEnum(uint flags); + [DllImport("wecapi.dll",CharSet=CharSet.Unicode,SetLastError=true)] [return:MarshalAs(UnmanagedType.Bool)] static extern bool EcEnumNextSubscription(IntPtr enumeration,uint size,StringBuilder name,out uint used); + [DllImport("wecapi.dll",SetLastError=true)] [return:MarshalAs(UnmanagedType.Bool)] static extern bool EcClose(IntPtr handle); + public static string[] Read() { + IntPtr handle=EcOpenSubscriptionEnum(0);if(handle==IntPtr.Zero)throw new Win32Exception(Marshal.GetLastWin32Error()); + try { + var names=new List(); + while(true) { + var name=new StringBuilder(4096);uint used; + if(!EcEnumNextSubscription(handle,4096,name,out used)) {int error=Marshal.GetLastWin32Error();if(error==259)return names.ToArray();throw new Win32Exception(error);} + if(used<2||used>4096||name.Length==0||name.Length+1!=used||names.Count>=64||names.Contains(name.ToString()))throw new InvalidOperationException("Disposable native subscription inventory is invalid, duplicated or exceeds its bound."); + names.Add(name.ToString()); + } + }finally {EcClose(handle);} + } + } +}