= 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 für 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 ausgeführt.
Gelingt es einem Angreifer einen solchen Dienst zu übernehmen, 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 für 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.

//Da dieses seit langer Zeit immer wieder für Sicherheitsprobleme sorgt, wurden
//viele Funktionen neu geschrieben.
Die herkömmlichen Funktionen für Zeichenketten besitzen Problem, dass die
Laengen von Speicherbereichen nicht beachtet werden.
Dadurch kommt es schnell zu Pufferüberlaeufen und Zugriffen auf nicht
allozierte Speicherbereiche.
Dadurch koennen Angreifer eigenen Programmcode einschleusen und ausführen
lassen, sowie Programme zum Absturz bringen.

Als Fallbeispiel wird das Problem anhand der Funktion +strcpy(3)+ erklaert.
Diese Funktion wird dafür 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 Null-Byte auftaucht.
Dabei kann die Funktion weder sicherstellen, dass sie nicht über den
Quellpuffer hinaus Bytes kopiert, sowie dass sie nicht über die Grenzen des
Zielpuffers hinaus schreibt.

Durch dieses Verhalten, kommt es immer wieder zu Pufferüberlaeufen, welche
zu unvorhersagbaren Folgen für den weiteren Programmablauf führen.

Um dieses Problem zu loesen wurden neue Zeichenkettenfunktionen in die
Standard-C-Bibliothek auf genommen, welche als Argument Maximallaengen
übergeben 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 über 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.

In der Praxis wird also z.B. ein Daemon mit Root-Rechten gestartet.
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.

=== Process-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 bei Servern, welche viele Dienste wie HTTP, FTP, SMTP, POP3 und
viele mehr anbieten, ist die Gefahr einer Sicherheitslücke sehr hoch.
Sollte einer dieser Dienste über eine Sicherheitslücke von einem Angreifer
übernommen werden, so koennte dieser ebenfalls die anderen Dienste und deren
Daten manipulieren.
Um dies zu vermeiden koennen diese Dienste in virtuellen Verzeichnisumgebungen
voneinander getrennt werden.
Eine Variante dafür ist der System-Call +chroot(2)+.
Dieser System-Call wechselt für einen Prozess und dessen Kind-Prozesse das
Root-Verzeichnis.
Dadurch kann ein Prozess auf Dateien außerhalb des neuen
Root-Verzeichnisses nicht mehr zugreifen.
Bereits geoeffnete Dateien koennen aber weiterhin verwendet werden,
auch wenn diese außerhalb des neuen Root-Verzeichnisses liegen.

Ein Beispiel für 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 jedoch in erster Linie keine
Sicherheitsfunktion,
da es definierte Wege gibt, ein chroot wieder zu verlassen.

Das Konzept von ``Jails'', wie etwa im FreeBSD-Betriebssystem verwendet,
sind dafür ausgelegt, verschiedene Prozesse sicher voneinander zu trennen.
Bei den ``Jails'' werden die System-Calls des eingeschlossenen Prozesses
gefiltert, um die Beeinflussung von anderen Prozessen zu verhindern.

Um verschiedene Prozesse voneinander zu trennen, kann auch die
UNIX-Rechte-Verwaltung verwendet werden.
Jeder Service sollte unter einem separaten Nutzerkonto laufen.
Es sollte vermieden werden, mehrere Dienste mit den gleichen Nutzungsrechten
auszuführen, da sich diese untereinander beeinflussen koennen.
Sollte ein Dienst keine eigenen Dateien benoetigen, kann dieser unter dem
speziellen Nutzer ``nobody'' ausgeführt werden.
//FIXME: Widerspruch der letzten beiden Satze
Unter vielen Unix-Systemen wird dieser speziell für diese Aufgabe verwendet.

=== Beschraenkung der Erreichbarkeit

Wenn ein Dienst nur für einen bestimmten Kreis von Nutzern bestimmt,
dann sollte man die Erreichbarkeit des Dienstes auf diesen Kreis beschraenken,
denn nur für wenige Systemdienste wird eine weltweite Erreichbarkeit benötigt.

Ein Systemdienst, welcher mit hoeheren Rechten laeuft und global über
Netzwerk erreichbar ist, stellt immer eine enormes Sicherheitsrisiko für das
Gesamtsystem dar.
Bevor man sich bei der Entwicklung eines Systemdienstes für einen bestimmten
Kommunikationsweg entscheidet, muss man sich bewusst sein, wer mit wem
kommuniziert und wo sich die Kommumikationspartner befinden.
Kommunikation innerhalb eines Hosts sollte über das Loopback-Interface
oder UNIX-Domain-Sockets geführt werden.
Ein Dienst, welcher über das Loopback-Interface kommuniziert, kann von jedem
Prozess erreicht werden.
UNIX-Domain-Sockets sind an Dateien gebundene Verbindungen,
mit den gewöhnlichen Berechtigungsparametern, durch die sich über die
systeminterne Benutzer- und Rechteverwaltung der Zugriff auf die Verbindung
steuern lässt.
Wird doch eine Netzwerkkommunikation benoetigt, kann über die geeignet Wahl
der IP-Adresse die Erreichbarkeit kontrolliert werden.
Für 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
für die verschiedensten Zwecke genutzt werden koennen.

In jedem Fall sollte die Erreichbarkeit von Diensten soweit wie moeglich
eingeschraenkt werden.
Auch Tunneltechniken 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 zu großen Problemen
mit Würmern gekommen, weil verwundbare Dienste über 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,
für die Sicherheit eines Systems.
So kann die Wahl eines anderen Betriebssystems dafür ausschlaggebend sein, ob
eine Programmierfehler zu einem Sicherheitsrisiko wird oder nicht.

Im Folgenden werden einige Aspekte des Kernel-Designs auf ihre Sicherheit für
das Gesamtsystem betrachtet.

=== Kernel-Architektur

Die meisten Betriebssysteme verwenden ein monolithisches Kernel-Design.
In diesem werden viele Aufgaben des Betriebssystems im Kernel erledigt.
Das bedeutet, dass der Programm-Code mit sehr hohen Berechtigungen
ausgeführt wird.

Ein Großteil dieser Aufgaben -- wie zum Beispiel die Gerätetreiber --
koennten auch von User-Prozessen übernommen werden.
Dies hat den Vorteil, dass Schutzfunktionen des Prozessors genutzt werden.
Diese Archtitekur nennt sich Mikro-Kernel-Design.

==== Nvidia-Treiber-Problem

Im +CVE-2012-0946+ wird eine Sicherheitslücke im Nvidia Grafikkartentreiber
für Linux, Solaris und FreeBSD beschrieben, die das Erlangen von hoeheren
Rechten ermoeglicht.
Das Ausnutzen dieses Programmierfehlers ist nur moeglich, weil der Treiber
im privilegierten Modus ausgeführt wird.

==== Minix

Bei dem Betriebssystem Minix, 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 Performanz-Nachteile gegenüber einem
monolithischen Design.
Will einen Anwendung Beispielsweise über das Netzwerk ein Paket versenden, so
muss für dieses Paket zwei mal der Userspace-Kernelspace-Kontext gewechselt
werden.
Dies kostet enorm viel Zeit, da der gesamte Prozessorkontext für den Uebergang
von Anwendung zum Kernel, sowie vom Kernel zum Netzwerkkartentreiber gesichert
werden muss.
In einem monolithischen Design gaebe es nur einen Kontextwechsel zum Kernel,
der das Netzwerkmodul enthaelt.
Somit muss an dieser Stelle Performanz und Sicherheit abwogen werden und für
den konkreten Einzelfall, sich für das wichtigere entschieden werden.

=== Kernel-Programmierung

//FIXME: Erster satz - konkrete Aussage?
Für die Entwicklung von Kernel-Modulen müssen ebenfalls hohes Mass an
Sicherheit und Sorgfalt gelten.
Hat eine Anwendung im Userspace-Bereich noch einen ganzen virtuellen
Speicherraum und den Prozessor-Kontext noch für sich alleine, müssen diese
Ressourcen innerhalb des Kernels geteilt werden.
Um für kritische Aufgaben einen unterbrechungsfreien Programmablauf zu
garantieren, koennen im Kernel Hardware-Interrupts blockiert werden.
Diese müssen nach Beendigung der Arbeit in jedem Fall wieder freigegeben
werden.
Ein haeufiger Fehler an dieser Stelle ist das Fehlen von Interrupt-Freigaben, in
Fehlerbehandlungsroutinen.
In einer Funktion wird dabei für 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+ geprüft und im Fehlerfall
die Funktion mit einem Rückgabewert verlassen, welcher einen Fehler anzeigt.
Vergessen wurde aber die Interrupts wieder freizugeben.
Die Funktion in die zurück gesprungen wird, erwartet einen Interrupt z. B.
von Tastatur, Maus oder Netzwerkkarte, aber wird einen solchen nie einen
erhalten.
Dadurch kann das gesamte System zum Stillstand kommen.

// vim: set syntax=asciidoc spell spelllang=de,en:
