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.