Realisierung geschützter Speicherbereiche für Funktionen des Linux-Systemkerns

Ordnungsmerkmale

erschienen in: <kes> 2003#3, Seite 49

Rubrik: BSI Forum

Schlagwort: Speicherschutz

Best Student Paper Award

Dieser Beitrag errang auf dem 8. Deutschen IT-Sicherheitskongress des BSI im Mai 2003 den Best Student Paper Award. Die Begründung des Programmbeirats lautete: Herr Stecklina hat mit seiner Diplomarbeit einen eindrucksvollen Beitrag hin zum anspruchsvollen Ziel zuverlässiger und sicherer Betriebssysteme vorgestellt. Eine kontrollierbare und vertrauenswürdige Speicherverwaltung als wichtige Voraussetzung für die Manipulationssicherheit ausführbarer Programmcodes ist ein wichtiger Schritt in die IT-sichere Zukunft.

Zusammenfassung: Die Entwicklung eines erweiterten Speichermanagements für das Betriebssystem Linux auf der Basis des Fiasco-Mikrokerns, dessen Design eine separate und damit sichere Speicherverwaltung für ladbare Module erlaubt, ist das Thema einer Diplomarbeit an der Brandenburgischen TU Cottbus. Die Module werden durch Adressraumgrenzen voreinander und vor Funktionen des Linux-Kerns geschützt. Die Interaktion zwischen den einzelnen Modulen kann nur über die zuvor definierten Schnittstellen erfolgen. Die angestrebten Schutzziele sind Vertraulichkeit und Integrität der Komponenten. Eine Funktion des Systems soll nicht in der Lage sein, auf Speicherbereiche einer anderen Komponente zuzugreifen ohne die von der Komponente exportierten Schnittstellen zu nutzen.

Autor: Von Oliver Stecklina, Brandenburgische TU Cottbus

Das Betriebssystem Linux besteht im Wesentlichen aus zwei Bereichen: dem Systemkern und den Nutzerprogrammen. Der Betriebssystemkern ist die Basiskomponente von Linux und wird im SuperVisor-Modus des Prozessors ausgeführt. Dies ist notwendig, da einige Operationen des Prozessors nur in diesem Modus aufgerufen werden können. Nutzerprogramme werden im User-Modus ausgeführt und verfügen über getrennte Adressräume. Dies verhindert, dass sie sich gegenseitig beeinflussen können.

Benötigt ein Nutzerprogramm Zugriff auf eine Systemkomponente, so ist dies nur über zuvor definierte Schnittstellen möglich. Damit kann verhindert werden, dass Nutzerprogramme unbeschränkten Zugriff auf Komponenten des Systems erlangen. Alle Zugriffe können durch Funktionen des Systemkerns reglementiert werden.

Während die Anzahl der Nutzerprogramme und damit der Funktionsumfang eines Systems zur Laufzeit beliebig verändert werden kann, hatte der Systemkern von Linux ursprünglich eine statische Struktur. Diese monolithische Architektur erwies sich als zu unflexibel. Eines der Probleme war, dass oftmals ein sehr großer Systemkern erstellt werden musste, der Funktionalität enthielt, die nur selten oder gar nicht verwendet wurde. Erst mit der Einführung von ladbaren Kernel-Modulen (LKM) war es möglich den Funktionsumfang des Systemkerns zur Laufzeit variabel zu gestalten. Ein LKM wird mithilfe eines Nutzerprogramms geladen und anschließend mittels Systemfunktionen in den Betriebssystemkern integriert. Eine Funktion eines Moduls besitzt somit die gleichen Rechte und Privilegien wie eine Operation des statischen Kerns. Damit wird es möglich, selten genutzte Gerätetreiber erst dann in den Kern zu integrieren, wenn sie benötigt werden.

Die globalen Rechte des Systemkerns und das Konzept der LKMs haben dazu geführt, dass in sicherheitskritischen Bereichen dem Linux-Kern nur bedingt vertraut werden kann. Ist es einem Angreifer mithilfe eines LKMs gelungen, zusätzlichen Programmcode in den Kern einzubringen, kann er das gesamte System kompromittieren. Im Internet existieren bereits so genannte LKM-Rootkits, die die Funktionalität des Systemkerns mithilfe von Kernel-Modulen verändern. Sie installieren Hintertüren oder geben bestimmten Nutzern zusätzliche Rechte innerhalb des Systems, ohne dass der Systemadministrator davon Kenntnis erhält.

Alle Mechanismen, die zum Schutz von Speicherbereichen in den Systemkern eingefügt werden, können durch Einbringen zusätzlichen Programmcodes im Nachhinein deaktiviert oder umgangen werden. Da der Kern alle Rechte innerhalb des Systems besitzt, kann keine Komponente hinzugefügt werden, die dessen Rechte wirksam einschränkt.

Bisherige Lösungen

Ein wesentliches Hindernis bei der Lösung des Problems ist die Nutzung von nur zwei Sicherheitsstufen durch den Kern. Linux unterstützt nur den SuperVisor- und den User-Modus der Paging-Architektur. Wobei der Systemkern im SuperVisor und alle Nutzerprogramme in User-Modus betrieben werden. Zusätzliche Sicherheitsstufen, wie sie beispielsweise bei der Segmentierung des Speichers durch die x86-Architektur verfügbar sind, werden aus Portabilitätsgründen von Linux nicht verwendet.

Das Palladium-Konzept [1] beschränkt sich auf die x86-Architektur und führt ein zusätzliches Segment für LKMs ein. Das Segment wird dem Level 1 zugeordnet, sodass Modul-Funktionen einen uneingeschränkten Zugriff auf alle Nutzerprogramme (Level 3), jedoch keinen Zugriff auf den Systemkern (Level 0) haben. Die Interaktion mit den Funktionen des Systemkerns muss über eine wohldefinierte Schnittstelle erfolgen, welche nicht umgangen werden kann.

Das Konzept bietet dem Systemkern einen wirksamen Schutz vor Funktionen aus LKMs, jedoch kann nicht sichergestellt werden, dass ein Modul andere Module oder Nutzerprogramme kompromittiert. Diese befinden sich immer noch im Zugriffsbereich eines geladenen Moduls. Die Vorteile dieses Konzeptes sind somit nur begrenzt nutzbar, hinzu kommt die Einschränkung auf die x86-Architektur.

Es existieren bisher keine anderen Konzepte zur Lösung des Problems. Da es nicht möglich ist, den Systemkern, wenn er im SuperVisor-Modus betrieben wird, auf bestimmte Speicherbereiche einzuschränken, ist nicht zu erwarten, dass weitere Änderungen folgen werden, die das Problem vollständig lösen.

Einschränkung des Systemkerns

Es ist nicht möglich einen Systemkern in seinen Rechten zu beschränken, wenn er im SuperVisor-Modus betrieben wird. Die Verlagerung von LKMs in andere System-Level ermöglicht nur einen Schutz des Systemkerns vor unbekannten Modulen. Eine generelle Trennung der Komponenten wäre nur realisierbar, wenn diese in separaten Adressräumen ablaufen. Dies ist allerdings nur dann sinnvoll, wenn die Rechte und Privilegien des Systemkerns ebenfalls eingeschränkt werden.

Bei der L4Linux-Architektur wird Linux als Server auf einem minimalen Mikrokern betrieben. Alle Server des Mikrokerns laufen im User-Modus und besitzen damit innerhalb des Systems nur die ihnen zugedachten Rechte. Wie [2] zeigt, treten dabei keine signifikanten Leistungsverluste auf (unter 10 %).

Im Folgenden wird eine Architektur vorgestellt, die es erlaubt, LKMs in getrennten Adressräumen (als L4-Server) ablaufen zu lassen, sodass sich diese nicht gegenseitig beeinflussen können und vor unbefugten Zugriffen geschützt sind. Dazu wird zunächst kurz die L4Linux-Architektur erläutert, bevor auf die eigentliche Implementierung eingegangen wird.

L4Linux

Die L4Linux-Architektur besteht aus zwei Komponenten: dem Mikrokern und dem L4Linux-Server. Der L4Linux-Server ist eine Portierung des Standard-Linux-Kerns auf die L4-Architektur. Die aktuelle Implementierung verwendet den Fiasco-Mikrokern. Hierbei handelt es sich um eine zum L4-Kern Schnittstellen-kompatible Implementierung eines Mikrokerns der Technischen Universität Dresden.

Der Mikrokern ist die Basiskomponente des Systems und wird als einziger im SuperVisor-Modus ausgeführt. Seine Funktionalität ist auf wenige Dienste, wie Adressraumverwaltung, Threads und Interprozess-Kommunikation (IPC) beschränkt. Er allein ist aber kein Betriebssystem. Nur in Verbindung mit seinen Tasks kann er den kompletten Funktionsumfang eines Betriebssystems erbringen.

Jeder Task kann in einem separaten Adressraum betrieben werden. Die Zuordnung von logischen zu physischen Adressen erfolgt mittels Funktionen des Mikrokerns. Der initiale Adressraum wird, wie in Abbildung 1 dargestellt, als Sigma0 bezeichnet und von einem gleichnamigen Task verwaltet. Alle weiteren Adressräume werden durch Ein- und Ausblenden von Seiten konstruiert. Jeder L4-Task kann seinen Speicher anderen Tasks zur Verfügung stellen, danach können beide Tasks auf diese Seiten zugreifen. Der Besitzer einer Seite kann diese jederzeit aus allen fremden Adressräumen ausblenden. Nutzerprozesse, die Adressräume verwalten, werden auch als Pager bezeichnet [3].

[Illustration]
Abbildung 1: L4Linux Speichermapping

Durch die Nutzung eines Mikrokerns ist es möglich, den Umfang des im SuperVisor-Modus ausgeführten Programmcodes auf die unbedingt notwendigen Komponenten zu beschränken. Nahezu alle Programmbefehle können im User-Modus des Prozessors durchgeführt werden, lediglich Instruktionen, die eines privilegierten Zugriffs auf den Prozessor bedürfen, sind Bestandteil des Mikrokerns.

L4Linux ist eine Portierung von Linux auf die L4-Architektur. Dabei wurde der Architektur-abhängige Teil von Linux dahingehend verändert, dass Linux als Server auf dem L4- beziehungsweise dem Fiasco-Kern und somit als L4-Task betrieben werden kann. Jedes Linux-Nutzerprogramm wird ebenfalls als eigener L4-Task ausgeführt, damit besitzt jeder Nutzerprozess weiterhin seinen eigenen Adressraum. Diese Tasks werden vom L4Linux-Server erzeugt, wobei er sich selbst als Pager für die neuen Tasks definiert. Dies hat zur Folge, dass der L4-Kern jeden fehlerhaften Speicherzugriff (PageFault) an den L4Linux-Server in Form eines Remote Procedure Calls (RPC) übermittelt. Dieser kann dann entscheiden, ob er die zugehörige Seite in den Adressraum des Nutzerprozesses einblendet, oder den Zugriff gänzlich verweigert.

Der Programmcode des Linux-Servers kann vollständig im Nutzer-Modus ausgeführt werden und benötigt keinen direkten Zugriff auf privilegierte Prozessorbefehle oder die Hardware-Seitentabellen. Damit kann nahezu sicher ausgeschlossen werden, dass der Linux-Server Daten eines anderen Adressraumes, der nicht seiner Kontrolle unterliegt, liest oder manipuliert. Operationen, die unbedingt im SuperVisor-Modus ausgeführt werden müssen, werden an den Mikrokern weitergeleitet.

LKMs enthalten Komponenten, die vom Kern nur zeitweise benötigt und nach Bedarf nachgeladen werden. Die Interaktion zwischen dem Kern und einem geladenen Modul erfolgt über exportierte Symbole und registrierte Modulfunktionen. Exportierte Symbole, deren Nutzung sowohl im Kern als auch in einem Modul möglich sein soll, müssen mittels einer speziellen Anweisung im Quellcode gekennzeichnet werden. Ein Zugriff auf zusätzliche Symbole ist nicht möglich beziehungsweise nicht notwendig.

LKMs laden

Wenn ein Modul übersetzt wird, werden alle Symbole, die nicht im Modul enthalten sind, durch symbolische Referenzen ersetzt. Jedes Symbol, welches der Kern oder ein Modul mittels des EXPORT_SYMBOL-Makros exportiert, wird in die Symboltabelle des Kerns übertragen. Die Symboltabelle einer Objektdatei (hierbei kann es sich sowohl um den Kern selbst als auch um ein Modul handeln) wird in der export-Sektion der Datei abgelegt. Die symbolische Referenz und der Eintrag in der export-Sektion werden vom Modul-Lader verwendet.

Bevor das Kernel-Modul verwendet werden kann, müssen alle Referenzen aufgelöst werden. Dies ist eine der Aufgaben des Modul-Laders. Er lädt zunächst das gesamte Modul in seinen Speicher. Anschließend durchsucht er das Modul nach unbekannten Symbolen. Jedes dieser Symbole sollte bereits im Kern vorhanden sein und in der Kernel-Symboltabelle einen entsprechenden Eintrag besitzen. Mittels einer Systemfunktion liest der Modul-Lader die gesamte Symboltabelle des Kerns ein und löst die Referenzen auf.

Symbole, die vom Modul exportiert werden, überträgt der Modul-Lader in die Symboltabelle des Kerns. Dadurch wird es möglich, dass ein Modul Funktionen eines anderen Moduls verwenden kann. Dabei muss zwingend die Reihenfolge, in der die Module geladen werden, eingehalten werden. Es ist nicht möglich, ein Modul zu laden, das unaufgelöste Referenzen enthält.

Modul-Funktionen anmelden

Ebenso wie ein Modul, darf auch der Systemkern keine unaufgelösten Referenzen enthalten. Aus diesem Grund kann ein Modul seine Funktionen nicht über das EXPORT_SYMBOL-Makro an den Kern exportieren. Hierzu sind so genannte Registrierfunktionen notwendig.

Wenn ein Modul erfolgreich geladen wurde, ruft der Kern immer die init-Funktion des Moduls auf. Jedes Modul sollte eine solche Funktion implementieren und nach deren Abarbeitung die Anmeldung beim Kern abgeschlossen haben. Die init-Funktion ruft in der Regel die notwendigen Registrierfunktionen des Kerns auf. Jede dieser Funktionen bekommt einen Zeiger auf eine Struktur übergeben. Diese Struktur enthält Zeiger auf Funktionen des Moduls, die später im Systemkern verwendet werden können [4, 6].

LKM als Server

Obwohl Linux als Server des Mikrokerns ausgeführt wird, bleibt dessen monolithische Struktur erhalten. Damit haben Funktionen des L4Linux-Systemkerns alle Rechte innerhalb ihres Adressraums. LKM-Rootkits, die in den Adressraum des Linux-Servers geladen werden, haben somit auch dieselben Möglichkeiten, das System zu kompromittieren. Um dies wirksam unterbinden zu können, muss der Linux-Server in unabhängige Komponenten unterteilt werden, die in separaten Adressräumen ausgeführt werden.

Eine Aufteilung von Linux ist aufgrund der monolithischen Architektur nur schwer möglich. Innerhalb des Systemkerns existieren nur wenige wohldefinierte Schnittstellen, die eine solche Trennung erlauben würden. Kernkomponenten, wie der Netzwerk-Stack oder das virtuelle Dateisystem, sind so stark in den Kern integriert, dass eine Auslagerung in einen fremden Adressraum nicht ohne weiteres möglich ist. Viele Komponenten werden in der aktuellen Implementierung vom Kern zwingend benötigt.

Das hier vorgestellte Konzept nutzt die Modul-Schnittstelle zur Unterteilung des L4Linux-Servers. Da Module nur über diese Schnittstelle angesprochen werden, lassen sie sich als Server in einem separaten Adressraum ausführen. Die Interaktion zwischen Kern und Modulen erfolgt ausschließlich über IPC. Hierzu wird jeder Funktionsaufruf, der über Adressraumgrenzen hinausgeht, in einen RPC umgewandelt. Für die Behandlung der RPCs enthalten die einzelnen Komponenten ein einheitliches Interface.

Die Auslagerung von Modulen in separate Server soll für Anwendungsprogramme transparent bleiben, sodass ein Nutzer beziehungsweise das Programm nicht wissen muss, ob das Modul als Server oder als Bestandteil von L4Linux betrieben wird. Änderungen sollten nur am Modul selbst, an der Modul-Schnittstelle und den Registrierfunktionen des Kerns notwendig sein.

Wenn die einzelnen Module als Server betrieben werden, ist eine Instanz notwendig, die deren Organisation übernimmt. Hierzu wird ein Modul-Server eingeführt, der die folgenden Aufgaben zu erfüllen hat:

Um eine transparente Gestaltung des Modul-Servers zu ermöglichen, bietet dieser eine ähnliche Schnittstelle zum Laden von Kernel-Modulen wie die ursprüngliche Linux-Implementierung. Neue Module werden mithilfe von zwei separaten Funktionen geladen. Es existiert zudem eine Funktion zum Entfernen von Modulen und eine Funktion zum Ermitteln der angebotenen Symbole. Zur Registrierung der exportierten Symbole wird eine zusätzliche init-Funktion eingeführt, die vor der eigentlichen init-Funktion des Moduls aufgerufen wird. Sie ist zusätzlich verantwortlich für die Einrichtung der notwendigen RPC-Infrastruktur, damit später, wenn das Modul vollständig initialisiert ist, immer ein RPC-Handler zur Verfügung steht.

Da die Implementierung der Module im Wesentlichen nicht verändert werden soll, erwarten diese immer noch, dass sie im Kontext des Linux-Kerns ausgeführt werden. Eine Aufgabe des Modul-Servers besteht darin, für die Module eine Art Kernel-Umgebung zu schaffen, sodass die Auslagerung in einen separaten Prozess für das Modul weitestgehend transparent bleibt. Hierzu wird das Konzept für L4Linux-Nutzerprozesse übernommen [5]. Der Modul-Server erzeugt die Modul-Prozesse und definiert sich selbst als Pager. So ist er in der Lage, alle Zugriffe auf externe Ressourcen zu kontrollieren und gegebenenfalls weiterzuleiten oder zu unterbinden.

Alle Symbole, die über Adressraumgrenzen hinaus mittels RPCs zugänglich gemacht werden sollen, müssen mithilfe eines eindeutigen Identifiers (ID) adressiert werden. Die ID besteht aus der Server-ID (SID) und der L4-Task und Funktions-ID (FID). Beim Übersetzen des Kerns und der einzelnen Module wird bereits eine Symboltabelle angelegt. Dies geschieht durch die Verwendung eines speziellen Makros im Programmcode. Dieses Makro wird dahin gehend erweitert, dass es für jedes Symbol eine FID definiert. Zur Laufzeit werden die FIDs in eine Symboltabelle des Modul-Servers übertragen. Zusätzlich kann nun auch die zugehörige SID ermittelt werden. Funktionen, die erst zu Laufzeit mithilfe von Registrierfunktionen öffentlich gemacht werden, müssen sich ebenfalls beim Modul-Server registrieren. Dies erfolgt in der neuen init-Funktion des Moduls, sodass bei der Ausführung der eigentlichen init-Funktion bereits alle exportierten Symbole bekannt sind.

Aufteilung des Speichers

Die Aufteilung des physischen Speichers wird mithilfe einer Konfigurationsdatei kontrolliert. Für jeden L4-Task kann festgelegt werden, wie viel Speicher er erhalten und aus welchem Bereich dieser genommen werden soll. Diesen Speicherbereich kann ein Task nicht verlassen. Damit wird sichergestellt, dass ausgelagerte Module beziehungsweise der Modul-Server nicht ungewollt durch Funktionen des Linux-Servers beeinflusst werden können.

In Abbildung 2 ist die Aufteilung der einzelnen Adressräume dargestellt. Obwohl jeder Task nur einen physisch abgegrenzten Speicherbereich nutzen kann, ist es möglich Speicherbereiche in den Adressraum eines anderen Prozesses einzublenden. Bei jedem Task ist der L4-Kern oberhalb von drei Gigabyte eingeblendet, damit steht jedem Prozess nur noch der Bereich der ersten drei Gigabyte zur Verfügung. L4Linux nutzt immer das erste Gigabyte für den Kernel-Speicher, der Bereich zwischen dem ersten und dem zweiten Gigabyte ist für Nutzerprozesse reserviert.

[Illustration]
Abbildung 2: Aufteilung der Adressräume

Der Modul-Server wird ebenfalls in den Speicherbereich unterhalb des ersten Gigabytes geladen, und zwar genau an die Stelle, wo sich auch das Kernel-Text-Segment befindet: Als Text-Segment bezeichnet man den Bereich eines Programms, in dem sich der Programmcode befindet. Globale Variablen werden im Daten-Segment abgelegt. Alles oberhalb des Text-Segmentes des Linux-Servers wird in den Adressraum des Modul-Servers eingeblendet. Damit ist er in der Lage, alle Daten des Linux-Servers zu lesen. Um nicht durch die Größe des Text-Segmentes des Kerns begrenzt zu sein, werden alle Module zunächst in den Bereich oberhalb des ersten Gigabytes geladen und mittels des Pagers in den unteren Bereich des jeweiligen Modul-Adressraumes eingeblendet.

Jedem Modul steht oberhalb der Eingigabytegrenze ein privater Speicherbereich zur Verfügung. In diesen Bereich können Daten abgelegt werden, die für andere Prozesse nicht lesbar sein sollen. Da dieser Bereich ebenfalls vom Modul-Server verwaltet wird, muss der Speicher mithilfe von Funktionen des Modul-Servers angefordert werden.

Schnittstellen

Die Interaktion zwischen dem Kern und einem Modul beziehungsweise umgekehrt lässt sich auf Funktionsaufrufe und die Nutzung von Variablen beschränken. Aus sicherheitstechnischen Gründen wird für jede Interaktion eine andere Umsetzung gewählt.

Darüber hinaus ist es sinnvoll, die Interaktionen hinsichtlich ihrer Richtung getrennt zu betrachten. Der Kern muss zunächst ohne die Symbole des Moduls lauffähig sein, wohingegen ein Modul die Funktionen des Kerns immer erwarten kann.

Zugriffe auf globale Variablen werden zur Laufzeit wesentlich häufiger als Funktionsaufrufe vorkommen. Die Verwendung eines RPC-Mechanismus wäre an dieser Stelle zu aufwändig und ineffizient, da innerhalb des RPCs nur eine Variable gelesen beziehungsweise geschrieben wird. Zum reinen Datenaustausch erscheint es effizienter, wenn alle Prozesse auf einem gemeinsamen Speicherbereich arbeiten.

Wie bereits zuvor erwähnt, wird der Kernel-Speicher des L4Linux-Servers in den Adressraum des Modul-Servers eingeblendet. Dieser kann dann innerhalb des Tasks lesend und schreibend genutzt werden. Das Einblenden des Speicherbereiches erfolgt bei der Initialisierung des Linux- und des Modul-Servers. Da er zudem Pager für alle Module ist, kann er deren Zugriffe auf diesen Datenbereich auflösen beziehungsweise verhindern. Die angeforderte Seite wird nur im Falle einer positiven Zugriffskontrolle in den Adressraum des Moduls eingeblendet.

Möchte ein Modul Variablen für andere Module bereitstellen, so müssen diese in einem für alle Server zugänglichen Datenbereich liegen. Statische Variablen werden im Daten-Segment des Moduls abgelegt. Da dieser Teil positionsunabhängig ist, kann er vom Modul-Server an jeder beliebigen Stelle eingeblendet werden. Datenbereiche, die erst zur Laufzeit angefordert werden, enthalten jedoch oft Verweise auf andere Adressen und können somit nicht verschoben werden. Möchte ein Modul auch solche Bereiche freigeben, so müssen diese mittels Funktionen des Linux-Servers angefordert werden. Diese Speicherbereiche befinden sich dann zwar im Adressraum des Linux-Servers, sind aber für alle Module zugänglich, da der gesamte Kernel-Speicher freigegeben wurde.

Damit ein Modul die Funktionen eines anderen Moduls nicht beeinflussen kann, werden diese nicht in andere Adressräume eingeblendet. Dies bedeutet jedoch, dass ein Modul exportierte Funktionen des Kerns beziehungsweise eines anderen Moduls nicht mehr direkt aufrufen kann. Ebenso kann der Kern Funktionen, die ihm mithilfe von Registrierfunktion zur Verfügung gestellt wurden, nicht mehr direkt verwenden. Funktionsaufrufe über Adressraumgrenzen hinaus werden als RPCs umgesetzt. Hierzu benötigt ein Task die eindeutige ID einer Funktion. Beides wird vom Modul-Server bereitgehalten.

Damit nicht bei jedem Funktionsaufruf die IDs erfragt werden müssen, werden RPC-Stubs eingeführt. Hierbei handelt es sich um Funktionen, die den eigentlichen RPC ausführen, jedoch die SID und die FID bereits wissen. Die RPC-Stubs werden zur Übersetzungszeit generiert und später in den Adressraum des aufrufenden Moduls eingefügt. Die FID kann bereits beim Übersetzen des Moduls eindeutig festgelegt werden. Zur Ermittlung der SID wird eine Registrierfunktion generiert, welche die aktuelle ID beim Modul-Server erfragt.

Funktionsaufrufe werden mithilfe von RPCs umgesetzt. Das ursprüngliche Konzept zum Auflösen von Referenzen darf hierbei nicht mehr verwendet werden. Auch wenn der Modul-Lader die Symboltabelle lesen kann, sind die enthalten Adressen für das exportierte Modul nicht zugreifbar. Sie befinden sich in einem fremden Adressraum und können im Programmcode des Moduls nicht verwendet werden.

Damit der Modul-Lader weiterhin verwendet werden kann, wird das Auflösen der Referenzen bereits beim Übersetzen des Moduls durchgeführt. Hierzu wurde ein Programm implementiert, welches die fertige Objektdatei des Moduls nach solchen Referenzen durchsucht und den notwendigen Stub-Code generiert. Dieser Stub-Code enthält für jede unbekannte Funktion einen RPC-Stub mit der gleichen Signatur. Diese Signatur einer Funktion besteht aus ihrem Namen, den Argumenten und ihrem Rückgabetyp. Die Objektdatei mit dem Stub-Code wird zum Modul hinzugelinkt, sodass dieses anschließend keine unaufgelösten Referenzen mehr enthält. Zusätzlich wird eine Funktion generiert, die für jedes dieser Symbole die zugehörige SID beim Modul-Server erfragt. Diese Funktion wird vor der eigentlichen init-Funktion des Moduls aufgerufen, sodass vor dessen Ausführung alle fremden Symbole bereits korrekt aufgelöst sind.

Ausblick

Das vorgestellte Konzept beschränkt sich im Wesentlichen auf die Auslagerung von LKMs in getrennte Adressräume. Dabei wurden einige Punkte, die sich bei der Ausarbeitung des Konzeptes ergeben haben, ausgelassen. Deren Ausformulierung und Implementierung war nicht Bestandteil der hier vorgestellten Diplomarbeit. Nichtsdestotrotz soll an dieser Stelle noch einmal kurz auf diese Punkte eingegangen werden, um mögliche Weiterentwicklungen aufzuzeigen.

Laden der Kernel-Module

Das hier vorgestellte Konzept verwendet den Modul-Lader von Linux zum Laden der Kernel-Module. Dies bedeutet, dass ein Modul zunächst in den Speicher des Kerns geladen wird, bevor es dem Modul-Server übergeben wird. Erst ab dieser Stelle wird es durch den Mikrokern vor unbefugten Zugriffen geschützt. Was innerhalb des Linux-Servers beim Laden des Moduls wirklich passiert ist, kann an dieser Stelle nicht mehr nachvollzogen werden. Es kann noch nicht einmal sichergestellt werden, dass das richtige Modul geladen wurde.

Um diese Schwachstelle zu umgehen, existieren zwei Lösungsansätze: Der erste sieht vor alle Module zu laden, bevor das eigentliche System dem Nutzer übergeben wird. Anschließend werden keine zusätzlichen Module geladen beziehungsweise als vertrauenswürdig gekennzeichnet. Hierzu müsste sichergestellt werden, dass die Initialisierung des Systems keine Schwachstellen enthält. Die andere Möglichkeit wäre, die Module über eine externe Instanz zu laden. Zum Beispiel könnten Module auf einer Smartcard gesichert und mithilfe eines separaten L4-Tasks dem Modul-Server übergeben werden. Die Schnittstelle des Modul-Servers ist relativ einfach gehalten und verlangt vom Modul-Lader keine umfangreichen Vorarbeiten, sodass ein solcher Task einfach zu implementieren wäre.

Zugriffskontrolle

Bisher kann eine Funktion, wenn sie mithilfe eines RPCs exportiert wurde, von jedem Server aufgerufen werden. Es existieren jedoch viele Anwendungsfälle, wo dies nicht notwendig oder gar nicht gewünscht ist. Beispielsweise wird ein Modul zur Verschlüsselung von Datenbereichen nur von einzelnen Kernel-Modulen verwendet, beispielsweise IPSec [7] oder CryptFS [8].

Mithilfe von Zugriffsregeln wäre es denkbar, die Datenbereiche eines Moduls noch umfangreicher zu schützen. Bisher ist es nicht möglich, die Verwendung von RPCs durch den L4-Kern zu kontrollieren. Es existieren bereits Konzepte zur Lösung des Problems, die jedoch noch nicht Bestandteil der aktuellen Implementierung des Mikrokerns sind. Ohne die Zugriffskontrolle des Kerns ist es lediglich möglich, dass ein Modul nur RPCs von bestimmten Modulen annimmt, beziehungsweise der Modul-Server die SID ausschließlich Modulen übergibt, welche die Berechtigung für einen späteren Aufruf besitzen. Damit könnte zwar nicht das gleiche Maß an Sicherheit erreicht werden, jedoch könnte sichergestellt werden, dass beispielsweise die ID der Ver- und der Entschlüsselungsfunktion eines Crypto-Moduls nur dem IPSec-Modul oder dem Cryptfs-Modul übergeben wird.

Verringerung der notwendigen Kommunikation

Da ein Wechsel des Adressraumes nicht ohne zusätzlichen Aufwand möglich ist, können zu viele RPC-Aufrufe die Leistung des Systems stark beieinträchtigen. Viele Funktionen des Kerns sind positionsunabhängig und könnten auch in einem fremden Adressraum ausgeführt werden. So ist es unter Umständen effizienter für einen Teil der exportierten Kernel-Funktionen keinen Stub-Code zu generieren, sondern eine Kopie der Funktion im Adressraum des Modul-Tasks anzulegen.

Da jedoch zum jetzigen Zeitpunkt keine Messungen für das hier vorgestellte Konzept existieren und die Anzahl der notwendigen RPCs nur abgeschätzt werden kann, ist eine genaue Einschätzung des Problems an dieser Stelle noch nicht möglich.

Auslagerung von integralen Kernel-Komponenten

Es wurde gezeigt, dass es möglich ist, für ein auf L4Linux basierendes System Speicherbereiche bereitzustellen, die nur von zuvor definierten Funktionen genutzt werden können. Damit können sicherheitskritische Systemkomponenten, die bisher als Kernel-Module implementiert sind, in eigene Adressräume ausgelagert werden. Allerdings verbleiben wichtige Komponenten des Systems im L4Linux-Server, die nur durch umfangreiche Änderungen an ihren Implementierungen in eigene Server ausgelagert werden können. Ein Anwendungsbeispiel ist die Auslagerung des Netzwerkprotokoll-Stacks, wie es das µSINA-Projekt als Ziel hat [9, 10, 11]. Angestrebt wird eine Verbindung beider Projekte, sodass eine Nutzung von bereits vorhandenen Modulen sowohl im Linux-Server als auch in ausgelagerten Systemkomponenten möglich wird und damit die Entwicklungskosten neuer Komponenten gering gehalten werden können.

Literatur

[1]
Tzi-cker Chiueh, G. Venkitachalam und P. Pradham, Intra-Address Space Protection using Segmentation Hardware, [externer Link] www.ecsl.cs.sunysb.edu/palladium.html
[2]
H. Härtig, M. Hohmuth, J. Liedtke, S. Schönberg und J. Wolter, The performance of m-kernel-based systems, in:16th ACM Symposium on Operating System Principles (SOPS), S. 66, 1997
[3]
M. Aron, Y. Park, T. Jaeger, J.Liedtke, K. Elphinstone und L. Deller, The SawMill Framework for Virtual Memory Diversity, Sixth Australasian Computer Systems Architecture Conference (ACSAC2001), Bond University, Gold Coast, Queensland, 2001
[4]
D. P. Bovet, M. Cesati, Understanding the Linux-Kernel, O'Reilly, ISBN 0-596-00213-0
[5]
M. Borriss, M. Hohmuth, J. Wolter und H. Härtig, Portierung von Linux auf den m-Kern L4, Int. wiss. Kolloquium Illmenau, 1997
[6]
M. Beck, H. Böhme, M. Dziadzka, U. Kunitz, R. Magnus, C. Schröter, D. Verworrner, Linux Kernel-Programmierung, Algorithmen und Strukturen der Version 2.2, 5. Aufl., Addison-Wesley, Bonn, 1999, ISBN 3-8273-1659-6
[7]
FreeS/WAN-Homepage, [externer Link] www.freeswan.org
[8]
E. Zadok, I. Badulescu und Alex Shender, Cryptfs: A Stackable Vnode Level Encryption File System, Computer Science Department, Columbia University, [externer Link] www.cs.columbia.edu/~ezk/research/cryptfs/, Columbia, 1998
[9]
TU Dresden, Institut für Systemarchitektur, Professur Betriebssysteme: µSina – Mikrokernbasierte Sichere Inter-Netzwerk-Architektur, 2002, [externer Link] http://os.inf.tu-dresden.de/mikrosina/
[10]
Bundesamt für Sicherheit in der Informationstechnik: Sichere Inter-Netzwerk-Architektur, 2002, [externer Link] www.bsi.bund.de/fachthem/sina/
[11]
secunet Security Networks AG, SINA – Hochsichere Inter-Netzwerk-Architektur für behördliche und private Anwender, 2002, [externer Link] www.secunet.com.