Reserve registry probe capacity and isolate exact-value cleanup

This commit is contained in:
Shirofune-Security committed 2026-09-22 14:59:21 +09:00
1 parent 9575c8204d
commit d72bf939a7
4 files changed
+39 -3

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@
./WELA.ps1 registry-probe -RegistryProbeAction Run -RegistryProbeOutputPath C:\WelaEvidence\registry-001
```
Prepare the diagnostic key and its auditing through a separately reviewed administrative process. Run requires an ordinary elevated current primary token with its existing SeSecurityPrivilege assigned, native 64-bit Windows PowerShell 5.1 or PowerShell7, an enabled/readable Security channel, Registry success auditing, audit precedence DWORD1, and an ordinary success SetValue SACL ACE matching the actual user or enabled group. No target prerequisites are silently repaired. The fixed key must have no children. Component-by-component native opens reject registry symbolic links and verify the held NT path. Full SDK-defined security-descriptor sections and a bounded inventory of raw typed values are retained; excessive or unreadable state fails closed.
Prepare the diagnostic key and its auditing through a separately reviewed administrative process. Run requires an ordinary elevated current primary token with its existing SeSecurityPrivilege assigned, native 64-bit Windows PowerShell 5.1 or PowerShell7, an enabled/readable Security channel, Registry success auditing, audit precedence DWORD1, and an ordinary success SetValue SACL ACE matching the actual user or enabled group. No target prerequisites are silently repaired. The fixed key must have no children. Component-by-component native opens reject registry symbolic links and verify the held NT path. Full SDK-defined security-descriptor sections and a bounded inventory of raw typed values are retained; excessive or unreadable state fails closed. Run reserves room for the temporary value before mutation (at most127 original values and sufficient raw-byte capacity). Cleanup queries the exact owned value independently, so unrelated inventory growth cannot strand its marker.
Microsoft describes [4657 and the Set Value SACL prerequisite](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/event-4657). The probe uses the documented [RegSetValueExW](https://learn.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regsetvalueexw) and [RegDeleteValueW](https://learn.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regdeletevaluew) APIs through the same held key. Existing values are never selected for modification. A collision refuses before writing; a changed owned value is preserved with failed cleanup rather than blindly deleted. Operations are not transactions with other administrators.
+8
View File
@@ -17,7 +17,10 @@ function Get-WelaRegistryValueProbeState {
$services=@(Get-Service 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.'}
$reader=Get-WelaChannelReader;$null=Get-WelaFileProbeReaderKey $reader
$computer=Get-CimInstance Win32_ComputerSystem -Property DomainRole,PartOfDomain -ErrorAction Stop
if(-not(Test-WelaFileProbeInteger $computer.DomainRole) -or $computer.PartOfDomain -isnot [bool]){throw 'Native Windows role and join observations are incomplete.'}
$hostState=Get-WelaChannelReadHost
if($hostState.DomainRole -ne $computer.DomainRole -or $hostState.DomainJoined -ne $computer.PartOfDomain){throw 'Native Windows role or join state changed during observation.'}
$target=[Wela.RegistryValueProbe.Target]::new($false)
try{$registry=$target.Read()}finally{$target.Dispose()}
$channel=[Diagnostics.Eventing.Reader.EventLogConfiguration]::new('Security')
@@ -31,6 +34,11 @@ function Get-WelaRegistryValueProbeStateKey {
param($State)
$null=Get-WelaFileProbeTokenKey $State.Token
if($State.Computer -isnot [string] -or -not $State.Computer -or -not(Test-WelaFileProbeInteger $State.Host.ProductType) -or $State.Host.ProductType -notin 1,2,3 -or -not(Test-WelaFileProbeInteger $State.Host.Build) -or $State.Host.Build -notin 22000,22621,22631,20348,26100,26200 -or $State.Host.DomainJoined -isnot [bool]){throw 'Complete supported native host context is required.'}
if(-not(Test-WelaFileProbeInteger $State.Host.DomainRole) -or
($State.Host.ProductType -eq 1 -and ($State.Host.DomainRole -notin 0,1 -or $State.Host.Build -notin 22000,22621,22631,26100,26200)) -or
($State.Host.ProductType -eq 2 -and ($State.Host.DomainRole -notin 4,5 -or $State.Host.Build -notin 20348,26100)) -or
($State.Host.ProductType -eq 3 -and ($State.Host.DomainRole -notin 2,3 -or $State.Host.Build -notin 20348,26100)) -or
($State.Host.DomainJoined -ne ($State.Host.DomainRole -in 1,3,4,5))){throw 'Contradictory Windows product/build/domain-role/join evidence.'}
if($State.Services -isnot [array] -or ($State.Services.Name -join ',') -cne 'EventLog,RpcSs,Winmgmt' -or @($State.Services|Where-Object Status -ne Running).Count){throw 'Required services must already be running.'}
if($State.Reader.ElevatedAdministrator -isnot [bool] -or -not $State.Reader.ElevatedAdministrator -or $State.Reader.TokenType -cne 'Primary' -or $State.Reader.Impersonation -cne 'Absent' -or $State.Reader.UserSid -cne $State.Token.Sid){throw 'Actual elevated non-impersonating primary token required.'}
$expected='HKEY_USERS\'+$State.Token.Sid+'\Software\WELA\AuditProbe'
+15 -1
View File
@@ -26,6 +26,12 @@ namespace Wela.RegistryValueProbe {
RawSecurityDescriptor sd=new RawSecurityDescriptor(b,0);
return sd;
}
public static void AssertProbeCapacity(Value[] values) {
if(values==null||values.Length>=128)throw new InvalidOperationException("Registry inventory has no capacity for an owned probe value.");
long total=0;foreach(Value value in values){if(value==null||value.DataBase64==null)throw new InvalidOperationException("Incomplete typed value inventory.");int size=Convert.FromBase64String(value.DataBase64).Length;if(size>65536)throw new InvalidOperationException("Value exceeds inventory bound.");total+=size;}
int reserved=Encoding.Unicode.GetByteCount("WELA_BEFORE_"+new string('0',32)+"\0");
if(total+reserved>1048576)throw new InvalidOperationException("Registry byte inventory has no capacity for an owned probe value.");
}
public static Snapshot Observe(string path,string identity,byte[] bytes) {
string encoded=Convert.ToBase64String(bytes);RawSecurityDescriptor sd=Parse(encoded);List<Ace> entries=new List<Ace>();
if(sd.SystemAcl!=null)foreach(GenericAce ace in sd.SystemAcl){CommonAce common=ace as CommonAce;bool ordinary=common!=null&&!common.IsCallback&&common.AceType==AceType.SystemAudit;entries.Add(new Ace {Binary=Bytes(ace),Type=(int)ace.AceType,Flags=(int)ace.AceFlags,Mask=ordinary?common.AccessMask:0,Sid=ordinary?common.SecurityIdentifier.Value:null,Ordinary=ordinary});}
@@ -118,7 +124,14 @@ namespace Wela.RegistryValueProbe {
values.Sort((a,b)=>String.CompareOrdinal(a.Name,b.Name));return values.ToArray();
}
static bool SameValues(Value[] a,Value[] b){if(a.Length!=b.Length)return false;for(int i=0;i<a.Length;i++)if(a[i].Name!=b[i].Name||a[i].Type!=b[i].Type||a[i].DataBase64!=b[i].DataBase64)return false;return true;}
Value Find(string name){foreach(Value v in ReadValues())if(String.Equals(v.Name,name,StringComparison.OrdinalIgnoreCase))return v;return null;}
Value Find(string name){
uint type,size=0;int error=RegQueryValueExW(handle,name,IntPtr.Zero,out type,IntPtr.Zero,ref size);
if(error==2)return null;if(error!=0)throw new Win32Exception(error,"Owned value size query failed.");
if(size>65536)throw new InvalidOperationException("Owned value exceeds bound; refusing modification.");
IntPtr data=Marshal.AllocHGlobal((int)Math.Max(size,1));
try{uint actual=size,currentType;error=RegQueryValueExW(handle,name,IntPtr.Zero,out currentType,data,ref actual);if(error!=0||actual!=size||currentType!=type)throw new InvalidOperationException("Owned value changed during exact query.");byte[] bytes=new byte[actual];if(actual>0)Marshal.Copy(data,bytes,0,(int)actual);return new Value{Name=name,Type=type,DataBase64=Convert.ToBase64String(bytes)};}
finally{Marshal.FreeHGlobal(data);}
}
static string Encoded(string value){return Convert.ToBase64String(Encoding.Unicode.GetBytes(value+"\0"));}
void Put(string name,string text){byte[] data=Encoding.Unicode.GetBytes(text+"\0");int error=RegSetValueExW(handle,name,0,1,data,(uint)data.Length);if(error!=0)throw new Win32Exception(error,"Owned probe value write failed.");Value v=Find(name);if(v==null||v.Type!=1||v.DataBase64!=Encoded(text))throw new InvalidOperationException("Owned value readback differs.");}
public Operation Run(string nonce,string expectedIdentity,string expectedDescriptor){
@@ -126,6 +139,7 @@ namespace Wela.RegistryValueProbe {
foreach(char c in nonce)if(!((c>='0'&&c<='9')||(c>='a'&&c<='f')))throw new InvalidOperationException("Invalid probe nonce.");
Operation r=new Operation{Nonce=nonce,Name="WELA_Probe_"+nonce,BeforeValue="WELA_BEFORE_"+nonce,AfterValue="WELA_AFTER_"+nonce,HandleId="0x"+handle.ToInt64().ToString("x"),CleanupComplete=false,Diagnostic=""};
r.Before=Read();if(r.Before.Identity!=expectedIdentity||r.Before.DescriptorBase64!=expectedDescriptor)throw new InvalidOperationException("Probe key changed after planning.");
Descriptor.AssertProbeCapacity(r.Before.Values);
if(Find(r.Name)!=null)throw new InvalidOperationException("Owned nonce value already exists; refusing overwrite.");
bool attempted=false;
try{
+15 -1
View File
@@ -7,7 +7,7 @@ function Reject([scriptblock]$Action){$threw=$false;try{& $Action|Out-Null}catch
function Copy-Value($Value){ConvertFrom-WelaArrivalJson (Get-WelaFileProbeKey $Value)}
function Fixture {
$script:token=[pscustomobject]@{Sid='S-1-5-21-1-2-3-1001';Name='FIXTURE\Reader';AuthenticationId='0x1234';AuthenticationType='Negotiate';ImpersonationLevel='None';TokenSource='Process';Groups=@([pscustomobject]@{Sid='S-1-1-0';Attributes=7});Privileges=@([pscustomobject]@{Luid='0x8';Attributes=0})}
$script:state=[pscustomobject]@{Computer='FIXTURE';Host=[pscustomobject]@{ProductType=3;Build=20348;DomainJoined=$false;Domain='WORKGROUP'};Services=@([pscustomobject]@{Name='EventLog';Status='Running'},[pscustomobject]@{Name='RpcSs';Status='Running'},[pscustomobject]@{Name='Winmgmt';Status='Running'});Reader=[pscustomobject]@{UserSid=$script:token.Sid;ElevatedAdministrator=$true;TokenType='Primary';Impersonation='Absent'};Token=Copy-Value $script:token;Registry=[pscustomobject]@{Path=('HKEY_USERS\'+$script:token.Sid+'\Software\WELA\AuditProbe');Kind='Registry';IsDirectory=$false;Identity='fixed:123';DescriptorBase64='AA==';SecurityInformation=511;Values=@();Aces=@([pscustomobject]@{Ordinary=$true;Type=2;Flags=64;Mask=2;Sid='S-1-1-0';Binary='AA=='})};AuditPolicies=[pscustomobject]@{'0CCE921E-69AE-11D9-BED3-505054503030'=1};Precedence=[pscustomobject]@{ValueExists=$true;Type='DWord';Value=1};Channel=[pscustomobject]@{Name='Security';Enabled=$true;SecurityDescriptor='O:SYG:SYD:'};Engine='C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe';EngineHash=('b'*64);Sources=[pscustomobject]@{Source=('c'*64)}}
$script:state=[pscustomobject]@{Computer='FIXTURE';Host=[pscustomobject]@{ProductType=3;Build=20348;DomainRole=2;DomainJoined=$false;Domain='WORKGROUP'};Services=@([pscustomobject]@{Name='EventLog';Status='Running'},[pscustomobject]@{Name='RpcSs';Status='Running'},[pscustomobject]@{Name='Winmgmt';Status='Running'});Reader=[pscustomobject]@{UserSid=$script:token.Sid;ElevatedAdministrator=$true;TokenType='Primary';Impersonation='Absent'};Token=Copy-Value $script:token;Registry=[pscustomobject]@{Path=('HKEY_USERS\'+$script:token.Sid+'\Software\WELA\AuditProbe');Kind='Registry';IsDirectory=$false;Identity='fixed:123';DescriptorBase64='AA==';SecurityInformation=511;Values=@();Aces=@([pscustomobject]@{Ordinary=$true;Type=2;Flags=64;Mask=2;Sid='S-1-1-0';Binary='AA=='})};AuditPolicies=[pscustomobject]@{'0CCE921E-69AE-11D9-BED3-505054503030'=1};Precedence=[pscustomobject]@{ValueExists=$true;Type='DWord';Value=1};Channel=[pscustomobject]@{Name='Security';Enabled=$true;SecurityDescriptor='O:SYG:SYD:'};Engine='C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe';EngineHash=('b'*64);Sources=[pscustomobject]@{Source=('c'*64)}}
$n='d'*32;$script:operation=[pscustomobject]@{Kind='WelaOwnedRegistryValueModification';Nonce=$n;ProcessId=1234;Executable=$script:state.Engine;RecordIdBefore=10;BeforeToken=Copy-Value $script:token;AfterToken=Copy-Value $script:token;LaunchedUtc='2026-09-21T00:00:00.0000000Z';ObservedUtc='2026-09-21T00:00:01.0000000Z';Native=[pscustomobject]@{Succeeded=$true;CleanupComplete=$true;Nonce=$n;Name=('WELA_Probe_'+$n);BeforeValue=('WELA_BEFORE_'+$n);AfterValue=('WELA_AFTER_'+$n);HandleId='0x888';Before=Copy-Value $script:state.Registry;After=Copy-Value $script:state.Registry;StartedUtc='2026-09-21T00:00:00.0001000Z';WriteReturnedUtc='2026-09-21T00:00:00.0001600Z';CompletedUtc='2026-09-21T00:00:00.0002000Z';Diagnostic=''}}
$script:stateReads=0;$script:attempts=0;$script:batchMode='match';$script:failArtifact=$null;$script:afterDrift=$false
}
@@ -22,6 +22,9 @@ foreach($bad in @('path','mask','precedence','channel','role','group','inherit-o
switch($bad){'path'{$script:state.Registry.Path='HKEY_LOCAL_MACHINE\Software\WELA\AuditProbe'};'mask'{$script:state.AuditPolicies.'0CCE921E-69AE-11D9-BED3-505054503030'=0};'precedence'{$script:state.Precedence.Value=$true};'channel'{$script:state.Channel.Enabled='true'};'role'{$script:state.Host.ProductType=$true};'group'{$script:state.Token.Groups[0].Attributes=16};'inherit-only'{$script:state.Registry.Aces[0].Flags=72};'failure'{$script:state.Registry.Aces[0].Flags=128};'callback'{$script:state.Registry.Aces[0].Ordinary=$false};'right'{$script:state.Registry.Aces[0].Mask=1};'descriptor'{$script:state.Registry.DescriptorBase64=$null};'sources'{$script:state.Sources.Source='bad'};'token'{$script:state.Token.TokenSource='Thread'}}
Reject {Get-WelaRegistryValueProbeStateKey $script:state}
}
foreach($badRole in @($null,$true,0,4)) {Fixture;$script:state.Host.DomainRole=$badRole;Reject {Get-WelaRegistryValueProbeStateKey $script:state}}
Fixture;$script:state.Host.DomainJoined=$true;Reject {Get-WelaRegistryValueProbeStateKey $script:state}
Fixture;$script:state.Host.Build=22631;Reject {Get-WelaRegistryValueProbeStateKey $script:state}
foreach($bad in @('nonce','success','cleanup','key','values','before','after','reverse','handle','token')){
Fixture
switch($bad){'nonce'{$script:operation.Nonce='invalid'};'success'{$script:operation.Native.Succeeded='true'};'cleanup'{$script:operation.Native.CleanupComplete=$false};'key'{$script:operation.Native.After.Path='wrong'};'values'{$script:operation.Native.After.Values=@('new')};'before'{$script:operation.Native.StartedUtc='2026-09-20T00:00:00Z'};'after'{$script:operation.Native.CompletedUtc='2026-09-22T00:00:00Z'};'reverse'{$script:operation.Native.WriteReturnedUtc='2026-09-21T00:00:00Z'};'handle'{$script:operation.Native.HandleId='0x0'};'token'{$script:operation.AfterToken.Privileges[0].Attributes=2}}
@@ -34,6 +37,17 @@ foreach($badXml in @($xml.Replace('</EventData>','<Data Name="OldValue">duplicat
function Initialize-WelaWmiProbeNative {}
Initialize-WelaRegistryValueProbe
Assert ([Wela.RegistryValueProbe.Descriptor]::SourceSha256 -ceq (Get-FileHash (Join-Path $script:ScriptRoot 'scripts/RegistryValueProbeNative.cs')).Hash.ToLowerInvariant()) 'Exact native source hash is bound.'
# The temporary value must fit the same inventory bounds used for final readback.
$small=[Wela.RegistryValueProbe.Value]::new();$small.Name='small';$small.Type=3;$small.DataBase64='AA=='
[Wela.RegistryValueProbe.Descriptor]::AssertProbeCapacity([Wela.RegistryValueProbe.Value[]]@((1..127|ForEach-Object{$small})))
Assert $true '127 original values leave room for exactly one owned marker.'
Reject {[Wela.RegistryValueProbe.Descriptor]::AssertProbeCapacity([Wela.RegistryValueProbe.Value[]]@((1..128|ForEach-Object{$small})))}
$large=[Wela.RegistryValueProbe.Value]::new();$large.Type=3;$large.DataBase64=[Convert]::ToBase64String([byte[]]::new(65536))
Reject {[Wela.RegistryValueProbe.Descriptor]::AssertProbeCapacity([Wela.RegistryValueProbe.Value[]]@((1..16|ForEach-Object{$large})))}
$reserved=[Text.Encoding]::Unicode.GetByteCount('WELA_BEFORE_'+('0'*32)+[char]0)
$tail=[Wela.RegistryValueProbe.Value]::new();$tail.Type=3;$tail.DataBase64=[Convert]::ToBase64String([byte[]]::new(65536-$reserved))
[Wela.RegistryValueProbe.Descriptor]::AssertProbeCapacity([Wela.RegistryValueProbe.Value[]](@((1..15|ForEach-Object{$large}))+@($tail)))
Assert $true 'Exactly reserved byte capacity is accepted.'
$writer=(Get-Command Write-WelaFileProbeArtifact).ScriptBlock
function Get-WelaFileProbeOutputKey {param($Path) 'private-fixture'}
function Write-WelaFileProbeArtifact {param($Root,$OutputKey,$Name,$Text) if($Name -eq $script:failArtifact){throw 'Injected artifact failure'};& $script:writer $Root $OutputKey $Name $Text}