mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-07 23:14:45 +02:00
Checkpoint an owned mounted hive through the public SACL catalog
This commit is contained in:
1 parent
52e09638ad
commit
021ebecc62
3 files changed
+142
No files matched your search
@@ -0,0 +1,40 @@
|
||||
name: Native public registry SACL lifecycle
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
paths:
|
||||
- 'tests/RegistrySacl*'
|
||||
- 'scripts/SelectedSacl*'
|
||||
- 'scripts/TargetedSaclPlanning.ps1'
|
||||
- 'WELA.ps1'
|
||||
- '.github/workflows/registry-sacl-lifecycle.yml'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
registry-sacl-lifecycle:
|
||||
timeout-minutes: 25
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-2022, windows-2025]
|
||||
engine: [powershell, pwsh]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
- name: Actual public registry lifecycle in Windows PowerShell 5.1
|
||||
if: matrix.engine == 'powershell'
|
||||
shell: powershell
|
||||
run: ./tests/RegistrySaclLifecycle.Windows.Tests.ps1 -AllowDisposableHiveWrite
|
||||
- name: Actual public registry lifecycle in PowerShell 7
|
||||
if: matrix.engine == 'pwsh'
|
||||
shell: pwsh
|
||||
run: ./tests/RegistrySaclLifecycle.Windows.Tests.ps1 -AllowDisposableHiveWrite
|
||||
- name: Retain public receipts, actual XML and exact cleanup
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
|
||||
with:
|
||||
name: registry-sacl-${{ matrix.os }}-${{ matrix.engine }}
|
||||
path: ${{ runner.temp }}/wela-registry-sacl-*/
|
||||
if-no-files-found: error
|
||||
@@ -0,0 +1,62 @@
|
||||
// Disposable CI fixture only. Never imported by WELA product code.
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Win32;
|
||||
namespace Wela.RegistrySaclFixture {
|
||||
sealed class Privilege : IDisposable {
|
||||
[StructLayout(LayoutKind.Sequential)] struct Luid {public uint Low;public int High;}
|
||||
[StructLayout(LayoutKind.Sequential)] struct Privileges {public uint Count;public Luid Id;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 handle);
|
||||
[DllImport("advapi32.dll",SetLastError=true)] static extern bool OpenProcessToken(IntPtr process,uint access,out IntPtr token);
|
||||
[DllImport("advapi32.dll",SetLastError=true)] static extern bool OpenThreadToken(IntPtr thread,uint access,bool self,out IntPtr token);
|
||||
[DllImport("advapi32.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern bool LookupPrivilegeValue(string system,string name,out Luid luid);
|
||||
[DllImport("advapi32.dll",SetLastError=true)] static extern bool AdjustTokenPrivileges(IntPtr token,bool all,ref Privileges requested,uint size,out Privileges previous,out uint required);
|
||||
IntPtr token;Privileges previous;
|
||||
public Privilege(string name){
|
||||
if(name!="SeBackupPrivilege"&&name!="SeRestorePrivilege")throw new InvalidOperationException("Unreviewed fixture privilege.");
|
||||
IntPtr thread;if(OpenThreadToken(GetCurrentThread(),8,true,out thread)){CloseHandle(thread);throw new InvalidOperationException("Impersonated fixture refused.");}int error=Marshal.GetLastWin32Error();if(error!=1008)throw new Win32Exception(error);
|
||||
if(!OpenProcessToken(GetCurrentProcess(),0x28,out token))throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
try{Luid id;if(!LookupPrivilegeValue(null,name,out id))throw new Win32Exception(Marshal.GetLastWin32Error());Privileges request=new Privileges{Count=1,Id=id,Attributes=2};uint needed;bool ok=AdjustTokenPrivileges(token,false,ref request,(uint)Marshal.SizeOf(typeof(Privileges)),out previous,out needed);error=Marshal.GetLastWin32Error();if(!ok||error!=0)throw new Win32Exception(error,"Existing fixture privilege is required: "+name);}catch{CloseHandle(token);token=IntPtr.Zero;throw;}
|
||||
}
|
||||
public void Dispose(){if(token==IntPtr.Zero)return;try{Privileges ignored;uint needed;bool ok=AdjustTokenPrivileges(token,false,ref previous,(uint)Marshal.SizeOf(typeof(Privileges)),out ignored,out needed);int error=Marshal.GetLastWin32Error();if(!ok||error!=0)throw new Win32Exception(error,"Fixture privilege restoration failed.");}finally{CloseHandle(token);token=IntPtr.Zero;}}
|
||||
}
|
||||
public sealed class Hive : IDisposable {
|
||||
static readonly IntPtr HKCU=new IntPtr(unchecked((int)0x80000001)),HKU=new IntPtr(unchecked((int)0x80000003));
|
||||
[DllImport("advapi32.dll",CharSet=CharSet.Unicode,ExactSpelling=true)] static extern int RegCreateKeyExW(IntPtr root,string path,int reserved,string cls,uint options,uint access,IntPtr security,out IntPtr result,out uint disposition);
|
||||
[DllImport("advapi32.dll")] static extern int RegCloseKey(IntPtr key);
|
||||
[DllImport("advapi32.dll",CharSet=CharSet.Unicode,ExactSpelling=true)] static extern int RegSaveKeyExW(IntPtr key,string file,IntPtr security,uint flags);
|
||||
[DllImport("advapi32.dll",CharSet=CharSet.Unicode,ExactSpelling=true)] static extern int RegLoadKeyW(IntPtr root,string name,string file);
|
||||
[DllImport("advapi32.dll",CharSet=CharSet.Unicode,ExactSpelling=true)] static extern int RegUnLoadKeyW(IntPtr root,string name);
|
||||
public readonly string Nonce,Sid,SeedPath,FilePath;
|
||||
public bool SeedCreated{get;private set;} public bool Saved{get;private set;} public bool Loaded{get;private set;}
|
||||
public Hive(string nonce,string file){
|
||||
if(!System.Text.RegularExpressions.Regex.IsMatch(nonce??"","^[a-f0-9]{32}$"))throw new InvalidOperationException("Exact fixture nonce required.");
|
||||
Nonce=nonce;Sid="S-1-5-21-"+Convert.ToUInt32(nonce.Substring(0,8),16)+"-"+Convert.ToUInt32(nonce.Substring(8,8),16)+"-"+Convert.ToUInt32(nonce.Substring(16,8),16)+"-1001";
|
||||
SeedPath="Software\\WELARegistrySaclSeed_"+nonce;FilePath=Path.GetFullPath(file);
|
||||
if(File.Exists(FilePath))throw new InvalidOperationException("Fixture hive file must be new.");
|
||||
}
|
||||
static void Check(int status,string operation){if(status!=0)throw new Win32Exception(status,operation);}
|
||||
static bool Exists(RegistryKey root,string name){using(RegistryKey key=root.OpenSubKey(name)){return key!=null;}}
|
||||
public void Prepare(){
|
||||
if(SeedCreated||Saved||Loaded||Exists(Registry.Users,Sid))throw new InvalidOperationException("Fixture identity already exists.");
|
||||
IntPtr key;uint disposition;Check(RegCreateKeyExW(HKCU,SeedPath,0,null,0,0xF003F,IntPtr.Zero,out key,out disposition),"Create owned seed");
|
||||
try{if(disposition!=1)throw new InvalidOperationException("Seed collided with an existing key.");SeedCreated=true;
|
||||
using(RegistryKey seed=Registry.CurrentUser.OpenSubKey(SeedPath,true)){seed.SetValue("WelaFixtureOwner",Nonce,RegistryValueKind.String);seed.Flush();}
|
||||
using(new Privilege("SeBackupPrivilege")){Check(RegSaveKeyExW(key,FilePath,IntPtr.Zero,2),"Save owned seed to a new hive file");Saved=true;}
|
||||
}finally{RegCloseKey(key);}
|
||||
if(Exists(Registry.Users,Sid))throw new InvalidOperationException("Fixture HKU mount collided.");
|
||||
using(new Privilege("SeBackupPrivilege"))using(new Privilege("SeRestorePrivilege")){Check(RegLoadKeyW(HKU,Sid,FilePath),"Load owned hive under its fresh SID");Loaded=true;}
|
||||
AssertOwned();
|
||||
}
|
||||
public void AssertOwned(){using(RegistryKey key=Registry.Users.OpenSubKey(Sid)){if(!Loaded||key==null||key.GetValueKind("WelaFixtureOwner")!=RegistryValueKind.String||!String.Equals(key.GetValue("WelaFixtureOwner") as string,Nonce,StringComparison.Ordinal))throw new InvalidOperationException("Owned hive marker changed.");}}
|
||||
public void CreateRunOnce(){AssertOwned();IntPtr key;uint disposition;Check(RegCreateKeyExW(HKU,Sid+"\\Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce",0,null,0,0xF003F,IntPtr.Zero,out key,out disposition),"Create owned catalog RunOnce target");try{if(disposition!=1)throw new InvalidOperationException("Owned target unexpectedly exists.");}finally{RegCloseKey(key);}}
|
||||
public void Dispose(){
|
||||
if(Loaded){AssertOwned();using(new Privilege("SeBackupPrivilege"))using(new Privilege("SeRestorePrivilege")){Check(RegUnLoadKeyW(HKU,Sid),"Unload owned fixture hive");Loaded=false;}}
|
||||
if(SeedCreated){using(RegistryKey seed=Registry.CurrentUser.OpenSubKey(SeedPath)){if(seed==null||seed.SubKeyCount!=0||seed.ValueCount!=1||seed.GetValueKind("WelaFixtureOwner")!=RegistryValueKind.String||!String.Equals(seed.GetValue("WelaFixtureOwner") as string,Nonce,StringComparison.Ordinal))throw new InvalidOperationException("Owned seed changed; refuse deletion.");}Registry.CurrentUser.DeleteSubKey(SeedPath,true);SeedCreated=false;}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
param([switch]$AllowDisposableHiveWrite)
|
||||
$ErrorActionPreference='Stop'
|
||||
if(-not $AllowDisposableHiveWrite -or $env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted' -or [Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or -not [Environment]::Is64BitProcess){throw 'Explicit disposable GitHub-hosted native Windows fixture only.'}
|
||||
$script:ScriptRoot=Split-Path $PSScriptRoot -Parent
|
||||
Import-Module (Join-Path $script:ScriptRoot 'modules/AuditProfiles.psm1') -ErrorAction Stop
|
||||
foreach($name in @('Configuration','WefArrival','WmiProbe')){. (Join-Path $script:ScriptRoot ('scripts/'+$name+'.ps1'))}
|
||||
Initialize-WelaWmiProbeNative
|
||||
Add-Type -Path (Join-Path $PSScriptRoot 'RegistrySaclFixtureNative.cs') -ErrorAction Stop
|
||||
$root=New-WelaArrivalOutput (Join-Path $env:RUNNER_TEMP ('wela-registry-sacl-'+[guid]::NewGuid().ToString('N'))) $script:ScriptRoot
|
||||
$files=Join-Path $root 'owned-hive-files';$null=New-Item -ItemType Directory $files
|
||||
function Save([string]$Name,$Value){[IO.File]::WriteAllText((Join-Path $root $Name),($Value|ConvertTo-Json -Depth 28),[Text.UTF8Encoding]::new($false))}
|
||||
function Hives {@([Microsoft.Win32.Registry]::Users.GetSubKeyNames()|Sort-Object)}
|
||||
function Key($Value){ConvertTo-Json -InputObject $Value -Depth 20 -Compress}
|
||||
$beforeHives=Hives;$beforeToken=[Wela.WmiProbe.Native]::Snapshot();$beforeMasks=Get-WelaEffectiveAuditPolicy;$beforePrecedence=Get-WelaRegistryState 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' SCENoApplyLegacyAuditPolicy
|
||||
Save 'before-hives.json' $beforeHives;Save 'before-token.json' $beforeToken;Save 'before-masks.json' $beforeMasks;Save 'before-precedence.json' $beforePrecedence
|
||||
$hive=[Wela.RegistrySaclFixture.Hive]::new([guid]::NewGuid().ToString('N'),(Join-Path $files 'owned.dat'));$failure='';$errors=@();$assertions=0
|
||||
try {
|
||||
$hive.Prepare();$hive.CreateRunOnce();$hive.AssertOwned()
|
||||
$mounted=Hives;if((Key $mounted) -cne (Key (@($beforeHives)+$hive.Sid|Sort-Object))){throw 'Unexpected HKU namespace change.'};$assertions++
|
||||
Save 'mounted.json' ([pscustomobject]@{Sid=$hive.Sid;File=$hive.FilePath;Seed=$hive.SeedPath;Loaded=$hive.Loaded;Target=('Registry::HKEY_USERS\'+$hive.Sid+'\Software\Microsoft\Windows\CurrentVersion\RunOnce')})
|
||||
if((Key ([Wela.WmiProbe.Native]::Snapshot())) -cne (Key $beforeToken)){throw 'Fixture preparation did not restore the primary token.'};$assertions++
|
||||
$engine=(Get-Process -Id $PID).Path;$old=$ErrorActionPreference
|
||||
try{$ErrorActionPreference='Continue';$out=@(& $engine -NoLogo -NoProfile -NonInteractive -File (Join-Path $script:ScriptRoot 'WELA.ps1') targeted-sacl -TargetSaclProfile asd-native-2021-10 -IncludeOptional -ResultsPath (Join-Path $root 'catalog.json') 2>&1);$code=$LASTEXITCODE}finally{$ErrorActionPreference=$old;$global:LASTEXITCODE=0}
|
||||
Save 'catalog-output.json' $out
|
||||
if($code -ne 0){throw "Actual public catalog exited $code : $($out -join ' ')"}
|
||||
$catalog=ConvertFrom-WelaArrivalJson ([IO.File]::ReadAllText((Join-Path $root 'catalog.json')))
|
||||
$matches=@($catalog.Catalog|Where-Object {$_.Definition.UserSid -ceq $hive.Sid -and $_.Definition.Path -ieq ('Registry::HKEY_USERS\'+$hive.Sid+'\Software\Microsoft\Windows\CurrentVersion\RunOnce')})
|
||||
if($matches.Count -ne 1 -or $matches[0].Id -cnotmatch '^sacl-[a-f0-9]{24}$' -or $matches[0].Definition.Resolution -cne 'Resolved'){throw 'The unchanged actual public catalog did not resolve exactly one owned registry target.'};$assertions++
|
||||
Save 'selected.json' $matches[0]
|
||||
} catch {$failure=$_.Exception.Message;throw} finally {
|
||||
try{$hive.Dispose()}catch{$errors+=$_.Exception.Message}
|
||||
$hivesOk=(Key (Hives)) -ceq (Key $beforeHives);$tokenOk=(Key ([Wela.WmiProbe.Native]::Snapshot())) -ceq (Key $beforeToken)
|
||||
$masks=Get-WelaEffectiveAuditPolicy;$masksOk=(Key ($masks.GetEnumerator()|Sort-Object Key)) -ceq (Key ($beforeMasks.GetEnumerator()|Sort-Object Key));$precedenceOk=(Key (Get-WelaRegistryState 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' SCENoApplyLegacyAuditPolicy)) -ceq (Key $beforePrecedence)
|
||||
if(-not $hive.Loaded -and $hivesOk){try{Remove-Item -LiteralPath $files -Recurse -Force -ErrorAction Stop}catch{$errors+=$_.Exception.Message}}
|
||||
$cleanup=[pscustomobject]@{Complete=($hivesOk -and $tokenOk -and $masksOk -and $precedenceOk -and -not $hive.SeedCreated -and -not(Test-Path $files) -and $errors.Count -eq 0);HivesRestored=$hivesOk;TokenRestored=$tokenOk;AuditMasksRestored=$masksOk;PrecedenceRestored=$precedenceOk;HiveUnloaded=(-not $hive.Loaded);SeedRemoved=(-not $hive.SeedCreated);FilesRemoved=(-not(Test-Path $files));Errors=$errors;Failure=$failure;Assertions=$assertions;AfterHives=(Hives);AfterToken=[Wela.WmiProbe.Native]::Snapshot();AfterMasks=$masks;AfterPrecedence=(Get-WelaRegistryState 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' SCENoApplyLegacyAuditPolicy)}
|
||||
Save 'cleanup.json' $cleanup
|
||||
if(-not $cleanup.Complete){throw ('Owned registry fixture cleanup incomplete: '+(Key $cleanup))}
|
||||
}
|
||||
Write-Host "Passed $assertions owned real hive/catalog checkpoint assertions; all cleanup confirmed. Evidence: $root"
|
||||
$global:LASTEXITCODE=0
|
||||
Reference in new issue
Block a user