Murray
Stokely
Contributed by
Sicheres Programmieren
Zusammenfassung
Dieses Kapitel beschreibt einige Sicherheitsprobleme,
die &unix;-Programmierer seit Jahrzehnten quälen, und
inzwischen verfügbare Werkzeuge, die Programmierern
helfen, Sicherheitslücken in ihrem Quelltext zu
vermeiden.
Methoden des sicheren Entwurfs
Sichere Anwendungen zu schreiben erfordert eine sehr
skeptische und pessimistische Lebenseinstellung.
Anwendungen sollten nach dem Prinzip der
geringsten Privilegien
ausgeführt
werden, sodass kein Prozess mit mehr als dem absoluten
Minimum an Zugriffsrechten arbeitet, die er zum
Erfüllen seiner Aufgabe benötigt. Wo es
möglich ist, sollte Quelltext, der bereits
überprüft wurde, wiederverwendet werden, um
häufige Fehler, die andere schon korrigiert haben,
zu vermeiden.
Eine der Stolperfallen der &unix;-Umgebung ist, dass es
sehr einfach ist Annahmen über die Gesundheit der
Umgebung zu machen. Anwendungen sollten Nutzereingaben (in
allen Formen) niemals trauen, genausowenig wie System-Ressourcen,
Inter-Prozess-Kommunikation oder dem zeitlichen Ablauf von
Ereignissen. &unix;-Prozesse arbeiten nicht synchron. Daher
sind logische Operationen selten atomar.
Puffer-Überläufe
Puffer-Überläufe gibt es schon seit den
Anfängen der Von-Neuman-Architektur .
Puffer-Überlauf
Von-Neuman
Sie fanden zum ersten Mal verbreitete Beachtung durch den
Internetwurm Morris im Jahr 1988. Unglücklicherweise
Morris Internetwurm
funktioniert der gleiche grundlegende Angriff noch heute. Von
den 17 CERT Sicherheitshinweisen im Jahr 1999 wurden zehn
CERT
Sicheitshinweise
direkt durch Puffer-Überläufe in Software
verursacht. Die bei weitem häufigste Form eines
Puffer-Überlauf-Angriffs basiert darauf den Stack
zu korrumpieren.
Stack
Arguments
Die meisten modernen Computer-Systeme verwenden einen
Stack, um Argumente an Prozeduren zu übergeben und
lokale Variable zu speichern. Ein Stack ist ein
last-in-first-out-Puffer (LIFO) im hohen Speicherbereich
eines Prozessabbilds. Wenn ein Programm eine Funktion
LIFO
Prozessabbild
Stack-Pointer
aufruft wird ein neuer "Stack-Frame" erzeugt. Dieser
besteht aus den Argumenten, die der Funktion übergeben
wurden, und einer dynamischen Speichermenge für lokale
Variablen. Der "Stack-Pointer" ist ein Register, das die
Stack-Frame
Stack-Pointer
aktuelle Position der Spitze des Stacks enthält.
Da sich dieser Wert andauernd ändert, wenn neue Werte
auf dem Stack abgelegt werden, bieten viele Implementierungen
einen "Frame-Pointer", der nahe am Anfang des Stack-Frames
liegt und es so leichter macht lokale Variablen relativ dazu
zu adressieren.
Die Rücksprungadresse
Frame-Pointer
Prozessabbild
Frame-Pointer
Rücksprungadresse
Stack-Überlauf
für Funktionsaufrufe wird ebenfalls auf dem Stack
gespeichert und das ist der Grund für
Stack-Überlauf-Exploits, da ein böswilliger Nutzer,
indem er eine lokale Variable in einer Funktion
überlaufen lässt, die Rücksprungadresse der
Funktion überschreiben und dadurch beliebigen Code
ausführen kann.
Obwohl Stack-basierte Angriffe bei weitem die
Häufigsten sind, ist es auch möglich den Stack
mit einem Heap-basierten (malloc/free) Angriff zu
überschreiben.
Die C-Programmiersprache führt keine automatischen
Bereichsprüfungen bei Arrays oder Zeigern durch, wie
viele andere Sprachen das tun. Außerdem enthält
die C-Standardbibliothek eine handvoll sehr
gefährlicher Funktionen.
strcpy(char *dest,
const char *src)
Kann den Puffer dest überlaufen
lassen
strcat(char *dest,
const char *src)
Kann den Puffer dest überlaufen
lassen
getwd(char *buf)
Kann den Puffer buf überlaufen
lassen
gets(char *s)
Kann den Puffer s überlaufen
lassen
[vf]scanf(const char
*format, ...)
Kann sein Argument überlaufen
lassen
realpath(char *path,
char resolved_path[])
Kann den Puffer path überlaufen
lassen
[v]sprintf(char *str,
const char *format, ...)
Kann den Puffer str überlaufen
lassen
Puffer-Überlauf Beispiel
Das folgende Quellcode-Beispiel enthält einen
Puffer-Überlauf, der darauf ausgelegt ist die
Rücksprungadresse zu überschreiben und die
Anweisung direkt nach dem Funktionsaufruf zu
überspringen. (Inspiriert durch
)
#include stdio.h
void manipulate(char *buffer) {
char newbuffer[80];
strcpy(newbuffer,buffer);
}
int main() {
char ch,buffer[4096];
int i=0;
while ((buffer[i++] = getchar()) != '\n') {};
i=1;
manipulate(buffer);
i=2;
printf("The value of i is : %d\n",i);
return 0;
}
Betrachten wir nun, wie das Speicherabbild dieses
Prozesses aussehen würde, wenn wir 160 Leerzeichen
in unser kleines Programm eingeben, bevor wir Enter
drücken.
[XXX figure here!]
Offensichtlich kann man durch böswilligere Eingaben
bereits kompilierten Programmtext ausführen (wie z.B.
exec(/bin/sh)).
Puffer-Überläufe vermeiden
Die offensichtlichste Lösung, um
Stack-Überläufe zu vermeiden, ist bei Operationen
auf Zeichenkette immer Funktionen und Speicher mit einer
begrenzten Länge zu verwenden.
strncpy und strncat
sind Teil der C-Standardbibliothek.
Zeichenketten-Kopierfunktioen
strncpy
Zeichenketten-Kopierfunktionen
strncat
Diese Funktionen akzeptieren einen Parameter length. Dieser
Wert sollte nicht größer sein als die Länge
des Zielpuffers. Die Funktionen kopieren dann bis zu
`length' Bytes von der Quelle zum Ziel. Allerdings gibt es
auch einige Probleme. Keine der Funktionen garantiert, dass
die Zeichenkette NUL-terminiert ist, wenn die
Größe
NUL-Terminierung
des Eingabepuffers so groß ist wie das Ziel.
Außerdem wird der Parameter length zwischen strncpy
und strncat inkonsistent benutzt, weshalb Programmierer
leicht bezüglich der korrekten Verwendung durcheinander
kommen können. Weiterhin gibt es einen spürbaren
Leistungsverlust im Vergleich zu
strcpy, wenn eine kurze Zeichenkette in
einen großen Puffer kopiert wird, da
strncpy bis zur angegebenen Länge
mit NUL auffüllt.
In OpenBSD wurde eine weitere Möglichkeit zum
OpenBSD
kopieren von Speicherbereichen implementiert, die dieses
Problem umgeht. Die Funktionen strlcpy
und strlcat garantieren, dass das Ziel
immer NUL-terminiert wird, wenn das Argument length ungleich
null ist. Für weitere Informationen über diese
Funktionen lesen Sie bitte . Die
OpenBSD-Anweisungen strlcpy und
strlcat sind seit Version 3.3 auch in
FreeBSD verfügbar.
Zeichenketten-Kopierfunktionen
strlcpy
Zeichenketten-Kopierfunktionen
strlcat
Compiler-basierte Laufzeitprüfung
von Grenzen
Prüfung von Grenzen
Compiler-basiert
Unglücklicherweise gibt es immer noch sehr viel
Quelltext, der allgemein verwendet wird und blind Speicher
umherkopiert, ohne eine der begrenzenden Funktionen zu
verwenden, die wir gerade besprochen haben.
Glücklicherweise gibt es eine weitere Lösung.
Es gibt einige Compiler-Erweiterungen und Bibliotheken,
die Grenzen in C/C++ zur Laufzeit überprüfen.
StackGuard
GCC
StackGuard ist eine solche Erweiterung, die als
kleiner Patch für den GCC-Code-Generator
implementiert ist. Von der StackGuard
Webseite (übersetzt):
"StackGuard erkennt und verhindert
sogenannte Stack-Smashing-Angriffe, indem es die
Rücksprungadresse auf dem Stack davor
schützt geändert zu werden. StackGuard
platziert ein "Canary"-Wort (Anmerkung des
Übersetzers: Kanarienvogel, nach einer
Sicherheitsvorkehrung von Bergleuten, um Gas
frühzeitig zu erkennen) neben der
Rücksprungadresse, wenn eine Funktion aufgerufen
wird. Wenn das Canary bei der Rückkehr der
Funktion geändert wurde, erkennt das Programm
den Versuch eines Stack-Smashing-Angriffs,
schickt eine Benachrichtigung an Syslog und
hält dann an."
"StackGuard ist als ein kleiner Patch für
den GCC-Code-Generator implementiert, um genau zu
sein für die Routinen function_prolog() und
function_epilog().
function_prolog() wurde erweitert, um Canaries beim
Start einer Funktion auf den Stack zu legen und
function_epilog() überprüft die
Integrität des Canaries beim Beenden der
Funktion. Daher wird jeder Versuch die
Rücksprungadresse zu verändern erkannt
bevor die Funktion zurückkehrt."
Puffer-Überlauf
Ihre Anwendungen mit StackGuard neu zu kompilieren ist
eine effektive Maßnahme, um sie vor den meisten
Puffer-Überlauf-Angriffen zu schützen, aber sie
können noch immer gefährdet sein.
Bibliotheks-basierte Laufzeitprüfung
von Grenzen
Prüfung von Grenzen
Bibliotheks-basiert
Compiler-basierte Mechanismen sind bei Software,
die nur im Binärformat vertrieben wird, und die somit
nicht neu kompiliert werden kann völlig nutzlos.
Für diesen Fall gibt es einige Bibliotheken, welche
die unsicheren Funktionen der C-Bibliothek
(strcpy, fscanf,
getwd, etc..) neu implementieren und
sicherstellen, dass nicht hinter den Stack-Pointer
geschrieben werden kann.
libsafe
libverify
libparanoia
Leider haben diese Bibliotheks-basierten
Verteidigungen mehrere Schwächen. Diese Bibliotheken
schützen nur vor einer kleinen Gruppe von
Sicherheitslücken und sie können das
eigentliche Problem nicht lösen. Diese
Maßnahmen können versagen, wenn die Anwendung
mit -fomit-frame-pointer kompiliert wurde.
Außerdem kann der Nutzer die Umgebungsvariablen
LD_PRELOAD und LD_LIBRARY_PATH überschreiben oder
löschen.
SetUID-Themen
seteuid
Es gibt zu jedem Prozess mindestens sechs verschiedene
IDs, die diesem zugeordnet sind. Deshalb müssen Sie
sehr vorsichtig mit den Zugriffsrechten sein, die Ihr Prozess
zu jedem Zeitpunkt besitzt. Speziell heißt das alle
seteuid-Anwendungen sollten ihre Privilegien abgeben, sobald
sie diese nicht mehr benötigen.
Benutzer-IDs
reale Benutzer-ID
Benutzer-IDs
effective Benutzer-ID
Die reale Benutzer-ID kann nur von einem
Superuser-Prozess geändert werden. Das Programm
login setzt sie, wenn sich ein
Benutzer am System anmeldet, und sie wird nur selten
geändert.
Die effektive Benutzer-ID wird von der Funktion
exec() gesetzt, wenn das seteuid-Bit
eines Programmes gesetzt ist. Eine Anwendung kann
seteuid() zu jeder Zeit aufrufen, um die
effektive Benutzer-ID entweder auf die reale Benutzer-ID oder
die gespeicherte set-user-ID zu setzen. Wenn die Funktion
exec() die effektive Benutzer-ID setzt,
wird der vorherige Wert als gespeicherte set-user-ID
abgelegt.
Die Umgebung ihres Programme einschränken
chroot()
Die herkömmliche Methode, um einen Prozess
einzuschränken, besteht im Systemaufruf
chroot(). Dieser Aufruf
ändert das Wurzelverzeichnis, auf das sich alle
Pfadangaben des Prozesses und seiner Kind-Prozesse beziehen.
Damit dieser Systemaufruf gelingt, muss der Prozess
Ausführungsrechte (Durchsuchrechte) für das
Verzeichnis haben, auf das er sich bezieht. Die neue Umgebung
wird erst wirksam, wenn Sie mittels
chdir() in Ihre neue Umgebung wechseln.
Es sollte erwähnt werden, dass ein Prozess recht einfach
aus der chroot-Umgebung ausbrechen kann, wenn er root-Rechte
besitzt. Das kann man erreichen, indem man Gerätedateien
anlegt, um Kernel-Speicher zu lesen, oder indem man einen
Debugger mit einem Prozess außerhalb seines
Gefängnisses verbindet, oder auf viele andere
kreative Wege.
Das Verhalten des Systemaufrufs
chroot() kann durch die
kern.chroot.allow_open_directories
sysctl-Variable beeinflusst werden. Wenn
diese auf 0 gesetzt ist, wird chroot()
mit EPERM fehlschlagen, wenn irgendwelche Verzeichnisse
geöffnet sind. Wenn die Variable auf den Standardwert 1
gesetzt ist, wird chroot() mit EPERM
fehlschlagen, wenn irgendwelche Verzeichnisse geöffnet
sind und sich der Prozess bereits in einer
chroot()-Umgebung befindet. Bei jedem
anderen Wert wird die Überprüfung auf
geöffnete Verzeichnisse komplett umgangen.
Die Jail-Funktionalität in FreeBSD
Jail
Das Konzept einer Jail (Gefängnis) erweitert
chroot(), indem es die Macht des
Superusers einschränkt, um einen echten 'virtuellen
Server' zu erzeugen. Wenn ein solches Gefängnis einmal
eingerichtet ist, muss die gesamte Netzwerkkommunikation
über eine bestimmte IP-Adresse erfolgen und die
"root-Privilegien" innerhalb der Jail sind sehr stark
eingeschränkt.
Solange Sie sich in einer Jail befinden, werden alle
Tests auf Superuser-Rechte durch den Aufruf von
suser() fehlschlagen. Allerdings
wurden einige Aufrufe von suser()
abgeändert, um die neue
suser_xxx()-Schnittstelle zu nutzen.
Diese Funktion ist dafür verantwortlich festzustellen,
ob bestimmte Superuser-Rechte einem eingesperrten Prozess
zur Verfügung stehen.
Ein Superuser-Prozess innerhalb einer Jail darf
folgendes:
Berechtigungen verändern mittels:
setuid,
seteuid,
setgid,
setegid,
setgroups,
setreuid,
setregid,
setlogin
Ressourcenbegrenzungen setzen mittels
setrlimit
Einige sysctl-Variablen (kern.hostname)
verändern
chroot()
Ein Flag einer vnode setzen:
chflags,
fchflags
Attribute einer vnode setzen wie Dateiberechtigungen,
Eigentümer, Gruppe, Größe, Zugriffszeit
und Modifikationszeit
Binden eines privilegierten Ports in der
Internet-Domain (ports < 1024)
Jails sind ein mächtiges
Werkzeug, um Anwendungen in einer sicheren Umgebung
auszuführen, aber sie haben auch ihre Nachteile.
Derzeit wurden IPC-Mechanismen noch nicht an
suser_xxx angepasst, sodass Anwendungen
wie MySQL nicht innerhalb einer Jail ausgeführt werden
können. Superuser-Zugriff hat in einer Jail nur eine
sehr eingeschränkte Bedeutung, aber man kann nicht genau
sagen was "sehr eingeschränkt" heißt.
&posix;.1e Prozess Capabilities
POSIX.1e Process Capabilities
TrustedBSD
&posix; hat einen funktionellen Entwurf (Working Draft)
herausgegeben, der Ereignisüberprüfung,
Zugriffskontrolllisten, feinkörnige Privilegien,
Informationsmarkierung und verbindliche Zugriffskontrolle
enthält.
Das ist laufende Arbeit und das Hauptziel des TrustedBSD-Projekts.
Ein Teil der bisherigen Arbeit wurde in &os.current;
übernommen (cap_set_proc(3)).
Vertrauen
Eine Anwendung sollte niemals davon ausgehen, dass
irgendetwas in der Nutzerumgebung vernünftig ist.
Das beinhaltet (ist aber sicher nicht darauf
beschränkt): Nutzereingaben, Signale,
Umgebungsvariablen, Ressourcen, IPC, mmaps, das
Arbeitsverzeichnis im Dateisystem, Dateideskriptoren,
die Anzahl geöffneter Dateien, etc..
positive Filterung
Datenvalidierung
Sie sollten niemals annehmen, dass Sie jede Art von
inkorrekten Eingaben abfangen können, die ein Nutzer
machen kann. Stattdessen sollte Ihre Anwendung positive
Filterung verwenden, um nur eine bestimmte Teilmenge an
Eingaben zuzulassen, die Sie für sicher halten.
Unsachgemäße Datenvalidierung ist die Ursache
vieler Exploits, besonders für CGI-Skripten im Internet.
Bei Dateinamen müssen Sie besonders vorsichtig sein,
wenn es sich um Pfade ("../", "/"), symbolische
Verknüpfungen und Shell-Escape-Sequenzen handelt.
Perl Taint-Modus
Perl bietet eine wirklich coole Funktion, den sogenannten
"Taint"-Modus, der verwendet werden kann, um zu verhindern,
dass Skripten Daten, die von außerhalb des Programmes
stammen, auf unsichere Art und Weise verwendet werden. Dieser
Modus überprüft Kommandozeilenargumente,
Umgebungsvariablen, Lokalisierungsinformationen, die
Ergebnisse von Systemaufrufen
(readdir(),
readlink(),
getpwxxx())
und alle Dateieingaben.
Race-Conditions
Eine Race-Condition ist ein abnormales Verhalten, das
von einer unerwarteten Abhängigkeit beim Timing von
Ereignissen verursacht wird. Mit anderen Worten heißt
das, ein Programmierer nimmt irrtümlicher Weise an, dass
ein bestimmtes Ereignis immer vor einem anderen stattfindet.
Race-Conditions
Signale
Race-Conditions
Zugriffsprüfungen
Race-Conditions
Öffnen von Dateien
Einige der häufigsten Ursachen für
Race-Conditions sind Signale, Zugriffsprüfungen und das
Öffnen von Dateien. Signale sind von Natur aus
asynchrone Ereignisse, deshalb ist besondere Vorsicht im
Umgang damit geboten. Das Prüfen des Zugriffs mittels
der Aufrufe access(2) gefolgt von
open(2) ist offensichtlich nicht atomar.
Benutzer können zwischen den beiden Aufrufen Dateien
verschieben. Stattdessen sollten privilegierte Anwendungen
seteuid() direkt gefolgt von
open() aufrufen. Auf die gleiche Art
sollte eine Anwendung immer eine korrekte Umask vor dem
Aufruf von open() setzen, um
störende Aufrufe von chmod() zu
umgehen.