.NET
SECURITY
Seminar aus
Softwareentwicklung (Inside Java und .NET), WS2003/2004
Johannes Kepler Universität Linz, System Software Group
Brigitte Rafael, 0021889
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.
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
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!
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.
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:
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.
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]
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]
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?
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. |
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. |
MessageQueue |
Access
to MSMQ features |
System.Data. |
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.
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 |
LocalIntranet |
SecurityPermission:
Execution | Assert | RemotingConfiguration |
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.
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.
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.
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:
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]
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]
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):
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.
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
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.
Im
Sicherheitssystem des .NET Frameworks gibt es zwei Möglichkeiten,
sicherheitsrelevante Prüfungen durchzuführen: imperativ oder deklarativ.
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 } |
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.
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.
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]
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.
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.
[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,
Weitere Quellen zu .NET
Security:
http://msdn.microsoft.com/security/
http://msdn.microsoft.com/security/securecode/dotnet/default.aspx
http://msdn.microsoft.com/netframework/using/Understanding/default.aspx