Veröffentlicht: 15. Dez 2001 | Aktualisiert: 19. Jun 2004
Von Keith Brown
Modulare Software mit zukaufbaren Komponenten erhöht die Wiederverwendbarkeit von Code und die Effizienz der Software-Entwicklung. Aber sie ist auch anfälliger für Sicherheitsprobleme. Die Laufzeitbibliothek von .NET (CLR) bietet Lösungen für diese Probleme.
Auf dieser Seite
Überblick
Vergabe von Rechten
Die Überredungsattacke
Implizite Rechte
Schützen Sie sich
Pochen Sie auf Ihr Recht
Quo vadis, Assert?
Deklarative Attribute
Attacken gegen die Codezugriffssicherheit
Sicherheitsrichtlinien
Beweis / Evidence
Auswertung der Sicherheitsrichtlinien
Feinabstimmung der Rechtemengen
Anzeige und Bearbeitung der Sicherheitsrichtlinien
Fazit
Diesen Artikel können Sie hier lesen dank freundlicher Unterstützung der Zeitschrift:
COM-Komponenten können ziemlich nützlich sein. Und ziemlich gefährlich. Die gängige Methode zur Entwicklung von Software für Windows dreht sich mehr und mehr um den Zukauf von COM-Komponenten von anderen Anbietern, oder auch DLLs mit klassischen C-Schnittstellen, und deren Zusammenbau zu einem Prozess. Sicher, ein sinnvolles Modulkonzept fördert die Wiederverwendung, die lose Kopplung und einige andere hochgepriesene Aspekte der modernen Softwareentwicklung. Oft genug fördert es aber auch die Sicherheitslöcher.
Im zweiten Teil meiner kleinen zweiteiligen Artikelserie über Sicherheit im IIS (Internet Information Services) ("Das HTTPS-Protokoll und Zertifikate", System Journal 05/2000, S. 38) habe ich den beliebtesten und gefürchtetsten Angriff auf öffentlich zugängliche Server erwähnt, nämlich den gezielten Pufferüberlauf.
Ein dummer, aber erstaunlich oft anzutreffender Fehler irgendwo in einer DLL kann einem entschlossenen Angreifer mit hinreichender krimineller Energie Tür und Tor öffnen - nicht nur die Gelegenheit, einen laufenden Prozess abzuschießen, sondern auch die Gelegenheit, den Sicherheitskontext zu sabotieren. Stellt man seine eigene Anwendung mit gebräuchlichen kommerziellen DLLs zusammen, wird das Problem eher noch größer, weil die Angreifer nun in aller Ruhe die einzelnen DLLs auf Herz und Sicherheitsloch überprüfen können. Angreifer haben praktisch alle Zeit der Welt, in ihrem Unterschlupf heimlich die Messer zu wetzen, bevor sie über ihre Opfer herfallen.
Zur Lösung des Problems wurde Authenticode entwickelt. Es ist zwar besser als nichts, hat aber den Nachteil, dass es sich eher auf die Bestrafung konzentriert und nicht auf die Prävention. Die Identität eines Angreifers herauszufinden ist nur ein schwacher Trost, nachdem er ihren engsten Freunden und Verwandten Listen mit Hinweisen auf die seltsamsten Websites geschickt hat - unter Ihrem Namen, versteht sich.
Wie will ein Laie herausfinden, welche DLL eigentlich den Schaden angerichtet hat? Viele Anwender installieren doch immer noch bedenkenlos jede DLL auf ihrem System, die ihnen irgendeinen Vorteil oder eine "besondere Online-Erfahrung" verspricht. Was geschieht, wenn durch den Angriff die Festplatte gelöscht wird, auf welcher die DLL installiert ist? Wie sieht dann die Beweislage aus? Wie bringt man den Angreifer mit Aussicht auf Erfolg vor Gericht?
Man braucht also nicht nur die Verantwortung, sondern auch die Zugriffskontrolle über den Code. Insbesondere über mobilen Code wie die ActiveX-Komponenten, von denen wir inzwischen weitgehend abhängig sind. Wenn ein Administrator einen Prozess fährt, der sich aus Komponenten zusammensetzt, muss man ihm irgendwie die Sicherheit geben können, dass sein Sicherheitskontext vor fehlerhaften oder bösartigen Komponenten geschützt ist. Die Eindämmung des Pufferüberlaufproblems durch die Überprüfung im verwalteten Code ist ein erster Schritt. Sicherheit beim Codezugriff (code access security) ist zwar kein Garantieschein, aber ein wichtiger zweiter Schritt und das zentrale Thema dieses Artikels.
Dieser Artikel beruht auf der Beta 1 der allen Sprachen gemeinsamen Laufzeitschicht von .NET (CLR). Bis zur endgültigen Version kann sich noch vieles ändern.
Überblick
Zuerst möchte ich Ihnen einen Überblick über die Funktionsweise der Codezugriffssicherheit geben. Dieser Abschnitt skizziert gewissermaßen grob das Gelände, damit wir uns später nicht in den Details verlaufen. Wer sich bereits mit dem Sicherheitsmodell von Java 2 auskennt, wird feststellen, dass die CLR ein ähnliches Modell einsetzt.
Im Prinzip kommen immer die entsprechenden Klassen aus der CLR zum Zuge, wenn Sie sicherheitssensitive Operationen ausführen und zum Beispiel Dateien auslesen oder beschreiben, Umgebungsvariablen ändern, auf die Zwischenablage zugreifen, Dialogfenster anzeigen und derlei Dinge mehr. Diese Klassen wurden so geschrieben, dass das System die Art der gewünschten Aktion erkennen kann und somit die Gelegenheit erhält, den Vorgang zu gestatten oder abzulehnen. Die Ablehnung eines Vorgangs durch das System erfolgt durch die Meldung einer Ausnahme des Typs SecurityException. Wie entscheidet das System, ob der Vorgang zulässig ist oder abgelehnt werden muss? Durch einen Blick auf die Sicherheitsrichtlinien, die sich nicht nur für jede Maschine separat festlegen lassen, sondern auch für jeden Anwender.
Die Sicherheitsrichtlinien (security policy) der CLR funktionieren auf der konzeptionellen Ebene ziemlich einfach. Anhand der Richtlinien werden den Baugruppen (Assemblies) beim Laden gewisse Fragen gestellt. Derzeit sind zwei Fragen üblich und sie lassen sich auf verschiedene Weisen beantworten (dazu später mehr): Woher stammt die Baugruppe? Wer hat die Baugruppe entwickelt?
Die Sicherheitsrichtlinien bestimmen nun gewissermaßen die Art und Weise, in der sich solche Fragen auf bestimmte Rechte abbilden lassen. So können Sie zum Beispiel sagen, dass der "Code, der von https://www.foobar.com/baz stammt", die Dateien aus dem Verzeichnis c:\quux und den dazugehörigen Unterverzeichnissen auslesen, aber keine Dialogfenster anzeigen darf. In diesem Fall lautet die zu stellende Frage also: "Von welchem URL stammt diese Baugruppe?" Und das ist im Prinzip nur eine kleine Variation der oben genannten ersten Frage. Damit wäre die Codezugriffssicherheit der CLR erklärt, zumindest die Form, in der sie sich aus 10 000 Metern Höhe präsentiert. Gräbt man etwas tiefer, wird die Sache aber interessanter. Tun wir das einmal. Graben wir.
Vergabe von Rechten
Stellen Sie sich vor, Sie entwickelten die folgende Klasse für einfache Lese- und Schreibzugriffe auf eine Datei (der Übersichtlichkeit halber habe ich die Implementierungen weggelassen):
public class MyFileAccessor {
public MyFileAccessor(String path,
bool readOnly) {}
public void Close() {}
public String ReadString() {}
public void WriteString(String stringToWrite) {}
}
Und so würde man diese Klasse normalerweise benutzen:
class Test {
public static void Main() {
MyFileAccessor fa = new MyFileAccessor("c:\foo.txt", false);
fa.WriteString("Hello ");
fa.WriteString("world");
fa.Close(); // Puffer durchschreiben.
}
}
Bei diesem Umgang mit den Objekten dürfte schnell klar werden, dass die Sicherheitsüberprüfung am besten im Konstruktor erfolgt. Aus den Parametern des Konstruktors geht genau hervor, um welche Datei es sich handelt und ob der Zugriff im Lesemodus oder im Lese-/Schreibmodus erfolgt. Wenn Sie diese Informationen mit den zugrundeliegenden Sicherheitsrichtlinien abgleichen und daraus eine SecurityException resultieren sollte, lassen Sie diese Fehlermeldung einfach bis zum Aufrufer weiterlaufen.
Da der Konstruktor seine Arbeit in diesem Fall nicht abschließen kann, erhält der Aufrufer einfach keinen Zugriff auf das gewünschte Objekt und ist somit auch nicht in der Lage, irgendwelche Funktionen des Objekts aufzurufen. Das vereinfacht die Sicherheitsüberprüfungen für die Klasse beträchtlich. Und das ist nicht nur aus der Sicht des Programmierers eine gute Sache, sondern auch unter Sicherheitsaspekten. Je weniger Sicherheitsüberprüfungen Sie im Code schreiben müssen, desto weniger Gelegenheit gibt es für Fehler.
Allerdings ist auch der Nachteil dieses Lösungsansatzes leicht zu erkennen. Wenn ein Client nach der anfänglichen Sicherheitsüberprüfung im Konstruktor eine Instanz Ihrer Klasse anlegt und der Client diese Referenz dann an einen anderen Client weitergibt (potentiell in einer anderen Baugruppe), so muss sich der neue Client nicht der konstruktororientierten Codezugriffsüberprüfung unterziehen. Das ähnelt der Art und Weise, in welcher der Systemkern heutzutage in Windows 2000 arbeitet. Auch hier zeigt sich wieder das Spannungsfeld zwischen Sicherheit und Schnelligkeit.
Nun, in der "realen Welt" würden Sie keine Klasse wie MyFileAccessor für den Dateizugriff entwickeln. Es ist wesentlich bequemer, auf eine Klasse wie System.IO.FileStream zurückzugreifen, die vom System bereitgestellt wird und schon mit den entsprechenden Sicherheitsüberprüfungen implementiert wurde. Oft erfolgen diese Sicherheitstests im Konstruktor, mit allen skizzierten Vor- und Nachteilen für Leistung und Sicherheit. Was die .NET-Architektur anbetrifft, können Sie die Sicherheitsmechanismen, die von der Klasse FileStream benutzt werden, aus Gründen der Erweiterbarkeit auch direkt in ihren eigenen Komponenten einsetzen. Listing L1 zeigt, wie man dieselbe Sicherheitsüberprüfung im Konstruktor von MyFileAccessor implementieren könnte, gäbe es keine entsprechende Systemklasse, die das bereits erledigt.
L1 Sicherheitstest im Konstruktor von MyFileAccessor
using System.Security.Permissions;
public class MyFileAccessor {
public MyFileAccessor(String path, bool readOnly)
{
path = MakeFullPath(path); // Hilfsfunktion
FileIOPermissionAccess desiredAccess =
readOnly ? FileIOPermissionAccess.Read
: FileIOPermissionAccess.AllAccess;
FileIOPermission p =
new FileIOPermission(desiredAccess, path);
p.Demand();
//
...
öffne die Datei
}
// ...
}
Der Code in Listing L1 führt den Sicherheitstest in zwei Schritten durch. Zuerst legt er ein Objekt an, das die fraglichen Rechte repräsentiert, in diesem Fall also den Dateizugriff. Und dann fordert er dieses Recht. Das veranlasst das System, die Rechte des Aufrufers zu überprüfen. Falls der Aufrufer das gewünschte Recht nicht hat, meldet Demand eine SecurityException. In der Praxis treibt Demand für den Test natürlich etwas mehr Aufwand, wie Sie noch feststellen werden.
Tabelle T1 listet die verschiedenen Codezugriffsrechte auf, die derzeit von der Laufzeitschicht angeboten werden. Es gibt außerdem eine Reihe von Codeidentitätsrechten, mit denen die Antworten auf die in der Übersicht erwähnten Fragen direkt getestet werden. Allerdings sind diese Codeidentitätsrechte (code identity permissions) aber weniger gebräuchlich, weil sie dazu führen, dass die Sicherheitsrichtlinien fest in den Code eingebaut werden. (Auch auf die Richtlinien komme ich gleich noch zurück.)
An diesem Punkt bleibt festzuhalten, dass das zugrundeliegende Betriebssystem zusätzlich seine eigenen Zugriffstests ausführt, auch wenn der Code die Sicherheitstests von .NET heil überstanden hat. (Windows NT und 2000 beschränken zum Beispiel den Zugriff auf NTFS-Partitionen mit Hilfe von Zugriffskontrolllisten.) Selbst wenn die .NET-Sicherheit einer Baugruppe den unbeschränkten Zugriff auf das lokale Dateisystem gewährt und die Komponenten aus dieser Baugruppe in einem Prozess eingesetzt werden, der als Alice läuft, können diese Komponenten nur solche Dateien öffnen, die Alice auch aus der Sicht des zugrundeliegenden Betriebssystems öffnen darf.
T1 Codezugriffsrechte
|
Recht
|
Beschreibung
|
|
SecurityPermission
|
Das ist eigentlich ein Meta-Recht, weil es die Benutzung der Sicherheitsinfrastruktur regelt. Verschiedene orthogonale Rechte werden in dieser Kategorie zusammengefasst: Execution, Assertion, UnmanagedCode, SkipVerification, SerializationFormatter, ControlDomainPolicy, ControlEvidence, ControlPolicy, ControlPrincipal und ControlThread.
|
|
FileIOPermission
|
Steuert Lese-, Schreib- und Ergänzungsoperationen (append) von einzelnen Dateien und Verzeichnisbäumen. Lässt sich auch zur Beschränkung aller Zugriffe auf das Dateisystem verwenden.
|
|
FileDialogPermission
|
Erlaubt den Lesezugriff auf Dateien, aber nur dann, wenn der Dateiname in einem speziellen Dateidialog des Systems vom interaktiven Anwender angegeben wird. Wird normalerweise benutzt, wenn die entsprechende FileIOPermission fehlt.
|
|
IsolatedStorage-FilePermission
|
Ermöglicht die Kontrolle über die Benutzung und Konfiguration von "isoliertem Speicher" (isolated storage).
|
|
ReflectionPermission
|
Ermöglicht die Erweiterung von normalen Reflektionsrechten. Mit diesem Recht können Sie alle Typen in einer Baugruppe (Assembly) aufzählen und deren Inhalt, einschließlich privater Datenelemente.
|
|
RegistryPermission
|
Steuert die Erstellung sowie Lese- und Schreibzugriffe auf Registrierschlüssel (einschließlich nachgeordneter Schlüssel). Lässt sich auch zur Beschränkung aller Zugriffe auf die Registratur einsetzen.
|
|
EnvironmentPermission
|
Steuert Lese- und Schreibzugriffe auf einzelne Umgebungsvariablen. Lässt sich auch einsetzen, um alle Zugriffe auf eine Umgebung zu sperren.
|
|
UIPermission
|
Lässt sich zur Beschränkung des Zugriffs auf die Zwischenablage einsetzen (Eine Anwendungsdomäne soll zum Beispiel nur in der Lage sein, Daten aus der Zwischenablage zu übernehmen, die aus derselben Anwendungsdomäne stammen). Lässt sich auch zur Beschränkung der einsetzbaren Fenster auf "sichere" Fenster benutzen, um Angriffe zu verhindern, in denen ein nachgemachter Dialog nach sensiblen Daten fragt, zum Beispiel nach einem Kennwort.
|
Die Überredungsattacke
Wie erwähnt lauten die beiden Grundfragen, die Sie einer Baugruppe bei der Überlegung stellen, welche Rechte sie erhalten soll, "Woher kommt diese Baugruppe?" und "Wer hat diese Baugruppe entwickelt?" Von den Antworten auf diese Fragen hängt es ab, welche Rechte die Baugruppe erhält.
Stellen Sie sich vor, MyFileAccessor sei mit Hilfe eines FileStream-Objekts implementiert worden, wie in Bild B1 skizziert. Der Code könnte so aussehen:
using System.IO;
public class MyFileAccessor {
private FileStream m_fs;
public MyFileAccessor(String path,
bool readOnly) {
m_fs = new FileStream(path, FileMode.Open,
readOnly ? FileAccess.Read
: FileAccess.ReadWrite);
}
// ...
}
Angenommen, Sie hätten MyFileAccessor in dieser Weise implementiert und Ihrer Freundin Alice gegeben, die sie auf ihrer lokalen Festplatte installiert hat. Welche Codezugriffsrechte hat die Baugruppe MyFileAccessor nun? Wenn ich mir meine eigenen Sicherheitsrichtlinien anschaue, dann sehe ich, dass sie lokalen Komponenten eine Gruppe von Rechten zugesteht, die FullTrust heißt. Dazu gehört auch der unbeschränkte Zugriff auf das Dateisystem (vergessen Sie nicht, dass das zugrundeliegende Betriebssystem diese Rechte noch weiter einschränken kann).
B1 MyFileAccessor
B2 Ein Beispiel für eine Verführungs-Attacke
Wie Bild B2 andeutet, könnte MyFileAccessor nach der Installation auf der lokalen Festplatte von verschiedenen Arten von Komponenten benutzt werden. Es ist einer lokalen Komponente, der man vertraut, durchaus möglich, MyFileAccessor für den Zugriff auf Dateien zu benutzen. Sollte Alice nun mit ihrem Browser eine bösartige Website ansteuern, könnte eine von dieser Website heruntergeladene .NET-Komponente MyFileAccessor auch für böswillige Aktionen missbrauchen. Das ist ein Beispiel für eine Art "Überredungsattacke". MyFileAccessor lässt sich dazu verleiten, böse Dinge zu tun (zum Beispiel zum Öffnen von privaten Dokumenten, die Alice lieber für sich behalten hätte).
Statt nun von jeder Komponente eigene Zugriffsüberprüfungen zu verlangen, um solche seltsamen Geschichten zu unterbinden, überprüft die CLR einfach, ob jeder Aufrufer in der Aufrufkette die Rechte hat, die von FileStream verlangt werden. Wenn eine lokale Komponente namens NotepadEx wie in Bild B2 mit MyFileAccessor Dateien öffnet, erhält sie unbeschränkten Zugriff, weil die gesamte Aufrufkette von lokal installierten Baugruppen ausgeht. Wenn die BöseKomponente allerdings mit MyFileAccessor Dateien öffnen will, wird die CLR den Stapel untersuchen, sobald FileStream Demand aufruft, und dann feststellen, dass einer der Aufrufer aus der Kette nicht über die erforderlichen Rechte verfügt. Folglich meldet Demand eine SecurityException.
Implizite Rechte
An dieser Stelle ist es sinnvoll, sich zu verdeutlichen, dass manche Rechte andere Rechte implizieren. Wenn Sie zum Beispiel den umfassenden Zugriff auf das Verzeichnis c:\temp gewähren, so gewähren Sie implizit auch dieselben Zugriffsrechte auf dessen Kinder, Enkel und so weiter. Davon können Sie sich mit der Methode IsSubsetOf überzeugen, die es in den Codezugriffsrecht-Objekten gibt. So liefert der Code aus Listing L2 zum Beispiel das folgende Ergebnis:
p2 is subset of p1
Solche impliziten Rechte erleichtern die Administration ungemein. Man sollte aber nicht vergessen, bei der Anforderung der Rechte so präzise wie möglich zu sein. Die CLR wird Ihre Forderung automatisch mit dem Bestand vergleichen und erkennen, ob es sich um eine Teilmenge eines bereits gewährten Rechts handelt.
L2 IsSubsetOf
CodeAccessPermission p1 = new FileIOPermission(
FileIOPermissionAccess.AllAccess,
"c:\\temp");
CodeAccessPermission p2 = new FileIOPermission(
FileIOPermissionAccess.Append,
"c:\\temp\\foo\\bar.txt");
if (p1.IsSubsetOf(p2))
System.Console.WriteLine("p1 is subset of p2");
else if (p2.IsSubsetOf(p1))
System.Console.WriteLine("p2 is subset of p1");
else
System.Console.WriteLine("no subset");
Schützen Sie sich
Baugruppen erhalten beim Laden gewisse Grundrechte. Die CLR und ihre Hosts erkennen diese Rechte, indem sie Fragen über die Baugruppe stellen und die Antworten an das Sicherheitsrichtliniensystem weiterleiten, das die Antworten in Rechte umwandelt. Allerdings bedeutet der Umstand, dass eine Baugruppe entsprechend einer Richtlinie bestimmte Grundrechte erhält, nicht automatisch, das diese Rechte während der Laufzeit zur Verfügung stehen. Lassen Sie mich das erklären.
Wird eine Baugruppe lokal installiert, so hat sie wahrscheinlich weitreichende oder sogar völlig unbeschränkte Rechte, was die Codezugriffssicherheit anbetrifft. Stellen Sie sich vor, eine dieser Baugruppen, denen man so weitgehend vertraut, riefe nun ein Skript vom Anwender auf. Je nachdem, was das Skript tun soll, wird die aufrufende Baugruppe die effektiven Rechte beschränken wollen, bevor der Aufruf erfolgt (Listing L3).
L3 Festlegung der Zugriffsrechte
using System.Security.Permissions;
using System.Security;
public interface IUserPluggableAlgorithm {
int Calculate(int a, int b);
}
// dieser Codeabschnitt benutzt den Algorithmus
PermissionSet ps =
new PermissionSet(PermissionState.None);
ps.AddPermission(new FileIOPermission(
FileIOPermissionAccess.AllAccess,
"c:\\sensitiveStuff"));
ps.AddPermission(new FileIOPermission(
FileIOPermissionAccess.AllAccess,
"c:\\moreSensitiveStuff"));
ps.AddPermission(new EnvironmentPermission(
PermissionState.Unrestricted));
ps.AddPermission(new UIPermission(
PermissionState.Unrestricted));
ps.Deny(); // verweigere diese Rechte
int result = a.Calculate(42, 64);
CodeAccessPermission.RevertDeny();
//
...
fahre mit der Arbeit fort
Dieser Code erlegt dem aktuellen Stapelrahmen zusätzliche Beschränkungen auf. Falls Calculate also versuchen sollte, auf Umgebungsvariablen zuzugreifen, einen Blick auf den Inhalt der Zwischenablage zu werfen oder in den beiden angegebenen Verzeichnissen mit sensiblen Daten herumzufummeln, oder wenn irgendeine Komponente, die von Calculate intern benutzt wird, dies versuchen sollte, stellt die CLR bei der Untersuchung der Rechte im Stapelrahmen fest, dass diese Rechte explizit aberkannt werden, und verweigert den Zugriff. Ich fasse in diesem Fall einige Rechte zu einem einzelnen PermissionSet zusammen und sperre dann die gesamte Gruppe. Für jeden Stapelrahmen lässt sich nämlich nur eine Gruppe von Rechten sperren und der Aufruf der Funktion Deny verdrängt die alte Gruppe vom aktuellen Stapelrahmen. Das bedeutet, dass die folgenden Zeilen nicht das bewirken, was Sie vielleicht erwarten:
FileIOPermission p1 = new FileIOPermission(
FileIOPermissionAccess.AllAccess,
"c:\\sensitiveStuff");
FileIOPermission p2 = new FileIOPermission(
FileIOPermissionAccess.AllAccess,
"c:\\moreSensitiveStuff");
p1.Deny(); // p1 wird gesperrt
p2.Deny(); // nun wird p2 gesperrt (nicht p1)
In diesem Code überschreibt der zweite Deny-Aufruf den ersten, so dass dem Code in diesem Fall nur die Rechte aus p2 entzogen werden. Durch den Einsatz einer Rechtegruppe ist es möglich, nicht nur ein Recht, sondern mehrere Rechte gleichzeitig zu entziehen. Der Aufruf der statischen Funktion RevertDeny aus der CodeAccessPermission-Klasse entfernt diese "Verweigerungsgruppe" vom aktuellen Stapelrahmen.
Falls Sie feststellen, dass Sie dem Code relativ viele Einzelrechte entziehen, könnte die Umstellung auf einen anderen Lösungsansatz sinnvoll werden. Statt mit Deny und RevertDeny arbeiten Sie dann mit PermitOnly und RevertPermitOnly. Dieser Weg empfiehlt sich, wenn Sie ganz genau wissen, welche Rechte Sie dem Code zugestehen wollen.
Pochen Sie auf Ihr Recht
Diese Art der Stapeluntersuchung dient zum Schutz von Allzweckklassen wie FileStream und MyFileAccessor, die sich auf viele verschiedene Weisen einsetzen lassen. So eignet sich FileStream zum Beispiel sehr schön dafür, Fehler in einer wohldefinierten Protokolldatei zu erfassen, die auf der Festplatte des Anwenders in einem wohldefinierten Verzeichnis liegt. Eine eher böswillige Verwendung von FileStream wäre der Versuch, mit ihrer Hilfe den Inhalt der Datei zu ändern, in der die lokalen Sicherheitsrichtlinien vermerkt sind. Der Mechanismus zur Stapeluntersuchung soll nun sicherstellen, dass sich in der Aufrufkette nur Baugruppen befinden, die das Recht auf den Dateizugriff haben, und zwar unabhängig von der Anzahl der beteiligten Baugruppen. Damit lassen sich solche "Überredungsattacken" vermeiden, von denen bereits die Rede war.
Bei allen seinen unbestreitbaren Vorteilen ist der Mechanismus zur Stapeluntersuchung aber gelegentlich auch im Weg. Listing L4 zeigt eine Klasse namens ErrorLogger, die den erwähnten wohldefinierten Fehlerprotokollierdienst implementiert.
L4 Ein kleiner Fehlerprotokollierdienst
using System;
using System.IO;
public class ErrorLogger {
public static void Log(String s) {
const String fname = "c:\\temp\\errlog.txt";
FileStream logStream = new FileStream(fname,
FileMode.Append, FileAccess.Write);
StreamWriter logWriter =
new StreamWriter(logStream);
logWriter.Write(s);
logWriter.Close();
logStream.Close();
}
}
Stellen Sie sich vor, die Klasse ErrorLogger sei auf der lokalen Festplatte von Alice installiert worden und die dazugehörige Baugruppe habe wegen der Richtlinien für die Codezugriffssicherheit, die auf der Maschine von Alice gelten, den vollständigen Zugriff auf das Dateisystem. (Nach den Standardrichtlinien erhalten die lokalen Komponenten derzeit den unbeschränkten Zugang zum Dateisystem.) Was geschieht nun, wenn diese Klasse ihre Dienste anderen Baugruppen anbietet, von denen nicht alle die Erlaubnis für Schreibzugriffe auf dem lokalen Dateisystem haben?
Beim Einsatz durch beliebige Komponenten ist der ErrorLogger wesentlich sicherer als der MyFileAccessor, der den Zugriff auf jede Datei erlaubt, die der Client angibt. ErrorLogger ist eine einfache Klasse, mit der man nur Strings an eine Datei anhängen kann, und zwar nur an eine einzige wohldefinierte Datei. Allerdings weiß der Mechanismus zur Stapeluntersuchung dies nicht, wenn der FileStream-Konstruktor die Überprüfung der Rechte auslöst. Folglich wird der Aufruf fehlschlagen, falls nicht jeder Teilnehmer an der Aufrufkette das erforderliche FileIOPermission-Recht hat. Dieses Hindernis lässt sich beseitigen, indem der ErrorLogger auf sein Recht pocht, die Protokolldatei beschreiben zu dürfen. Listing L5 zeigt die neue Implementierung.
L5 Die geänderte Klasse ErrorLogger2
public class ErrorLogger2 {
public static void Log(String s) {
const String fname = "c:\\temp\\errlog.txt";
FileIOPermission p = new FileIOPermission(
FileIOPermissionAccess.Append, fname);
p.Assert();
FileStream logStream = new FileStream(fname,
FileMode.Append, FileAccess.Write);
StreamWriter logWriter =
new StreamWriter(logStream);
logWriter.Write(s);
logWriter.Close();
logStream.Close();
}
}
Die neue Version der ErrorLogger-Klasse erhält ebenfalls den vollen Zugriff auf das Dateisystem, wenn sie als lokale Komponente installiert wird. In diesem Fall besteht sie mit Assert auf ihrem Dateizugriffsrecht, noch bevor sie die Datei mit FileStream öffnet. Natürlich können Sie auf diese Weise nur Rechte einfordern, die Ihrer Baugruppe tatsächlich gewährt wurden.
In jedem Stapelrahmen kann es solch ein geltend gemachtes Zugriffsrecht geben. Sobald die Untersuchung des Stapels diesen Stapelrahmen erreicht, werden die geltend gemachten Rechte anerkannt und die Untersuchung ist beendet. Die Untersuchung wird nur dann fortgesetzt, wenn Zugriffsrechte erforderlich sind, die über die geltend gemachten Rechte hinausgehen. Ich mache mir übrigens nicht die Mühe, RevertAssert aufzurufen. RevertAssert ist in diesem Fall nicht erforderlich, denn die mit Assert geltend gemachten Rechte können problemlos eingetragen bleiben, bis die Log-Funktion zum Aufrufer zurückkehrt. An diesem Punkt wird der Stapelrahmen samt dem Eintrag mit den geltend gemachten Rechten sowieso demontiert. Das gilt auch für Deny und PermitOnly.
Quo vadis, Assert?
Mit Sicherheit lässt sich dieser Assert-Mechanismus zur Durchsetzung der eigenen Rechte auch missbrauchen. So könnte eine lokal installierte Komponente, der vollständig vertraut wird, einfach alle Rechte einfordern und dann tun, was sie gerade für richtig hält, und zwar im Auftrag eines beliebigen Clients. Eine Horror-Vorstellung. Aber wie kann man sicher sein, dass eine Komponente nach ihrer Installation auf der eigenen Maschine nicht zum Berserker mutiert? Da der Assert-Mechanismus so durchsetzungsfähig ist und das Potential für den Missbrauch schon in sich trägt, wird auch sein Einsatz mit einer speziellen Rechteklasse geregelt, nämlich mit SecurityPermission. Diese Klasse fasst eine ganze Reihe von verschiedenen Rechten zusammen, die sich auf Sicherheitsaspekte und Richtlinien auswirken. In gewissem Umfang kann man sich die meisten dieser Rechte als eine Art Meta-Befugnis vorstellen.
Werfen Sie noch einmal einen Blick auf die ErrorLogger2-Klasse in Listing L5. Hat sie vielleicht dadurch, dass sie auf ihrem Recht besteht, Schreibzugriffe auf eine einzelne, genau bezeichnete Datei durchzuführen, die Sicherheitsvorkehrungen des gesamten Systems unterlaufen? Welche Arten von Angriffen sind möglich? Eine böswillige Komponente könnte falsche Fehlermeldungen einschleusen, um den Anwender zu verwirren. Sie könnte auch riesige Textmengen übermitteln, bis die Festplatte des Anwenders voll ist. Obwohl die Klasse ErrorLogger2 wesentlich sicherer als eine Allzweckklasse wie MyFileAccessor ist, sind noch bestimmte Angriffe denkbar, die erst durch ihr Beharren auf den eigenen Rechten möglich werden.
Sollte man nun den Einsatz von Assert wegen solcher Komplikationen vermeiden? Wie bei den meisten Sicherheitsfragen gibt es keine definitive Antwort. Assert macht das Sicherheitsmodell komplexer und schwieriger zu durchschauen. Als Faustregel könnte man wohl empfehlen, die Aufrufe von Assert mit Kollegen zu besprechen, quasi als eine Art Jury. Ihr Beharren auf den eigenen Rechten wird von den Sicherheitsrichtlinien ignoriert, denn viele Administratoren werden solche Rechthabereien sperren wollen, vielleicht mit Ausnahme eines kleinen Kreises an absolut vertrauenswürdigen lokalen Komponenten. Ihre Anwendung wird dann Probleme bekommen, falls sie auf Assert angewiesen ist. Es dürfte sinnvoll sein, die SecurityExceptions abzufangen, die von Assert ausgelöst werden, und die anstehende Arbeit dann möglichst auf dem konservativen Wege zu erledigen.
Allerdings ist dieser Starrsinn, was die eigenen Rechte anbetrifft, absolut unverzichtbar, sobald die Grenze zwischen verwaltetem und unverwaltetem Code überschritten wird. Nehmen wir die vom System definierte Klasse FileStream als Beispiel. Diese Klasse muss auf jeden Fall Funktionen des zugrundeliegenden Betriebssystems aufrufen, um die Dateien öffnen, auslesen, beschreiben und wieder schließen zu können. Diese Funktionen wurden im nicht verwalteten Code implementiert. Die Interop-Schicht wird für diese Aufrufe eine SecurityPermission verlangen, insbesondere das Recht UnmanagedCode. Sollte sich diese Forderung den Stapel hinauf durchsetzen, wird kein Code mehr Dateien öffnen können, solange er nicht auch das Recht hat, Aufrufe in den unverwalteten Code hinein durchzuführen.
Im Endeffekt wandelt die FileStream-Klasse dieses extrem allgemeine Recht in eine speziellere Anforderung um, nämlich in die Forderung nach einer FileIOPermission. Sie fordert diese FileIOPermission in ihrem Konstruktor an. Gibt das System dieser Forderung nach, fühlt sich das FileStream-Objekt berechtigt, auch das Recht auf UnmanagedCode zu verlangen, bevor es den Aufruf ins Betriebssystem durchführt.
Bei den unverwalteten Aufrufen des FileStream-Objekts handelt es sich nicht um mehr oder weniger zufällige Aufrufe in den unverwalteten Code hinein, sondern um Aufrufe, mit denen eine bestimmte Datei zu einem bestimmten Zweck geöffnet werden soll. Diese Absicht wird durch die ursprüngliche Forderung deutlich, die im Konstruktor gestellt wurde. Die Baugruppe mscorlib, in der FileStream und andere vertrauenswürdige Komponenten liegen, wird als hinreichend vertrauenswürdig angesehen, um diese Umwandlung einer FileIOPermission in UnmanagedCode vornehmen zu dürfen. Also wird auch der Forderung nach dem UnmanagedCode stattgegeben. Bevor Sie aber anderen Baugruppen beim Assert trauen, sollten Sie sich absolut sicher sein, dass diese Baugruppen Ihre Vorstellungen von Sicherheit fördern und nicht unterwandern.
Deklarative Attribute
Wenn Sie vorhaben, in Ihren Komponenten Deny, PermitOnly oder Assert einzusetzen, sollten Sie dabei bedenken, dass sich das Ziel nicht nur durch Funktionsaufrufe erreichen lässt, sondern auch deklarativ. So zeigt Listing L6 zum Beispiel eine dritte Implementierung des ErrorLoggers, die mit einem deklarativen Attribut arbeitet, in diesem Fall mit FileIOPermissionAttribute. In C# kann man den Suffix Attribute bei der Deklaration des Attributs übrigens weglassen.
L6 Die Klasse ErrorLogger3
public class ErrorLogger3 {
const String fname = "c:\\temp\\errlog.txt";
[FileIOPermission(SecurityAction.Assert,
Append=fname)
]
public static void Log(String s) {
FileStream logStream = new FileStream(fname,
FileMode.Append, FileAccess.Write);
StreamWriter logWriter =
new StreamWriter(logStream);
logWriter.Write(s);
logWriter.Close();
logStream.Close();a
}
}
Dieser Lösungsansatz hat einige Vorteile. Erstens ist er etwas einfacher zu schreiben. Zweitens, und das ist wichtiger, werden deklarative Attribute in die Metadaten der Komponente aufgenommen und lassen sich via Reflektion leicht entdecken. Somit kann ein Entwicklungswerkzeug zum Beispiel die Baugruppe untersuchen und herausfinden, ob es den Assert-Mechanismus einsetzt. Außerdem könnte es die Methoden und Klassen auflisten, in denen der Code auf seinen Rechten beharrt. Und es könnte potentielle Konflikte mit den Sicherheitsrichtlinien erkennen. Asserts werden, wie erwähnt, häufig gesperrt. Besonders dann, wenn die Komponente nicht auf der lokalen Festplatte installiert ist.
Der wesentliche Nachteil dieser Lösung besteht darin, dass die Methode keine Fehlermeldung mehr abfangen kann, sollte das geforderte Recht nicht gewährt werden. Dieser besondere Nachteil gilt für die Forderung der Rechte mit dem Assert-Mechanismus. Sie werden dieses Problem aber nie haben, wenn Sie mit den deklarativen Attributen einfach nur Rechte beschränken.
Die Aufzählung SecurityAction wird in deklarativen Berechtigungsattributen benutzt und umfasst einige Optionen, mit denen sich die Rechte genauer festlegen lassen, die dem Code eingeräumt werden. Außerdem lassen sich damit die Anforderungen an die Clients festlegen, sei es beim Laden oder zur Laufzeit. Tabelle T2 aus der Dokumentation vom .NET Framework SDK listet diese Optionen auf. Vergleichen Sie zum Beispiel die beiden folgenden Attributsdeklarationen:
[SecurityPermission(SecurityAction.Demand,
UnmanagedCode = true)]
[SecurityPermission(SecurityAction.LinkDemand,
UnmanagedCode = true)]
Wenn die erste dieser Deklarationen auf eine Methode angewendet wird, erfolgt zur Laufzeit bei jedem Aufruf dieser Methode eine normale Überprüfung des Stapels. Wird dagegen die zweite Deklaration eingesetzt, findet die Überprüfung nur einmal für jede Referenz auf die geschützte Methode statt. Das geschieht bei der JIT-Kompilierung (just-in-time). Außerdem verlangt die zweite Deklaration das Recht nur von dem Code, zu dem die Verbindung erfolgt. Eine vollständige Untersuchung des Stapels findet für LinkDemand nicht statt. Im weiteren Verlauf dieses Artikels werde ich gelegentlich auf das eine oder andere Attribut aus dieser Liste zurückkommen.
T2 Die SecurityAction-Aufzählung
|
Befugnis
|
Wann
|
Ziel
|
Anmerkung
|
|
LinkDemand
|
JIT-Zeit
|
Klasse, Methode
|
prüfe direkten Aufrufer
|
|
InheritanceDemand
|
Ladezeit
|
Klasse, Methode
|
prüfe abgeleitete Klassen
|
|
Demand
|
Ladezeit
|
Klasse, Methode
|
prüfe alle Aufrufer
|
|
Assert
|
Laufzeit
|
Klasse, Methode
|
|
|
Deny
|
Laufzeit
|
Klasse, Methode
|
|
|
PermitOnly
|
Laufzeit
|
Klasse, Methode
|
|
|
RequestMinimum
|
Zeitpunkt der Erteilung
|
Baugruppe
|
für Betrieb erforderlich
|
|
RequestOptional
|
Zeitpunkt der Erteilung
|
Baugruppe
|
für Betrieb erwünscht
|
|
RequestRefuse
|
Zeitpunkt der Erteilung
|
Baugruppe
|
nicht erwünscht
|
Attacken gegen die Codezugriffssicherheit
Wenn ich mehr Zeit finde, um mit der Infrastruktur für die Codezugriffssicherheit zu experimentieren, werden sich vermutlich auch noch weitere Angriffspunkte gegen die Codezugriffssicherheit abzeichnen. Derzeit kann ich jedenfalls sagen, dass es einige Angriffsarten gibt, die mit falsch eingesetzten Assertion- und UnmanagedCode-Rechten zusammenhängen. Welche Probleme das Beharren auf den eigenen Rechten (Assertion) mit sich bringt, habe ich bereits angedeutet. Aufrufe in den unverwalteten Code sind ein weiterer kniffeliger Bereich.
Wenn eine Baugruppe Aufrufe in den unverwalteten Code hinein durchführen darf, dann kann sie praktisch alle Vorkehrungen zur Codezugriffssicherheit umgehen. Hat eine Baugruppe zum Beispiel kein Zugriffsrecht auf das lokale Dateisystem, darf aber unverwalteten Code aufrufen, so kann sie ihre unerwünschte Arbeit einfach mit direkten Aufrufen von Win32-Funktionen erledigen. Wie schon erwähnt, unterliegen diese Aufrufe zwar allen weiteren Sicherheitsüberprüfungen, die das Betriebssystem selbst durchführt, aber das ist letztlich keine Garantie. Besonders dann nicht, wenn der Code des Angreifers in eine privilegierte Umgebung geladen wird, zum Beispiel in den Browser des Administrators oder in einen Dämon-Prozess, der in der Systemsitzung läuft.
Von der Anmeldesitzung des Administrators aus kann man sich unschwer vorstellen, wie ein Angreifer mit Hilfe der Win32-Funktionen die Sicherheitsrichtlinien für die lokale Maschine kurzerhand neu definiert. Immerhin werden diese ja "nur" in einer XML-Datei festgelegt, die vom Administrator geändert werden kann. Bei der Gelegenheit könnte der Angreifer dieselbe Win32-Programmierschnittstelle auch benutzen, um die CLR-Maschine zu ersetzen. Nehmen Sie besser keine Wetten mehr an, wenn es dem Angreifer gelingt, mit den Rechten des Administrators unverwalteten Code auszuführen. Allerdings lassen sich solche Angriffspläne durch die sorgfältige Gestaltung und Verwaltung der Sicherheitsrichtlinien durchkreuzen.
Ein weiterer Angriffspunkt hat mit dem Ausufern der Rechte bei einer Ortsänderung zu tun. Wenn eine Baugruppe aus dem Internet via Internet-URL benutzt wird, hat sie normalerweise wesentlich weniger Befugnisse als bei der lokalen Installation. Eines der ersten Ziele eines Angreifers wird es sein, sein Opfer dazu zu überreden, eine Kopie der Baugruppe auf seiner lokalen Festplatte zu installieren. Und schon hat die Baugruppe mehr Rechte. Da so viele Anwender ActiveX-Steuerelemente aus dem Internet installieren, ohne auch nur eine Sekunde darüber nachzudenken, wird sich hier noch ein ernstes Problem auftun. Im Laufe der Zeit werde ich wohl noch weitere potentielle Angriffe finden und bei Gelegenheit beschreiben.
Sicherheitsrichtlinien
In diesem Artikel war immer wieder einmal die Rede von den "Sicherheitsrichtlinien" für die Zuteilung von Codezugriffsrechten. Diese Richtlinien können ziemlich kompliziert werden und lassen sich natürlich leichter verstehen, wenn man erst einmal die Grundlagen begriffen hat. Vor allem ist es wichtig, dass die Rechte auf Baugruppenbasis vergeben werden. Ich habe den Vorgang zur Ermittlung dieser Rechte in drei Basisschritte aufgeteilt - und halten Sie sich fest, denn die Sprachregelung ist (wieder einmal) etwas seltsam:
-
Verschaffen Sie sich einen Beweis (Evidence).
-
Legen Sie diesen Beweis dem System für die Sicherheitsrichtlinien vor und beschaffen Sie sich die zugewiesene Rechtegruppe.
-
Passen Sie diese Rechtegruppe an die Erfordernisse der Baugruppe an.
Beweis / Evidence
Als ich mit meinen CLR-Experimenten begann, schien mir "Beweis" (Evidence) in diesem Zusammenhang noch ein recht seltsamer Begriff zu sein. Es hörte sich so an, als sei die Sicherheitsinfrastruktur von ein paar Anwälten entworfen worden und nicht von Computerwissenschaftlern. Nachdem ich mich aber einige Zeit lang an die CLR gewöhnen konnte, wurde mir klar, das der Name gar nicht schlecht gewählt ist. Im Gerichtssaal liefert ein Beweisstück Informationen, die bei der Beantwortung der Art von Fragen helfen, wie sie in Gerichtssälen üblich sind: "Was war die Tatwaffe?" oder "Wer hat den Vertrag unterschrieben?"
Im Fall der CLR ist ein Beweis die Gruppe von Antworten auf Fragen, die sich aus den Sicherheitsrichtlinien ergeben. Derzeit handelt es sich um folgende Fragen:
-
Von welcher Site wurde die Baugruppe beschafft?
-
Von welchem URL wurde die Baugruppe beschafft?
-
Aus welcher Zone wurde die Baugruppe beschafft?
-
Wie lautet der starke Name der Baugruppe?
-
Wer hat die Baugruppe signiert?
Die ersten drei Fragen sind nur verschiedene Versuche, die Bezugsquelle der Baugruppe festzustellen, während die restlichen beiden Fragen dem Autoren der Baugruppe gelten.
Im Gerichtssaal wird der Beweis von einer Partei vorgelegt, kann aber von der Gegenpartei angezweifelt werden. Gelegentlich muss der Richter aushelfen und entscheiden, ob der Beweis zugelassen wird oder nicht. Im Falle der CLR gibt es zwei Instanzen, die Beweise sammeln können, nämlich die CLR selbst und der Host der Anwendungsdomäne. Da es sich hier natürlich um ein automatisiertes System handelt, gibt es keine Geschworenen. Wer immer Beweise liefert, die berücksichtigt werden sollen, muss soweit vertrauenswürdig sein, dass er keine Beweise fälscht. Das ist der Grund für das spezielle Sicherheitsrecht ControlEvidence. Der CLR traut man natürlich das Vorlegen von Beweisen zu, denn man muss ja sowieso schon darauf vertrauen, dass sie die Sicherheitsrichtlinien durchsetzt. Daher gibt es das Sicherheitsrecht ControlEvidence für die Hosts. Derzeit werden drei Hosts automatisch erfasst, nämlich der Internet Explorer, das ASP.NET und der Shell-Host, der die CLR-Anwendungen von der Systemschale aus startet.
Nehmen wir als Beispiel die folgende Funktion aus der Klasse System.AppDomain:
public int ExecuteAssembly(
string fileName,
Evidence assemblySecurity,
);
Obwohl ein Browser die Baugruppe bereits in einen Cache auf dem lokalen Dateisystem heruntergeladen haben könnte, sollte er den Beweis für den Ursprung der Baugruppe über den zweiten Parameter erbringen.
Auswertung der Sicherheitsrichtlinien
Sobald der Host und die CLR den Beweis zusammengestellt haben, wird er ans System für die Sicherheitsrichtlinien weitergeleitet, und zwar in Form einiger Objekte, die von einem Sammlungsobjekt des Typs Evidence zusammengefasst werden. Die Art eines jeden in dieser Sammlung vertretenen Objekts zeigt die Art des Beweises an, für den es steht. Und es gibt so etwas wie "Beweisklassen", die jeweils eine der oben angeführten Fragen repräsentieren:
Site Url ApplicationDirectory Zone StrongName Publisher
Die Sicherheitsrichtlinien lassen sich auf drei verschiedene Ebenen aufteilen. Jede dieser Ebenen wird durch eine Sammlung serialisierter Objekte repräsentiert. Jedes dieser Objekte wiederum wird "Codegruppe" (code group) genannt und repräsentiert eine Frage, die der Baugruppe gestellt wird, sowie eine Referenz auf die Rechtemenge, die sich daraus ergibt, wenn der Beweis die Frage positiv beantwortet. Technisch wird die Frage "Mitgliedsbedingung" (membership condition) genannt. Die Rechtemenge ist benannt, so dass der Administrator sie wiederverwenden kann. Bild B3 zeigt eine Mitgliedsbedingung und eine entsprechend benannte Rechtemenge.
B3 Eine Codegruppe mit einer Mitgliedsbedingung
Condition = Bedingung
Permission Set = Rechtemenge
Code Group = Codegruppe
Named Permission Set = Rechtemenge (mit Namen)
Read Variable = Lies Variable
(Rest unübersetzt)
Ich fand den Ausdruck "Codegruppe" immer etwas verwirrend. Und da mir noch keine bessere Bezeichnung eingefallen ist, stelle ich mir eine Codegruppe als einen Knoten in dem Graphen vor, der eine Ebene der Sicherheitsrichtlinien darstellt. Bild B4 zeigt, wie eine Reihe von Codegruppen (oder Knoten) eine Hierarchie bilden, die eine einzige Wurzel hat. Vergessen Sie bitte nicht, dass jeder Knoten in der betreffenden Ebene der Sicherheitsrichtlinien eine Mitgliedsbedingung darstellt und eine Referenz auf eine Rechtegruppe. Indem sie die gesammelten Beweise mit den Knoten in der Hierarchie abgleicht, erhält die CLR die Vereinigungsmenge der Rechtemengen, die jene Rechte repräsentiert, die auf dieser Ebene der Sicherheitsrichtlinien gewährt werden. Und weil der Wurzelknoten einfach nur der Anfangspunkt für den Gang durch diese Hierarchie darstellt, passt er zu jedem Code (all code) und bezieht sich von Haus aus auf die Rechtemenge mit dem vielsagenden Namen Nothing. Darin sind - Sie ahnen es - kein Rechte verzeichnet.
B4 Graph einer Ebene der Sicherheitsrichtlinien
Für den Gang durch diesen Graphen gelten einige Regeln. Erstens wird keines der Kinder auf eine Übereinstimmung getestet, wenn schon der Elternknoten nicht passt. Das ermöglicht es dem Graphen, so etwas wie logische AND und OR-Operatoren darzustellen. Zweitens kann jeder Knoten selbst Attribute haben, die sich auf den Gang durch den Graphen auswirken. Das Attribut, von dem hier die Rede ist, heißt Exclusive. Kommt es zu einer Übereinstimmung mit einem Knoten, der das Exclusive-Attribut hat, wird nur die Rechtemenge für diesen speziellen Knoten benutzt. Natürlich hat es unter solchen Umständen keinen Sinn, wenn zwei übereinstimmende Knoten in einer Ebene dieses Attribut haben, was als Fehler angesehen wird. Es ist Sache des Systemadministrators, dafür zu sorgen, dass so etwas nicht geschehen kann. Kommt es trotzdem vor, meldet das System eine PolicyException und die Baugruppe wird nicht geladen.
Bild B5 zeigt ein Beispiel für eine Baugruppe, die von der berühmten ACME Corporation signiert und von https://q.com/downloads/foobar.dll heruntergeladen wurde. Sie sehen, dass nur einer der Publisher-Knoten beim Gang durch den Graphen passt. (RM steht für Rechtemenge.) Die linke Hälfte dieses Graphen stellt einige logische AND-Beziehungen für Code dar, der von der ACME Corporation stammt. Sie besagt: "Code, der von der ACME Corporation herausgegeben UND aus dem Internet heruntergeladen wurde, erhält die Rechtemengen bar und baz, während Code, der von der ACME Corporation herausgegeben UND lokal installiert wurde, die Rechtemengen foo und gimp erhält."
B5 Gang durch den Graphen der Sicherheitsrichtlinien.
Vielleicht fragen Sie sich an diesem Punkt, warum ich ständig von Ebenen der Sicherheitsrichtlinien rede. Der Grund ist, dass es drei solcher Ebenen gibt, von denen jede einen solchen Graphen mit Knoten enthält, wie in Bild B4 gezeigt. Es gibt eine Ebene mit den Sicherheitsrichtlinien der Maschine, eine zweite mit den Sicherheitsrichtlinien der Anwender und eine dritte mit den Richtlinien der Anwendungsdomäne (Application Domain). In dieser Reihenfolge werden sie auch ausgewertet. Die resultierende Rechtemenge ist die Schnittmenge (intersection) der Rechtemengen, die sich beim Gang durch die Graphen aus diesen drei Ebenen ergeben haben.
Die Ebene der Anwendungsdomäne ist aus technischer Sicht optional und wird dynamisch vom Host bereitgestellt. Das naheliegendste Beispiel dafür ist ein Webbrowser, der sich die Option freihalten möchte, die Richtlinien in seinen Anwendungsdomänen zu verschärfen.
Bild B6 zeigt, wie man sich das Zusammenspiel der Ebenen vorstellen kann. Mit einem bestimmten Attribut (eines von sehr vielen, die uns in der nächsten Zeit noch begegnen werden) lässt sich der Gang durch die Richtlinienebenen an einem bestimmten Knoten beenden, nämlich mit dem Attribut LevelFinal. Wird dieses Attribut an einem passenden Knoten entdeckt, so werden keine weiteren Ebenen durchlaufen. Das gibt dem Domänenadministrator zum Beispiel die Möglichkeit, auf der Ebene der Maschinenrichtlinien Vorgaben einzurichten, die sich von den Anwendern nicht mehr durch andere Vorgaben auf der Anwenderebene ändern lassen.
B6 Die drei Ebenen der Sicherheitsrichtlinien
Feinabstimmung der Rechtemengen
Nachdem die CLR die Rechtemenge bestimmt hat, die sich aus diesen drei Ebenen ergeben, erhält die Baugruppe im nächsten Schritt selbst die Gelegenheit, sich zu äußern. Wie erwähnt, kann der Code zur Laufzeit eine Feinabstimmung an der verfügbaren Rechtemenge vornehmen, sei es programmgesteuert oder deklarativ, indem er Rechte fordert oder ablehnt. Nun, eine Baugruppe kann die Rechte, die ihr zugestanden werden, durch den sorgfältigen Einsatz der folgenden drei Elemente aus der SecurityAction-Aufzählung optimieren (siehe auch Tabelle T2):
SecurityAction.RequestMinimum SecurityAction.RequestOptional SecurityAction.RequestRefuse
Eigentlich sagen die Namen der Elemente schon alles. Wenn die Sicherheitsrichtlinien es nicht zulassen, dass der Baugruppe die Mindestmenge an Rechten zugestanden wird, dann läuft die Baugruppe eben nicht. Vorsichtig eingesetzt ermöglicht es dieser Wert, bestimmte Annahmen über die Umgebung zu machen. Unter Umständen wird dadurch die Programmierung etwas einfacher. Übertreibt man es aber, hinterlässt dieser Wert einen pelzigen Belag auf der Zunge. Wenn Sie zum Beispiel mit RequestMinimum nach allen möglichen Rechten fragen, die Ihre Baugruppe vielleicht braucht, wird sie sich öfter als nötig nicht laden lassen. Das könnte auch einen Administrator dazu verleiten, seine Ansprüche etwas weiter als erforderlich zu lockern, nur damit Ihre Komponente endlich läuft.
RequestRefuse scheint dagegen zumindest in diesen Anfangszeiten ein Hilfsmittel zu sein, das man liberaler einsetzen darf. Damit können Sie sich selbst Rechte absprechen, die Sie laut Sicherheitsrichtlinie eigentlich haben. Nutzen Sie die Gelegenheit, die Rechtemenge abzulehnen, von der Sie wissen, dass Ihre Baugruppe sie nicht braucht. Es kann wohl nicht schaden, auf Nummer Sicher zu gehen.
RequestOptional schließlich ermöglicht die Angabe von bestimmten optionalen Rechten, auf die man zwar verzichten könnte, die man aber auch ausnutzen würde, sofern sie verfügbar sind. Dieser Wert ist von Nutzen, wenn Ihre Baugruppe eine zusätzliche optionale Funktionalität anbietet, für die sie ein paar zusätzliche Rechte braucht.
Ist die Menge der Rechte gegeben, die sich aus der Sicherheitsrichtlinie ableiten, und sind die Mindestrechte, die optionalen Rechte und die von der Baugruppe abgelehnten Rechte bekannt, so ergibt die folgende Formel laut CLR-Dokumentation die Rechte, die der Baugruppe gewährt werden:
Rechte = M + (O ∩ P) - R
Mit M = Minimum request, O = Optional request, P = Rechte nach Sicherheitsrichtlinien (Policy) und R = Refused permissions.
Anzeige und Bearbeitung der Sicherheitsrichtlinien
Wenn Sie sich näher mit den Sicherheitsrichtlinien beschäftigen möchten, dann schauen Sie sich CASPOL.EXE an, das Hilfsprogramm für die Richtlinien zur Codezugriffssicherheit. Die folgenden Zeilen zeigen einige von meinen Lieblingsbefehlen, die Ihnen den Einstieg erleichtern werden:
caspol -a -listgroups
caspol -a -resolvegroup c:\inetpub\wwwroot\bar.dll
caspol -a -resolveperm c:\inetpub\wwwroot\bar.dll
Die erste Zeile veranlasst das Programm zur Anzeige der Codegruppen aus der Maschinen- und Anwenderebene. Wenn Sie sich die Ergebnisse genauer anschauen, werden Sie eine Knotenhierarchie erkennen, wobei jeder Knoten eine Mitgliedsbedingung hat, gefolgt vom Namen einer Rechtemenge. Das zweite Beispiel fordert eine Liste der übereinstimmenden Codegruppen für eine bestimmte Baugruppe an, während das dritte die Rechte der Baugruppe ermittelt.
Schauen Sie sich an, wie sich die Verhältnisse ändern, wenn Sie dieselbe Baugruppe über HTTP ansprechen, zum Beispiel:
caspol -a -resolvegroup http://localhost/foo.dll
Obwohl man CASPOL.EXE auch zur Änderung der Sicherheitsrichtlinien verwenden kann, ziehe ich es doch vor, einfach einen Texteditor zu starten und die Richtliniendatei von Hand zu bearbeiten - imerhin handelt es sich um ein XML-Dokument. Machen Sie unbedingt mindestens eine Sicherungskopie der Originaldatei, falls Sie das auch vorhaben. Derzeit findet man die Datei unter %SYSTEMROOT%\ComPlus\v2000.14.1812\security.cfg. Vielleicht lautet die Versionsnummer bei Ihnen anders, aber Sie werden die Datei schon finden. Die Anwender-Sicherheitsrichtlinien werden im selben Pfad unter dem Anwenderprofilverzeichnis gespeichert. Derzeit gewähren die Standardrichtlinien dem gesamten Code FullTrust, was im Endeffekt bedeutet, dass die Sicherheitsrichtlinien völlig von den Maschinenrichtlinien bestimmt werden.
Fazit
Das Konzept der Codezugriffssicherheit führt in Kombination mit der Codeverifizierung in der CLR einen wesentlichen Schritt von der Laisser-faire-Haltung weg, die sich auf den bisherigen Generationen der Plattform breitgemacht hatte, wo es modern war, mit DLLs umherzuwerfen statt große monolithische Anwendungen zu entwickeln, die zwar klobiger, möglicherweise aber auch sicherer waren.
Die Codezugriffssicherheit berücksichtigt endlich den Umstand, dass Anwendungen heutzutage aus Komponenten zusammengesetzt werden. Außerdem fließt dabei die Herkunft der Komponenten in die Entscheidungen ein, die unter Sicherheitsaspekten getroffen werden. Schwerpunkt der Vorkehrungen ist die Prävention und nicht die nachträgliche Verfolgung, nachdem das Kind in den Brunnen gefallen ist. Davon sollten auch die vielen Anwendungen profitieren, die sich zunehmend für mobile Geräte herausbilden.
Codezugriffssicherheit ist allerdings kein Allheilmittel. Sie bringt eine ganze Menge Komplexität mit sich, von der nicht zuletzt die Administration betroffen ist. Ohne fähige Administratoren, die bereit sind, die Zeit zu investieren, die erforderlich ist, um sich mit neuen Konzepten vertraut zu machen, könnte sich dieser Mechanismus auch zu dem Schutzschirm entwickeln, unter dem viele neue Angriffsarten geboren werden. Es ist zu empfehlen, sich mit der nicht ganz unbefleckten Historie der Sicherheitskonzepte unter Java zu beschäftigen, wo sich die Entwickler nun schon einige Jahre mit mobilem Code herumschlagen, und dies mit wechselndem Erfolg. Man sollte diese Erfahrungen bei der Bewertung der neuen Architektur nicht außer Acht lassen.
Besuchen Sie mich auf meiner Webseite, wo ich die neusten Nachrichten zum Thema .NET-Sicherheit sammle und entsprechenden Beispielcode vorstelle, neben Referenzen auf wichtige Arbeiten über Sicherheit im mobilen Code.