Windows-Sicherheitsfunktionen für eigene Anwendungen nutzen

Veröffentlicht: 05. Jul 2004 | Aktualisiert: 14. Nov 2004

Von

Möchten Sie wissen, wie Sie die vielfältigen Sicherheitsfunktionen des Windows-Betriebssystems auch in Ihre eigenen Anwendungen integrieren können? Dieser Artikel erläutert die Grundlagen der Windows-Sicherheitsautorisierung und zeigt, wie Sie Ihre eigenen Sicherheitsbeschreibungen erstellen. Laden Sie das zugehörige Codebeispiel SecuringPrivateObjects.exe (in Englisch) herunter.

Auf dieser Seite

Einführung Einführung
Token oder "Wer bist Du?" Token oder "Wer bist Du?"
Grundlagen zu Sicherheitsbeschreibungen Grundlagen zu Sicherheitsbeschreibungen
Speicherverwaltung Speicherverwaltung
Private Sicherheitsbeschreibungen Private Sicherheitsbeschreibungen
Berechtigungen Berechtigungen
Zugriffssteuerungslisten (ACLs) Zugriffssteuerungslisten (ACLs)
Editor für die Zugriffssteuerung Editor für die Zugriffssteuerung
Schlussfolgerung Schlussfolgerung

Einführung

Haben Sie sich schon einmal gefragt, wie Sie die vielfältigen Sicherheitsfunktionen des Windows-Betriebssystems auch in Ihre eigenen Anwendungen integrieren können? Wünschen Sie sich, dass Sie für Ihre eigenen Geschäftsobjekte dieselbe Sicherheitsstufe implementieren können wie bei den Sicherheits-Editoren des Windows-Dateisystems? In diesem Artikel werde ich Ihnen zuerst die Grundlagen der Windows-Sicherheitsautorisierung erklären. Danach zeige ich Ihnen schrittweise, wie Sie Sicherheitsbeschreibungen ändern, Ihre eigenen Beschreibungen erstellen und diese schließlich auf verschiedene Weise bearbeiten können. Nachdem Sie diesen Artikel gelesen haben, sollten Sie über alle notwendigen Informationen verfügen, um diese Techniken bei Ihren eigenen Anwendungen einzusetzen.

Ein Ziel dieses Artikels ist es, das Programmieren von Sicherheit durchführbar und zugänglich zu machen. Ich werde daher bei der Erklärung einer bestimmten Funktion nicht immer bis in die kleinsten Details gehen. Stattdessen stelle ich Ihnen eine Reihe von Hilfsfunktionen und Klassen vor, mit deren Hilfe Sie Ihren Sicherheitscode robuster und verwaltbarer gestalten. Die Hilfsfunktionen und Beispiele zeigen Ihnen nicht nur, wie die verschiedenen Sicherheitsfunktionen eingesetzt werden, sondern vor allem, wie sie sicher und zuverlässig bei Ausnahmen und anderen Fehlerbedingungen verwendet werden sollten.

 

Token oder "Wer bist Du?"

Dieser Artikel beschäftigt sich mit der Verwaltung der Zugriffssteuerung, auch Autorisierung genannt. Bevor wir jedoch in dieses Thema einsteigen können, müssen wir erst einmal einen Weg finden, den Benutzer zu identifizieren, der auf die gesicherten Ressourcen zugreifen will. Hier kommen Token ins Spiel. Token stellen die Anmeldesitzungen auf einem Computer dar. Sobald ein Benutzer interaktiv oder remote auf einen Computer zugreift, wird eine Anmeldesitzung erstellt. Ein Token ist ein Handle, mit dem die Anmeldesitzung abgefragt oder bearbeitet werden kann. Sobald Sie ein Token haben, können Sie feststellen, für welchen Benutzer diese Anmeldesitzung geöffnet wurde, und entscheiden, ob Sie ihm Zugriff auf die gesicherten Ressourcen gestatten.

Da alle Anwendungen im Rahmen von Anmeldesitzungen ausgeführt werden, liegt immer eine Art von Token vor, das den Benutzer identifiziert. Verwirrend dabei ist lediglich, dass zu einem Zeitpunkt ein oder mehrere unterschiedliche Token oder Sicherheitskontexte vorhanden sein können. Jedes Token kann einen anderen Benutzer darstellen. Mit dem Prozess ist jedoch mindestens ein Token verbunden. Sie können dieses Token mithilfe der OpenProcessToken-Funktion abrufen.

CHandle token; 
Helpers::CheckError(::OpenProcessToken(::GetCurrentProcess(), 
                    TOKEN_QUERY, 
                    &token.m_h));

CHandle ist eine von ATL (Active Template Library) zur Verfügung gestellte Wrapperklasse, die durch Aufrufen der CloseHandle-Funktion sicherstellt, dass das Handle geschlossen wird, wenn das Token den Gültigkeitsbereich verlässt. CheckError ist eine Hilfsfunktion im Helpers-Namespace. CheckError gibt HRESULT mit einer Beschreibung des aufgetretenen Fehlers aus. Da ein Fehler in der Windows-C-Programmierung auf viele unterschiedliche Arten dargestellt werden kann, werde ich aus Gründen der Konsistenz standardmäßig HRESULT verwenden. Der Helpers-Namespace ist mit dem Download für diesen Artikel erhältlich. Die GetCurrentProcess-Funktion gibt ein Pseudohandle zurück, das den aktuellen Prozess darstellt. Da dies kein echtes Handle ist, müssen Sie das zurückgegebene HANDLE nicht mithilfe der CloseHandle-Funktion freigeben.

Ein anderes verfügbares Token ist das Threadtoken. Das Threadtoken kann mithilfe der OpenThreadToken-Funktion abgerufen werden.

CHandle token; 
Helpers::CheckError(::OpenThreadToken(::GetCurrentThread(), 
                    TOKEN_QUERY, 
                    TRUE, // open as self 
                    &token.m_h));

Wie auch GetCurrentProcess gibt GetCurrentThread ein Pseudohandle zurück. Das heißt, CloseHandle muss nicht aufgerufen werden. Im Gegensatz zu OpenProcessToken kann es bei OpenThreadToken zu einem ERROR_NO_TOKEN-Fehler kommen, wenn kein Token mit dem aktuellen Thread verknüpft ist.

Manchmal kann sogar ein drittes Token vorliegen, das einen weiteren Sicherheitskontext darstellt. ASP.NET erlaubt es Ihnen beispielsweise, den Identitätswechsel des Clients zu deaktivieren. Sie können dann die Identität des Clients explizit über die HttpContext-Klasse abrufen.

Das Einfachste, was Sie mit einem Token machen können, ist die Abfrage nach Informationen zur Anmeldesitzung. Dazu können Sie die OpenProcessToken-Funktion verwenden. Da sich GetTokenInformation für das Abfragen verschiedener Informationsklassen eignet und ziemlich schwierig einzusetzen ist, habe ich eine Wrapperfunktionsvorlage geschrieben, die es weniger fehleranfällig macht. Das nachfolgende Beispiel zeigt Ihnen, wie Sie das Token nach seinen Benutzerinformationen abfragen.

CLocalMemoryT<PTOKEN_USER> tokenUser(Helpers::GetTokenInformation<TOKEN_USER>(token, 
                                                   TokenUser)); 
CComBSTR string = Helpers::ConvertSidToStringSid(tokenUser->User.Sid);

Die ConvertSidToStringSid-Hilfsfunktion konvertiert die binäre Sicherheitskennung (SID, Security Identifier) in eine für Menschen lesbare Zeichenfolge. SIDs werden für die Darstellung von Benutzerkonten in einem für Computer lesbaren Format verwendet. Wenn es Sie interessiert, was genau meine Wrapperfunktionen machen, laden Sie den Quellcode für diesen Artikel herunter. Mit der CLocalMemoryT-Klassenvorlage werden wir uns später im Abschnitt zur Speicherverwaltung beschäftigen.

 

Grundlagen zu Sicherheitsbeschreibungen

Da wir jetzt wissen, wie wir den Benutzer identifizieren, müssen wir auf irgendeine Weise die verschiedenen Berechtigungen verwalten, über die die einzelnen Benutzer verfügen. Hier kommen Sicherheitsbeschreibungen ins Spiel. Eine Sicherheitsbeschreibung enthält verschiedene Informationen. Am interessantesten dabei sind die Sicherheitskennung (SID) des Besitzers und die beiden Zugriffssteuerungslisten (ACLs, Access Control Lists). Die SID des Besitzers identifiziert den Benutzer, dem das Objekt gehört. Bei den beiden ACLs handelt es sich um die freigegebene ACL (DACL, Discretionary Access Control List) und die System-ACL. Da die System-ACL nichts mit der Zugriffssteuerung zu tun hat, konzentrieren wir uns in diesem Artikel auf die DACL.

Sicherheitsbeschreibungen werden über die SECURITY_DESCRIPTOR-Struktur dargestellt. Diese Struktur ist nicht dokumentiert, Sie sollten sie daher nicht direkt bearbeiten. Microsoft bietet eine Vielzahl nützlicher Funktionen, mit denen sich Sicherheitsbeschreibungen für integrierte Objekte wie Dateien und Registrierungsschlüssel abfragen und bearbeiten lassen. Das nachfolgende Beispiel zeigt, wie Sie die Besitzer-SID und DACL für das Verzeichnis Program Files (Programme) von Ihrem Arbeitsplatz abrufen.

CLocalMemory securityDescriptor; 
PSID sid = 0; 
PACL dacl = 0; 
 
Helpers::CheckError(::GetNamedSecurityInfo(_T("javascript:void(null);"), 
                                           SE_FILE_OBJECT, 
                                           OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, 
                                           &sid, 
                                           0, // group 
                                           &dacl, 
                                           0, // sacl 
                                           &securityDescriptor.m_ptr));

GetNamedSecurityInfo ist eine sehr vielseitige Funktion. Mit ihr können Sie die meisten (wenn nicht sogar alle) integrierten sicheren Objekte abfragen. Der erste Parameter ist der Name (oder Pfad) des Objekts, das Sie abfragen möchten. Der zweite Parameter gibt den Objekttyp an. In diesem Beispiel frage ich ein Dateisystemobjekt ab. Sie könnten aber auch SE_REGISTRY_KEY verwenden, um beispielsweise ein Registrierungsobjekt abzufragen. Als Nächstes geben Sie mithilfe der SECURITY_INFORMATION-Enumeration die Informationen an, an denen Sie interessiert sind. Die nächsten vier Parameter dienen als Zeiger auf die vier Hauptteile der Sicherheitsbeschreibung. Glücklicherweise können Sie für die Teile, an denen Sie nicht interessiert sind, einfach 0 übergeben. Der letzte Parameter ist ein Zeiger auf die Sicherheitsbeschreibung selbst. Dabei handelt es sich eigentlich um eine Kopie der Sicherheitsbeschreibung, die Sie mithilfe der LocalFree-Funktion freigeben müssen.

Darüber hinaus gibt es eine Funktion mit Namen SetNamedSecurityInfo, über die Sie die Sicherheitsbeschreibung eines integrierten Objekts aktualisieren können. Da sie mehr oder weniger wie die anderen Funktionen arbeitet, gehe ich hier nicht genauer auf sie ein.

 

Speicherverwaltung

Bevor wir zum nächsten Schritt kommen, sollten wir uns erst einmal mit der Speicherverwaltung beschäftigen. Speicherverwaltung spielt beim Schreiben von sicherem und robustem Code eine sehr wichtige Rolle. Die beste Art, Speicherverwaltungscode zu schreiben, besteht darin, gar keinen zu schreiben. Zunächst müssen Sie die Speicherverwaltungstechniken verstehen, die von den verschiedenen Funktionen eingesetzt werden, mit denen Sie arbeiten. Dann müssen Sie den Speicherverwaltungscode in eine oder zwei Klassen wrappen, um zur rechten Zeit eine korrekte Bereinigung sicherzustellen. Andernfalls verursacht Ihr Code Ressourcenverluste oder (noch schlimmer) Fehler, durch die böswillige Benutzer Zugriff auf sichere Ressourcen erhalten.

Ich habe weiter oben die CLocalMemoryT-Klassenvorlage erwähnt, ohne ihren Zweck wirklich zu erklären. Die meisten Sicherheitsfunktionen, die mit Sicherheitsbeschreibungen in Zusammenhang stehen, nutzen lokalen Speicher. Lokaler Speicher hat seinen Ursprung im 16-Bit-Windows, wo die Speicherverwaltung noch wesentlich komplexer war. Die lokalen Speicherfunktionen wie LocalAlloc und LocalFree stehen heute meist nur noch aus Gründen der Abwärtskompatibilität mit 16-Bit-Anwendungen und älteren API-Funktion zur Verfügung, die diese Funktionen als Teil ihrer Semantik benötigen.

Um die Arbeit mit lokalem Speicher zu erleichtern, habe ich eine einfache Klasse geschrieben, die einen lokalen Speicherzeiger wrappt. Sie können sich CLocalMemoryT als eine intelligente Zeigerklasse vorstellen, da ich den Operator für die Memberauswahl (operator ->) überladen habe. Dies ist möglich, weil es sich hierbei um eine Klassenvorlage handelt und Sie den Typ bzw. die Struktur des Speichers, auf den gezeigt wird, durch den Vorlagenparameter angeben können. Sie können CLocalMemoryT dazu verwenden, einen neuen lokalen Speicherblock zu erstellen. Normalerweise hängen Sie diese Klassenvorlage jedoch an einen bereits vorhandenen Speicherblock an, der von einer Funktion zurückgegeben wird. Der Destruktor gibt den Speicher dann durch Aufrufen von LocalFree frei. Einige der von den Sicherheitsfunktionen verwendeten Datenstrukturen sind nicht transparent. Um ihre Verwendung zu vereinfachen, habe ich folgende Typdefinition am Ende der CLocalMemory-Headerdatei definiert.

typedef CLocalMemoryT<PVOID> CLocalMemory;

Dies ist beispielsweise nützlich, wenn Sie den Speicher für ein SECURITY_DESCRIPTOR-Objekt verwalten möchten.

 

Private Sicherheitsbeschreibungen

GetNamedSecurityInfo und SetNamedSecurityInfo eignen sich sehr gut für integrierte Objekte. Aber was ist mit privaten Objekten, die sich zum Beispiel in der Geschäftslogik meiner eigenen Anwendungen befinden? Kann man diese Funktionen dahingehend erweitern, dass sie private Objekte unterstützen? Das geht leider nicht. Da jede Ressource (z.B. Dateisystem, Registrierung) ihre eigene Vorgehensweise für das Speichern von Sicherheitsbeschreibungen festlegt, können diese Funktionen unmöglich wissen, wie sie die Sicherheitsinformationen für von Ihnen erstellte private Objekte abfragen oder festlegen sollen. Glücklicherweise gibt es dafür jedoch eine Lösung.

Zuerst einmal müssen wir einen Weg finden, wie wir private Sicherheitsbeschreibungen erzeugen. Sie können eine völlig neue Sicherheitsbeschreibung mithilfe der CreatePrivateObjectSecurityEx-Funktion erstellen. Diese Funktion ist recht vielseitig und wird hauptsächlich dazu verwendet, neue Sicherheitsbeschreibungen zu erstellen und die Vererbung von bereits vorhandenen Sicherheitsbeschreibungen zu aktualisieren. Der Prototyp der Funktion lautet wie folgt.

BOOL CreatePrivateObjectSecurityEx(PSECURITY_DESCRIPTOR parentDescriptor, 
                                   PSECURITY_DESCRIPTOR defaultDescriptor, 
                                   PSECURITY_DESCRIPTOR* newDescriptor, 
                                   GUID* type, 
                                   BOOL isContainer, 
                                   ULONG autoInheritFlags, 
                                   HANDLE token, 
                                   PGENERIC_MAPPING genericMapping);

parentDescriptor dient dazu, das übergeordnete Objekt festzulegen, dessen ACL geerbt werden soll. Wenn es für das Objekt kein übergeordnetes Objekt gibt, übergeben Sie einfach 0. defaultDescriptor wird hauptsächlich dafür eingesetzt, eine bereits vorhandene Sicherheitsbeschreibung mit vererbbaren Zugriffssteuerungseinträgen (ACEs, Access Control Entries) zu aktualisieren, nachdem parentDescriptor geändert wurde. Dafür sollten Sie jedoch eine neue Sicherheitsbeschreibung erstellen, die vom newDescriptor-Parameter zurückgegeben wird, und dann das Original freigeben. Um explizite Einträge für die ACL des Objekts zu übertragen, übergeben Sie die vorhandene Sicherheitsbeschreibung als den defaultDescriptor-Parameter.

Bei Active Directory-Objektsicherheit wird type verwendet. Mit isContainer wird angegeben, ob das von der Sicherheitsbeschreibung dargestellte Objekt ein Container für andere sichere Objekte ist. autoInheritFlags wird eingesetzt, um zu beeinflussen, wie unterschiedliche vererbbare ACEs in die neu erstellte Sicherheitsbeschreibung fließen. Um alle vererbbaren ACEs zu erben, können Sie einfach SEF_DACL_AUTO_INHERIT übergeben. Wenn Sie jedoch die vererbbaren ACEs von einer übergeordneten Sicherheitsbeschreibung erneut einfließen lassen, sollten Sie auch die Flags SEF_AVOID_PRIVILEGE_CHECK und SEF_AVOID_OWNER_CHECK mit einschließen, um Zugriffsüberprüfungen für den Benutzer zu vermeiden, die bei einer Aktualisierung der Vererbung unnötig sind. Weitere Informationen zum Verwalten von ACL-Vererbung finden Sie in dem Buch Programming Windows Security (in Englisch) von Keith Brown.

Um den Benutzer zu identifizieren, für den das Objekt erstellt wird, verwenden Sie token. Damit werden Standardwerte für die neue Sicherheitsbeschreibung wie die Besitzer-SID abgerufen. Die Tatsache, dass das Benutzertoken explizit übergeben wird, ist für Serveranwendungen sehr bequem, da somit kein Identitätswechsel erforderlich ist.

Zum Schluss werden mithilfe von genericMapping Informationen dazu bereitgestellt, wie explizite, objektspezifische Berechtigungen den vier generischen Berechtigungen (Read, Write, Execute und All) zugeordnet werden. Genaue Erklärungen zu Berechtigungen finden Sie im nächsten Abschnitt.

Wenn Sie die Sicherheitsbeschreibung fertig gestellt haben, müssen Sie sie mit einem Aufruf an DestroyPrivateObjectSecurity freigeben.

Da wir jetzt in der Lage sind, unsere eigenen Sicherheitsbeschreibungen zu erstellen, sollten wir sie auch abfragen und bearbeiten können. Auch wenn sich GetNamedSecurityInfo und SetNamedSecurityInfo nicht für private Objekte eignen, stehen doch andere Funktionen für diesen Zweck zur Verfügung. Verwenden Sie zur Bearbeitung einer privaten Sicherheitsbeschreibung die SetPrivateObjectSecurityEx-Funktion. Der Prototyp der Funktion lautet wie folgt.

BOOL SetPrivateObjectSecurityEx(SECURITY_INFORMATION securityInformation, 
                                PSECURITY_DESCRIPTOR modificationDescriptor, 
                                PSECURITY_DESCRIPTOR* securityDescriptor, 
                                ULONG autoInheritFlags, 
                                PGENERIC_MAPPING genericMapping, 
                                HANDLE token);

In der Dokumentation zu dieser Funktion wird dem securityDescriptor-Parameter fälschlicherweise [out] zugeordnet. Dies sollte stattdessen [in, out] sein, da der Parameter bei der Eingabe auf eine gültige Sicherheitsbeschreibung zeigen muss. SetPrivateObjectSecurityEx ruft bei Bedarf LocalReAlloc auf, um ausreichend Speicher für die neu festzulegenden Informationen zuzuweisen. Aus diesem Grund wird ein Zeiger auf die Sicherheitsbeschreibung benötigt. Nach Aufrufen von SetPrivateObjectSecurityEx wird sich der Speicherort, auf den securityDescriptor zeigt, höchstwahrscheinlich geändert haben.

Wie Sie sehen können, stellt SetPrivateObjectSecurityEx keine separaten Parameter für das Festlegen einzelner Teile der Sicherheitsbeschreibung zur Verfügung, wie dies bei SetNamedSecurityInfo der Fall ist. Bei SetPrivateObjectSecurityEx müssen Sie eine bereits vorhandene Sicherheitsbeschreibung bereit stellen, aus der Werte kopiert werden können. Glücklicherweise ist es ziemlich einfach, ein Sicherheitsbeschreibungsobjekt auf dem Stapel zu erstellen und mit Informationen auszustatten, die Sie in Ihre private Sicherheitsbeschreibung kopieren möchten. Hier ein Beispiel:

CWellKnownSid adminSid = CWellKnownSid::Administrators(); 
 
SECURITY_DESCRIPTOR templateDescriptor = { 0 }; 
 
Helpers::CheckError(::InitializeSecurityDescriptor(&templateDescriptor, 
                                                   SECURITY_DESCRIPTOR_REVISION)); 
 
Helpers::CheckError(::SetSecurityDescriptorOwner(&templateDescriptor, 
                                                 &adminSid, 
                                                 false)); 
 
Helpers::CheckError(::SetPrivateObjectSecurityEx(OWNER_SECURITY_INFORMATION, 
                                                 &templateDescriptor, 
                                                 &securityDescriptor, 
                                                 SEF_AVOID_PRIVILEGE_CHECK, 
                                                 &genericMapping, 
                                                 0));

templateDescriptor ist eine stapelbasierte Sicherheitsbeschreibung. Beachten Sie, dass ich den Speicher lösche, bevor ich fortfahre. InitializeSecurityDescriptor legt die Revisionsebene fest, nimmt aber sonst keinen Eintrag in der Sicherheitsbeschreibung vor. SetSecurityDescriptorOwner legt als Besitzer-SID die bekannte lokale Administrators-Gruppe fest. Meine CWellKnownSid-Klasse, die im Download für diesen Artikel enthalten ist, hilft dabei. Zum Schluss wird SetPrivateObjectSecurityEx aufgerufen, um die Besitzerinformationen von unserer Vorlagenbeschreibung in die private Sicherheitsbeschreibung zu kopieren, die in securityDescriptor enthalten ist.

Vielleicht fragen Sie sich nun, warum Sie diese Funktionen nicht einfach dazu verwenden können, die Teile der privaten Sicherheitsbeschreibung direkt festzulegen. Für Sicherheitsbeschreibungen gibt es zwei verschiedene Speicherformate. Eine absolute Sicherheitsbeschreibung verfügt über Zeiger auf die in ihr enthaltenen Sicherheitsinformationen. Dieser Speicher wird getrennt vom Speicher für die Struktur der Sicherheitsbeschreibung zugewiesen. Eine selbstbezogene Sicherheitsbeschreibung hingegen speichert alle Informationen in einem zusammenhängenden Speicherblock. Anstelle von Zeigern speichert die Sicherheitsbeschreibung Offsets in den Speicher.

Wenn Sie verstehen, auf welch unterschiedliche Weise die Sicherheitsbeschreibungen im Speicher dargestellt werden können, sollten Ihnen die Dinge klar werden. Private Sicherheitsbeschreibungen sind stets selbstbezogen. Aus diesem Grund sind die Funktionen für die Arbeit mit diesen Sicherheitsbeschreibungen komplexer. Das Erstellen der Sicherheitsbeschreibung auf dem Stapel war einfach, weil es sich dabei um eine absolute Sicherheitsbeschreibung handelte und Funktionen wie SetSecurityDescriptorOwner lediglich einen Zeigerwert in der stapelbasierten Sicherheitsbeschreibung festlegen müssen.

Das Abfragen einer privaten Sicherheitsbeschreibung gestaltet sich glücklicherweise einfacher. Mithilfe der Standardfunktionen, wie GetSecurityDescriptorOwner und GetSecurityDescriptorDacl, erhalten Sie Zugang zu den verschiedenen Teilen. Es gibt auch eine Funktion mit Namen GetPrivateObjectSecurity, die sich jedoch nicht sehr gut für das Abfragen einer Sicherheitsbeschreibung eignet. Wir können sie nutzen, wenn wir mit dem Editor für die Zugriffssteuerung arbeiten, dem wir uns später noch widmen werden.

 

Berechtigungen

Ich habe bis jetzt noch nicht über Berechtigungen gesprochen, auch Zugriffsrechte genannt. Dies ist ein ebenso spannendes Thema wie Speicherverwaltung. Dennoch ist es wichtig zu verstehen, wie Sie Berechtigungen für Ihre privaten Objekte gestalten müssen. Berechtigungen werden jedes Mal verwendet, wenn Sie einen Aufruf an eine Funktion durchführen, die Ihnen Zugriff auf eine sichere Ressource verschaffen kann. Die bekannte CreateFile-Funktion enthält beispielsweise einen Parameter, der eine Zugriffsbitmaske verwendet, bei der jedes Bit eine bestimmte Berechtigung darstellen kann.

So wie auch das Dateisystem spezifische Berechtigungen festlegt, wie beispielsweise FILE_APPEND_DATA und FILE_TRAVERSE, müssen Sie dies ebenfalls für Ihre eigenen Objekte durchführen. Von der 32-Bit-Zugriffsmaske stehen 16 Bit für spezifische Berechtigungen zur Verfügung. Nachdem Sie spezifische Berechtigungen für Ihre Objekte definiert haben, müssen Sie sie vier generischen Berechtigungskategorien zuweisen. Die generischen Berechtigungen sind GENERIC_READ, GENERIC_WRITE, GENERIC_EXECUTE und GENERIC_ALL. Auf diese Weise legt ein Programmierer ganz einfach fest, dass Lesezugriff erforderlich ist, und die Berechtigungen, die dem Lesezugriff logisch zugeordnet sind, angewendet werden. Da allerdings keine der Sicherheitsfunktionen weiß, wie die spezifischen Berechtigungen den generischen Berechtigungen zugeordnet sind, müssen Sie dies in einer GENERIC_MAPPING-Struktur eintragen, die dann an viele der Sicherheitsfunktionen übergeben wird.

Mit diesem Grundverständnis hinsichtlich Berechtigungen legen wir nun folgende Berechtigungen für eine bestimmte Widget-Ressource fest.

namespace Permissions 
{ 
 const DWORD AddWidget       = 0x0001; 
 const DWORD ListWidgets     = 0x0002; 
 const DWORD RenameWidget    = 0x0004; 
 const DWORD ReadWidgetData  = 0x0008; 
 const DWORD WriteWidgetData = 0x0010; 
 
 const DWORD GenericRead     = STANDARD_RIGHTS_READ | 
                               ListWidgets | 
                               ReadWidgetData; 
 
 const DWORD GenericWrite    = STANDARD_RIGHTS_WRITE | 
                               AddWidget | 
                               RenameWidget |  
                               WriteWidgetData; 
 
 const DWORD GenericExecute  = STANDARD_RIGHTS_EXECUTE; 
 
 const DWORD GenericAll      = STANDARD_RIGHTS_REQUIRED | 
                               GenericRead | 
                               GenericWrite | 
                               GenericExecute; 
}

Sie sollten jetzt eine globale GENERIC_MAPPING-Struktur füllen und einen Zeiger darauf an alle Funktionen übergeben können, die diesen benötigen. Ein Programmierer kann nun einfach eine generische Berechtigung verwenden, wie beispielsweise GENERIC_WRITE. Diese wird dann für unseren Widget Permissions::GenericRead zugeordnet. Ein vorsichtiger Programmierer setzt nicht voraus, dass der Benutzer alle Berechtigungen besitzt. In diesem Fall kann er eine der spezifischen Berechtigungen verwenden, wie zum Beispiel Permissions::RenameWidget. Natürlich ist es weiterhin möglich, eine der Standardberechtigungen (z.B. DELETE) einzusetzen. Dabei müssen wir dieser jedoch eine bestimmte Bedeutung für unseren Widget zuweisen.

 

Zugriffssteuerungslisten (ACLs)

Die DACL bildet das Kernstück der Sicherheitsbeschreibung. Jeder Zugriffssteuerungseintrag (ACE, Access Control Entry) in der Liste definiert die Berechtigungen, die ein bestimmter Benutzer oder eine bestimmte Gruppe für die Ressource besitzt. Ein ACE kann entweder positiv oder negativ sein. Ein positiver ACE zeigt an, dass dem Benutzer bzw. der Gruppe die bestimmten Berechtigungen gewährt werden. Ein negativer ACE hingegen bedeutet, dass die Berechtigungen verweigert werden sollen. Wenn ein Benutzer nicht in der ACL enthalten ist, entweder direkt oder als Mitglied einer Gruppe, wird ihm jeglicher Zugriff verweigert.

Eine ACL wird als zusammenhängender Speicherblock gespeichert, der aus einer ACL-Struktur gefolgt von einer geordneten Liste von ACEs besteht. Das nachfolgende Beispiel zeigt Ihnen, wie eine einfache ACL erstellt wird.

CLocalMemoryT<PACL> acl(100); 
 
Helpers::CheckError(::InitializeAcl(acl.m_ptr, 
                                    100, 
                                    ACL_REVISION)); 
 
DWORD inheritFlags = CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE; 
 
CWellKnownSid everyoneSid = CWellKnownSid::Everyone(); 
 
Helpers::CheckError(::AddAccessAllowedAceEx(acl.m_ptr, 
                                            ACL_REVISION, 
                                            inheritFlags, 
                                            GENERIC_READ, 
                                            &everyoneSid)); 
 
CLocalMemoryT<PSID> userSid(Helpers::GetUserSid(token)); 
 
Helpers::CheckError(::AddAccessAllowedAceEx(acl.m_ptr, 
                                            ACL_REVISION, 
                                            inheritFlags, 
                                            GENERIC_ALL, 
                                            userSid.m_ptr));

Da ACLs zusammenhängend gespeichert werden, muss ein Speicherblock zugewiesen werden, der groß genug für den ACL-Header und alle seine Einträge ist. Wie viel Speicher Sie zuweisen, ist nicht so wichtig, solange er für alle Einträge ausreicht. Der Erstellung einer ACL folgt normalerweise ein Aufruf eines Ressourcen-Managers, der eine Kopie davon anfertigt. Ich verwende die AddAccessAllowedAceEx-Funktion, um allen Benutzern Leseberechtigungen und dem durch das Token repräsentierten Benutzer alle Berechtigungen zuzuweisen.

Das direkte Erstellen und Bearbeiten von realistischeren und komplexeren ACLs kann sehr schwierig und fehleranfällig sein. Hauptgrund hierfür ist, dass die Reihenfolge der ACEs in der Liste von Bedeutung ist, da die Liste bei der Zugriffsüberprüfung von oben nach unten durchlaufen wird. Wenn Sie am Ende der Liste einen negativen ACE hinzufügen und dem Benutzer durch einen ACE am Anfang der Liste der Zugriff gewährt wird, fällt die Zugriffsüberprüfung positiv aus, obwohl dem Benutzer der Zugriff verweigert wurde. Die Zugriffsüberprüfung führt diese Umgehungstechnik aus Effizienzgründen durch. Solange Sie die ACEs korrekt ordnen, ist alles OK. Um die Dinge noch weiter zu komplizieren, hat Windows 2000 ein neues Modell für ACL-Vererbung eingeführt, das zwar wesentlich leistungsstärker ist, bei der Implementierung jedoch besondere Vorsicht erfordert. Der beste Rat, den ich Ihnen geben kann, ist, eine direkte Bearbeitung der ACLs vollständig zu vermeiden. Im nächsten Abschnitt erfahren Sie, wie dies möglich ist.

 

Editor für die Zugriffssteuerung

Der von der Windows-Shell verwendete Editor für die Zugriffssteuerung kann von Programmierern für ihre eigenen Anwendungen eingesetzt werden (siehe Abbildung 1). Der Editor für die Zugriffssteuerung ist ein leistungsstarker und flexibler Editor zur Bearbeitung aller Teile einer Sicherheitsbeschreibung. Der Editor wird über zwei trügerisch einfache Funktionen offen gelegt. CreateSecurityPage erstellt die vertraute Sicherheitseigenschaftenseite, die Sie Ihrem eigenen Eigenschaftsblatt hinzufügen können. EditSecurity ist eine Hilfsfunktion, die die Sicherheitsseite in ein Eigenschaftsblatt integriert und für Sie anzeigt. Das scheint alles ganz einfach. Allerdings gibt es einen Haken an der Sache. Für beide Funktionen müssen Sie eine recht unorthodoxe COM-Schnittstelle mit Namen ISecurityInformation implementieren. Sie ist unorthodox, weil sie nicht den standardmäßigen COM-Speicherverwaltungsregeln folgt. Stattdessen verwendet sie weiterhin die Speicherverwaltungsfunktionen und -techniken, die von vielen Sicherheitsfunktionen eingesetzt werden, und nutzt dabei globalen Speicher und LocalAlloc. Das macht es sehr schwierig, diese Schnittstelle in C# oder einer anderen verwalteten Sprache zu implementieren. Betrachten Sie ISecurityInformation einfach als glorifizierten C-Rückrufmechanismus.

Windows-Sicherheitsfunktionen_01.gif

Abbildung 1. Sicherheitseigenschaftenseite im Editor für die ZugriffssteuerungDie GetObjectInformation-Methode erlaubt Ihnen anzugeben, wie Sie den Editor für die Zugriffssteuerung anpassen möchten. Sie können mit dieser Methode beispielsweise die Schaltfläche Advanced (Erweitert) anzeigen oder ausblenden. Sie erhalten dadurch einen im Vergleich zur Standardeigenschaftenseite erweiterten Sicherheitsdeskriptor-Editor (siehe Abbildung 2). Darüber hinaus können Sie noch weitere Optionen festlegen, beispielsweise ob der Benutzer die Besitzer-SID und System-ACL der Sicherheitsbeschreibung anzeigen und ändern darf.

Windows-Sicherheitsfunktionen_02.gif

Abbildung 2. Dialogfeld für erweiterte SicherheitseinstellungenDie GetSecurity-Methode wird zu unterschiedlichen Zeiten aufgerufen, wenn der Editor verschiedene Steuerelemente mit Informationen zur Sicherheitsbeschreibung füllen muss. Die von mir bereits weiter oben in diesem Artikel erwähnte GetPrivateObjectSecurity-Funktion ist dabei sehr nützlich. Die GetSecurity-Methode muss eine Kopie eines Teils der gerade in Bearbeitung befindlichen Sicherheitsbeschreibung zurückgeben. Genau dies wird von GetPrivateObjectSecurity übernommen.

Die SetSecurity-Methode wird aufgerufen, nachdem der Benutzer Änderungen im Editor vorgenommen hat, die gespeichert werden müssen. Die Implementierung von SetSecurity kann in kürzester Zeit mithilfe der SetPrivateObjectSecurityEx-Funktion erfolgen, wie ich bereits erklärt habe.

Durch Aufrufen der GetAccessRights-Methode wird eine Liste spezifischer und allgemeiner Berechtigungen für den gerade bearbeiteten Objekttyp abgerufen. Für die Implementierung muss ein Array von SI_ACCESS-Strukturen erstellt werden. Jede SI_ACCESS-Struktur identifiziert eine spezifische oder allgemeine Berechtigung sowie deren Bitmaskenbits, Anzeigename und andere Flags. Da mit diesen Informationen alle Instanzen einer Sicherheitsbeschreibung beschrieben werden, wird das Array normalerweise als statisches Array deklariert.

Von den verbleibenden Methoden interessiert uns lediglich noch GetInheritTypes. Durch Aufrufen dieser Methode kann der Editor bestimmen, wie ACEs für die Vererbung zugewiesen werden sollen. Sie sollten ein statisches Array mit SI_INHERIT_TYPE-Strukturen erstellen, um die verschiedenen Kombinationen an Vererbungsflags zu beschreiben, die zur Verfügung stehen müssen. Wenn ein Objekt die Vererbung unterstützt, bietet es normalerweise die nachfolgenden Optionen.

Betrifft

Flags

Objekt und untergeordnete Objekte

CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE

Nur untergeordnete Objekte

CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE | INHERIT_ONLY_ACE

Nur Objekt

0

Die Implementierung von ISecurityInformation kann anfangs sehr schwierig erscheinen. Aber mit ein bisschen Übung und einigen guten Beispielen als Anleitung ist das bald kein Problem mehr für Sie. Der Download zu diesem Artikel enthält mehrere nützliche Hilfsfunktionen sowie meine CSecurityDescriptor-Klasse, mit der Sie ACL-Vererbung in Windeseile erstellen, bearbeiten und verwalten können. Eine Beispielimplementierung von ISecurityInformation ist ebenfalls im Download enthalten, und das C++-Projekt bietet ein einfaches Beispiel für die Verwendung dieser Klassen.

 

Schlussfolgerung

Eine Erweiterung des Windows-Sicherheitsmodells auf private Objekte erfordert ein gutes Verständnis der Sicherheitsbeschreibungen und der mit der Verwaltung von privaten Sicherheitsbeschreibung verbundenen Funktionen. Mithilfe des Windows-Editors für die Zugriffssteuerung können Sie vielfältige und sicherheitsbewusste Anwendungen erstellen. Sie sollten jetzt in der Lage sein, selbst mit Sicherheitsbeschreibungen zu experimentieren und die in diesem Artikel beschriebenen Techniken für objektzentrierte Zugriffssteuerung in Ihren eigenen Anwendungen einsetzen können.