.NET SECURITY

 

 

Seminar aus Softwareentwicklung (Inside Java und .NET), WS2003/2004
Johannes Kepler Universität Linz, System Software Group

Brigitte Rafael, 0021889

 

 

Zusammenfassung

 

Sicherheit ist ein wichtiges Thema im Rahmen von Softwareentwicklung. Diese Seminararbeit soll einen Einblick in das Sicherheitssystem des .NET Frameworks geben. Dabei werden die internen Vorgänge erklärt, die dem User im Allgemeinen verborgen ablaufen, weiters wird aber auch gezeigt, wie man aktiv Sicherheitseinstellungen modifizieren und das Sicherheitssystem individuell an seine Bedürfnisse anpassen kann.

 

 

Inhalt

 

1        Motivation

2        Prinzipien im .NET Framework
2.1  Role-based Security
2.2  Code-based Security

3        CAS
3.1  Was ist CAS?
3.2  Ziele der CAS

4        Vergabe von Rechten
4.1  Permissions
4.2  Permission Sets
4.3  Zuweisung von Permissions
       4.3.1  Evidence
       4.3.2  Security Policy
       4.3.3  Policy Levels
       4.3.4  Codegruppen
       4.3.5  Teilnahme eines Assemblies am Evaluationsprozess

5        Policy Enforcement
5.1  Stack Walk
5.2  Modifikation des Stack Walks
5.3  Imperative vs deklarative Sicherheit
        5.3.1  Imperative Sicherheit
        5.3.2  Deklarative Sicherheit

6        Benutzerdefinierte Konfigurationen
6.1  Modifikation der Defaulteinstellungen
6.2  Entwurf von Sicherheitskomponenten

7        Ausblick

8        Bibliographie

 

 

1  Motivation

 

Sicherheit ist ein sehr allgemeiner Begriff. In Bezug auf Computer wird Sicherheit durch Technologien verwirklicht, die entworfen wurden, um sensible Ressourcen zu schützen. Es gibt dabei eine Vielzahl von Ressourcen, einige davon sind das Dateisystem, der Systemspeicher, Kommunikationsdaten, Bandbreite (CPU, Netzwerk, ...), GUI-Daten, ...

Auch bei den Sicherheitstechnologien zum Schutz dieser Ressourcen gibt es verschiedene Formen: Authentifizierung und Benutzerverwaltung, Autorisierung und Zugangskontrolle, Verschlüsselung und Schlüsselverwaltung, Verwaltung von Adressräumen und Prozessgrenzen, Kommunikationsprotokolle, ...

 

Gerade im Zeitalter des Internets wird der Schutz von Ressourcen immer wichtiger. Ein Schlagwort ist hier das "Mobile Code Scenario". Unter "Mobile Code" versteht man Code, der von Remote-Quellen (lokales Netzwerk oder Internet) heruntergeladen und dann lokal ausgeführt wird. Dabei sind die Entwickler meistens unbekannt und der Benutzer weiß nicht, ob er dem Code vertrauen kann oder ob dieser Schaden im lokalen System anrichten könnte. Der Vorteil des mobilen Codes ist, dass man die Performance und Features des eigenen Rechners nutzen kann, die bei einer serverseitigen Implementierung oft nicht gegeben sind.

Im Rahmen dieses Szenarios entstehen viele Probleme wie zum Beispiel potenzielle Schäden durch Würmer, Viren oder Trojaner. Ein Ziel des .NET-Frameworks ist es daher, solche Probleme zu beseitigen und so das Mobile Code Scenario zu unterstützen. [Dev01]

 

Was würde passieren, gäbe es kein Sicherheitssystem in .NET? Jeder hat wohl schon einmal ein Programm aus dem Internet heruntergeladen. In den meisten Fällen ist nicht bekannt, wer den Code  dieses Programmes geschrieben hat, und daher weiß man auch nicht, ob es nicht irgendeinen Schaden anrichten könnte. Ohne die oben genannten Sicherheitstechnologien würde das Programm womöglich auf das lokale Dateisystem zugreifen und wichtige Daten löschen oder sonstige schwerwiegende Änderungen durchführen.

Code, den man nicht selbst geschrieben hat, sollte also grundsätzlich vor der Ausführung auf Sicherheitsrisiken geprüft werden. Jeder, der schon einmal mit einem Virus auf seinem Rechner gekämpft hat, weiß, wie wichtig ein umfassendes Sicherheitssystem ist!

 

 

2  Prinzipien im .NET Framework

 

Eine Hauptaufgabe der Sicherheitsverwaltung im .NET Framework ist die Zugangskontrolle zu Ressourcen. Wie wird aber bestimmt, was  Code darf und was nicht? Auf welche Ressourcen darf er zugreifen, auf welche nicht?

Um diese Fragen zu beantworten, gibt es zwei grundlegende Prinzipien: role-based und code-based security.

 

 

 2.1  Role-based Security

 

Das Prinzip der role-based security könnte man zusammenfassen mit:

"Sag mir, wer du bist, und ich sag dir, was du darfst."

 

Die Rechtevergabe bei diesem Prinzip wird so gehandhabt, wie  auch aus den meisten Betriebssystemen bekannt ist: Code erhält Rechte ausgehend vom eingeloggten User. Derselbe Code kann also auf demselben Rechner verschiedene Rechte erhalten, wenn er von verschiedenen Usern ausgeführt wird.

 

Ein User wird in .NET durch die Klasse Principal identifiziert. Einem Principal-Objekt ist immer genau eine Identity zugeordnet, die den Namen des Users enthält, sowie keine, eine oder mehrere Rollen. Wenn die Hostplattform des .NET Frameworks Windows ist, kann über WindowsPrincipal und WindowsIdentity auf den gerade am System angemeldeten User zugegriffen werden. Es gibt aber auch die Möglichkeit, logische User mittels GenericPrincipal und GenericIdentity zu definieren. [WaLa02]

 

Die folgende Grafik und das Codebeispiel aus [WaLa02] sollen die Verwendung der oben angeführten Klassen verdeutlichen:

 

 

 

 

Codebeispiel:

 

 

using System;

using System.Threading;

using System.Security;

using System.Security.Principal;

 

namespace RoleBasedSecurity

{

   class Sample

   {

      static void Main(string[] args)

      {

         String [] roles = {"Lecturer", "Examiner"};

         GenericIdentity i = new GenericIdentity("Damien");

         GenericPrincipal g = new GenericPrincipal(i, roles);

         Thread.CurrentPrincipal = g;

         if(Thread.CurrentPrincipal.Identity.Name == "Damien")

            Console.WriteLine("Hello Damien");

         if(Thread.CurrentPrincipal.IsInRole("Examiner"))

            Console.WriteLine("Hello Examiner");

         if(Thread.CurrentPrincipal.IsInRole("Employee"))

            Console.WriteLine("Hello Employee");

      }

   }

}

 

 

 

Output:

 

Hello Damien

Hello Examiner

 

 

Rollenbasierte Sicherheit ist zwar einfach zu verwirklichen, wirft aber oft Probleme auf, da sie sehr unflexibel ist. Folgendes Szenario: Ein User, der nur sehr wenige Rechte hat, möchte ein Programm ausführen, das vertrauenswürdig ist und mehr Rechte benötigt, als dem User zustehen. Dann wird das Programm nicht laufen, auch wenn man ihm voll vertraut. Es ist durchaus oft sinnvoller, einem Programm Rechte unabhängig vom ausführenden User zu vergeben, so dass es bei jedem User läuft.

Ein anderer, noch viel gefährlicherer Fall, könnte auch eintreten: Ein User loggt sich als Systemadministrator ein und hat dadurch volle Rechte. Dann lädt er ein Programm aus dem Internet herunter und führt es aus. Jeder kann sich wohl vorstellen, was alles passieren kann, wenn das Programm nun alle Administratorenrechte hat!

Um diese beiden Fälle zu vermeiden, gibt es das zweite Prinzip:

 

 

2.2  Code-based Security

 

Analog zu oben könnte man das Prinzip der code-based security auch zusammenfassen mit:

"Sag mir, woher du kommst, und ich sag dir, was du darfst."

 

Bei der codebasierten Sicherheit werden Rechte nicht an User vergeben, sondern direkt an Code selbst. Dabei wird jedes Assembly einzeln behandelt, wodurch eine sehr feine Granularität entsteht.  Code kann also unabhängig von den Rechten des Users laufen und es wird garantiert, dass ein auf einem Rechner von verschiedenen Usern ausgeführtes Programm beide Male dieselben Rechte erhält.

 

Da .NET unter anderem für verteilte Anwendungen entwickelt wurde, hat dieses Prinzip aufgrund seiner feinen Granularität und der Flexibilität bei der Vergabe von Rechten mehr Bedeutung für das .NET Framework als die rollenbasierte Sicherheit. Aus diesem Grund und auch, weil der Ansatz noch nicht so weit verbreitet ist wie der von Betriebssystemen bekannte rollenbasierte Ansatz, wird in den folgenden Kapiteln näher auf die codebasierte Sicherheit eingegangen. Letztere wird im .NET Framework  im Subsystem CAS verwirklicht.

 

 

3  CAS

 

3.1  Was ist CAS?

 

Beim Entwurf des Sicherheitssystems für das .NET Framework wollte man "das Rad nicht neu erfinden". Es gibt viele Sicherheitstechnologien, die bereits von verschiedenen Betriebssystemen implementiert sind (zum Beispiel Verschlüsselung, Authentifizierung, ...). Solche Funktionen werden im .NET Framework nicht neu implementiert; es gibt aber Wrapperklassen, um die Funktionen des darunter liegenden Betriebssystems nutzen zu können. Das bedeutet natürlich, dass in Fällen, in denen diese Funktionen gebraucht werden, die Sicherheit des .NET Frameworks vom Betriebssystem abhängig ist, auf das es aufsetzt.

 

Zusätzlich zu den Funktionen, die über das jeweilige Betriebssystem angesprochen werden, gibt es in .NET auch ein eigenes, von der Hostplattform unabhängiges Sicherheitssystem: die CAS. Die Abkürzung steht für Code Access Security und bezeichnet ein Subsystem des .NET Frameworks, das die Zugangskontrolle auf geschützte Ressourcen verwaltet. [Dev01]

 

 

3.2  Ziele der CAS

 

Ziele der Code Access Security sind unter anderem:

 

 

Diese Ziele (besonders das letzte) erinnern wieder an das oben beschriebene "Mobile Code Scenario" und zeigen dadurch, das CAS (und damit auch das .NET Framework) dieses Szenario unterstützt. [Dev01]

 

Aufgrund der feinen Granularität wird CAS auch als komponentenzentriertes Sicherheitsmodell bezeichnet. Es ist daher ideal für komponentenbasierte Softwareentwicklung, da einzelnen Komponenten einer Anwendung unterschiedliche Grade von Vertrauen entgegengebracht werden können. [BoS03]

 

 

4  Vergabe von Rechten

 

Bevor erläutert werden kann, wie Rechte an Assemblies vergeben werden, sollte erst folgende Frage geklärt werden: Was sind Rechte eigentlich bzw. wie werden sie im .NET Framework dargestellt?

 

 

 4.1  Permissions

 

Permissions sind Objekte, die das Recht repräsentieren, auf eine geschützte Ressource zuzugreifen. Sie beschreiben, welche Operationen auf eine bestimmte Ressource durchgeführt werden dürfen.  Eine FileIOPermission repräsentiert zum Beispiel das Recht, Dateien im lokalen Dateisystem zu erzeugen, zu lesen, zu schreiben oder zu ändern. Meistens ist es sinnvoll, Permissions durch zusätzliche Informationen genauer zu definieren. So kann eine FileIOPermission dahingehend konfiguriert werden, dass nur auf eine bestimmte Datei oder auf alle Dateien eines bestimmten Verzeichnisses zugegriffen werden darf. [MS03-2]

 

Die folgende Tabelle aus [Bo03] gibt einen Überblick über einige vordefinierte Permissions, die schon in der .NET Bibliothek vorhanden sind. Die Basisklasse all dieser Permissions ist die abstrakte Klasse System.Security.CodeAccessPermission.

 

 

Namespace

Name

Description

System.Security.
Permissions

Security

Basic execution environment capabilities

Reflection

Read and write CLR metadata

Environment

Access to OS environment variables

UrlIdentity

Asserts codebase URL in assembly evidence

SiteIdentity

Asserts Site in assembly evidence

ZoneIdentity

Asserts SecurityZone in assembly evidence

StrongNameIdentity

Asserts public key in assembly name

PublisherIdentity

Asserts certificate in assembly evidence

Registry

Access to Windows registry

FileIO

Access to directories and files

IsolatedStorage

Access private storage system

FileDialog

Display file dialogs to the user

UI

Access to window hierarchy and clipboard

System.Drawing

Printing

Access to attached and default printer

System.Net

Dns

DNS address translation

Socket

Low-level socket usage (accept/connect)

Web

High-level Web access (accept/connect)

System.
Messaging

MessageQueue

Access to MSMQ features

System.Data.
Common

DBData

Access to database provider features

 

 

Es ist außerdem möglich, eigene Permissions zu definieren. Diese müssen die Interface System.Security.IPermission und damit folgende Methoden implementieren: Copy, Demand, Union, Intersect und IsSubsetOf. [BBMW02]

 

Da ein Assembly in den meisten Fällen mehr als nur eine Permission erhalten wird, können Permissions in einem Permission Set zusammengefasst werden.

 

 

 4.2  Permission Sets

 

Ein Permission Set stellt eine Menge von Permissions dar. Das .NET Framework enthält bereits einige eingebaute Permission Sets, die sogenannten "named Permission Sets". Diese Permission Sets enthalten jeweils eine vordefinierte Menge von Permissions. Die folgende Tabelle aus [Bo03] gibt einen kleinen Überblick:

 

 

Permission Set

Description

Nothing

The empty permission set (grants nothing)

FullTrust

Implicitly grants unrestricted permissions for all permission types

Everything

Explicitly grants unrestricted permissions for all built-in permission types

SkipVerification

SecurityPermission: SkipVerification

Execution

SecurityPermission: Execution

Internet

FileDialogPermission: Open
IsolatedStoragePermission: DomainIsolationByUser (quota = 10240)
UIPermission: OwnClipboard | SafeTopLevelWindows
Printing Permission: SafePrinting

LocalIntranet

SecurityPermission: Execution | Assert | RemotingConfiguration
FileDialogPermission: Unrestricted
IsolatedStoragePermission: AssemblyIsolationByUser (no quota)
UIPermission: Unrestricted
PrintingPermission: DefaultPrinting
EnvironmentPermission: Read-only (USERNAME/TMP/TEMP)
ReflectionPermission: ReflectionEmit
DNSPermission: Yes
EventLog: Instrument

 

 

Ein Permission Set darf nur eine Permission pro Permissiontyp enthalten. Sollen mehrere Permissions eines Typs in ein Permission Set aufgenommen werden, so wird aus diesen Permissions mittels der Methode Union die Vereinigung gebildet.

 

 

 

 

Die Grafik aus [Bo03] zeigt, wie zwei Permission Sets mittels Union vereinigt werden und wie durch Intersect ihr Durchschnitt gebildet werden kann. Dabei werden die einzelnen Permissions des einen Permission Sets jeweils mit Permissions desselben Typs aus dem zweiten Permission Set verglichen. Gibt es zu einer Permission aus dem PS (Permission Set) A keine entsprechende Permission im PS B, so wird die Permission bei Union einfach übernommen, bei Intersect wird sie weggelassen (UIPermission und RegistryPermission im obigen Beispiel). Kommt ein Permissiontyp in beiden PS vor (im Beispiel die SecurityPermission), so wird aus diesen beiden Permissions die Vereinigung bzw. der Durchschnitt gebildet.

 

 

4.3  Zuweisung von Permissions

 

Nachdem nun bekannt ist, wie Rechte im .NET Framework repräsentiert werden, bleibt noch die Frage offen: Wie kommen Assemblies zu den Rechten, die ihnen zustehen?

Die Zuweisung von Permissions geschieht, wenn ein Assembly geladen wird. Dabei wird die Herkunft (= Evidence) des Assemblies evaluiert und vom Security Manager anhand von Sicherheitspolitiken (Security Policy) auf ein Permission Set abgebildet. Die folgende Grafik aus [WaLa02] veranschaulicht diesen Vorgang:

 

 

 

 

Die einzelnen Komponenten, die an dem Vorgang beteiligt sind, werden in den folgenden Kapiteln erklärt.

 

 

 4.3.1  Evidence

 

Evidence beschreibt die Herkunft eines Assemblies. Es gibt sieben vordefinierte Evidence-Typen [WaLa02]:

 

·        Zone: hier wird das gleiche Konzept wie im Internet Explorer verwendet (eine Auflistung der möglichen Zonen zeigt die Tabelle aus  [Dev01])

·        URL: gibt eine URL im Internet oder im lokalen Dateisystem an, die eine Ressource identifiziert

·        Site: die Seite, von der das Assembly geladen wurde (kommt das Assembly aus dem lokalen Dateisystem, hat Site den Wert Null)

·        ApplicationDirectory: das Verzeichnis, von dem das Assembly geladen wurde

·        StrongName: der "starke Name" identifiziert ein Assembly eindeutig

·        Publisher: gibt an, wer den Code geschrieben hat (über eine digitale Signatur)

·        Hash: der Hashwert des Assemblies

 

 

Zone

Description

Local

Code executed from the local system.  Code in this zone has full trust.

Intranet

Code executed from a share or URL on the enterprise network.  Limited access to local resources.

Internet

Code downloaded from the Internet.  Minimal access to local resources.

Restricted

Code in the restricted zone is not allowed to execute.

 

 

Codebasierte Sicherheit wird auch oft als "Evidence-based Security" bezeichnet. Die Herkunft eines Assemblies ist also ein essentieller Faktor in diesem Modell.

 

 

4.3.2  Security Policy

 

Die Sicherheitspolitik bestimmt, wie der Security Manager einem Assembly ausgehend von dessen Evidence Rechte zuweist. Sie beinhaltet also Regeln, wie Rechte vergeben werden. Dabei wird die Security Policy in vier Ebenen gegliedert, die sogenannten Policy Levels:

 

 

4.3.3  Policy Levels

 

Security Policy wird in folgende Ebenen gegliedert ([MS03-2]):

 

 

Policy level

Description

Enterprise policy

Defined by enterprise administrators who set policy for enterprise domains.

Machine policy

Defined by machine administrators who set policy for one computer.

User policy

Defined by users who set policy for a single logon account.

Application domain policy

Defined by the runtime host (any application that hosts the common language runtime) for setting load-time policy.  This level cannot be administered.

 

 

Die drei oberen Ebenen (Enterprise, Machine und User) können administriert und konfiguriert werden, die unterste Ebene ist nicht veränderbar.

 

Auf jeder Ebene wird durch die Regeln der Sicherheitspolitik ein Permission Set bestimmt. Das Permission Set, das einem Assembly vom Security Manager zugewiesen wird, ist der Durchschnitt dieser vier Permission Sets. Administratoren unterer Ebenen können also die Sicherheitspolitik nicht lockern, sondern nur noch weiter verschärfen.

 

 

 

 

Die Grafik aus [Bo03] veranschaulicht die Durchschnittsbildung der Permission Sets der vier Ebenen. Der gelbe Bereich repräsentiert die Permissions, die dem Assembly schließlich zugewiesen werden.

 

Jedes Policy Level besteht aus folgenden Komponenten:

 

 

Die Liste der Named Permission Sets enthält dabei die in Kapitel 4.3 angeführten vordefinierten Permission Sets der jeweiligen Ebene. Unter Policy Assemblies versteht man Assemblies, die für den Prozess der Policy Evaluation gebraucht werden (zum Beispiel benutzerdefinierte Permissions). Um zyklische Abhängigkeiten zu vermeiden, wird bei diesen Assemblies die Policy Evaluation kurzgeschlossen. Sie erhalten automatisch "FullTrust". [WaLa02]

 

 

4.3.4  Codegruppen

 

Jede Policy Ebene besteht aus einer baumartigen Hierarchie von Codegruppen. Wie sehen nun diese Codegruppen aus?

 

Eine Codegruppe besteht aus:

 

 

Bei dem Prozess der Rechtevergabe wird die Baumstruktur der Codegruppen in Preorder traversiert. Dabei vergleicht der Security Manager jeweils die Evidence des Assemblies mit der Membership Condition.

 

Beispiele für Membership Conditions sind:

Stammt das Assembly von einem bestimmten Hersteller? (Publisher = "Microsoft")

Wurde das Assembly von einer bestimmten Seite geladen? (Site = http://www.jku.at)

 

Erfüllt das Assembly die Bedingung, so wird ihm auf der entsprechenden Ebene das Permission Set der Codegruppe zugewiesen. Das Ergebnis der Traversierung ist ein Permission Set, das aus der Vereinigung der Permission Sets aller Codegruppen besteht, deren Bedingung das Assembly erfüllt hat.

 

Die Grafik aus [WaLa02] zeigt eine mögliche Hierarchie von Codegruppen:

 

 

 

 

Das Ergebnis der Traversierung dieses Baumes für ein Assembly, das von der Seite www.microsoft.com geladen wurde, aber nicht den StrongName Office besitzt, wäre die Vereinigung der Permission Sets Nothing, Internet und MSPSet. Das Assembly würde damit auf dieser Ebene  ein Permission Set mit allen Permissions der drei genannten Permission Sets erhalten.

 

Auf jeder Policy Ebene wird also anhand der Codegruppen ein Permission Set bestimmt. Der Durchschnitt der Permission Sets der vier Ebenen bildet dann das Permission Set, das ein Assembly erhält.

 

Der Vorgang der Evaluation wird in der Grafik aus [Sab02] noch einmal visualisiert:

 

 

 

Wie weiter oben bereits erwähnt wurde, kann eine Codegruppe noch zusätzliche Attribute haben. Mit Hilfe der Attribute LevelFinal und Exclusive haben Administratoren der höheren Ebenen die Möglichkeit, Einschränkungen der Sicherheitspolitik auf niedrigeren Ebenen zu ignorieren.

 

LevelFinal: Hat eine Codegruppe das Attribut LevelFinal, so wird garantiert, dass ein Assembly, das die Membership Condition erfüllt, nie weniger Permissions erhält, als im Permission Set dieser Codegruppe enthalten sind. Niedrigere Policy Levels werden dann nicht mehr evaluiert (Ausnahme: das Application Domain Level wird immer evaluiert). Hat zum Beispiel eine Codegruppe des Machine-Levels das Attribut LevelFinal, so hat die Userpolitik keinen Effekt auf Assemblies, die der Bedingung der Codegruppe entsprechen.

 

Exclusive:  Dieses Attribut erlaubt einer Codegruppe, die alleinige Entscheidung für eine gesamte Ebene zu treffen. Erfüllt ein Assembly die Bedingung einer mit Exclusive markierten Codegruppe, so werden alle restlichen Codegruppen dieser Ebene nicht evaluiert. Im Gegensatz zu LevelFinal werden aber die Codegruppen der anderen Ebenen sehr wohl evaluiert. Wichtig ist auch, dass pro Ebene nur eine Codegruppe das Attribut Exclusive haben kann. Gibt es mehrere Codegruppen mit diesem Attribut, so kann der Code des Assemblies nicht ausgeführt werden.  [Bur02]

 

 

4.3.5  Teilnahme eines Assemblies am Evaluationsprozess

 

Bis jetzt wurde das Assembly im Evaluationsprozess nur passiv behandelt. Die Evidence des Assemblies wird evaluiert, und anhand der Security Policy wird ein Permission Set ermittelt, das dem Assembly zugewiesen wird.

Nun hat aber das Assembly selbst die Möglichkeit, aktiv an dem Prozess der Rechtevergabe mitzuwirken. Um Sicherheitsrisiken zu vermeiden, ist ein Ziel der Sicherheitspolitik, nur so wenig Rechte wie nötig anzufordern. Oft braucht ein Assembly gar nicht alle Rechte, die ihm vom Security Manager zugewiesen werden können. In solchen Fällen ist es sinnvoll, dass das Assembly bekannt gibt, welche Permissions es zur fehlerfreien Ausführung des Codes braucht. Außerdem hat ein Assembly die Möglichkeit, Rechte von vorne herein auszuschließen, um Sicherheitslücken zu vermeiden.

 

Um die Rechtevergabe zu beeinflussen, gibt es drei Mengen von Permissions [BBMW02]:

 

·        RequestMinimum:   Diese Menge enthält die Permissions, die ein Assembly unbedingt braucht, damit es laufen kann. Ist die Menge der geforderten Permissions keine Teilmenge der maximal gewährten Permissions, so wird eine PolicyException geworfen.

·        RequestOptional:   In dieser Menge sind Permissions enthalten, die der Code nicht unbedingt zum Laufen braucht, ohne die aber bestimmte Funktionen nicht angeboten werden können.

·        RequestRefuse:       Die Permissions in dieser Menge werden ausgeschlossen, um mögliche Sicherheitsrisiken zu vermeiden.

 

Wenn man die Menge der Permissions, die ein Assembly nach dem Evaluationsprozess anhand seiner Evidence bekommen würde, mit MaxGrant bezeichnet, so kann man folgende Aussage treffen:

 

Granted Permissions = (MaxGrant È (RequestMinimum Ç RequestOptional)) – RequestRefuse

 

Die Grafik aus [Bo03]  fasst den Vorgang der Rechtevergabe noch einmal zusammen (auf die Wirkung von Deny und PermitOnly wird im Kapitel 5.2 eingegangen):

 

 

 

 

 

5  Policy Enforcement

 

Nachdem in den letzten Kapiteln ausführlich der Prozess der Rechtevergabe erklärt wurde, stellt sich nun die Frage: Wozu passiert das überhaupt? Wozu braucht ein Assembly die Rechte, die ihm zugewiesen werden?

 

Die Rechte eines Assemblies werden immer dann geprüft, wenn es auf eine geschützte Ressource zugreifen will. Die Technologie, die das ermöglicht, ist der Security Stack Walk.

 

 

5.1  Stack Walk

 

Der Stack Walk ist ein essentieller Teil des Sicherheitssystems. Er wird jedes Mal ausgelöst, wenn auf eine geschützte Ressource zugegriffen werden soll. Bei einem Stack Walk wird geprüft, ob alle Methoden in der Ruferkette (also alle Methoden, die am Call Stack liegen) die entsprechenden Permissions besitzen. Hat eine einzige Methode die geforderten Rechte nicht, so wird der Stack Walk abgebrochen und eine SecurityException geworfen.

 

 

Anhand der Grafik aus [WaLa02] soll der Stack Walk veranschaulicht werden: Assembly A ruft eine Methode von Assembly B auf, diese ruft eine Methode von Assembly C auf, die wiederum eine Methode in Assembly D aufruft. Auf dem Call Stack liegen nun also die jeweiligen Methoden der Assemblies A, B, C und D. Die Methode im Assembly D will auf eine Ressource zugreifen, die eine Permission (oder ein Permission Set) p fordert. Nun beginnt der Stack Walk. Jede Methode am Call Stack wird geprüft, ob sie die geforderte Permission p hat:

 

p.isSubsetOf(Permission Set von Assembly D)?  wenn nein: SecurityException wird geworfen; sonst: nächstes Element im Call Stack prüfen:

p.isSubsetOf(Permission Set von Assembly C)?  wenn nein: SecurityException wird geworfen; sonst: nächstes Element im Call Stack prüfen:

p.isSubsetOf(Permission Set von Assembly B)?  wenn nein: SecurityException wird geworfen; sonst: nächstes Element im Call Stack prüfen:

p.isSubsetOf(Permission Set von Assembly A)?  wenn nein: SecurityException wird geworfen; sonst: Ende des Call Stacks wurde erfolgreich erreicht; der Stack Walk wird positiv beendet; Assembly D darf auf die Ressource zugreifen

 

 

5.2  Modifikation des Stack Walks

 

Stack Walks werden meist implizit vom Sicherheitssystem des .NET Frameworks ausgelöst, wenn Code auf geschützte Ressourcen zugreifen will. Als Entwickler hat man aber auch die Möglichkeit, selber explizit Stack Walks auszulösen oder einen vom System ausgelösten Stack Walk zu modifizieren.

 

Demand:   Mit Demand kann explizit ein Stack Walk ausgelöst werden. Dabei sollten keine unnötigen Stack Walks durchgeführt werden, da jeder Stack Walk natürlich Zeit kostet. Wird zum Beispiel eine Methode aufgerufen, von der man weiß, dass sie ein Demand auf eine Permission durchführt, sollte in der aufrufenden Methode nicht noch einmal diese Permission gefordert werden, da es sonst zu einem doppelten Stack Walk kommt.

                     Eine schwächere Form des Demands ist das Link-Demand. Dabei wird nicht der ganze Call Stack geprüft, sondern nur die nächsttiefere Ebene, also der aktuelle Rufer.

 

Assert:       Mit einem Assert kann eine Methode für alle Rufer, die unter ihr im Call Stack liegen, "bürgen". Der Stack Walk wird dann positiv abgebrochen und der Zugriff auf die geschützte Ressource kann erfolgen. Eine Methode, die Assert verwendet, muss selbst die Permissions haben, die gefordert werden, und zusätzlich eine besondere Permission, die SecurityPermission.  Dadurch kann nicht beliebiger Code einfach ein Assert aufrufen; schließlich bedeutet ein Assert immer ein erhöhtes Sicherheitsrisiko.

                     Assert wird häufig in Kombination mit Demand verwendet. Bei einem Demand wird einmalig ein Stack Walk ausgeführt. Kommt es dabei zu keiner Exception, so wird garantiert, dass alle Rufer die geforderten Permissions besitzen. Wird anschließend ein Assert durchgeführt, so kommt es im weiteren Verlauf der Methode bei einem Zugriff auf die Ressource zu keinem Stack Walk mehr. Dadurch hat man die Vorteile des Asserts ohne die sonst damit verbundene Sicherheitslücke.

 

Deny:       Im Gegensatz zu Assert wird beim Deny der Stack Walk negativ abgebrochen und eine SecurityException geworfen. Deny wird verwendet, um den Zugriff auf eine Ressource explizit zu verweigern. Es wird eingesetzt, wenn zum Entwicklungszeitpunkt schon bekannt ist, dass auf eine bestimmte Ressource nicht zugegriffen werden darf.

 

PermitOnly:  PermitOnly kann ebenfalls zu einem negativen Abbruch des Stack Walks führen. Im Gegensatz zu Deny wird bei PermitOnly die Permission (oder das Permission Set) angegeben, die den Stack Walk nicht abbricht, sondern weiterlaufen lässt. Ist die geforderte Permission keine Teilmenge der PermitOnly-Menge, wird der Stack Walk negativ abgebrochen.

 

In der Assert-, Deny- und PermitOnly-Menge kann jeweils nur eine Permission oder ein Permission Set pro Methode liegen. Soll zum Beispiel ein Assert für mehrere Permissions gelten, müssen diese in einem Permission Set zusammengefasst werden. Außerdem ist zu beachten, dass es zwischen den einzelnen Mengen eine Ordnung nach Prioritäten gibt. Deny wirkt am stärksten, gefolgt von Assert und PermitOnly.

Es gibt die Möglichkeit, Deny, Assert und PermitOnly wieder aufzuheben. Das erfolgt durch RevertDeny, RevertAssert und RevertPermitOnly. [HoL03]

 

Folgendes Codebeispiel aus [WaLa02] soll die Verwendung von Deny, Assert und PermitOnly veranschaulichen:

 

 

using System;

using System.Security;

using System.Security.Permissions;

 

namespace PermissionDemand

{

   class EntryPoint

   {

      static void Main(string[] args)

      {

         String f = @"c:\System Volume Information";

         FileIOPermission p =

            new FileIOPermission(FileIOPermissionAccess.Write, f);

         p.Demand();

         p.Deny();

         p.Demand();

         CheckDeny(p);

         p.Assert();

         CheckDeny(p);

      }

      static void CheckDeny(FileIOPermission p)

      {

         try

         {

            p.Demand();

         }

         catch(SecurityException)

         {

            Console.WriteLine("Demand failed");

         }

      }

   }

}

 

 

 

Bei Ausführung des Programms erhält man als Output:

 

 

Demand failed

Demand failed

 

 

 

Auf den ersten Blick erscheint die Ausgabe nicht einleuchtend. Was passiert in dem Programm? In der Main-Methode wird eine Permission p gefordert. Anschließend wird diese Permission in die Deny-Menge aufgenommen und wieder mittels Demand gefordert. Trotzdem kommt es zu keiner Exception. Das liegt daran, dass der aktuelle Code bei Demand nicht geprüft wird, sondern nur die Rufer im Call Stack. Obwohl also in der Main-Methode die Permission p abgelehnt wird, wird das Demand erfolgreich beendet. Bei Aufruf der Methode CheckDeny kommt es dann aber zu einer Exception, da jetzt die Main-Methode als Rufer auftritt und deshalb der Stack Walk aufgrund des Denies negativ abgebrochen wird.

In der Main-Methode wird dann durch Assert die Permission p zugesichert und wieder CheckDeny aufgerufen. Trotzdem kommt es zu einer Exception und zu der Ausgabe "Demand failed". Warum? Das liegt nun an den oben erwähnten Prioritäten. Auf die Permission p wurde zuvor ein Deny ausgeführt, welches eine höhere Priorität als das Assert besitzt. Deny wird zuerst evaluiert und damit der Stack Walk negativ abgebrochen, das Assert wird dadurch nicht mehr erreicht.

Dieser Code soll zeigen, wie wichtig eine sparsame Verwendung von Deny, Assert und PermitOnly ist. Alle drei sollten nur so lange wie nötig aktiv bleiben und so bald wie möglich mit dem entsprechenden Revert aufgehoben werden, um Sicherheitsrisiken zu vermeiden.

 

 

5.3           Imperative vs deklarative Sicherheit

 

Im Sicherheitssystem des .NET Frameworks gibt es zwei Möglichkeiten, sicherheitsrelevante Prüfungen durchzuführen: imperativ oder deklarativ.

 

 

5.3.1  Imperative Sicherheit

 

Bei der imperativen Sicherheit werden Sicherheitsanforderungen dynamisch entwickelt, indem Permissions erzeugt und dann darauf Methoden wie Demand oder Assert ausgeführt werden. Die Beispiele in diesem Bericht waren bis jetzt alle in imperativer Form.

 

Der Vorteil der imperativen Sicherheit ist ihre Dynamik und die dadurch mögliche flexible Anpassbarkeit. Permissions werden erst zur Laufzeit erzeugt, deshalb können auch für Attribute der Permissions Parameter verwendet werden, die zur Kompilierzeit noch unbekannt sind. Dadurch ist es möglich, Permissions genauer zu spezifizieren und die Wahrscheinlichkeit von Sicherheitslücken zu verringern.

 

Das folgende Beispiel aus [BBMW02] zeigt eine Situation, in der ein Attribut einer Permission erst zur Laufzeit bekannt wird:

 

 

public void ReadFile(string filename){

  CodeAccessPermission p;

  p = new FileIOPermission(FileIOPermissionAccess.Read, filename);

  p.Demand();

  ... // Datei filename lesen

}

 

 

 

5.3.2  Deklarative Sicherheit

 

Bei der deklarativen Sicherheit werden Sicherheitsanforderungen durch benutzerdefinierte Attribute ausgedrückt. Diese Attribute sind vom abstrakten Basisattribut SecurityAttribute abgeleitet und können sich jeweils auf Assemblies, Typen oder Methoden beziehen. Die deklarative Form könnte im Gegensatz zur imperativen, dynamischen auch als statische Form der Sicherheitsimplementierung bezeichnet werden.

 

Das Beispiel aus [WaLa02] zeigt, wie deklarative Sicherheit verwendet wird. Dieses Beispiel geht außerdem auch noch einmal auf die zu Beginn erläuterte rollenbasierte Sicherheit ein:

 

namespace RoleBased

{

   class Sample

   {

      [PrincipalPermissionAttribute(SecurityAction.Demand, Name=@"culex\damien")]

      public static void UserDemandDamien()

      {

         Console.WriteLine("Hello Damien!");

      }

 

      [PrincipalPermissionAttribute(SecurityAction.Demand, Name=@"culex\dean")]

      public static void UserDemandDean()

      {

         Console.WriteLine("Hello Dean!");

      }

 

      static void Main(string[] args)

      {

         AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);

         try

         {

            UserDemandDamien();

            UserDemandDean();

         }

         catch(Exception)

         {

            Console.WriteLine("Exception thrown");

         }

      }

   }

}

 

 

 

Der Vorteil der deklarativen Sicherheit ist, dass die Sicherheitsanforderungen in die Metadaten aufgenommen werden. Es ist dadurch zum Beispiel möglich, mit Hilfe eines Tools vor Ausführung eines Codes zu prüfen, welche Sicherheitsanforderungen dieser für seine Ausführung benötigt, und eventuell nötige Änderungen in den Sicherheitseinstellungen vorzunehmen.

 

Ein Nachteil dieser Form ist ihre mangelnde Flexibilität. Das Beispiel der oben beschriebenen Methode ReadFile wäre deklarativ gar nicht implementierbar, da der Filename schon zur Kompilierzeit bekannt sein müsste. Eine Möglichkeit, über Umwege den gleichen Effekt zu erzielen, wäre in diesem Fall, die Methode wie folgt zu deklarieren:

 

 

[FileIOPermission(SecurityAction.Demand, Read="c:\\Temp")]

public void ReadFile(string filename){ /* Datei filename lesen */}

 

 

Dabei müsste aber sichergestellt werden, dass die Datei filename immer im Verzeichnis c:\\Temp liegt, wodurch die Flexibilität des Programms sehr eingeschränkt wird.

 

 

6  Benutzerdefinierte Konfigurationen

 

Im Allgemeinen hat ein Entwickler, der mit dem .NET Framework arbeitet, explizit nichts mit dem Sicherheitssystem zu tun. Die Bibliotheksklassen führen Sicherheitsüberprüfungen auf geschützte Ressourcen durch, somit muss sich der Entwickler nicht weiter darum kümmern. Wichtig ist in diesem Fall nur, dass eventuell geworfene SecurityExceptions (wenn geforderte Rechte nicht gegeben sind) abgefangen werden, um einen unkontrollierten Absturz des Programms zu verhindern.

 

Beim Zugriff auf geschützte Ressourcen ohne den Umweg über bestehende Bibliotheken (und besonders bei der Entwicklung neuer Bibliotheken), muss der Entwickler sich selbst um die Aufrechterhaltung der Sicherheit kümmern. Mit Hilfe der imperativen oder deklarativen Sicherheit kann er Sicherheitsanforderungen formulieren und so dafür sorgen, dass alle Sicherheitslücken geschlossen werden.

 

Die Defaulteinstellungen des .NET Frameworks betreffend Sicherheit sind in den meisten Fällen zielführend. Manchmal kann es jedoch durchaus sein, dass ein Benutzer sie konfigurieren möchte.

 

 

6.1  Modifikation der Defaulteinstellungen

 

Wann sollten die Defaulteinstellungen geändert werden?

Code, der nicht vom lokalen Computer stammt, hat nur sehr geringe Rechte. So darf zum Beispiel Code aus dem Internet oder aus dem lokalen Intranet nicht lesend oder schreibend auf lokale Laufwerke oder auf die Registry zugreifen, dafür kann er mit der Webseite kommunizieren, von der er stammt.

Code auf der lokalen Maschine hat dafür "Full Trust", also volles Vertrauen und damit alle Rechte.

 

Die Security Policy sollte deshalb geändert werden, wenn man

 

Die einfachst Art, Defaulteinstellungen zu ändern, ist die Verwendung des .NET Framework Configuration Tools mscorcfg.msc oder des Code Access Security Policy Tools caspol.exe.

 

Hat man nun eine Anwendung, der man voll vertraut und die man auf jeden Fall ausführen möchte, kann man wie folgt vorgehen:

Mit Hilfe des Permission View Tools permview.exe kann bestimmt werden, welche Permissions die Anwendung zum Laufen braucht (siehe RequestMinimum in Kapitel 4.3.5). Als nächstes muss ein Charakteristikum gefunden werden, anhand dessen das Assembly eindeutig identifiziert werden kann (Strong Name, Hash, Public Key, ...). Dann wird mit mscorcfg.msc oder caspol.exe eine Codegruppe erstellt, deren Membership Condition auf das gefundene Charakteristikum prüft. Dieser Codegruppe muss dann ein Permission Set zugeordnet werden, das genau die minimal geforderten Permissions des Assemblies enthält. Damit ist gewährleistet, dass die Anwendung alle Rechte bekommt, die sie braucht. [MS03-2]

 

 

6.2  Entwurf von Sicherheitskomponenten

 

Die Defaulteinstellungen können auch durch eigene Komponenten erweitert werden. So ist es zum Beispiel möglich, neue Evidence-Typen zu definieren, anhand derer dann Assemblies identifiziert werden können. Häufiger kommt es aber vor, dass man als Entwickler eigene Permissions oder Permission Sets entwerfen will.

 

Beim Entwurf einer neuen Permission sollte wie folgt vorgegangen werden [Bur02]:

 

Design der Permission: Die neue Permission sollte sich möglichst nicht mit bestehenden Permissions überschneiden. Außerdem müssen Abhängigkeiten beachtet werden, die durch die neue Permission entstehen könnten.

Implementierung: Hier gibt es zwei Möglichkeiten: Die neue Permission kann entweder von der Klasse CodeAccessPermission erben oder die Interface IPermission implementieren. In beiden Fällen muss jedoch die Interface IUnrestrictedPermission implementiert werden.

Benachrichtigung der Security Policy: Die neue Permission muss dem Sicherheitssystem mitgeteilt werden. Dafür muss die Permission durch eine XML-Datei beschrieben und mit caspol.exe importiert werden.

Aufnahme der neuen Permission in die Liste der Policy Assemblies: Die neue Permission muss in die Liste der fully trusted Assemblies aufgenommen werden, damit der Evaluierungsprozess beim Laden des Assemblies kurzgeschlossen wird und es immer alle Rechte erhält.

 

Sind diese Schritte abgeschlossen, kann die neue Permission verwendet werden. Das heißt, sie kann in Permission Sets aufgenommen und beliebigen Codegruppen zugewiesen werden.

 

Weitere Komponenten wie Permission Sets, Codegruppen können einfach mit den Tools mscorcfg.msc und caspol.exe erstellt werden.

 

 

7  Ausblick

 

Das .NET Framework bietet ein umfassendes Sicherheitssystem, das für den normalen Gebrauch ausreichende Defaulteinstellungen zur Verfügung stellt. Gerade im Zeitalter des Internets und durch die steigende Entwicklung von verteilten und komponentenbasierten Anwendungen wird die Bedeutung der in .NET verwendeten codebasierten Sicherheit immer größer.

Das Sicherheitssystem des .NET Frameworks soll in Zukunft noch weiter ausgebaut werden. Wenn sich die Plattform des Frameworks auch auf leichtgewichtige Geräte wie PDAs verbreitet, müssen einige Services (wie zum Beispiel Encryption), die jetzt auf das Betriebssystem zurückgreifen, direkt in der Klassenbibliothek des Frameworks implementiert werden. [Dev01]

In seinem aktuellen Entwicklungsstand bietet das Sicherheitssystem des .NET Frameworks dem Benutzer großflächige Sicherheit und trotzdem die Möglichkeit, das System  seinen Wünschen und Bedürfnissen anzupassen.

 

 

8  Bibliographie

 

 

[BoS03]       Don Box, Chris Sells: Essential .NET, The Common Language Runtime, Addison-  Wesley 2003, Kapitel 9

 

[Bur02]        Kevin Burton: .NET Common Language Runtime Unleashed, Sams Publishing 2002, Kapitel 16

 

[BBMW02]  W.Beer, D.Birngruber, H.Mössenböck, A.Wöß: Die .NET-Technologie, dpunkt.verlag 2002, Kapitel 3

 

[HoL03]       Michael Howard, David LeBlanc: Writing Secure Code. Microsoft Press, 2003, Kapitel 18

 

[Bo03]         Don Box: The Security Infrastructure of the CLR Provides Evidence, Policy, Permissions, and Enforcement Services, http://msdn.microsoft.com/security/securecode/dotnet/default.aspx?pull=/msdnmag/issues/02/09/SecurityinNET/default.aspx, 2003

 

[Dev01]       DevHood: Security in the .NET Environment (Tutorial Document),

                    http://www.devhood.com/training_modules/dist-c/Security.NET/Security.NET.htm, 2001

 

[MS03-1]    Microsoft Corporation: About .NET Security, http://www.gotdotnet.com/team/clr/about_security.aspx, 2003

 

[MS03-2]    Microsoft Corporation: Security Policy Best Practices,   http://www.gotdotnet.com/team/clr/SecurityPolicyBestPractices.htm, 2003

 

[Sab02]       Enrico Sabbadin: Code access security in the .NET Framework,   http://www.vb2themax.com/HtmlDoc.asp?Table=Articles&ID=500, 2002

 

[WaLa02]    Dr. Demian Watkins, Sebastian Lange: An Overview of Security in the .NET Framework,

                    http://msdn.microsoft.com/security/default.aspx?pull=/library/en-us/dnnetsec/html/netframesecover.asp, 2002

 

 

Weitere Quellen zu .NET Security:

 

http://msdn.microsoft.com/security/

 

http://msdn.microsoft.com/security/securecode/dotnet/default.aspx

 

http://www.vb2themax.com/

 

http://msdn.microsoft.com/netframework/using/Understanding/default.aspx