= Sichere Software-Entwicklung im systemnahen Bereich :author: Jan Klemkow, Benjamin Franzke :lang: de :toc: == Kurzfassung Dieses Dokument beschreibt Sicherheitsrisiken von Systemprogrammen mit dem Fokus auf UNIX-Systemen. Systemnahe meint in diesem Dokument alle Programme welche direkt auf die Systemschnittstellen (System-Calls) zugreifen. Es werden einleitend Problemfelder von systemnahen Programmen geschildert und ein historischer Vergleich zu Anwendungsprogrammen gezogen. :numbered: == Einleitung Sicherheit fuer die Software-Entwicklung im systemnahen Bereich spielt eine wesentliche Rolle, da es hier oft nicht nur um die Anwendung selbst, sondern auch um die Sicherheit des Gesamtsystems geht. Systemdienste werden in vielen Faellen mit hoeheren Rechten ausgefuehrt. Gelingt es einem Angreifer einen solchen Dienst zu uebernehmen, so erlangt er selbst hoehere Rechte auf dem Zielsystem. Bei Endanwendungen ist es in der Regel nicht der Fall, dass sie mit höhren Rechten ausgeführt werden, sodass der Angreifer, bei einem erfolgreichen Angriff, nur Teile des Zielsystems unter seine Kontrolle bringen kann. Jene, welche mit den Rechten der Anwendung steuerbar sind. Historisch betrachtet hat sich die Lage in diesem Bereich in den vergangenen zehn Jahren verbessert. Die grossen Angriffe, wie etwa der Sasser-Wurm, welche sich zu Beginn des 21. Jahrhunderts noch auf Systemdienste richteten, sind heute eher selten geworden. Momentan stehen die Endanwendungen, wie Browser oder Dokumentenbetrachter im Fokus der Angriffe. //Im systemnahen Bereich gibt es viele Programme welche fuer die Sicherheit des //Gesamtsystems eine enorme Rolle spielt. // Angefangen beim Kernel des Betriebssystems //== Angriffsvektoren In den folgenden Kapiteln werden verschiedene Angriffsvektoren und Schwachstellen von Systemprogrammen erlaeutert. Zudem werden werden verschiedene Verteidigungssmaßnahmen erlaeutert, welche das Risiko der Angriffsmoeglichkeiten senken. == Standard-C-Bibliothek Viele Systemprogramme sind in der Programmiersprache ``C'' geschrieben. Deren Standardbibliothek unterlag im Laufe der Zeit einer ganzen Reihe von Veraenderungen, welche unter anderem die Sicherheit von Programmen erhoehten. Eines der groessten Probleme in ``C''-Programmen sind Funktionen zur Verarbeitung von Zeichenketten. Dies ist der Abbildung der Zeichenketten geschuldet. Sie werden hier, im Gegensatz zu anderen Programmiersprachen, nicht als komplexes Objekt, sondern als loser Speicherbereich betrachtet. Somit muss ein Programmierer bei Zeichenketten immer den dahinterliegenden Speicherbereich und dessen Laenge bei jeder Operation bedenken. //TODO: Hinleitung auf C-Funktion die durch folgendes "diesen" referenziert //werden. //Da dieses seit langer Zeit immer wieder fuer Sicherheitsprobleme sorgt, wurden //viele Funktionen neu geschrieben. Bei diesen Funktionen ist das Problem, dass diese die Laengen von Speicherbereichen nicht beachten. Dadurch kommt es schnell zu Pufferueberlaeufen und Zugriffen auf nicht allozierte Speicherbereiche. Dadurch koennen Angreifer eigenen Programmcode einschleusen und ausfuehren lassen, sowie Programme zum Absturz bringen. Als Fallbeispiel wird das Problem anhand der Funktion +strcpy(3)+ erklaert. Diese Funktion wird dafuer benutzt um eine Zeichenkette von einem Speicherplatz zu einem anderen zu kopieren. Als Parameter bekommt die Funktion die Startadressen von Quelle und Ziel im Speicher. Der Funktion ist dabei die Groesse der jeweiligen Speicherplaetze nicht bekannt. Es wird nun eine Speicherzelle nach der Anderen kopiert, solange bis in der Quelle ein NUL-Byte auftaucht. Dabei kann die Funktion weder sicherstellen, dass sie nicht ueber den Quellpuffer hinaus Bytes kopiert, sowie dass sie nicht ueber die Grenzen des Zielpuffers hinaus schreibt. Durch dieses Verhalten, kommt es immer wieder zu Pufferueberlaeufen, welche zu unvorhersagbaren Folgen fuer den weiteren Programmablauf fuehren. Um dieses Problem zu loesen wurden neue Zeichenkettenfunktionen in die Standard-C-Bibliothek auf genommen, welche als Argument Maximallaengen uebergeben bekommen. [source,c] ------------------- strcpy(char *dst, const char *src); strncpy(char *dst, const char *src, size_t len); strlcpy(char *dst, const char *src, size_t dstsize); ------------------- Hierbei gibt es zwei Ansaetze mit Laengen umzugehen. TODO: strncpy() strlcpy() * gets() * strlen() * strcpy() * str..() == Netzwerk-Programmierung Ein grosser Angriffsvektor auf Computer-Systemen sind laufende Dienste, welche ueber Netzwerk und Internet erreichbar sind. Ein Fehler in ihrer Programmierung koennte von ausserhalb ausgenutzt werden, da Internetdienste eine staendige weltweite Erreichbarkeit aufweisen. Somit kann ein solcher Dienst potentiell zu jeder Zeit und von jedem Ort aus angegriffen werden. Zum Schutz solcher Dienste gibt es verschiedene Mechanismen, welche im Folgenden erlaeutert werden. === Privilege-Revocation Die meisten Anwendung benoetigen ausschliesslich waehrend der Initialisierung hoehere Rechte, um etwa einen Socket mit einem Well-Known-Port zu oeffnen. Zur Laufzeit werden diese hoeheren Rechte dann nicht mehr benoetigt. Somit geben diese Prozesse nach ihrer Initialisierung diese Rechte wieder ab und arbeiten mit normalen Benutzer-Rechten weiter. //TODO: einfachster Fall: Gibt es kompliziertere? Im einfachsten Fall startet ein Daemon mit Root-Rechten. Dieser alloziert benötigte Ressourcen, wie Sockets mit Well-Known-Ports oder einen Filedeskriptoren auf eine Datei. Im folgenden gibt der Daemon die Root-Rechte ab, in dem eine andere effektive Nutzer-ID gesetzt wird und laeuft ab dem Zeitpunkt im User-Mode. Er ist damit nicht mehr in der Lage, Operationen auszuführen, die Root-Rechte verlangen. === Privilege-Separation Bei der Privilege-Separation wird ein Programm in verschiedene Prozesse mit unterschiedlichen Berechtigungen aufgeteilt. Das Ziel dabei ist es, den Programmcode mit so wenig Rechten wie moeglich ausführen zu lassen. Als Beispiel sei der Window-Compositer Weston genannt, der im Rahmen des Wayland-Projektes implementiert wird. Das Programm besteht aus dem Programm ``weston-launch'' und dem eigentlichen Hauptprogramm ``weston''. Der Compositor behandelt in seiner Hauptaufgabe das Darstellen der Anwendungsfenster mit Hilfe der Grafikkarte, so wie das Einlesen und Verarbeiten von Nutzereingaben über Eingabegeräte. Das Ziel ist es, den Compositor ohne Root-Rechte laufen zu lassen. Das Öffnen von Eingabegeräten zum Einlesen bedarf Root-Rechte, damit nicht ein beliebiges Programm -- in der Funktion eines Keyloggers -- die Nutzereingaben, wie z.B. Passwörter, unauthorisiert lesen kann. Für Eingabegeräte reicht die Privilege-Revocation nicht aus, denn durch Hotplug Funktionalität können zur Laufzeit neue Eingabegeräte hinzukommen. Das Gerät muss außerhalb der Initialisierungsphase mit Root-Rechten geöffnet werden. Desweiteren ist es beim Linux Kernel-Mode-Setting nötig, dass zur Laufzeit ein Master für die Grafikkarte gesetzt wird, wenn ein VT-Switch auftritt. Der Compositor agiert als ein solcher Master, wenn er aktiv ist. Gefordert ist also ein Mechanismus, um ausgewählte begrenzte Operationen zuzulassen: Das Startprogramm ``weston-launch'' ist ein mit Root-Rechten gestartetes Vorprogramm für das Hauptprogramm ``weston''. Es erstellt einen Unix-Domain-Sockets, der den Kommunikationskanal bildet. Dieser ist die Grundlage für den erwähnten Mechanismus. Es startet anschließend weston mit eingeschränkten Rechten (Nutzer-Rechte) und übergibt beim Start den Deskriptor für den Kommunikationskanal. Wenn nun zur Laufzeit weston ein Eingabe-Hotplug-Ereignis erhält und das Gerät öffnen möchte, so nutzt weston nicht -- wie andere Programme -- open(2), denn um das Gerät direkt zu Öffnen fehlen die Rechte. Stattdessen wird eine Nachricht über den Kommunikationskanal gesendet, die mit einem OpCode beschreibt, dass ein Gerät geöffnet werden soll, und als Parameter den Pfad zum Gerät, z.b. /dev/input/event0 enthält. Weston-launch empfängt diese Anfrage und prüft, ob es ein zur Öffnung erlaubter Pfad ist (z.B. beginnend mit /dev/input/). Falls erlaubt, wird das Gerät geöffnet und der Deskriptor über den Kommunikationskanal als Socket-Control-Message an ``weston'' als Antwort übertragen. Weston erhält damit einen Deskriptor, der sich nicht von einem durch direkten Aufruf von open(2) geöffneten unterscheidet. // TODO: Ähnliches Szenario: DRM Set Master Weston läuft somit im User-Mode und kann bestimmte Operationen, über weston-launch ausführen lassen. Wenn eine Sicherheitslücke in Weston nun zur Ausführung von schädlichem Programm-Code führen sollte, könnten nur die Root-Operationen ausgeführt werden, die ``weston-launch'' erlaubt -- nicht aber alle für Root erlaubten. === Service-Separation Da auf einem Computer zumeist mehr als nur ein Dienst laeuft, welcher mit einem Netzwerk oder dem Internet verbunden ist, ist auch die Angriffsmoeglichkeit auf dieses System sehr hoch. Vor Allem beim Servern, welche viele Dienste wie HTTP, FTP, SMTP, POP3 und viele mehr anbieten ist die Gefahr einer Sicherheitsluecke sehr hoch. Sollte einer dieser Dienste ueber eine Sicherheitsluecke von einem Angreifer uebernommen werden, so koennte diese ebenfalls die anderen Dienste und deren Daten manipulieren. Um dieses zu vermeiden, koennen diese Dienste in kuenstlichen Umgebungen von einander getrennt werden. Eine Variante dafuer ist der System-Call +chroot(2)+. Dieser System-Call wechselt fuer einen Prozess und dessen Kind-Prozesse das Root-Verzeichnis. Somit kann ein Prozess auf Dateien ausserhalb des neuen Root-Verzeichnisses nicht mehr zugreifen. Bereits geoeffnete Dateien koennen aber weiterhin verwendet werden, auch wenn diese Ausserhalb des neuen Root-Verzeichnisses liegen. Ein Beispiel fuer die Anwendung dieser Technik ist der Apache-Http-Server, welcher meist in das Verzeichnis +/var/www+ als Root-Verzeichnis wechselt. Der +chroot(2)+-System-Call ist in erster Linie aber keine Sicherheitsfunktion. Es gibt definierte Wege aus einem chroot wieder heraus zu wechseln. Das Konzept von ``Jails'' wie etwas im FreeBSD-Betriebssystem verwendet werden, sind dafuer ausgelegt verschiedene Prozesse sicher von einander zu trennen. Bei den ``Jails'' werden die System-Calls des eingeschlossenen Processes explizit gefiltert, um die Beeinflussung von anderen Prozessen zu verhindern. Um verschiedene Prozesse von einander zu trennen kann auch das UNIX-Rechte-Verwaltung verwendet werden. Jeder Service sollte unter einem separaten Nutzer-Konto laufen. Es sollte vermieden werden mehrere Dienste mit den selben Nutzungsrechten laufen zu lassen, da sich diese untereinander beeinflussen koennen. Sollte ein Dienst keine eigenen Dateien benoetigen, kann dieser unter dem speziellen Nutzer ``nobody'' ausgefuehrt werden. Unter vielen Unix-Systemen wird dieser speziell fuer diese Aufgabe verwendet. === Beschraenkung der Erreichbarkeit Sollte ein Dienst nur fuer einen bestimmten Kreis von Nutzern bestimmt sein, dann sollte man die Erreichbarkeit des Dienstes auf diesen Kreis beschraenken. Systemdienste muessen nur in wenigen Faellen eine weltweite Erreichbarkeit haben. Ein Systemdienst, welcher mit hoeheren Rechten laeuft und global ueber Netzwerk erreichbar ist, ist immer eine enormes Sicherheitsrisiko fuer das Gesamtsystem. Bevor man sich bei der Entwicklung eines Systemdienstes fuer einen bestimmten Kommunikationsweg entscheidet, muss man sich bewusst sein, wer mit wem kommuniziert und wo sich die Kommumikationspartner befinden. Kommunikation innerhalb eines Hosts sollen ueber das Loopback-Interface oder UNIX-Domain-Sockets gefuehrt werden. Ein Dienst welcher ueber das Loopback-Interface kommuniziert, kann von jedem Prozess erreicht werden, welcher einen Netzwerk-Socket oeffnen kann. UNIX-Domain-Sockets sind an Dateien gebundene Verbindungen. Dadurch laesst sich ueber die systeminterne Benutzer- und Rechteverwaltung der Zugriff auf die Verbindung steuern. Wird doch eine Netzwerkkommunikation benoetigt, kann ueber die geeignet Wahl der IP-Adresse die Erreichbarkeit kontrolliert werden. Fuer die Kommunikation von Diensten innerhalb einer Broadcast-Domain sollen link-local-Adressen verwendet werden. Im Hinblick auf IPv6 gibt es in diesem Bereich viele Spezialadressen welche fuer die verschiedensten Zwecke genutzt werden koennen. In jedem Fall sollte die Erreichbarkeit von Diensten soweit wie moeglich eingeschraenkt werden. Auch Tunnel-Techniken wie IPSec und andere VPN-Loesungen koennen verwendet werden, um einen Dienst ausschliesslich ausgewaehlten Nutzern zugaenglich zu machen. Vor allem in Windows-Systemen ist es in der Vergangenheit oft grossen Problemen mit Wuermern gekommen, weil verwundbare Dienste ueber das Netzwerk erreichbar waren. == Kernel Beim erstellen einer sicherheitskritischen Anwendung ist nicht nur die Architektur und Programmierung der Anwendung selbst wichtig. Die Umgebung in der die Anwendung laeuft ist ebenfalls ein wichtiger Faktor, fuer die Sicherheit eines Systems. So kann die Wahl eines anderen Betriebssystems dafuer ausschlaggebend sein, ob eine Programmierfehler zu einem Sicherheitsrisiko wird oder nicht. Im Folgenden werden einige Aspekte des Kernel-Designs auf ihre Sicherheit fuer das Gesamtsystem betrachtet. === Kernel-Architektur Die meisten Betriebssysteme verwenden ein monolitisches Kernel-Design. In diesem werden viele Aufgaben des Betriebssystems im Kernel erledigt. Das bedeutet, dass der Programm-Code mit sehr hohen Berechtigungen ausgefuehrt wird. Ein Grossteil dieser Aufgaben koennen auch von User-Prozessen uebernommen werden. Dieses hat den Vorteil, dass Schutzfunktionen des Prozessors genutzt werden koennen. Bei Betriebssystemen mit einem Mikro-Kernel-Design ist dieses anders. Dort werden viele Aufgaben an User-Space-Prozesse ausgelagert, zum Beispiel die Geraetetreiber. ==== Nvidia-Treiber-Problem Im +CVE-2012-0946+ wird eine Sicherheitsluecke im Nvidia Grafikkartentreiber fuer Linux, Solaris und FreeBSD beschrieben, welche das Erlangen von hoeheren Rechten ermoeglicht. Das Ausnutzen dieses Programmierfehlers ist nur moeglich, weil der Treiber im privilegierten Modus ausgefuehrt wird. ==== Minix Bei dem Betriebssystem Minix etwa, werden Geraetetreiber als User-Prozess gestartet. Dadurch unterliegen sie den gleichen Sicherheitsvorkehrungen wie alle anderen nicht privilegierten Prozesse. So werden z. B. Speicherbereichsverletzungen vom Prozessor erkannt. Ein fehlerhaft programmierter Treiber kann so nicht mehr das gesamte System gefaehrden. Dieses Kernel-Architektur hat grosse Performence-Nachteile gegenueber eines Monolitischen-Designs. Denn will einen Anwendung Beispielsweise mit der ueber das Netzwerk ein Paket versenden, so muss fuer dieses Paket zwei mal der Userspace-Kernelspace-Kontext gewechselt werden. Dieses kostet enorm viel Zeit, da der gesamte Prozessorkontext fuer den Uebergang von Anwendung zum Kernel, sowie vom Kernel zum Netzwerkkartentreiber gesichert werden muss. In einem monolitischen Design gaebe es nur einen Kontextwechsel von zum Kernel, welche das Netzwerkmodule enthaelt. Somit muss man an dieser Stelle Performence und Sicherheit abwiegen und fuer den konkreten Einzelfall entscheide welches wichtiger ist. === Kernel-Programmierung Fuer die Entwicklung von Kernel-Modulen muessen ebenfalls hohes Mass an Sicherheit und Sorgfalt gelten. Hat eine Anwendung im Userspace-Bereich noch einen ganzen virtuellen Speicherraum und den Prozessor-Kontext noch fuer sich alleine, muessen diese Ressourcen innerhalb des Kernels geteilt werden. Um fuer kritische Aufgaben einen unterbrechungsfreien Programmablauf zu garantieren, koennen im Kernel Hardware-Interrupts blockiert werden. Diese muessen nach Beendigung der Arbeit in jedem Fall wieder freigegeben werden. Ein haeufiger Fehler an dieser Stelle ist fehlen von Interrupt-Freigaben, in Fehlerbehandlungsroutinen. In einer Funktion wird dabei fuer eine wichtige Aufgabe ein Unterbrechungsfreier Prozessorkontext erzeugt, indem die Interrupts blockiert werden. Am Ende der Routine wird die Blockierung wieder aufgehoben und die Funktion verlassen. Der Fehler liegt dann oft in den Fehlerbehandlungen mitten in der Routine. Es wird Beispielsweise ein Zeiger auf +NULL+ geprueft und im Fehlerfall die Funktion mit einem Rueckgabewert verlassen, welcher einen Fehler anzeigt. Vergessen wurde aber die Interrupts wieder freizugeben. Die Funktion in die zurueck gesprungen wird, erwartet einen Interrupt z. B. von Tastatur, Maus oder Netzwerkkarte, aber wird nie einen erhalten. Dadurch kann das gesamte System zum Stillstand kommen. // vim: set syntax=asciidoc spell spelllang=de,en: