Was darf mein Code? - Das Sicherheitsmodell der Common Language Runtime

Veröffentlicht: 13. Feb 2006

Von Michael Willers

Mit der enormen Verbreitung des Internet ist es eine Tatsache geworden: Jeder Rechner, der "online geht", wird früher oder später Opfer eines Angriffs. Das haben Schreckensgespenster wie "Sasser" in der Vergangenheit leider eindrucksvoll bewiesen. Häufige Ursache dieser Angriffe ist mobiler Code, der über das Internet oder Intranet heruntergeladen und auf dem lokalen Rechner ausgeführt wird. Prominentes Beispiel dafür sind ActiveX-Controls. Sie werden heruntergeladen und mit den Rechten des aktuell angemeldeten Benutzers ausgeführt. Sie können ein erhebliches Sicherheitsrisiko verursachen, da die meisten Benutzer noch immer mit Administratorrechten arbeiten.

Code Access Security ist das Sicherheitsmodell der Common Language Runtime (CLR), um mobilen Code abzusichern. Dabei wird Code grundsätzlich in einer abgeschotteten Umgebung (Sandbox) ausgeführt und die CLR erlaubt oder unterbindet den Zugriff auf Systemressourcen anhand einer Sicherheitsrichtlinie. Diese Richtlinie basiert auf den Merkmalen des Codes, der geladen und ausgeführt werden soll.

In der Praxis sorgt dieses Sicherheitssystem oft für Unmut, da es beispielsweise Zugriffe auf Netzwerklaufwerke scheinbar konsequent unterbindet. Höchste Zeit also, die Konzepte genauer zu beleuchten.

Auf dieser Seite

 Und warum?
 So funktioniert's!
 Frisch an's Werk!
 Nicht "Wer hat's erfunden?", sondern "Was braucht's?"
 Windows hat das letzte Wort
 Stand der Dinge
 Der Autor

Und warum?

Mit dem bekannten Sicherheitsmodell von Windows kann die Ausführung von mobilem Code nicht überwacht werden. Dieses System arbeitet ressourcen-basiert. Ein Benutzer meldet sich Namen und Passwort am System an und bekommt einen sogenannten Security-Token zugewiesen. Ressourcen werden über sogenannte Access Control Lists (ACLs) abgesichert.

Der Token "wandert", vereinfacht gesagt, durch das System und immer dann, wenn auf eine Ressource, wie z.B. eine Datei zugegriffen wird, prüft das System anhand es Tokens, ob der Benutzer in der ACL eingetragen ist oder nicht. Entsprechend wird der Zugriff dann gewährt oder unterbunden.

Dieses Modell kann also nur funktionieren, wenn der Benutzer von vornherein bekannt ist. Aber genau das ist bei mobilem Code, der auf den lokalen Rechner heruntergeladen wird, nicht der Fall. Hier können keine Annahmen über den Benutzer gemacht werden, der den Code ausführen wird. Er ist schlichtweg nicht bekannt.

Code Access Security (CAS) arbeitet hingegen code-basiert. Sobald ein Assembly geladen wird, untersucht die CLR dieses Assembly und legt fest, welche Codemerkmale (Evidence) es besitzt. Auf der Basis dieser Merkmale werden dann dem Assembly anhand einer Sicherheitsrichtlinie Zugriffsrechte zugeordnet.

Kurz gesagt ergänzt CAS das Sicherheitsmodell von Windows.

 

So funktioniert's!

Das Sicherheitssystem der CLR schützt den Zugriff auf Systemresourcen über Berechtigungen (Permissions). Berechtigungen können mit Hilfe eines Berechtigungsatzes (Permission Set) gruppiert werden. Dieser Vorgang ist mit den von Windows her bekannten Benutzern und Benutzergruppen vergleichbar. Ein Berechtigungssatz ist dabei grundsätzlich einer Codegruppe (Code Group) zugeordnet.

Sobald ein Assembly geladen wird, untersucht die CLR dieses Assembly und legt fest, welche Codemerkmale (Evidence) es besitzt. Die Zuordnung eines Codemerkmals zu einer Codegruppe ? und damit den Rechten, die das Assembly zur Laufzeit erhält ? erfolgt über die sogenannte Zugehörigkeitsbedingung (Membership Condition). Bild 1 zeigt diesen Zusammenhang.

Wichtig dabei: Besitzt ein Assembly mehrere Codemerkmale, wird es auch mehreren Codegruppen gleichzeitig zugeordnet. Die jeweiligen Rechtemengen werden dabei zu einer Vereinigungsmenge zusammengeführt (ODER-Verknüpfung)!

Durch dieses Verhalten kann ein Assembly mehr Rechte zugewiesen bekommen, als ursprünglich vorgesehen. Aus diesem Grund kann eine Codegruppe mit dem Attribut exklusiv versehen werden. Das Assembly bekommt dann nur die Berechtigungen dieser einzelnen Gruppe zugewiesen.

Sofern in Assembly gleichzeitig zwei oder mehreren Codegruppen zugeordnet ist, die das Attribut Exklusiv besitzen, "belohnt" die CLR diesen Umstand mit einer Policy Exception und fordert so indirekt auf, das Design der Richtlinie zu überdenken.

Code Access Security illustriert
Bild 1: Code Access Security illustriert

 

Frisch an's Werk!

Um die gewonnenen Erkenntnisse auch in die Praxis umzusetzen, soll als Beispiel eine bewusst einfach gehaltene Anwendung dienen, die aus zwei Assemblies mit den Bezeichnungen FormApp und FormLib besteht. Ihr Funktionsumfang lässt sich im Wesentlichen auf folgenden Satz reduzieren: Das Assembly FormApp.Exe wird gestartet und lädt das Assembly FormLib.dll, welches eine Datei erzeugt und beschreibt. Listing 1 zeigt die die entsprechenden Codefragmente.

// ----------
// FormApp.cs
// ----------
//...
//
private void button1_Click(object sender, System.EventArgs e)
{
try
{
string codebase = textBox1.Text;
Assembly a = Assembly.LoadFrom(codebase);
Type t = a.GetType("samples.Security.FormLib");
object o = Activator.CreateInstance(t);
Form f = (Form)o;
f.ShowDialog();
o = null;
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
//
// ...
//
// ----------
// FormLib.cs
// ----------
private void button1_Click(object sender, System.EventArgs e)
{
try
{
string file = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
file += @"\test.log";
StreamWriter logfile = File.AppendText(file);
logfile.WriteLine(textBox1.Text);
logfile.Close();
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
//
// ...
//

Listing 1: Arbeit im Duo - Laden eines Assemblies und Schreiben auf eine Datei

Die Beispielanwendung in Aktion!
Bild 2: Die Beispielanwendung in Aktion!

Startet man nun diese Anwendung lokal, funktioniert sie so wie erwartet. Der Anwender klickt auf "Load Form", anschließend auf "Write text file" und es wird eine Datei mit der Bezeichnung test.log im Dokumentverzeichnis (My Documents) des Anwenders abgelegt (siehe Bild 2).

Kopiert man diese Anwendung nun auf einen Webserver oder ein Netzwerklaufwerk (wie z.B. https://localhost/FormTest/) und startet die Anwendung dann erneut, so lässt sich das Assembly FormLib.dll zwar jetzt laden, aber das Schreiben auf die Datei wird mit einer Exception "belohnt" und der Zugriff verweigert (Achtung: Vergessen Sie nicht, den Eintrag in der Textbox um die URL bzw. den Pfad zum Fileshare zu ergänzen).

Offensichtlich greift hier das Sicherheitssystem von .NET ein. Und in der Tat verhindern die bereits vordefinierten Richtlinien das Schreiben auf eine Datei. Aber alles der Reihe nach:

  • Die Common Language Runtime "schaut" sich den Code an, der geladen und ausgeführt werden soll, und stellt fest, dass er ein Merkmal (Evidence) "Zone" besitzt. Dieses Merkmal besitzt in den Wert "Local Intranet".

  • Jedes Codemerkmal wird über eine sogenannte "Membership Condition" einer Codegruppe zugeordnet. Die vorhandenen Richtlinien sorgen dafür, dass der Code der Codegruppe "LocalIntranet_Zone" zugeordnet wird.

  • An einer Codegruppe "hängen" die Rechte, die definieren, auf welche Systemressourcen der Code Zugriff hat. Sie lassen sich zu Rechtemengen, auch Permission Sets genannt, zusammenfassen. Der Codegrupe "LocalIntranet_Zone" ist das Permission Set "LocalIntranet" direkt zugeordnet

  • Das Permission Set "LocalIntranet" besitzt nicht das Recht, auf eine Datei zu schreiben, da das Recht "File IO" dort nicht aufgeführt ist. Die Anwendung quittiert diese Tatsache mit einer Exception und die Ausführung wird vorzeitig beendet.

Die Bilder 3 und 4 zeigen diesen Zusammenhang anhand der .NET Administrationskonsole im Detail.

Einteilung nach Maß ? Codemerkmal und Zugehörigkeitsbedingung
Bild 3: Einteilung nach Maß ? Codemerkmal und Zugehörigkeitsbedingung

Sag mir, was du darfst ? Rechte gruppiert
Bild 4: Sag mir, was du darfst ? Rechte gruppiert

Um die Anwendung zum "Laufen" zu bringen sind die nachfolgend aufgeführten Schritte erforderlich:

  1. Festlegen eines Codemerkmals (Evidence)

  2. Definieren der Zugehörigkeitsbedingung (Membership Condition) und zuordnen zur Codegruppe

  3. Erstellen einer Codegruppe

  4. Ermitteln der benötigten Rechte

  5. Gruppieren der Rechte zu einer neuen Rechtemenge (Permission Set) und zuordnen zur Codegruppe

 

Nicht "Wer hat's erfunden?", sondern "Was braucht's?"

Standardmäßig sind im .NET Framework insgesamt sieben verschiedene Codemerkmale (Evidence) bereits vordefiniert. Als da wären: Application Directory, Hash, Publisher, Site, Strong Name, URL und Zone. Stellt sich nun die Frage, welches Merkmal für die Beispielanwendung in Frage kommt.

Das Merkmal ist von zentraler Bedeutung und sollte daher fälschungssicher sein. Anders gesagt: Es muss kryptografisch stark sein. Damit bleiben Hash, Publisher und Strong Name übrig. Ein Hash ist der digitale Fingerabdruck eines Assemblies. Das bedeutet, dass jedes Assembly einen anderen Hash besitzt.

Das Problem dabei: Die Sicherheitsrichtlinie soll für die gesamte Anwendung gelten und es muss dafür gesorgt werden, dass alle Assemblies, die zur Anwendung gehören, derselben Richtlinie unterliegen.

Kurz: Alle Assemblies müssen das gleiche Merkmal besitzen und ein Hash scheidet daher aus. Geht man davon aus, das eine Infrastruktur zum Erstellen und Überprüfen von Zertifikaten nicht unbedingt überall vorhanden ist, bleibt letztenendes nur der Strong Name als Merkmal übrig.

Ein weitaus schwierigerer Punkt ist das Ermitteln der einzelnen Rechte, die die Anwendung benötigt. Ein Klassiker in den Newsgroups ist oftmals der Spruch: "Setz es auf "Full Trust", dann geht's". Das allerdings ist kein gute Idee, sondern geradezu eine Provokation für einen Angreifer, Ihren Rechner doch mal nach allen Regeln der Kunst auszuhebeln. Wie aber findet man nun die benötigten Rechte?

Wichtig ist es, die Anwendungsfälle (Use Cases) zu dokumentieren. Mit ihrer Hilfe lässt sich die zentrale Frage klären, auf welche Ressourcen (Verzeichnisse, Drucker, Registry, etc.) eine Anwendung zugreift und welche Rechte sie damit benötigt.

Nur mit diesem Wissen besteht eine Chance, im Falle eines Angriffs systematisch vorzugehen und das Problem zu orten. Anders gesagt: Auf diesem Weg können die benötigten Rechte klar dokumentiert und die Angriffsfläche von Anfang an auf ein Minimum reduziert werden.

Führt man diese Analyse für die Beispielanwendung durch, bleibt am Ende die nachfolgend aufgeführte Liste von Rechten (Permissions) übrig:

  • Security - Enable assembly execution
    Das Assembly darf geladen und dessen Code ausgeführt werden. Das Benutzen von Funktionen des .NET Frameworks, die als gefährlich angesehen werden, bleibt jedoch untersagt. Dazu zählt beispielsweise das Anlegen und Verwalten eigener Application Domains. Mehr dazu unter [1].

  • User Interface - Windowing: Safe top-level windows
    Ein Top Level Window ist das Hauptfenster einer Anwendung und dient als "Parent" für weitere Fenster der Anwendung (Forms, MessageBoxen, etc.). Safe bedeutet in diesem Zusammenhang, dass das Aussehen des Fensters per Code nicht verändert werden kann. Entsprechende Eigenschaften wie Größe, Sichtbarkeit, Transparenz, Titelleiste oder der dessen Koordinaten auf dem Desktop werden vom .NET Framework als "read only" markiert. Somit werden Angriffsversuche über unsichtbare Fenster oder geschickte Fenstermanipulationen wie etwa dem "Vorgaukeln" von Login-Dialogen von vornherein verhindert. Weitere Informationen zum Thema "Top level windows und Subwindows" finden sich unter [2] und [3].

  • User Interface - Clipboard: No Clipboard
    Die Zwischenablage wird schlichtweg nicht gebraucht. Deswegen wird der Zugriff auch nicht erlaubt.

  • Web Access - Host: https://localhost/Formtest/FormLib.dll - Accept, Connect
    Connect ist erforderlich, damit der Client eine Verbindung zum Server herstellen kann, um das Assembly FormLib.dll herunterzuladen. Es wird auf dem Client ebenso wie das Assembly FormApp.exe in einem gesonderten Speicherbereich, dem sogenannten Download Assemby Cache, ablegt. Aus diesem Grund muss ebenfalls das Flag Accept gesetzt sein, da vom Server aus der Zugriff auf eine lokale Ressource auf dem Client erfolgt.

  • File IO - C:\Documents and Settings - Read, Write, Append, Path Desc.
    Hier kann nur ein fester Pfad "eingebrannt" werden (z.B. C:\Temp\Test.log oder C:\Temp). Zudem bedeutet hier die Angabe eines Verzeichnisses ohne Dateiname, dass die vorgenommenen Einstellungen "by design" für das Verzeichnis und sämtliche Unterverzeichnisse gelten!

    Damit entsteht aber ein gewichtiges Problem: Ein Benutzer sollte immer nur in sein eigenes Benutzerprofil schreiben. Alle anderen Verzeichnisse werden vom Betriebssystem standardmäßig über entsprechende Access Control Lists (ACLs) so abgesichert, dass ein normaler Benutzer dort keine Schreibrechte hat. Dieser Pfad ist jedoch für jeden Benutzer unterschiedlich, da sein Name Bestandteil des Profils ist (auf der Maschine des Autors beispielsweise C:\Documents and Settings\Michael Willers).

    Oder anders gesagt: Das Benutzerprofil wird zur Laufzeit ermittelt. Die File IO-Permission verlangt durch die Angabe eines absoluten Pfades aber eine Entscheidung zur Entwicklungszeit!! Der Schlüssel zu einer möglichen Lösung liegt dabei im Zusammenspiel beider Sicherheitssysteme:

    • Der Code der Anwendung sorgt dafür, dass das Anlegen und Beschreiben von Dateien nur im Dokumentverzeichnis (My Documents) des Benutzerprofils erfolgt. Die Zeile Environment.GetFolderPath(Environment.SpecialFolder.Personal); erledigt diese Aufgabe.

    • Man schaltet das Basisverzeichnis für Benutzerprofile (Documents and Settings) über die .NET Sicherheitsrichtlinen frei.

    • Das .NET Sicherheitssystem setzt auf dem Sicherheitssystem des Betriebssystems auf. Letzteres sorgt dafür, dass ein Benutzer standardmäßig immer nur in sein Profil schreiben darf.

    • Somit ist zur Laufzeit gewährleistet, dass der Code lediglich Schreibrechte im Benutzerprofil hat und Daten ausschliesslich im Dokumentverzeichnis abgelegt werden.

    Das Flag Path Desc. (Path Discovery) bedeutet übrigens, dass das .NET Framework relevate Informationen über den angegebenen Pfad selbst ermitteln kann. Dazu zählt beispielsweise die darunterliegende Verzeichnisstruktur. Dieses Flag wird von der API-Funktion GetFolderPath benötigt, um beispielsweise das physikalische Verzeichnis zu ermitteln, das einem Benutzerprofil zugeordnet ist.

Mit diesem Wissen gerüstet, kann nun die Sicherheitsrichtlinie für die Beispielanwendung erstellt werden. Allerdings sind dazu natürlich Administratorechte erforderlich. Nachfolgend die notwendigen Schritte:

  1. Öffen der .NET Administrationskonsole (mscorfcg.msc, Bestandteil des .NET Framework SDK)

  2. Öffnen des Knotens "Runtime Security Policy"

  3. Öffnen des Knotens "Machine"

  4. Öffnen des Knotens "Code Groups"

  5. Öffnen des Knotens "All_Code"

  6. Anlegen einer neuen Codegruppe (Rechte Maustaste / Eintrag New...)

  7. Namen für die Codegruppe festlegen

  8. Festlegen der Zugehörigkeitsbedingung (Strong Name)

  9. Die Datei FormApp aus der Beispielanwendung auswählen, um den Strong Name zu ermitteln (Klick auf "Import...")

  10. Anlegen eines neuen Berechtigungssatzes

  11. Namen für den Berechtigungssatz festlegen

  12. Die Rechte (siehe oben) festlegen

  13. Einrichten der Sicherheitsrichtlinie abschließen (Klick auf "Next", dann "Finish")

Um dafür zu sorgen, dass ausschließlich die soeben erstellte Sicherheitsrichtline ausgewertet wird, muß die Codegruppe noch, wie eingangs erwähnt, mit dem Attribut "Exklusiv" versehen werden. Dazu sind folgende Schritte notwendig:

  1. Öffen der .NET Administrationskonsole (mscorfcg.msc, Bestandteil des .NET Framework SDK)

  2. Öffnen des Knotens "Runtime Security Policy"

  3. Öffnen des Knotens "Machine"

  4. Öffnen des Knotens "Code Groups"

  5. Öffnen des Knotens "All_Code"

  6. Die soeben angelegte Codegruppe markieren und deren Eigenschaften anzeigen

  7. Häkchen setzen beim Eintrag "This policy level will only have the permissions..."

  8. Geänderte Eigenschaften übernehmen

Jetzt stellt sich natürlich spontan die Frage, ob es nicht wesentlich einfacher geht und sich der gesamte Vorgang nicht automatisieren lässt. In der Praxis wird oft der einfachste Weg gewählt: Als Zugehörigkeitsbedingung wird der Strong Name benutzt und als Berechtigungssatz "Full Trust" gewählt.

Das funktioniert, ist aber keine wirklich gute Idee. Denn wenn der Code vollstes Vertrauen besitzt, kann er eben wirklich "alles tun".

 

Windows hat das letzte Wort

Soweit vorgedrungen stellt sich die Frage, wer denn nun zuerst "dran" kommt: Das Sicherheitssystem von Windows oder Code Access Security?

Beim Starten einer .NET-Anwendung wird zuerst immer ein Betriebssystemprozess gestartet. Dieser Prozess lädt die CLR, die dann ihrerseits die .NET-Anwendung ausführt. Die Anwendung läuft also mit den Benutzerrechten des Prozesses und das sind in der Regel die Rechte des aktuell angemeldeten Benutzers. Die spannende Frage lautet also: Was passiert, wenn das Sicherheitssystem der CLR den Zugriff auf eine Systemressource ? wie z.B. eine Datei - erlaubt, der dazugehörige Betriebssystemprozess dies aber aufgrund seiner Rechte verweigert?

Kurze Antwort: Der Zugriff wird verweigert. Das Betriebssystem hat immer das letzte Wort. Und frei nach den Worten des regierenden Bürgermeisters von Berlin ist das auch gut so. Wäre es anders, könnte ein Stückchen Code das komplette Sicherheitssystem von Windows aushebeln.

 

Stand der Dinge

Code Access Security ist das Sicherheitsmodell der Common Language Runtime (CLR), um mobilen Code abzusichern. Es setzt auf dem Sicherheitsmodell von Windows auf und ergänzt dessen Mechanismen.

Dabei wird Code grundsätzlich in einer abgeschotteten Umgebung (Sandbox) ausgeführt und die CLR erlaubt oder unterbindet den Zugriff auf Systemressourcen anhand einer Sicherheitsrichtlinie. Diese Richtlinie basiert auf den Merkmalen des Codes, der geladen und ausgeführt werden soll. Sie muss auf dem Client, auf dem die Anwendung ausgeführt werden soll, vorhanden sein.

Damit stellt sich automatisch die Frage: Wie installiert man Sicherheitsrichtlinien automatisch auf dem Client? Sie wird Dreh- und Angelpunkt des nächsten Artikels zum Thema Code Access Security sein.

[1] https://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpconDangerousPermissionsPolicyAdministration.asp

[2] https://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnwui/html/msdn_styles32.asp

[3] https://msdn.microsoft.com/library/default.asp?url=/library/en-us/vbcon/html/vbconadditionalsecurityconsiderationsinwindowsforms.asp

 

Der Autor

Digitale Sicherheit und Softwareentwicklung sind für Michael Willers die anspruchsvollsten Aufgaben unserer Zeit. Er hat über 10 Jahre Projekterfahrung in der Softwareentwicklung in den Branchen Versicherung, Mobilfunk, Bankwesen und Robotik. Im Zentrum seiner Tätigkeit stehen dabei E-Commerce-Lösungen und B2B-Integration auf der Microsoft .NET Plattform. Nach einem mehrjährigen Engagement als technischer Berater bei Microsoft Deutschland unterstützt er heute als Senior Software Architect die newtelligence AG und deren Kunden bei der Verwirklichung ihrer Ziele und Visionen. Seine technischen Schwerpunkte sind WebServices und verteilte Anwendungen mit Fokus auf Systemsicherheit. Sie finden sein Blog unter http://staff.newtelligence.net/michaelw

Artikel-Serie „Code Access Security“ im Überblick:

Teil 1: Was darf mein Code? - Das Sicherheitsmodell der Common Language Runtime

Teil 2: Sandkastenspiele. Deployment von Sicherheitsrichtlinien für die Common Language Runtime