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
- 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.
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:"CA2209 warning: No valid permission requests were foundfor assembly You should always specify the minimumsecurity permissions using SecurityAction.RequestMinimum If assembly permission requests have beenspecified, they are not enforceable; use the PermView.exe tool to view the assembly’s permissions. Whidbeycustomers can use PermCalc.exe which gives evenmore detailed information."
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.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.
- Policy Customizing
- Sandboxing
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
[assembly: AssemblyKeyFile(...\.SomeKey.Snk)][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 Anwendung wird in die Domäne geladen und ausgeführt.
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.
Listing 2public static void ResolveMembershipCondition(Evidence evidence){IEnumerator policyEnumerator = SecurityManager.PolicyHierarchy();while (policyEnumerator.MoveNext()){PolicyLevel currentLevel = (PolicyLevel)policyEnumerator.Current;System.Diagnostics.Trace.WriteLine("\n\t" + currentLevel.Label + " Level");CodeGroup cg1 = currentLevel.ResolveMatchingCodeGroups(evidence);System.Diagnostics.Trace.WriteLine("\t\tCodeGroup = " + cg1.Name);IEnumerator cgE1 = cg1.Children.GetEnumerator();while (cgE1.MoveNext()){System.Diagnostics.Trace.WriteLine("\t\t\tGroup = " + ((CodeGroup)cgE1.Current).Name);}System.Diagnostics.Trace.WriteLine("\tStoreLocation = " + currentLevel.StoreLocation);}}
FullTrust + PS = FullTrust
Listing 3public static void CreateSource(string source, string log){try{EventLogPermission ePerm = new EventLogPermission();ePerm.Assert();if (!EventLog.Exists(source)){EventLog.CreateEventSource(source, log);}}finally{CodeAccessPermission.RevertAssert();}}
Listing 4Erzeugen eines Custom Policy Levelpublic static PolicyLevel CreateDomainLevelPolicy(){// Create an AppDomain policy level.PolicyLevel pLevel = PolicyLevel.CreateAppDomainLevel();// No any permission added.PermissionSet rootPs = new PermissionSet (PermissionState.None);// The root code group of the policy level combines all// permissions of its children.UnionCodeGroup rootCodeGroup;// No any code has permission to any resource.rootCodeGroup = new UnionCodeGroup(new AllMembershipCondition(),new PolicyStatement(rootPs, PolicyStatementAttribute.Nothing));// Create Full-Trust permission.PermissionSet myPs1 = new PermissionSet(PermissionState.Unrestricted);// Create the FullTrust to all assemblies from the// specified Url.UnionCodeGroup myCodeGroup1 = new UnionCodeGroup(new UrlMembershipCondition("file:///D:\*"),new PolicyStatement(myPs1, PolicyStatementAttribute.Nothing));// Set some name.myCodeGroup1.Name = "MyCodeGroup1";// Add code groups to the domain policy level.rootCodeGroup.AddChild(myCodeGroup1);pLevel.RootCodeGroup = rootCodeGroup;return pLevel;}
Listing 5Definition einer neuen Code-Gruppe mit Full-Trust-Berechtigungen// Set some name.myCodeGroup1.Name = "MyCodeGroup1";// Create Full-Trust permission.PermissionSet myPs1 = new PermissionSet(PermissionState.None);// Adds File IO permission to c:\temp folder.myPs1.AddPermission(new System.Security.Permissions.FileIOPermission(FileIOPermissionAccess.AllAccess, @"c:\Temp"));// Create the FullTrust to all assemblies from the// specified Url.UnionCodeGroup myCodeGroup1 = new UnionCodeGroup(new UrlMembershipCondition("file:///D:\*"),new PolicyStatement(myPs1, PolicyStatementAttribute.Nothing));
Listing 6public static void CreateSandbox(PolicyLevel pLevel, string app){AppDomain sandBoxedDomain = AppDomain.CreateDomain("Sandboxed Domain");sandBoxedDomain.SetAppDomainPolicy(pLevel);sandBoxedDomain.ExecuteAssembly(app);}
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.




