18 - 04 - 2024

CSharp Security-Editor

Bewertung:  / 4
SchwachSuper 

SecurityEditorIn einer .NET-Applikation bietet sich für die Steuerung von Zugriffsrechte, z.B. auf einen Dienst oder eine Datei, die Nutzung des Security Descriptors an. Auch für dessen komfortable Bearbeitung unter Windows bietet die Windows-API bereits mit der Funktion EditSecurity() eine komfortable Möglichkeit um wahlweise entweder den Reiter 'Sicherheit' in eine eigene Property-Implementation zu integrieren oder den Editor gar als eigenständigen Dialog aufpoppen zu lassen.

Allerdings ist die Nutzung unter C# problematisch, weil die Konfiguration dieser Funktion über ein COM-Interface auf nicht verwalteten Speicher erfolgt. In diesem Blog-Eintrag hatte ich mich deshalb mit der Frage beschäftigt inwieweit die WinAPI-Funktion EditSecurity()auch in C# nativ genutzt werden kann.

Die WinAPI-Funktion EditSecurity() ist deshalb so interessant weil sie einen Reiter 'Sicherheit' bereits fix und fertig bereitstellt wahlweise zur Integration in eigene Implementationen eines Property-Dialoges oder gar als eigener einfacher Property-Dialog den Windows (nicht die .NET-Applikation!) dann nach Aufruf von EditSecurity() aufpoppt und steuert. Dies ermöglicht dem Windows-Benutzer sich die Discretionary Access Control List (kurz DACL) zum Security-Descriptor des betreffenden Objekt anzuschauen und für jedes einzelne Element daraus den Zugriff für Benutzer oder Gruppen zu erlauben oder zu verbieten.

SecurityEditorDie WinAPI-Funktion EditSecurity() kann man zwar, via P/Invoke, nach C# übersetzen, jedoch erwartet die Funktion in ihrer Signatur die Übergabe eines COM-Objekts vom Typ ISecurityInformation, das seinerseits Methoden verwendet um den zu bearbeiten Security-Descriptor zu konfigurieren und das zwingend im nicht verwalteten Arbeitsspeicher!

Es handelt sich nunmal dabei um eine WinAPI-Funktion die allen Windows-Programmen zur Verfügung stehen soll, also beispielsweise auch C++-Applikationen die bekanntermaßen den verwalteten Arbeitsspeicher des .NET-Frameworks nicht kennen.

Einen Security Descriptor in Managed Code zu lesen und zu benutzen würde den Rahmen dieses Blog-Artikels sprengen und ist auch schon an anderen Stellen im Internet ausführlich behandelt worden. Ich wollte  die Zugriffsrechte auf einen von mir entwickelten Windows Service regeln und brauchte dafür lediglich eine konfigurierbare DACL mit gültigen ACEs. Genau dafür bietet sich die Security Descriptor Definition Language (kurz SDDL) an, welche eine DACL auch in String-Form abbildet. Die Konfiguration der DACL für das Windows-Verzeichnis C:\Windows\system32 zum Beispiel könnte dann, in SDDL geschrieben, so aussehen:

D:AI(A;ID;FA;;;S-1-5-80-880578595-1860270145-482643319-2788375705-1540778122)(A;ID;FA;;;SY)(A;ID;FA;;;BA)

Die DACL für die Zugriffssteuerung meines Windows Service in einem SDDL-String zu speichern ist problemlos möglich: Der String wird in einer geschützten Umgebung des Service abgespeichert so dass dieser nur vom Service gelesen und verwendet werden kann. Die Interpreation des SDDL-String setze ich an dieser Stelle mal voraus und möchte nicht weiter darauf eingehen. Allerdings ist für die Bearbeitung des SDDL-String ein komfortabler Security-Editor erforderlich - und zwar nicht eine eigene Implementation sondern der originale Security-Editor, den Windows mit der WinAPI-Funktion EditSecurity() bereits zur Verfügung stellt.

Anleitung zur Implementation

Um den Security-Editor nutzen zu können muss zunächst das Interface ISecurityInformation auch unter .NET verwendbar gemacht werden und dafür  passender Code für eine C#-Implementierung für das COM-Interface bereitgestellt werden:

CSharp Security-Editor - Code zum Interface 'ISecurityInformation'.

Bitte achten Sie darauf nicht möglicherweise bereits vorhandenen Code aus anderen Quellen im Internet für das Interface zu verwenden. Andere Lösungsansätze gehen von der Notwendigkeit eines C++-Wrappers aus und beschreiben den Code auf das Interface im Wrapper und nicht auf das COM-Objekt selbst! Um Verwechselungen zu vermeiden wird dem Interface deshalb gerne auch der Name ISecurityInformation2 gegeben. Der hier von mir vorgestellte Code für das Interface ISecurityInformation ist dagegen auf die direkte Nutzung des COM-Objekts von einer .NET-Applikation aus ausgelegt und Voraussetzung für die Implementierung der Klasse SecurityEditor im nächsten Schritt.

Implementierung der Klasse SecurityEditor

Im nächsten Schritt wird nun die eigentliche Hauptklasse SecurityEditor angelegt und von dem neu implementieren Interface ISecurityInformation abgeleitet:

	using System;
	using System.Runtime.InteropServices;
	using System.Windows;
	using System.Windows.Interop;
			
	/// <summary>
	/// The ISecurityInformation interface enables the access control editor to communicate with the caller of the CreateSecurityPage and EditSecurity functions. The editor calls the interface methods to retrieve information that is used to initialize its pages and to determine the editing options available to the user. The editor also calls the interface methods to pass the user's input back to the application.
	/// </summary>
	public abstract class SecurityEditor : ISecurityInformation
	{
	    /// <summary>
        /// The GetObjectInformation method requests information that the access control editor uses to initialize its pages and to determine the editing options available to the user.
        /// </summary>
        ///	<param name="object_info" />A pointer to an SI_OBJECT_INFO structure. Your implementation must fill this structure to pass information back to the access control editor.</param>
		public abstract void GetObjectInformation(ref SI_OBJECT_INFO pObjectInfo);
		
		/// <summary>
        /// The GetAccessRights method requests information about the access rights that can be controlled for a securable object. The access control editor calls this method to retrieve display strings and other information used to initialize the property pages. For more information, see Access Rights and Access Masks.
        /// </summary>
		///	<param name="pguidObjectType" />A pointer to a GUID structure that identifies the type of object for which access rights are being requested. If this parameter is NULL or a pointer to GUID_NULL, return the access rights for the object being edited. Otherwise, the GUID identifies a child object type returned by the ISecurityInformation::GetInheritTypes method. The GUID corresponds to the InheritedObjectType member of an object-specific ACE.</param>
        /// <param name="dwFlags" />A set of bit flags that indicate the property page being initialized. This value is zero if the basic security page is being initialized. Otherwise, it is a combination of the following values. </param>
        ///	<param name="ppAccess" />A pointer to an array of SI_ACCESS structures. The array must include one entry for each access right. You can specify access rights that apply to the object itself, as well as object-specific access rights that apply only to a property set or property on the object.</param>
        /// <param name="pcAccesses" />A pointer to ULONG that indicates the number of entries in the ppAccess array.</param>
        /// <param name="piDefaultAccess" />A pointer to ULONG that indicates the zero-based index of the array entry that contains the default access rights. The access control editor uses this entry as the initial access rights in a new ACE.</param>
		public abstract void GetAccessRight(IntPtr pguidObjectType, SI_ACCESS_RIGHT_FLAG dwFlags, out SI_ACCESS[] ppAccess, ref uint pcAccesses, ref uint piDefaultAccess);
		
        /// <summary>
        /// The GetSecurity method requests a security descriptor for the securable object whose security descriptor is being edited. The access control editor calls this method to retrieve the object's current or default security descriptor.
        /// </summary>
        ///	<param name="RequestInformation" />A set of SECURITY_INFORMATION bit flags that indicate the parts of the security descriptor being requested. This parameter can be a combination of the following values.</param>
        ///	<param name="ppSecurityDescriptor" />A pointer to a variable that your implementation must set to a pointer to the object's security descriptor. The security descriptor must include the components requested by the RequestedInformation parameter. The system calls the LocalFree function to free the returned pointer.</param>
        /// <param name="fDefault" />If this parameter is TRUE, ppSecurityDescriptor should return an application-defined default security descriptor for the object. The access control editor uses this default security descriptor to reinitialize the property page. The access control editor sets this parameter to TRUE only if the user clicks the Default button. The Default button is displayed only if you set the SI_RESET flag in the ISecurityInformation::GetObjectInformation method. If no default security descriptor is available, do not set the SI_RESET flag. If this flag is FALSE, ppSecurityDescriptor should return the object's current security descriptor.</param>
		public abstract void GetSecurity(SI_SECURITY_INFORMATION RequestInformation, IntPtr ppSecurityDescriptor, bool fDefault);
		
		/// <summary>
        /// The SetSecurity method provides a security descriptor containing the security information the user wants to apply to the securable object. The access control editor calls this method when the user clicks Okay or Apply.
        /// </summary>
        /// <param name="SecurityInformation" />A set of SECURITY_INFORMATION bit flags that indicate the parts of the security descriptor to set. This parameter can be a combination of the following values. </param>
        /// <param name="pSecurityDescriptor" />A pointer to a security descriptor containing the new security information. Do not assume the security descriptor is in self-relative form; it can be either absolute or self-relative.</param>
		public virtual void SetSecurity(SI_SECURITY_INFORMATION SecurityInformation, IntPtr pSecurityDescriptor)
		{
			Store = true;
		}
		
        /// <summary>
        /// The GetInheritTypes method requests information about how ACEs can be inherited by child objects. For more information, see ACE Inheritance.
        /// </summary>
        ///	<param name="ppInheritTypes" />A pointer to a variable you should set to a pointer to an array of SI_INHERIT_TYPE structures. The array should include one entry for each combination of inheritance flags and child object type that you support.</param>
        /// <param name="pcInheritTypes" />A pointer to a variable that you should set to indicate the number of entries in the ppInheritTypes array.</param>
		public virtual void GetInheritTypes(ref SI_INHERIT_TYPE ppInheritTypes, IntPtr pcInheritTypes) 
   		{
   		}
		
        /// <summary>
        /// The PropertySheetPageCallback method notifies an EditSecurity or CreateSecurityPage caller that an access control editor property page is being created or destroyed.
        /// </summary>
        ///	<param name="hwnd" />If uMsg is PSPCB_SI_INITDIALOG, hwnd is a handle to the property page dialog box. Otherwise, hwnd is NULL.</param>
        ///	<param name="uMsg" />Identifies the message being received. This parameter is one of the following values.
        ///	<param name="uPage" />A value from the SI_PAGE_TYPE enumeration type that indicates the type of access control editor property page being created or destroyed.</param>
		public virtual void PropertySheetPageCallback(IntPtr hwnd, int uMsg, SI_PAGE_TYPE uPage) 
   		{
   		}
		
        /// <summary>
        /// The MapGeneric method requests that the generic access rights in an access mask be mapped to their corresponding standard and specific access rights. For more information about generic, standard, and specific access rights, see Access Rights and Access Masks.
        /// </summary>
        /// <param name="pguidObjectType" />A pointer to a GUID structure that identifies the type of object to which the access mask applies. If this member is NULL or a pointer to GUID_NULL, the access mask applies to the object itself.</param>
        /// <param name="pAceFlags" />A pointer to the AceFlags member of the ACE_HEADER structure from the ACE whose access mask is being mapped.</param>
        /// <param name="pMask" />A pointer to an access mask that contains the generic access rights to map. Your implementation must map the generic access rights to the corresponding standard and specific access rights for the specified object type.</param>
        public virtual void MapGeneric(IntPtr pguidObjectType, IntPtr pAceFlags, IntPtr pMask) 
   		{
       		//MapGenericMask(pMask, ref _generic_mapping);
   		}
        
        /// <summary>
        /// Shows the Security Editor as a modal dialog box with the specified owner.
        /// </summary>
        /// <param name="owner">Any object that implements Window that represents the top-level window that will own the modal dialog box. </param>
        /// <returns>TRUE if the user has changed the Security Descriptor, otherwise FALSE.</returns>
   		public bool ShowDialog(Window owner)
   		{
       		EditSecurity((owner == null ? IntPtr.Zero : new WindowInteropHelper(owner).Handle), this);
       		return _Store;
   		}
		
        #region Protected Area
		
        protected const uint SDDL_REVISION_1 = 1;
        protected const uint S_OK = 0;
        protected const uint E_ACCESSDENIED = 0x80070005;
        
		[DllImport("advapi32.dll")]
        protected static extern void MapGenericMask(IntPtr Mask, ref GENERIC_MAPPING map);
        
		[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        protected static extern bool ConvertStringSecurityDescriptorToSecurityDescriptor([In] IntPtr pStringSd, [In] uint dwRevision, [In][Out] IntPtr pSecurityDescriptor, [Out] out uint SecurityDescriptorSize);
        
		[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        protected static extern bool ConvertSecurityDescriptorToStringSecurityDescriptor(IntPtr SecurityDescriptor, uint StringSDRevision, SI_SECURITY_INFORMATION SecurityInformation, out IntPtr StringSecurityDescriptor, out uint StringSecurityDescriptorSize);
        
		#endregion
        
		#region Private Area
        
		[DllImport("aclui.dll")]
        private static extern bool EditSecurity(IntPtr hwnd, ISecurityInformation psi);
        private bool _Store = false;
        
		#endregion 
	}

Weitere Enumerationen und Struct-Elemente

Sowohl das Interface als auch die Klasse SecurityEditor verwenden weitere Enumerationen und Struct-Elemente, welche ebenfalls noch implementiert werden müssen:

 
 

Verwendung der Klasse SecurityEditor

Mit dem Aufruf der Methode ShowDialog() wird die WinAPI-Funktion EditSecurity() aufgerufen und sobald Windows diese Funktion ausführt, ruft es (ähnlich wie bei einem Callback-Mechanismus) die einzelnen Methoden der Klasse SecurityEditor auf um den Security-Editor zu konfigurieren und anschließend anzuzeigen.

Natürlich können jetzt die einzelnen Methoden der Klasse SecurityEditor oben ausimplementiert werden. Ich bin jedoch den Weg gegangen die Klasse SecurityEditor als abstract zu markieren und die eigentliche Konfiguration für ein Objekt in einer eigenen Ableitung mit dem Name SDDLEditor zu implementieren:

				
	using System;
	using System.Runtime.InteropServices;
	using System.Windows;
	
	internal class SDDLEditor : SecurityEditor
    {
        public SDDLEditor(string sddl)
        {
            _SDDL = sddl;
        }
		
        public override void GetObjectInformation(ref SI_OBJECT_INFO pObjectInfo)
        {
            pObjectInfo = new SI_OBJECT_INFO(SI_OBJECT_FLAGS.SI_EDIT_PERMS | SI_OBJECT_FLAGS.SI_PAGE_TITLE,
                                             IntPtr.Zero, Environment.MachineName, "Object-Title", "Security");
        }
		
        public override void GetAccessRight(IntPtr pguidObjectType, SI_ACCESS_RIGHT_FLAG dwFlags, out SI_ACCESS[] ppAccess, ref uint pcAccesses, ref uint piDefaultAccess)
        {
            ppAccess = new SI_ACCESS[1];
            ppAccess[0] = new SI_ACCESS(SI_ACCESS_MASK.READ_CONTROL, "Full Access", SI_ACCESS_FLAG.SI_ACCESS_GENERAL);
            pcAccesses = (uint)ppAccess.Length;
            piDefaultAccess = 0;
        }
		
        public override void GetSecurity(SI_SECURITY_INFORMATION RequestInformation, IntPtr pSecurityDescriptor, bool fDefault)
        {
            if (RequestInformation == SI_SECURITY_INFORMATION.Dacl || RequestInformation == SI_SECURITY_INFORMATION.Sacl)
            {
                IntPtr pSDDL = IntPtr.Zero;
                try
                {
                    pSDDL = Marshal.StringToHGlobalAuto(_SDDL);
                    uint Size;
                    if (!ConvertStringSecurityDescriptorToSecurityDescriptor(pSDDL, SDDL_REVISION_1, pSecurityDescriptor, out Size))
                        throw new ArgumentException("SDDL conversion failed, Error: " + Marshal.GetLastWin32Error());
                }
                finally
                {
                    if (pSDDL != IntPtr.Zero)
                        Marshal.FreeHGlobal(pSDDL);
                }
            }
        }
		
        public override void SetSecurity(SI_SECURITY_INFORMATION SecurityInformation, IntPtr pSecurityDescriptor)
        {
            if (SecurityInformation == SI_SECURITY_INFORMATION.Dacl || SecurityInformation == SI_SECURITY_INFORMATION.Sacl)
            {
                IntPtr pSD;
                uint Size;
                if (!ConvertSecurityDescriptorToStringSecurityDescriptor(pSecurityDescriptor, SDDL_REVISION_1, SI_SECURITY_INFORMATION.Dacl, out pSD, out Size))
                    throw new ArgumentException("SDDL conversion failed, Error: " + Marshal.GetLastWin32Error());
                _SDDL = Marshal.PtrToStringAuto(pSD);
                base.SetSecurity(SecurityInformation, pSecurityDescriptor);
            }
        }
		
        public string SDDL
        {
            get { return _SDDL; }
        }
		
        #region Private Area
        
		private string _SDDL;
        
		#endregion
    }
	

Verwendung der Klasse SDDLEditor

Im Konstruktor wird der zu verwendende SDDL-String übergeben, der über die Property SDDL nach Aufruf der Methode ShowDialog() ggf. aktualisiert wieder gelesen werden kann.

Die Methode GetObjectInformation() liefert dem Windows Informationen darüber welche Titel im Dialog bzw. im Reiter angezeigt werden sollen. Environment-Machine führt dazu, dass alle Authentifizierungen gegen den lokalen Computer erfolgen.

Mit der Methode GetAccessRight() wird eine Liste der für das Objekt geltenden Rechte übergeben. Im Code oben habe ich nur ein Recht mit dem Name "Full Access" implementiert.

Mit den Methoden GetSecurity() und SetSecurity() ruft Windows den zu verwendenden Security-Descriptor ab bzw. schreibt einen neuen Security-Descriptor. Letztere Methode wird nur dann durchlaufen wenn der Benutzer Änderungen an der DACL vorgenommen hat und den Dialog mit Ok bestätigt wurde. In beiden Methoden  wird der SDDL-String in einen Security-Descriptor umgewandelt bzw. umgekehrt.

Nun kann ein Security-Editor in der eigenen .NET-Applikation mit wenigen Programmzeilen aufgerufen werden bzw. ein SDDL-String komfortabel bearbeitet werden:


string sSDDL = "D:PARAI(A;;RC;;;LA)";

SDDLEditor sd_editor = new SDDLEditor(sSDDL);
sd_editor.ShowDialog();

Console.WriteLine(sd_editor.SDDL);

Weitere Funktionen

Die WinAPI-Funktion EditSecurity() bietet noch mehr Funktionalität. So ist es möglich beispielsweise den "Advanced"-Button im Security-Editor zu aktivieren und dann auch weitere Funktionen des Security-Editor zu verwenden, wie z.B. die Bearbeitung des Owners oder der Vererbung von Rechten. Wenn Sie hier mehr machen wollen, können Sie gewünschte weitere Objekt-Flags unter GetObjectInformation() aktivieren, müssen dann jedoch (je nachdem was Sie aktivieren) weitere Methoden aus dem Interface in den Klassen SecurityEditor bzw. SDDLEditor implementieren.

Ich würde mich freuen wenn diese Code-Beispiele ihnen weiterhelfen und natürlich auch wenn Sie den Code um weitere nützliche Funktionen erweitert haben und bereit sind die Erweiterung mit mir zu teilen.

Kommentare  

 
# X220 2016-12-18 02:26
Sehr Gut! Genau sowas suche ich zurzeit... habe gerade ca. 28 Tabs im Explorer offen und mir raucht der Kopf beim einarbeiten ins Windows Sicherheitskonz ept^^
Ich arbeite zwar mit VisualBasic, aber dein Code ist schon sehr hilfreich :)
Antworten | Antworten mit Zitat | Zitieren