Donnerstag, 24. Mai 2012


Artikel

Mai 2006 | Artikel

Sicherheitsphilosophie à la .NET

(Link zum Artikel: http://www.entwickler.de/dotnet//000830)

Best Practices zum Thema Sicherheit

Text: Damir Dobric
  • Teilen
  • kommentieren
  • empfehlen
  • Bookmark and Share
Da .NET-Anwendungen laut Umfragen noch keinen großen Anteil an der IT-Infrastruktur eines Unternehmens besitzen, spielen sie für die zuständigen Administratoren keine wichtige Rolle. Dieselben Umfragen zeigen aber, dass das Thema Sicherheit bei vielen Entwicklern noch unterbesetzt ist und daher in der Architektur der Anwendung nicht berücksichtigt wird. Bevor eine Lawine an Sicherheitsproblemen ausgelost wird, zeigt dieser Artikel einige notwendige und hochinteressante Best Practices, die auch als offizielle Microsoft-Empfehlung gelten.

Sicherheit ist ein Begriff, der in der Informationstechnologie in der letzten Dekade immer mehr an Bedeutung gewonnen hat. Wenn es nach Microsoft ginge und im nächsten Jahr wieder eine Jahrtausendwende anstünde, wäre das Wort Sicherheit ganz bestimmt das meist wiederholte Wort des Microsoft-Milleniums. Das mag wohl vielleicht etwas ironisch klingen, aber einige Fakten sprechen dafür, dass die meisten IT-Profis (beispielsweise ein klassischer Systemadministrator) eigentlich noch nichts von Begriffen wie CAS, Code-Sicherheit, Policy Levels oder Permission Demands gehört haben. Dieser Artikel geht davon aus, dass die Leser mit den Grundlagen der .NET-Security bereits vertraut sind. Er wird sich daher ausschließlich auf eine Reihe neuer Best Practices konzentrieren, die in den nächsten Jahren zur allgemeinen IT-Bildung gehören werden.

Die Definition der Sicherheit
Gleich zu Beginn eine etwas ernüchternde Feststellung: Den Begriff Sicherheit in der IT-Welt zu definieren, ist gar nicht so einfach. Er ist nicht mit einer Formel definierbar und häufig sehr subjektiv. Einfach ausgedrückt, Sicherheit bedeutet das Schützen von Ressourcen. Das können Daten, E-Mails oder Webseiten sein. Wie gut die Ressourcen gesichert sind, lässt sich ebenfalls nicht einfach beschreiben. Deshalb ist die Sicherheit eines Systems sehr relativ. Der Sicherheitsgrad lässt sich kaum auf eine messbare Zahl reduzieren. Selbst wenn es so eine Zahl geben würde, wäre dies bestimmt die Phantomzahl 42. Es ist für viele vielleicht schwer zu glauben, aber es existieren Verfahren (z.B. DREAD), die es Fachleuten ermöglichen die Sicherheit zumindest statistisch zu berechnen.
Die vier Säulen der Sicherheit
Wenn Sicherheit ein Vektor wäre, wäre sie durch folgende Werte bestimmt: Authentication, Authorization, Confidential-ity und Integrity – sie bilden die Säulen der Sicherheit. Wer sie nicht kennt, weiß nicht, was Sicherheit ist und kann keine sichere Anwendung implementieren und noch weniger konzipieren. Authentication bestimmt, wer der Benutzer bzw. Caller ist. Das ist in der Windows-Welt am häufigsten ein Active-Directory-Benutzer. In der Welt von Embedded-Anwendungen spricht man selten von Benutzern und häufiger von Geräten, die authentifiziert werden sollen. Authorization bestimmt, was der authentifizierte Benutzer bzw. das Gerät tun darf. Sie bestimmt die Zugriffsrechte auf alle Ressourcen in einer bestimmten Problemdomäne. Die Autorisierung bildet eine Relation zwischen Benutzer (bzw. Gerät), Zugriffsberechtigungen und Ressourcen. Hier ein Beispiel: Der Benutzer U1 besitzt eine IO-Permission für die Datei Abc.xml. Allgemein ausgedrückt bestimmt Autorisierung Folgendes: “Wer besitzt welche Berechtigung an welchen Ressourcen?” Confidentiality (häufig als privacy bekannt) sichert, dass bestimmte Inhalte von unerlaubten Personen nicht gelesen werden können. Die Confidentiality steht immer in direktem Zusammenhang mit Kryp-tografie. Auch hier ein Beispiel: Eine E- Mail wird mit S/MIME- oder PGP-Verfahren verschlüsselt, sodass sie nur von be- stimmten Personen gelesen werden kann. Integrity wird häufig mit Confidentiality verwechselt. Während bei Confidentiality die Daten verschlüsselt werden, werden bei Integrity die Inhalte signiert oder quasisigniert. Es soll gewährleistet werden, dass bei der Übertragung die Daten nicht geändert wurden. Bei der Signierung der Daten spielen die Hash-Funktionen eine tragende Rolle. In der neueren Zeit spricht man häufig von zwei weiteren Säulen: Auditing und Availability. Sie stärken auf ihre praktische und weniger theoretische Weise den Weg zum stabilen und sicheren Betrieb eines Systems. Bei Auditing geht es vor allem um effektives Logging, sodass immer verfolgt werden kann, welcher Benutzer welche Aktivitäten durchgeführt hat. Bei Availability wiederum soll ein sicheres System den dauerhaften Betrieb des Systems bestimmen.
Kategorisierung eines Angriffes - STRIDE
In der letzten Zeit gibt es in der IT-Theorie verständlicher Weise die Tendenz eine Lösung im Kontext verteilter Anwendungen zu betrachten. Das bedeutet, dass eine Anwendung kaum noch isoliert betrachtet werden kann. Die moderne Konzipierung bei der Sicherheit sieht eine Gruppierung mehrerer Anwendungen in einem verteilten System vor. Ein solches System läuft in einem Rechenzentrum auf Basis einer Infrastruktur. Damit ergeben sich auch die typischen Rollen: Anwender, Administrator, Security-Administrator, Datenbank-Administrator, Entwickler, Architekt, Tester usw. Jeder Beteiligte dieser Kette hat in seiner Problemdomäne eigene Anforderungen. Die Idee ist, durch verschiedene Blickwinkel das Problem Sicherheit zu kategorisieren und dadurch alle Anforderungen zu erfüllen. Sicherheit ließe sich damit so kategorisieren:
  • Netzwerksicherheit
  • Host-Sicherheit
  • Anwendungssicherheit
Das Ziel bei dieser Aufteilung ist nicht die Sicherheit akademisch zu betrachten, sondern einen praktischen Fahrplan zum Beispiel für ein Security-Design und -Review zu erstellen. So gibt es für jede Schicht eine weitere Aufteilung der potentiellen Angriffsszenarien. Bei Netzwerksicherheit teilt man die Angriffe in diese Unterkate-gorien auf: Information Gathering, Sniffing, Spoofing, Session Hijacking und Denial of Service. Bei der Host-Sicherheit spricht man von Viren, Trojanern, Würmern, Foot printing, Profiling, Password Cracking, Unauthorized Access und Denial of Service. Bei der Anwendungssicherheit sind Input-Validierung, Autorisierung und Authentifizierung das Mindeste, was ein Entwickler tun muss, um eine Anwendung zu sichern. Was viele Entwickler nicht wissen: Ein sauberes Exception-Handling und Logging zählen nicht nur zur Anwendungslogik, sondern auch zur Sicherheit allgemein. Aus der Perspektive jeder dieser Schichten werden bei Microsoft alle möglichen Angriffe allgemein wie folgt kategorisiert:
  • Spoofing ist ein Angriff, bei dem der Angreifer versucht, sich als jemand anders auszugeben. Das könnte dann der Fall sein, wenn der Angreifer die Zugangsdaten von einem anderem Benutzer gestohlen hat oder wenn der Angreifer seine IP-Adresse fälscht. Der Schaden, der durch Spoofing entstehen könnte, kann unter anderem durch erfolgreichen Einsatz von Authentication und Authorization minimiert werden.
  • Tampering stellt die unerlaubte Veränderung von Daten dar, die z.B. über das Netzwerk übertragen werden oder in einer Datei enthalten sind. Solche Attacken kann man in der Regel durch Einführung einer digitalen Signatur unterbinden (siehe Integrity).
  • Reputation ist ein Vorfall, bei dem ein Benutzer nicht zugibt, dass er eine unerlaubte Aktion durchgeführt hat. Solche Vorfälle kann man mit der Einführung von effektivem Auditing unterbinden.
  • Information Disclosure ist ein Prozess, bei dem ein Angreifer die geheimen oder nicht öffentlichen Informationen offen legt oder offen legen könnte. Um diesen zu unterbinden, verwendet man z.B. Verschlüsselung (siehe Confidentiality).
  • Denial of Service ist ein Versuch ein System lahm zu legen. Dies geschieht am einfachsten, wenn man einen Dienst ständig mit verschiedenen Anfragen “bombardiert”.
  • Elevation of Privilege tritt ein, wenn ein Benutzer mit wenigen Rechten die Rechte eines Benutzers anwendet, der über mehr Rechte verfügt. Das passiert am einfachsten, wenn z.B. ein Administrator seinen Arbeitsplatz verlässt, ohne den Rechner zu locken.
Um sich diese sechs Angriffstypen ein wenig einfacher merken zu können, fasst man den ersten Buchstaben jeder dieser Akronyme zusammen - das Ergebnis: STRIDE. Eine solche Kategorisierung ist sehr nützlich, weil sie uns zwingt, bewusst die potentiellen Sicherheitslücken zu kategorisieren und daraus eine wirkungsvolle Verteidigungsstrategie abzuleiten (siehe “Die Säulen der Sicherheit”). Es lässt sich an dieser Stelle darüber streiten, ob eine solche Aufteilung tatsächlich die einzige oder die beste ist. Wenn Sie daran zweifeln, versuchen Sie einen Angriff zu finden, der nicht in eine der STRIDE-Kategorien passt. Abbildung 1 zeigt ein Architekturbild einer Webanwendung mit allen potentiellen Schwachstellen einer verteilten Anwendung. Ähnlich zeigt die Abbildung 2 die Schwachstellen einer einzigen Assembly.
Bereits diese zwei unterschiedlichen Beispiele zeigen erstaunlich viele potentielle Sicherheitslücken. An dieser Stelle könnte man daher berechtigt und ein wenig provozierend fragen: “Wie sicher ist überhaupt eine Anwendung, die von einem Hobbyprogrammierer mit einem Rapid Development Tool entwickelt wurde?”.
Partially-Trusted-Anwendungen
Umfragen unter Entwicklern zeigen, dass .NET-Anwendungen in der IT-Infrastruktur noch keine allzu große Rolle spielen. Sie werden daher von Administratoren auch nicht besonders unter die Lupe genommen. Ein gewöhnlicher Administrator konzentriert sich gegenwärtig vor allem auf die üblichen Sicherheitsrichtlinien (User Security), die mit .NET CAS und Code Security koexistieren. Der offensichtliche Nachteil, der an dieser Stelle auch nicht weiter kommentiert werden soll, ist, dass diese Nichtbeachtung Entwicklern sehr viel freien Raum gibt. Entwickler dürfen und sollten daher nicht davon ausgehen, dass irgendjemand dafür sorgen wird, dass ihre Anwendungen die notwendigen Berechtigungen auch tatsächlich erhalten - Eigeninitiative ist gefragt. Um potentiellen Angreifern möglichst wenig Angriffsfläche in Gestalt von Sicherheitslöchern zu bieten, lautet ein ungeschriebenes Gesetz, Software möglichst defensiv zu entwickeln. Das bedeutet, dass man nicht wie heute noch üblich von einer Full-Trust-Umgebung ausgehen darf. Entwickler sollten sich vielmehr darauf einstellen, dass ihre Anwendung nicht alles tun darf, sondern Berechtigungen gezielt festgelegt werden müssen. Häufig vergisst man deklarativ (explizit) die notwendigen Berechtigungen anzufordern. Um diese schlechte Gewohnheit zu umgehen, gibt es unter anderem, folgende Warnung im Visual Studio 2005:
  1. "CA2209 warning: No valid permission requests were found
  2. for assembly You should always specify the minimum
  3. security permissions using SecurityAction.Request
  4. Minimum If assembly permission requests have been
  5. specified, they are not enforceable; use the PermView.
  6. exe tool to view the assembly’s permissions. Whidbey
  7. customers can use PermCalc.exe which gives even
  8. more detailed information."
Beim Entwurf einer Anwendung sollte es daher neben der Anwendungsarchitektur auch eine Sicherheitsarchitektur geben, in der unter anderem alle Berechtigungen definiert werden, welche die Anwendung benötigt. Am Ende werden dann diese Berechtigungen überprüft und es wird sichergestellt, dass zur Laufzeit die Anwendung nur mit diesen Berechtigungen läuft. Ein solches Konzept wird das kommende Visual Studio 2005 über Click Once anbieten.
Sandboxing durchleuchtet
Die Forderung nach einer Sicherheitsarchitektur klingt logisch und einfach. Leider ist dieses Problem sehr komplex. Als Lösung bietet sich das Konzept oder Entwurfsmuster der Partially-Trusted-Anwendungen an. Dies sind Anwendungen, die keine Full-Trust-Berechtigungen besitzen (siehe Kasten “APCTA und Partially Trusted Assemblies”). Dabei sind zwei wichtige Aspekte zu unterscheiden: Der eine besteht darin, Teile einer Anwendung (oder einer Assembly) unabhängig von der Sicherheits-Policy zu mache.
APCTA und Partially Trusted Assemblies
Die Abkürzung APTCA steht für „Allow Partially Trusted Callers Assemblies“. Eine Assembly ist per Definition Partially Trusted, wenn sie keine Full-Trust-Berechtigung besitzt. Auf der anderen Seite kann eine Strong Named Assembly per Definition nie aus einer Partially Trusted Assembly aufgerufen werden. Wenn eine Full Trusted Assembly über eine Methode aus einer anderen Assembly aufgerufen wird, gibt die .NET-Laufzeit im Hintergrund ein so genanntes Link Demand auf alle Public-Methoden aus: [PermissionSet(SecurityAction.LinkDemand, Name=“FullTrust“)] Dieser Link Demand überpruft, ob der Aufrufer Full-Trust-Berechtigungen besitzt. Da dieses Demand nicht den ganzen Call-Stack überprüft, ist die Performanz wesentlich besser als bei einem üblichen Demand. Dadurch ist aber auch die Sicherheit möglicherweise nicht so hoch. Um das Problem zu umgehen, existiert ein Attribut mit dem Namen AllowPartiallyTrustedCallersAttribute. Dieses Attribut stellt ein Signal für die .NET-Laufzeit dar, dass für Assemblies mit einem starken Namen kein Full Trust Link Demand im Hintergrund ausgegeben werden soll. Der folgende Codeausschnitt zeigt die Deklaration des Attributes AllowPartiallyTrustedCallersAttribute, das auch die Aufrufe aus Partially Trusted Assemblies ermoglicht:
[assembly: AllowPartiallyTrustedCallersAttribute()]
namespace AptcaStrongNamedAssembly
{
public class Class1
{
public Class1()
{
}
}
}
Beachten Sie, dass auch eine nicht-signierte Assembly „full trusted“ sein kann und in diesem Fall eine signierte Assembly aufrufen darf. Dies ist z.B. der Fall, wenn eine Anwendung von der lokalen Maschine ausgeführt wird. Abbildung 3 zeigt die Fehlermeldung, die in dem kommenden Visual Studio 2005 (Beta 1) erscheint, wenn eine Partially Trusted Assembly eine Full Trusted Assembly aufruft. Damit das alles nicht so einfach bleibt, kann es noch immer passieren, dass eine Strong Named Assembly mit AllowPartiallyTrustedCallersAttribute-Attribut einige Typen besitzt, die explizit ein Full Trust Demand ausgeben. In diesem Fall bleibt das Attribut fur alle anderen Typen in der Strong Named Assembly gültig. Der Code-Ausschnitt in Listing 1 zeigt zwei Klassen. Die Klasse Class1 kann dank des Attributs von Partially Trusted Assemblies instanziiert werden. Die Klasse Class2 kann durch expliziten Link Demand dagegen nicht von Partially Trusted Assemblies verwendet werden: Da fast alle Systembibliotheken im GAC installiert sind (also Strong Named und Full Trusted), könnten sie ohne Attribut gar nicht von Partially Trusted Assemblies aufgerufen werden. Aus diesem Grund sind folgende Bibliotheken mit diesem Attribut versehen: System.Windows.Forms.dll, System.Drawing.dll, System.dll, Mscorlib.dll, IEExecRemote.dll, Accessibility. dll, MicrosoftVisualBasic.dll, System.Xml.dll, System.Web.dll, System.Webservices.dll und System.Data.dll.
Bei der zweiten Variante wird angestrebt, die ganze Anwendung von den benötigten Berechtigungen isoliert zu starten. Beide Probleme werden mit dem Sandboxing-Verfahren gelöst. Die meisten Entwickler verbinden mit dem Begriff Sandboxing lediglich das Reduzieren von Berechtigungen auf einen kleinsten gemeinsamen Nenner. Das ist nur bedingt richtig, denn die Berechtigungen können durch Sandboxing auch erhöht werden. Hier ein Fall aus der Praxis: Eine schon seit langem problemlos laufende Anwendung kann plötzlich auf einzelne Ressourcen, wie das Eventlog, die Registry oder OleDb nicht mehr zugreifen, nachdem eine neue Sicherheitspolicy aktiviert wurde. Auch wenn es als Entwickler Ihr Job sein wird, dieses Problem zu lösen, muss die politische Frage nach dem Schuldigen erlaubt sein. Ist es der Administrator, der eine wichtige Policy aktiviert hat, ohne den Entwickler zu fragen, oder am Ende doch der Entwickler, weil er für diesen Fall keine Vorkehrungen getroffen hat? Ein solches Szenario kann zum Beispiel dann eintreten, wenn die Anwendung in einer anderen Anwendung gehostet wird (zum Beispiel als Webanwendung). Das kommende ASP.NET 2.0 wird daher die Möglichkeit bieten, eine Webanwendung unter einer beliebigen Policy (nicht nur Full Trust) auszuführen. Wird diese für eine Anwendung aktiviert, deren Entwickler von Full Trust ausgegangen ist, gibt es grundsätzlich zwei Möglichkeiten das Problem zu umgehen:
  • Policy Customizing
  • Sandboxing
Variante 1 bedeutet, dass der Entwickler seiner Anwendung einfach mehr Berechtigungen zuweist. Diese Variante ist attraktiv, weil keine Änderungen am Code erforderlich sind. Leider haben es Entwickler mehr und mehr mit Administratoren zu tun, die Microsoft entweder nicht viel Vertrauen entgegenbringen oder die deren Produkte einfach nicht besonders mögen. Dann bleibt nur noch ein Ausweg: Das Sandboxing-Pattern.
Das Sandboxing-Pattern
Vereinfacht formuliert bietet Sandboxing Entwicklern die Möglichkeit, ihre Anwendung ohne äußeren Einfluss (z.B. durch “böse Administratoren”) mit notwendigen Berechtigungen isoliert auszuführen. Es ist daher wichtig zu unterscheiden, dass das Sandboxing-Pattern auf eine Anwendung oder auch auf eine Assembly angewendet werden kann (abhängig von der Motivation). Das Assembly-Sandboxing-Pattern kann im Allgemeinen so beschrieben werden:
  • Alle notwendigen Funktionalitäten werden in eine Wrapper-Assembly (der Sandbox) zusammengefasst. Das wird bei ASP.NET-2.0-Anwendungen hilfreich sein, weil die Anwendung selbst in der Hand des Administrators liegt. Es ist generell eine gute Idee, den größten Teil des Code in eine andere DLL zu verlagern und möglichst wenig dem Host zu überlassen.
  • Die Assembly wird in der Regel im GAC installiert. Dies ist eine Möglichkeit, der Assembly Full-Trust-Berechtigungen zu geben.
  • Es sollte ein Assert für alle relevanten Berechtigungen (siehe Kasten “Logging als Sandbox”) ausgeführt werden. Damit wird eine Stack-Überprüfung für die betroffene Berechtigung unterbunden. Somit muss der Aufrufer kei- ne Berechtigungen für diese Ressource besitzen, um sie zu verwenden
  1. [assembly: AssemblyKeyFile(...\.SomeKey.Snk)]
  2. [assembly: AllowPartiallyTrustedCallersAttribute()]
Vereinfachungen bei .NET 2.0
Wird eine Anwendung (nicht eine Assembly) unter dem kommenden .NET Framework 2.0 mit Click Once installiert, wird sie automatisch mit den in der Manifestdatei definierten Rechten hochgefahren. Dafür sorgen die Tools Dfsvc.exe (Deployment Framework Service) und AppLaunch.exe. Dfsvc.exe ist für die Installation zuständig und AppLaunch.exe sorgt für Sandboxing-Funktionalität. Wenn eine Anwendung mit Full-Trusted-Berechtigungen läuft, wird AppLaunch.exe nicht benötigt.
Sandboxing beim .NET Framework 1.x
Da Click Once erst mit dem kommenden Visual Studio 2005 zur Verfügung stehen wird, wird ein Weg gesucht, durch den sich das Sandboxing selbst “stricken” lässt. Ein solches Pattern könnte wie folgt aussehen:
  • Die Sicherheitseinstellungen sollten im Code erstellt oder aus einer Datei geladen werden.
  • Es muss eine neue AppDomain erzeugt werden.
  • Die Sicherheitseinstellungen werden der neuen Domäne zugewiesen.
  • Die Anwendung wird in die Domäne geladen und ausgeführt.
Ein Beispiel zu diesem sicherlich hochinteressanten Pattern enthält Listing 4. Es zeigt wie die Einstellungen komplett neu erstellt werden können. Der Code sieht zwar etwas trocken aus, verrät aber sehr viel über die Funktionsweise der .NET-Sicherheit. Frei nach dem Motto: “Das meiste, was nicht schmeckt, ist gesund”. Zuerst wird mit dem Aufruf von CreateAppDomainLevel ein Application Domain Policy Level erzeugt (siehe Kasten “Policy Level Evaluation”). Jeder Policy Level muss ein Root-Element (Code-Gruppe) besitzen. Ein Root-Element hat einen speziellen Charakter und enthält immer eine AllMembershipCondition und ein PermissionSet . Dies wird durch eine UnionCodeGroup -Codegruppe erstellt. Das bedeutet, dass alle Rechte in dieser Code-Gruppe einer Union-Operation unterliegen. Sie werden addiert und nicht gegenseitig ausgeschlossen (Intersection). Die AllMembershipCondition bezeichnet den ganzen ausführbaren Code. Das zugewiesene PermissionSet rootPs auf diesem Level kann, je nach Bedarf, entweder die Ausführung grundsätzlich sperren oder aber erlauben: PermissionSet.None oder PermissionSet.Unrestricted .
Policy Level Evaluation
In .NET gibt es vier Richtlinienebenen (Policy Levels): Enterprise, Machine, User und Domain. Die ersten drei sind den meisten Entwicklern durch den .NET Framework Configuration Wizard (das .NET-Konfigurationsprogramm der Systemsteuerung) bekannt. Der Domain-Level kann nicht mit dem Wizard konfiguriert werden. Historisch hat sich dieser Policy-Level eigentlich schon in .NET 1.0 durch ASP.NET-Anwendungen ergeben. Heute wird er für Sandboxing verwendet und gehört zum festen Bestandteil einer Click-Once-Anwendung. Jeder Policy-Level enthält die Code-Gruppen mit entsprechenden Rechten. Jede Code-Gruppe enthält eine MembershipCondition und definiert ein PermissionSet . Die MembershipCondition stellt den Ursprung des Codes fest und weist diesem einen Satz an Berechtigungen, die über das PermissionSet definiert sind, zu. Beim Start der Anwendung wird innerhalb jedes Policy-Levels überpruft, welche Gruppe fur den auszuführenden Code zuständig ist. Alle Gruppen, die vorhandene MembershipConditions erfüllen, werden zusammenaddiert (vereint) und bilden somit eine Menge von Rechten. Die resultierenden Rechte von allen vier Policy Levels (es muss beachtet werden, dass der AppDomain Policy Level nicht immer vorhanden ist) bilden durch eine Schnittmenge die endgültige Menge von Rechten ab. Der Codeausschnitt in Listing 2 zeigt, wie sich herausfinden lasst, welche MembershipCondition vom gerade auszuführenden Code zutrifft. Das ist eine sehr interessante Technik, die Ihnen ermöglicht, die Sicherheitsprobleme zu verstehen, die mit keinem gängigen Tool nachzuvollziehen sind.
  1. Listing 2
  2. public static void ResolveMembershipCondition(Evidence evidence)
  3. {
  4. IEnumerator policyEnumerator = SecurityManager.PolicyHierarchy();
  5. while (policyEnumerator.MoveNext())
  6. {
  7. PolicyLevel currentLevel = (PolicyLevel)policyEnumerator.Current;
  8. System.Diagnostics.Trace.WriteLine("\n\t" + currentLevel.Label + " Level");
  9. CodeGroup cg1 = currentLevel.ResolveMatchingCodeGroups(evidence);
  10. System.Diagnostics.Trace.WriteLine("\t\tCodeGroup = " + cg1.Name);
  11. IEnumerator cgE1 = cg1.Children.GetEnumerator();
  12. while (cgE1.MoveNext())
  13. {
  14. System.Diagnostics.Trace.WriteLine("\t\t\tGroup = " + ((CodeGroup)cgE1.Current).Name);
  15. }
  16. System.Diagnostics.Trace.WriteLine("\tStoreLocation = " + currentLevel.StoreLocation);
  17. }
  18. }
Wenn eine Codegruppe (im Root des Policy Level) erstellt wird, die zu einem späteren Zeitpunkt konfiguriert werden soll, muss Sie immer ein Full Trust (Unrestricted) PermissionSet enthalten. Beispiele für solche Gruppen sind die root- Elemente des Enterprise und User Policy Level. Viele .NET-Entwickler sind der Meinung, dass diese zwei Gruppen keine Auswirkung haben. Das ist deshalb der Fall, weil alle manuell erstellten Gruppen mit Full-Trust-Berechtigungen kombiniert und aus diesem Grund mathematisch einfach ignoriert werden. Um dies zu testen, erzeugen Sie im Enterprise Policy Level eine neue Gruppe namens MySecurityApp (Abbildung 4). Egal welches PermissionSet (PS) Sie dieser Gruppe zuweisen, die Einstellungen werden ignoriert, weil der resultierende Berechtigungssatz eine Vereinigung (Union) aus einem Full-Trust-Berechtigungssatz und Ihrem Berechtigungssatz ist:
  1. FullTrust + PS = FullTrust
Die Frage liegt nahe, warum dies im Machine Policy Level funktioniert? Die Antwort lautet: Weil das Root PermissionSet im Enterprise Level auf PermissionSet . Unrestricted und das entsprechende PermissionSet des Machine Level auf PermissionSet.None gesetzt ist. Genau diese kleine “versteckte” Einstellung deutet dezent an, dass die Enterprise und User Policy Level nicht funktionieren würden.
  1. Listing 3
  2. public static void CreateSource(string source, string log)
  3. {
  4. try
  5. {
  6. EventLogPermission ePerm = new EventLogPermission();
  7. ePerm.Assert();
  8. if (!EventLog.Exists(source))
  9. {
  10. EventLog.CreateEventSource(source, log);
  11. }
  12. }
  13. finally
  14. {
  15. CodeAccessPermission.RevertAssert();
  16. }
  17. }
Um dies zu umgehen, gibt es zwei Möglichkeiten: Sie setzen die Code-Gruppe auf exclusive (Abbildung 5) oder das PermissionSet im Root Node vom Enterprise bzw. User Policy Level auf PermissionSet.None . Beachten Sie dabei, dass Sie in diesem Fall dafür sorgen müssen, dass die abgeleiteten Code-Gruppen richtig konfiguriert sind. Ansonsten werden viele Anwendungen auf den betroffenen Maschinen einfach nicht mehr funktionieren.
  1. Listing 4
  2. Erzeugen eines Custom Policy Level
  3. public static PolicyLevel CreateDomainLevelPolicy()
  4. {
  5. // Create an AppDomain policy level.
  6. PolicyLevel pLevel = PolicyLevel.CreateAppDomainLevel();
  7. // No any permission added.
  8. PermissionSet rootPs = new PermissionSet (PermissionState.None);
  9. // The root code group of the policy level combines all
  10. // permissions of its children.
  11. UnionCodeGroup rootCodeGroup;
  12. // No any code has permission to any resource.
  13. rootCodeGroup = new UnionCodeGroup(
  14. new AllMembershipCondition(),
  15. new PolicyStatement(rootPs, PolicyStatementAttribute.Nothing));
  16. // Create Full-Trust permission.
  17. PermissionSet myPs1 = new PermissionSet(PermissionState.Unrestricted);
  18. // Create the FullTrust to all assemblies from the
  19. // specified Url.
  20. UnionCodeGroup myCodeGroup1 = new UnionCodeGroup(
  21. new UrlMembershipCondition("file:///D:\*"),
  22. new PolicyStatement(myPs1, PolicyStatementAttribute.Nothing));
  23. // Set some name.
  24. myCodeGroup1.Name = "MyCodeGroup1";
  25. // Add code groups to the domain policy level.
  26. rootCodeGroup.AddChild(myCodeGroup1);
  27. pLevel.RootCodeGroup = rootCodeGroup;
  28. return pLevel;
  29. }
Noch einmal zurück zum letzten Beispiel. Nachdem die Root-Gruppe erstellt wurde, ist es notwendig, die gewünschten Berechtigungen hinzuzufügen. In dem Beispiel wird die Codegruppe myCodeGroup1 erzeugt, in der alle Anwendungen auf Laufwerk D: eine Full Trust Permission besitzen. Da aber Full Trust eigentlich der Realität nicht ganz entspricht, sollte man die Berechtigungen etwas mehr “tunen”. Der Codeausschnitt in Listing 5 zeigt, wie alle Anwendungen auf Laufwerk D: den uneingeschränkten Zugriff auf das C:\Temp-Verzeichnis erhalten (weitere Informationen finden Sie in der Beispielanwendung zu diesem Artikel).
  1. Listing 5
  2. Definition einer neuen Code-Gruppe mit Full-Trust-Berechtigungen
  3. // Set some name.
  4. myCodeGroup1.Name = "MyCodeGroup1";
  5. // Create Full-Trust permission.
  6. PermissionSet myPs1 = new PermissionSet(PermissionState.None);
  7. // Adds File IO permission to c:\temp folder.
  8. myPs1.AddPermission(
  9. new System.Security.Permissions.FileIOPermission(
  10. FileIOPermissionAccess.AllAccess, @"c:\Temp"));
  11. // Create the FullTrust to all assemblies from the
  12. // specified Url.
  13. UnionCodeGroup myCodeGroup1 = new UnionCodeGroup(
  14. new UrlMembershipCondition("file:///D:\*"),
  15. new PolicyStatement(myPs1, PolicyStatementAttribute.Nothing));
Am Ende entsteht eine Domäne, welche die erzeugten Richtlinien der Domäne zuweist und die gewünschte Anwendung in die Domäne lädt und startet. Mehr ist nicht erforderlich. Der Codeausschnitt in Listing 6 zeigt wie dies geht.
  1. Listing 6
  2. public static void CreateSandbox(PolicyLevel pLevel, string app)
  3. {
  4. AppDomain sandBoxedDomain = AppDomain.CreateDomain("Sandboxed Domain");
  5. sandBoxedDomain.SetAppDomainPolicy(pLevel);
  6. sandBoxedDomain.ExecuteAssembly(app);
  7. }
Logging als Sandbox
Der folgende Code-Ausschnitt zeigt am Beispiel des EventLogs wie durch Sandboxing einer Assembly die vollständige Kontrolle über die Sicherheit übernommen werden kann. In diesem konkreten Beispiel geht es um die Rechte, die eine Anwendung benötigt, um in das Ereignisprotokoll schreiben zu können. Dahinter verbirgt sich das Recht auf einen Registry-Zugriff im Rahmen der EventLog-Klasse. Abhängig davon, ob die verwendete EventLog-Source bereits existiert, benötigen Sie für diesen Zweck die Lese- oder Lese- und Schreibe-Berechtigungen für die Registry. Der kritischere Fall ist das Schreiben, weil dabei ein neuer Key in die Registry eingetragen werden muss. Grundsätzlich gilt, dass wenn Sandboxing für eine Assembly (eine Funktionalität) vorgenommen werden soll, sollten Sie eine Wrapper Assembly erstellen und die gewünschten Funktionalitäten implementieren. Diese Assembly soll zuerst signiert und mit dem APCTA-Attribut versehen werden.
[assembly: AssemblyKeyFile(...\.SomeKey.Snk)]
[assembly: AllowPartiallyTrustedCallersAttribute()]
Dadurch ist es gewährleistet, dass Partially Trusted Assemblies die Sandbox Assembly aufrufen können (siehe Kasten „PCTA und Partially Trusted Assemblies“). In der Wrapper Assembly ist es außerdem notwendig, möglichst am Anfang einen Assert-Befehl für die betroffenen Berechtigungen (in diesem Fall EventLogPermission) auszuführen. Dieser stellt sicher, dass die Berechtigungen des Aufrufers nicht auf dem Call Stack (außerhalb der Sandboxed Assemblies) abgefragt werden. Der Code-Ausschnitt in Listing 3 zeigt vereinfacht das Sandboxing-Design-Pattern an einem konkreten Beispiel. Beachten Sie, dass dieselbe Technik für alle anderen Ressourcen angewendet werden kann. Es ist wichtig zu erwähnen, dass das Wrapper Assembly „Full Trusted“ sein muss. Bei Webanwendungen sollte die im GAC installierte Assembly zusätzlich in die assembly-Liste (siehe Assembly Tag in Web.Config) eingetragen werden.
Fazit
In diesem Artikel wurden viele interessante Sicherheitseinsätze und Betrachtungen vorgestellt, die Entwicklern, Architekten und mutigen Administratoren wichtige theoretische und praktische Sicherheitshinweise an die Hand geben sollen. Das .NET Framework 1.1 bietet ein Sicherheitskonzept, das dem jetzigen Stand der Technik ohne Zweifel entspricht. Es darf aber nicht vergessen werden, dass gerade diese Vielzahl von Möglichkeiten einige Probleme in Zukunft mit sich bringt. Der Autor hofft daher, dass diese kostbaren Beispiele Ihnen helfen, sichere Anwendungen zu schreiben.
Damir Dobric ist Geschäftsführer und Managing Developer der Firma DAENET GmbH, deren Schwerpunkt die Entwicklung von Unternehmenslösungen ist. Er ist Experte im Bereich der Microsoft-Technologien, publiziert regelmäßig in diversen Fachzeitschriften zum Thema .NET und ist Sprecher auf Entwicklerkonferenzen. Sie erreichen Ihn unter ddobric@daenet.de.

Kommentare