Files
WELA/scripts/SelectedSaclNative.cs
T
田中ザック Isaac Mathis bcd4e9717e Verify reviewed descendant SACL propagation and preservation (#429)
* Verify reviewed descendant SACL propagation and preservation

* Reference descendant SACL PR429 in release notes

* Prepare protected disposable SACL fixtures through native handles

* Use read-control handles for disposable native SACL protection
2026-09-21 09:12:56 +09:00

153 lines
13 KiB
C#

// Handle-bound local file/registry SACL reads and additive audit writes only.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
namespace Wela.SelectedSacl {
public sealed class Ace {
public string Binary; public int Type; public int Flags; public int Mask; public string Sid; public bool Ordinary;
}
public sealed class Snapshot {
public string Path; public string Kind; public string Identity; public bool IsDirectory;
public string DescriptorBase64; public string Owner; public string Group; public string DaclBase64;
public int ControlFlags; public int SecurityInformation; public string DescriptorScope; public Ace[] Aces;
}
public sealed class Children { public string[] Names; public bool Truncated; }
public sealed class Privilege : IDisposable {
[StructLayout(LayoutKind.Sequential)] struct Luid { public uint Low; public int High; }
[StructLayout(LayoutKind.Sequential)] struct TokenPrivileges { public uint Count; 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 value);
[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 disable,ref TokenPrivileges value,uint size,out TokenPrivileges previous,out uint required);
IntPtr token; TokenPrivileges previous;
public Privilege() {
IntPtr threadToken;
if(OpenThreadToken(GetCurrentThread(),8,true,out threadToken)) {CloseHandle(threadToken);throw new InvalidOperationException("Impersonated callers are outside the selected-SACL workflow.");}
int error=Marshal.GetLastWin32Error();if(error!=1008)throw new Win32Exception(error,"Cannot establish absence of an impersonation token.");
if(!OpenProcessToken(GetCurrentProcess(),0x28,out token))throw new Win32Exception(Marshal.GetLastWin32Error());
try {
Luid luid;if(!LookupPrivilegeValue(null,"SeSecurityPrivilege",out luid))throw new Win32Exception(Marshal.GetLastWin32Error());
TokenPrivileges requested=new TokenPrivileges {Count=1,Luid=luid,Attributes=2};uint required;
bool ok=AdjustTokenPrivileges(token,false,ref requested,(uint)Marshal.SizeOf(typeof(TokenPrivileges)),out previous,out required);
error=Marshal.GetLastWin32Error();if(!ok||error!=0)throw new Win32Exception(error,"SeSecurityPrivilege is not available.");
}catch {CloseHandle(token);token=IntPtr.Zero;throw;}
}
public void Dispose() {
if(token==IntPtr.Zero)return;
try {TokenPrivileges ignored;uint required;bool ok=AdjustTokenPrivileges(token,false,ref previous,(uint)Marshal.SizeOf(typeof(TokenPrivileges)),out ignored,out required);int error=Marshal.GetLastWin32Error();if(!ok||error!=0)throw new Win32Exception(error,"SeSecurityPrivilege restoration failed.");}
finally {CloseHandle(token);token=IntPtr.Zero;}
}
}
public sealed class Target : IDisposable {
[StructLayout(LayoutKind.Sequential,Pack=4)] struct FileInfo {public uint Attributes;public long Created;public long Accessed;public long Written;public uint Volume;public uint SizeHigh;public uint SizeLow;public uint Links;public uint IndexHigh;public uint IndexLow;}
[DllImport("kernel32.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern IntPtr CreateFile(string name,uint access,uint share,IntPtr security,uint disposition,uint flags,IntPtr template);
[DllImport("kernel32.dll",SetLastError=true)] static extern bool CloseHandle(IntPtr value);
[DllImport("kernel32.dll",SetLastError=true)] static extern bool GetFileInformationByHandle(IntPtr handle,out FileInfo information);
[DllImport("kernel32.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern uint GetFinalPathNameByHandle(IntPtr handle,StringBuilder path,uint size,uint flags);
[DllImport("kernel32.dll")] static extern IntPtr LocalFree(IntPtr value);
[DllImport("advapi32.dll",CharSet=CharSet.Unicode)] static extern int RegOpenKeyEx(IntPtr key,string path,uint options,uint access,out IntPtr opened);
[DllImport("advapi32.dll",CharSet=CharSet.Unicode)] static extern int RegQueryValueEx(IntPtr key,string name,IntPtr reserved,out uint type,IntPtr data,ref uint size);
[DllImport("advapi32.dll",CharSet=CharSet.Unicode)] static extern int RegQueryInfoKey(IntPtr key,IntPtr cls,IntPtr clsSize,IntPtr reserved,IntPtr subKeys,IntPtr maxSubKey,IntPtr maxClass,IntPtr values,IntPtr maxValueName,IntPtr maxValue,IntPtr securitySize,out long written);
[DllImport("advapi32.dll",CharSet=CharSet.Unicode)] static extern int RegEnumKeyEx(IntPtr key,uint index,StringBuilder name,ref uint length,IntPtr reserved,IntPtr cls,IntPtr clsLength,IntPtr written);
[DllImport("advapi32.dll")] static extern int RegCloseKey(IntPtr key);
[DllImport("advapi32.dll")] static extern uint GetSecurityInfo(IntPtr handle,uint kind,uint flags,out IntPtr owner,out IntPtr group,out IntPtr dacl,out IntPtr sacl,out IntPtr descriptor);
[DllImport("advapi32.dll")] static extern uint GetSecurityDescriptorLength(IntPtr descriptor);
[DllImport("advapi32.dll")] static extern uint SetSecurityInfo(IntPtr handle,uint kind,uint flags,IntPtr owner,IntPtr group,IntPtr dacl,IntPtr sacl);
IntPtr handle;readonly List<IntPtr> keys=new List<IntPtr>();readonly string path;readonly string kind;readonly uint objectType;
public Target(string kind,string path) : this(kind,path,false) {}
public Target(string kind,string path,bool enumerate) {
this.kind=kind;this.path=path;objectType=kind=="FileSystem"?1U:4U;
try {
if(kind=="FileSystem") {
// No DELETE sharing: keep the opened object stable while reading/writing it.
handle=CreateFile(path,0x01020000,3,IntPtr.Zero,3,0x02200000,IntPtr.Zero);
if(handle==new IntPtr(-1)){handle=IntPtr.Zero;throw new Win32Exception(Marshal.GetLastWin32Error());}
FileInfo info;if(!GetFileInformationByHandle(handle,out info))throw new Win32Exception(Marshal.GetLastWin32Error());
if((info.Attributes&0x400)!=0)throw new InvalidOperationException("Reparse-point file target refused.");
StringBuilder final=new StringBuilder(32768);uint length=GetFinalPathNameByHandle(handle,final,(uint)final.Capacity,0);
if(length==0||length>=final.Capacity)throw new InvalidOperationException("Cannot verify final local file path.");
if(!String.Equals(final.ToString(),"\\\\?\\"+path,StringComparison.OrdinalIgnoreCase))throw new InvalidOperationException("Final handle path differs from selected path (link/redirection).");
} else if(kind=="Registry") {
string[] parts=path.Split('\\');IntPtr current;
if(parts[0]=="HKEY_LOCAL_MACHINE")current=new IntPtr(unchecked((int)0x80000002));
else if(parts[0]=="HKEY_USERS")current=new IntPtr(unchecked((int)0x80000003));
else throw new InvalidOperationException("Only explicitly selected HKLM/HKU keys are supported.");
if(parts.Length<2)throw new InvalidOperationException("A registry hive root cannot be selected.");
for(int i=1;i<parts.Length;i++) {
IntPtr opened;int error=RegOpenKeyEx(current,parts[i],8,0x01020101U|((enumerate&&i==parts.Length-1)?8U:0U),out opened); // OPEN_LINK, 64-bit view, query/read-control/SACL.
if(error!=0)throw new Win32Exception(error);keys.Add(opened);current=opened;
uint type;uint size=0;error=RegQueryValueEx(current,"SymbolicLinkValue",IntPtr.Zero,out type,IntPtr.Zero,ref size);
if(error==0&&type==6)throw new InvalidOperationException("Registry symbolic-link component refused.");
if(error!=0&&error!=2)throw new Win32Exception(error,"Registry link state is unreadable.");
}
handle=current;
} else throw new InvalidOperationException("Unsupported selected target kind.");
}catch {Dispose();throw;}
}
public Children Enumerate(int maximum) {
if(maximum<1||maximum>129)throw new ArgumentOutOfRangeException("maximum");
if(handle==IntPtr.Zero)throw new ObjectDisposedException("Target");
List<string> names=new List<string>();bool truncated=false;
if(kind=="Registry") {
for(uint index=0;;index++) {
StringBuilder name=new StringBuilder(256);uint length=256;
int error=RegEnumKeyEx(handle,index,name,ref length,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero);
if(error==259)break;if(error!=0)throw new Win32Exception(error,"Registry child enumeration failed.");
if(names.Count==maximum){truncated=true;break;}names.Add(name.ToString());
}
} else {
// The verified parent handle remains open without DELETE sharing during enumeration.
foreach(string entry in Directory.EnumerateFileSystemEntries(path)) {
if(names.Count==maximum){truncated=true;break;}names.Add(System.IO.Path.GetFileName(entry));
}
}
HashSet<string> seen=new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach(string name in names)if(String.IsNullOrEmpty(name)||name=="."||name==".."||name.IndexOfAny(new char[]{'\\','/','\0'})>=0||!seen.Add(name))throw new InvalidOperationException("Ambiguous or duplicate child name.");
names.Sort(StringComparer.OrdinalIgnoreCase);return new Children {Names=names.ToArray(),Truncated=truncated};
}
static string Bytes(GenericAcl acl){if(acl==null)return null;byte[] bytes=new byte[acl.BinaryLength];acl.GetBinaryForm(bytes,0);return Convert.ToBase64String(bytes);}
static string Bytes(GenericAce ace){byte[] bytes=new byte[ace.BinaryLength];ace.GetBinaryForm(bytes,0);return Convert.ToBase64String(bytes);}
public Snapshot Read() {
if(handle==IntPtr.Zero)throw new ObjectDisposedException("Target");
// Explicit current SDK section union: owner/group/DACL/audit/label/resource/CAP/trust/access-filter.
// BACKUP_SECURITY_INFORMATION itself returns ACCESS_DENIED on the tested file handles;
// do not acquire broader privileges or silently retry with an incomplete section subset.
const uint securityInformation=0x000001ff;
IntPtr owner,group,dacl,sacl,descriptor;uint error=GetSecurityInfo(handle,objectType,securityInformation,out owner,out group,out dacl,out sacl,out descriptor);
if(error!=0)throw new Win32Exception((int)error,"GetSecurityInfo explicit sections 0x1ff failed for "+kind+" ("+error+").");
byte[] bytes;
try {uint length=GetSecurityDescriptorLength(descriptor);if(length<20||length>1048576)throw new InvalidOperationException("Invalid descriptor size.");bytes=new byte[length];Marshal.Copy(descriptor,bytes,0,(int)length);}
finally {LocalFree(descriptor);}
RawSecurityDescriptor sd=new RawSecurityDescriptor(bytes,0);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});
}
string identity;bool directory=false;
if(kind=="FileSystem") {FileInfo info;if(!GetFileInformationByHandle(handle,out info))throw new Win32Exception(Marshal.GetLastWin32Error());directory=(info.Attributes&16)!=0;identity=info.Volume+":"+info.IndexHigh+":"+info.IndexLow+":"+info.Created;}
else {long written;int result=RegQueryInfoKey(handle,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,out written);if(result!=0)throw new Win32Exception(result);identity=path+":"+written;}
return new Snapshot {SecurityInformation=(int)securityInformation,DescriptorScope="WinSDK-defined sections 0x1ff; future sections unobserved",Path=path,Kind=kind,Identity=identity,IsDirectory=directory,DescriptorBase64=Convert.ToBase64String(bytes),Owner=sd.Owner==null?null:sd.Owner.Value,Group=sd.Group==null?null:sd.Group.Value,DaclBase64=Bytes(sd.DiscretionaryAcl),ControlFlags=(int)sd.ControlFlags,Aces=entries.ToArray()};
}
public Snapshot Add(string expectedIdentity,string expectedDescriptor,string sid,int mask,int flags) {
Snapshot before=Read();if(before.Identity!=expectedIdentity||before.DescriptorBase64!=expectedDescriptor)throw new InvalidOperationException("Target changed after the recovery snapshot.");
if(mask<=0||(flags&~195)!=0||(flags&192)==0)throw new InvalidOperationException("Invalid selected audit ACE.");
RawSecurityDescriptor sd=new RawSecurityDescriptor(Convert.FromBase64String(before.DescriptorBase64),0);
RawAcl acl=sd.SystemAcl??new RawAcl(2,1);
acl.InsertAce(acl.Count,new CommonAce((AceFlags)flags,AceQualifier.SystemAudit,mask,new SecurityIdentifier(sid),false,null));
byte[] bytes=new byte[acl.BinaryLength];acl.GetBinaryForm(bytes,0);IntPtr buffer=Marshal.AllocHGlobal(bytes.Length);
try {Marshal.Copy(bytes,0,buffer,bytes.Length);uint error=SetSecurityInfo(handle,objectType,8,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,buffer);if(error!=0)throw new Win32Exception((int)error);}
finally {Marshal.FreeHGlobal(buffer);}
return Read();
}
public void Dispose(){if(kind=="FileSystem"&&handle!=IntPtr.Zero)CloseHandle(handle);for(int i=keys.Count-1;i>=0;i--)RegCloseKey(keys[i]);keys.Clear();handle=IntPtr.Zero;}
}
}