So mancher Admin ist unbeabsichtigt Mitverursacher der eigenen Sicherheitsprobleme, wenn er zu Editor und Compiler greift und Skripte schreibt oder Programme ändert. Dieser Workshop erklärt häufige und gefährliche Fehler im Zusammenhang mit der Prozessumgebung und zeigt, wie es besser geht.
Vom berüchtigten Pufferüberlauf und von Cross-Site-Skripting oder Format-String-Attacken haben die meisten Admins längst gehört. Dicke Bücher widmen sich der Beschreibung von Fehlern und zeigen, wie man sie vermeidet[1],[2]. Anders als vielleicht erwartet ist dieses Know-how aber nicht nur für Software-Entwickler wichtig. Auch und gerade Admins müssen wissen, wie sie sichere Skripte schreiben (unter Zeitdruck) oder beim Ändern von Applikationen typische Fehler vermeiden.
Selbst wenn die Lösung von der reinen Lehre abweicht oder die Effizienz leidet: Sicherheitslöcher darf niemand leichtfertig riskieren[10]. Gelegentlich heißt es sogar: Programm deaktivieren oder selber die Löcher stopfen. Dann muss der Admin beurteilen, was nötig ist und ob ein Patch tatsächlich einen Fehler behebt. Das nötige Hintergrundwissen vermittelt dieser Workshop.
Die Umgebung (Environment), in der ein Programm oder Skript abläuft, ist ein verblüffend komplexes Gebilde. Sie enthält Umgebungsvariablen, das Arbeitsverzeichnis, Rootverzeichnis, Rechte, Ressourcenlimits, Umask, File-Deskriptoren, Signalhandler und einiges mehr. Gelingt es einem Angreifer, die Umgebung fremder Prozesse gezielt zu beeinflussen, droht Gefahr. Vorsicht ist vor allem geboten, wenn Programme mit gesetztem Set-UID-Bit laufen.
Kenne deine Umgebung
Privilegierte Prozesse bieten eine besonders große Angriffsfläche. Saboteure manipulieren eventuell schon den Programmstart, um die Software in einer unerwarteten Umgebung laufen zu lassen. Beim Start erbt der Prozesses das Environment seines Vaterprozesses, auch Shell und Kernel nehmen darauf Einfluss. Viele Charakteristika stammen indirekt aus dem übrigen System, zum Beispiel der verfügbare Speicher oder der Aufbau der Dateisysteme (siehe Abbildung 1).
Umweltbewusstsein
Das kleine Linux-Werkzeug Env_audit[3] stellt viele Eigenschaften eines Prozesses in lesbarer Form dar. Der Kasten “Umgebungs-Audit” erläutert die Installation und Anwendung des Tools. Listing 1 zeigt die Ausgabe von »env_audit«, das unter der Standardinstallation eines Apache 1.3.26 auf Debian Woody 3.0r2 als CGI-Skript lief.
Die Zeilen 3 bis 12 informieren über die Prozessverwaltung und die Rechte. Die CGI-Skripte arbeiten unter der User- und der Gruppenkennung »www-data«. Das hat recht unangenehmen Konsequenzen, wenn der Server die Präsenzen mehrerer Kunden hostet, die CGI-Skripte installieren dürfen. Dann könnten die Prozesse des einen Kunden denen der anderen Signale schicken, etwa »kill -9«, den fremden Arbeitsspeicher über das »/proc«-Dateisystem einsehen oder temporäre Dateien manipulieren.
Laut Zeile 14 verzichtet die Umask darauf, bei neu erzeugten Dateien die Leserechte anderer Benutzer einzuschränken. Skripte, die mit Geheimnissen hantieren, sollten das beachten und ihre Umask auf einen sicheren Wert setzen. Ab Zeile 18 listet Env_audit die Umgebungsvariablen. »PATH« sieht vernünftig aus – Apache hat einen Standardwert benutzt. Das ist oft wichtig, damit ein Angreifer kein ».« einschmuggelt.
Neben »PATH« ist die »IFS«-Variable besonders gefährlich. Sie enthält die Zeichen, an denen die Shell einzelne Wörter auf der Kommandozeile trennt. Meist sind das Leerzeichen, Tabulatoren und Zeilenumbrüche. In Zeile 46 warnt Env_audit, dass IFS gar nicht gesetzt ist.

Abbildung 1: Die Umgebung eines Prozesses führt interne Eigenschaften wie Umask oder UID auf sowie Schnittstellen zur Umgebung – von Umgebungsvariablen bis zur Ein- und Ausgabe. Auch das übrige System wirkt sich auf einen Prozess aus.
Freiwillige Selbstverpflichtung
Der nächste Abschnitt (Zeilen 50 bis 62) stellt tabellarisch die Grenzen für den Einsatz der Systemressourcen dar, ähnlich wie »ulimit -a«, aber ausführlicher. Aufschlussreich ist die Aufteilung in aktuelle Grenzen (Current) und Maximalgrenzen (Max). Erstere kann ein normaler User verändern, etwa den verfügbaren Speicher auf dem Stack »RLIMIT _STACK«, indem er die »setrlimit()«-Funktion aufruft. Die Maximalwerte darf nur Root ändern. Der Administrator begrenzt hier am besten den Ressourcenverbrauch.
|
Listing 1: Gekürzte |
|---|
003 Process ID: 10369 004 Parent Process ID: 10353 005 User ID: 33 - www-data 006 Group ID: 33 - www-data 007 Effective User ID: 33 - www-data 008 Effective Group ID: 33 - www-data 009 Supplemental Groups: www-data 010 Process Group ID: 10299 011 Session ID: 10299 012 Parent Session ID: 10299 013 Current Working Dir: /var/www/cgi-bin 014 Umask: 22 015 Process Priority: 5 018 Environmental Variables 028 $PATH=/bin:/usr/bin:/sbin:/usr/sbin 046 WARNING $IFS undefined 050 Resource Limits 051 Name Current Max 052 RLIMIT_CORE (infinity) (infinity) 053 RLIMIT_CPU (infinity) (infinity) 054 RLIMIT_DATA (infinity) (infinity) 055 RLIMIT_FSIZE (infinity) (infinity) 056 RLIMIT_MEMLOCK (infinity) (infinity) 057 RLIMIT_NOFILE 1024 1024 058 RLIMIT_OFILE 1024 1024 059 RLIMIT_NPROC 6144 6144 060 RLIMIT_RSS (infinity) (infinity) 061 RLIMIT_STACK 8388608 (infinity) 062 RLIMIT_AS (infinity) (infinity) 065 Open file descriptor: 0 066 User ID of File Owner: www-data 067 Group ID of File Owner: www-data 068 Descriptor is stdin. 069 No controlling terminal 070 File type: fifo, inode - 10051, device - 7 071 The descriptor is: pipe:[10051] 072 File descriptor mode is: read only 085 Open file descriptor: 2 086 User ID of File Owner: root 087 Group ID of File Owner: root 088 Descriptor is stderr. 089 No controlling terminal 090 File type: regular file, inode - 222552, device - 769 091 The descriptor is: /var/log/apache/error.log 092 File's actual permissions: 644 093 File descriptor mode is: write only, append 106 Open file descriptor: 4 107 User ID of File Owner: root 108 Group ID of File Owner: root 109 WARNING - Descriptor is leaked from parent. 110 File type: regular file, inode - 333258, device - 769 111 The descriptor is: /tmp/session_mm_apache0.sem 112 File's actual permissions: 600 113 File descriptor mode is: read and write |
Zum Schluss stehen ab Zeile 65 offene Dateideskriptoren. Deskriptor »0« (Standardeingabe, Zeilen 65 bis 72) ist ein Fifo (eine Pipe) zum Vaterprozess, ebenso die Standardausgabe (nicht abgedruckt). Interessant ist Deskriptor »2« (Fehlerausgabe, Zeilen 85 bis 93). Das CGI-Skript darf in die Logdatei »/var/log/apache/error.log« (Zeile 91) schreiben (Zeile 93), obwohl sie Root gehört (Zeilen 86 und 87). Das heißt, das Skript kann beliebige Meldungen fälschen und an seiner Disk-Quota vorbei das Dateisystem mit Daten füllen. Das ist vermutlich nicht im Sinne des Admin.
Bei Deskriptor »4« (ab Zeile 106) warnt Env_audit vor einem Deskriptor-Leck im Vaterprozess (Zeile 109). Nur anhand der Ausgaben ist aber nicht zu entscheiden, ob die Apache-Entwickler das beabsichtigt haben. Wieder kann das CGI-Programm die Disk-Quota umgehen, diesmal im »/tmp«-Verzeichnis.
Erstaunlich, was sich alles in diesem Beispiel verbirgt. Wie schnell das zu Sicherheitslöchern führt, zeigt Listing 2. Es ist als Set-UID-Root-Programm gedacht, unter dem Namen »sudo-clone« implementiert es einen hausgemachten Sudo-Mechanismus. Das Programm tappt jedoch in die Fallen 3, 4 und 5 der im Folgenden beschriebenen 9er-Liste. Die Folge: Ein Angreifer kann die Sudo-clone-Passwortdatei lesen oder zerstören. Wie er das schafft und wie der Programmierer dagegen vorgeht, beschreibt der Abschnitt “Angriff der Klonkrieger” am Ende des Artikels.
Wer auf ausgefeilte Techniken und alle technischen Details Wert legt, wird in[1] und[4] fündig. Die neun Fallen lassen sich – wie nachfolgend beschrieben – aber recht einfach umgehen, sowohl in C- und C++-Programmen als auch in Shellskripten. Skriptsprachen haben es schwerer als C-Programme, ihr Environment aufzuräumen. Das Programm Super[5] schafft Abhilfe. Es sorgt für eine Umgebung, in der Skripte gefahrlos mit Root-Rechten ablaufen. Sudo[6] ähnelt Super, arbeitet aber weniger strikt.
Falle 1: Speicherabzüge
Bei Abstürzen hinterlassen Programme einen Speicherabzug im Arbeitsverzeichnis. Zum Debugging ist die »core«-Datei sehr praktisch, führt aber auch zu Problemen. Core-Dateien enthalten gelegentlich vertrauliche Informationen wie Passwörter, die nicht im Klartext auf die Festplatte gehören. Auch wenn Linux die Core-Files mit den Rechten 600 erzeugt, es schadet auf keinen Fall, die Umask strikt einzustellen.
Zudem kann ein Core recht groß ausfallen. Im ungünstigsten Fall landet der gesamte virtuelle Speicher einschließlich Swapspace auf der Platte, das sind je nach Maschine eventuell einige GByte Daten. Möglicherweise kann ein lokaler oder externer Angreifer solche Abstürze auslösen und so einen Denial of Service (DoS) starten.
Immerhin hinterlassen Linux-Programme mit Set-UID- oder Set-GID-Bit niemals einen Core. Außerdem kann der Benutzer mit Linux und anderen Unix-Varianten die maximale Größe der Core-Dateien begrenzen. Der Shellbefehl »ulimit -a | grep core« gibt die gültigen Schranken in 512-Byte-Blöcken aus:
core file size (blocks, -c) unlimited
Lösung für die Shell: Startet ein Skript einen Prozess, erbt dieser die Einstellungen der Shell. Die Zeile »ulimit -c 0« am Anfang eines Skripts oder in einer Shell-Konfigurationsdatei unterdrückt Core-Dateien aller Abkömmlinge.
Lösung für C und C++: »setrlimit()« leistet unter Linux dieselben Dienste wie »ulimit« in der Shell (Listing 4, Funktion »disable_core_dumps()«).
Falle 2: Zugriffsrechte
Bei neuen Dateien erlaubt es das System normalerweise jedem Benutzer, sie zu lesen und zu schreiben (die Zugriffsrechte lauten in oktaler Schreibweise 666). Ein lokaler User darf dann zum Beispiel temporäre Files lesen und schreiben und vielleicht sogar Passwörter ausspähen. Open Office ist unlängst wegen einer zu laxen Umask ins Rampenlicht gerückt[7].
|
Listing 2: Falsches |
|---|
01 /* Das Set-UID-Root-Bit muss gesetzt sein.
02 * Die Passwörter stehen in /etc/Passworte
03 * im Zeilenformat "user-id:passwort".
04 */
05 #include <stdio.h>
06 #include <stdlib.h>
07 #include <unistd.h>
08
09 int main(void) {
10 char pwd[9], suche[99], zeile[99];
11 uid_t uid;
12 FILE *df;
13
14 /* Datenbank öffnen */
15 df = fopen("/etc/Passworte", "r+");
16 /* Zur Vorsicht: Datenpufferung aus */
17 setvbuf(df, 0, _IONBF, 0);
18
19 /* UID und Passwort einlesen */
20 printf("nBitte User-Id eingeben : ");
21 fscanf(stdin, "%d", &uid);
22 printf("Bitte Passwort eingeben: ");
23 fscanf(stdin, "%8s", pwd);
24
25 /* Eintrag in der Datenbank suchen */
26 sprintf(suche, "%d:%s", uid, pwd);
27 while (1) {
28 /* Zeilenweise suchen */
29 if (fscanf(df, "%98s", zeile) != 1)
30 exit(1); /* Dateiende */
31 if (strcmp(zeile, suche) == 0)
32 break; /* gefunden */
33 }
34
35 setreuid(uid, uid); /* Root abgeben */
36 execl("/bin/Skript", 0); /* starten */
37 return 255;
38 }
|
|
Umgebungs-Audit |
|---|
|
Das kleine, aber sehr nützliche Werkzeug Env_audit prüft gezielt die Umgebung eines Prozesses. Nach dem Laden und Auspacken des Tar-Archivs »env_audit-2.0.tar.gz« von[3] genügt ein »make«-Aufruf, um es zu übersetzen. Sollte dies – wie auf dem System des Autors – abbrechen, weil die Headerdatei »sys/capability.h« fehlt, dann klappt es nach ein paar Änderungen auch ohne Capabilities. In »env_audit.c« vor Zeile 48 ein »#undef« einfügen: #undef _POSIX_CAP 48 #ifdef _POSIX_CAP 49 #include <sys/capability.h> 50 #endif Außerdem aus Zeile 22 im »Makefile« die Bibliothek »-lcap« entfernen. Im Verzeichnis »examples« des Archivs befinden sich zahlreiche Anwendungsbeispiele. Bei jedem Aufruf erzeugt »env_audit« eine neue Datei »/tmp/env_audit XXXX.log«, wobei XXXX für eine vierstellige Zahl steht, beginnend bei 0000. Einige Beispiele gehen noch vom Namen »env_audit.log« aus – hier muss der Anwender gegebenenfalls selbst Hand anlegen. CGI-Skripte in ihrer natürlichen UmgebungEnv-Audit prüft beispielsweise die Umgebung, in der CGI-Skripte auf dem Webserver laufen. Dazu »env_audit« in das »cgi-bin«-Verzeichnis kopieren und ihm Ausführungsrechte geben, »chmod 555« genügt. Die Abfrage von »http://localhost/cgi-bin/env_audit« liefert nach zehn Sekunden die gewünschten Informationen. Damit dies klappt, wertet das Tool die Umgebungsvariable »HTTP_ACCEPT« aus. Ist sie gesetzt, schreibt »env_audit« statt in ein Logdatei in die Standardausgabe, die wiederum im Browser landet. |
Mit strenger Umask
Die Umask begrenzt diese Rechte. Viele Distributionen haben als Voreinstellung »umask 022«. Damit darf jedermann in neue Files schauen, während nur der Eigentümer schreiben darf.
Lösung für die Shell: Ein Aufruf des Befehls »umask 077« sichert das Skript gleich zu Beginn.
Lösung für C und C++: Bald nach dem Programmstart ruft der Programmierer die Funktion »umask(0077)« auf (Oktalzahlen, mit 0 beginnen). Vorsicht ist geboten: Es genügt nicht, gleich nach dem Erzeugen eines File mit »chmod()« dessen Zugriffsrechte zu ändern. Ein Angreifer könnte zwischendurch die Datei bereits geöffnet haben, es entsteht eine Race Condition. Angriffe dieser Art lassen sich gut automatisieren und sind verblüffend erfolgreich.
Falle 3: Variablen
Umgebungsvariablen sind eine tolle Sache. Der Anwender konfiguriert damit seine Shell und andere Programme oder spart Programmoptionen und Tipparbeit. Aus demselben Grund werden sie zur Gefahr, wenn ein Angreifer manipulierte Inhalte unterjubelt. Es gibt zahlreiche Variablen, die Risiken bergen: »IFS«, »PATH«, »TZ«, viele, die mit »LC_« und »LD_« beginnen, und noch weitere. Immer wenn ein Programm den Variablen vertraut (vielleicht sogar ohne es zu wissen), wird es gefährlich. Startet Root zum Beispiel ein Programm mit manipuliertem »PATH«, ruft es vielleicht statt »/bin/rm« eine Version »/tmp/rm« auf. Welche Variable wann wie benutzt wird, ist selten völlig klar.
Die Prozessumgebung speichert Variablen als Null-terminierte Zeichenketten der Form Name=Wert. Mit der Funktion »execve()« kann der Angreifer jeden Schrott setzen, etwa leere Variablennamen, Einträge ohne »=« oder mehrfache Zuweisungen derselben Variablen.
Lösung für die Shell: Die Bash und andere aktuelle Shells allein geben dem Admin keine zuverlässige Lösung, ausgenommen die Zsh[8] (siehe Listing 3). Der Programmierer ist darauf angewiesen, dass der Admin seiner Software eine kontrollierte Umgebung bereitstellt. Dazu bietet sich das Programm Super an, das alle Variablen löscht und einige wichtige mit Standardwerten belegt. Weniger geeignet ist Sudo, weil es zwar als bösartig bekannte Variablen filtert, aber alles Unbekannte unangetastet lässt.
Neuer Satz Variablen
Gut funktioniert »env -i«. Es löscht das Environment, gibt dem Programm aber keine neuen Rechte. Ähnliches klappt mit »exec -c Programm«. In beiden Fällen muss das Skript jedoch erst ein neues Programm starten, das in den Genuss des sauberen Environment kommt – bis dahin war der Angreifer aber vielleicht schon erfolgreich.
Für minimale Sicherheit belegt ein Skript gleich zu Beginn die Variablen »IFS« und »PATH« mit Standardwerten:
#!/bin/sh IFS=" tn" PATH="/bin:/usr/bin:/sbin:/usr/sbin" export IFS; export PATH
Lösung für C und C++: Das Programm ersetzt die Umgebung durch eine frisch erzeugte Struktur, die nur Standardwerte enthält. Es kopiert lediglich Variablen in die neue Umgebung, die es unbedingt benötigt und vorher eingehend prüft. Der Code für radikales Löschen sieht etwa so aus wie die Funktion »set_minimal_env()« aus Listing 4.
Falle 4: Offene Deskriptoren
Die Anzahl offener Dateien, Sockets und anderer Deskriptoren ist für jeden Prozess begrenzt. Die geöffneten Deskriptoren gehen aber auf Kindprozesse über. Ein Angreifer kann ein Programm daran hindern, korrekt abzulaufen, indem er es mit vielen bereits geöffneten Deskriptoren ausführt. Die Möglichkeiten hängen stark vom jeweiligen Programm ab. Über Abstürze und DoS-Angriffe ist bis zu Root-Exploits alles denkbar. Ein Kindprozess erhält auch alle Rechte seines Vaters. Öffnet zum Beispiel ein Programm unter Root »/etc/passwd« zum Schreiben, dann kann das auch jeder Abkömmling.
Lösung für die Shell: Sudo und Super schließen alle Dateideskriptoren, bevor sie einen Befehl aufrufen. Der Aufruf »exec n> Datei« öffnet eine Datei zum Schreiben mit Deskriptor n, »exec n>&-« schließt ihn. Zum Lesen ist »>« durch »<« zu ersetzen.
Sichere Gabel
Lösung für C und C++: Das Programm schließt zu Beginn und nach einem »fork()« alle unerwartet offenen Deskriptoren (Listing 4, Funktion »close_descriptors()«). Den höchsten möglichen Deskriptor liefert unter Linux (und nur dort) die Funktion »sysconf()« mit dem Argument »_SC_OPEN_MAX«. Auf anderen Systemen muss sich der Programmierer mit »getdtablesize()« oder mit der Konstanten »OPEN_MAX« zufrieden geben. Wichtig: »getrlimit()« liefert nicht die gesuchte Zahl, sondern nur die Grenze für neue Deskriptoren.
Auch die Funktion »popen()« ruft intern »fork()« auf. Weil der Programmierer keinen Einfluss auf den Ablauf hat, ist es besser, »popen()« zu meiden. Die Funktion »execve()« schließt Deskriptoren nur auf besonderen Wunsch. Sinnvollerweise folgt direkt hinter »open()« der Aufruf von »fcntl( Deskriptor, F_SETFD, F_CLOEXEC);«.
Falle 5: Standard-Ein- und Ausgabe
Die Dateideskriptoren »0«, »1« und »2« sind meist mit der Standard-Ein- und Ausgabe belegt, wenn der Aufrufende diese Datenkanäle zuvor nicht geschlossen hat. Öffnet in dieser Situation ein Programm eine Datei zum Schreiben, weist ihr das System den ersten freien Deskriptor zu, etwa »2« (Standard-Fehlerausgabe). Ab diesem Zeitpunkt landet jede Fehlerausgabe ungewollt in der Datei. Da Angreifer die Fehlermeldung oft beeinflussen können, ändern sie damit gezielt den Inhalt von Files, die sie eigentlich nicht ändern dürfen.
Lösung für die Shell: Die üblichen Shells sorgen nicht automatisch für sinnvolle Deskriptoren. In Bash und Zsh öffnen die folgenden Zeilen bei Bedarf die Standard-Deskriptoren erneut und verbinden sie mit »/dev/null«:
test -e /dev/fd/0 || exec < /dev/null test -e /dev/fd/1 || exec 1>&/dev/null test -e /dev/fd/2 || exec 2>&/dev/null
Lösung für C und C++: Der Entwickler prüft, ob Stdin, Stdout und Stderr offen sind, und verbindet sie nötigenfalls mit »/dev/null«. Die Funktion »open_stdfiles()« (Listing 4) zeigt, wie\’s geht.
|
Listing 3: Variablen mit Zsh |
|---|
01 function zsh_clear_env () {
02 for V in `set +`; do case "$V" in
03 '!'|'$'|'*'|'@'|'?'|'-'|'#'|[0-9]|V);;
04 **) typeset +r "$V"; unset "$V";;
05 esac; done
06 unset V
07 emulate zsh
08 export IFS=" tn"
09 export PATH="/bin:/usr/bin:/sbin:/usr/sbin"
10 }
|
Falle 6: Benutzerkennungen
Moderne, Posix-konforme Unix-Systeme ordnen jedem Prozess drei Benutzerkennungen zu. Aus der Effektive UID (EUID) leiten sich die Zugriffsrechte ab, die Real UID (RUID) ist die des Benutzers, der das Programm aufgerufen hat. Intern besitzt der Prozess noch die Saved UID (SVUID). Bei Set-UID-Programmen unterscheiden sich RUID und EUID, bei normalen Prozessen sind die UIDs identisch. Unter anderem erlaubt das System einem Programm, zwischen den drei Kennungen zu wechseln. Ein S-Bit-Programm vereint also die Rechte des aufrufenden Benutzers mit denen des Programmdatei-Eigentümers. Gleiches gilt für die Gruppenkennungen (RGID, EGID und SVGID).
Um unnötige Risiken zu vermeiden, sollte jedes Programm zusätzliche Rechte so früh wie möglich abgeben. Das »passwd«-Programm braucht zum Beispiel Root-Rechte nur, um ein neues Passwort in »/etc/shadow« zu schreiben, aber nicht, während es ein Passwort vom User abfragt oder überprüft. Ein guter Designer wird sich bemühen, die zusätzlichen Privilegien so früh wie möglich unwiderruflich abzugeben.
Lösung für die Shell: Die meisten Shells haben keine Funktion, mit der ein Skript die Benutzerkennungen ändern könnte, unter denen es läuft. In diesen Fällen zerlegt der Entwickler die Aufgabe in zwei Teile. Das privilegierte Skript erledigt seine Arbeit und ruft den unprivilegierten Partner mit Super oder Sudo auf; auch der umgekehrte Weg ist möglich.
Z-Shell
Ein Zsh-Skript kann Werte an die speziellen Variablen UID und EUID zuweisen, um zwischen den Benutzerkennungen zu wechseln, wenn es über die nötigen Rechte verfügt. Wegen der Unterschiede in den Betriebssystemen sollte man sich aber nicht auf ein bestimmtes Verhalten verlassen.
Lösung für C und C++: Die zusätzlichen Rechte abgeben ist nicht leicht, wenn das Programm portabel sein soll. Linux und andere Posix-Systeme benutzen die Funktion »setreuid()«, die alle drei Kennungen in einem Rutsch ändert (Listing 4, Funktion »set_credentials()«). Unter BSD verhält sich »setreuid()« aber anders.
|
Listing 4: Programm |
|---|
001 [...]
002 #define NIFS "IFS= tn"
003 #define NPATH "PATH="_PATH_STDPATH
004
005 static void disable_core_dumps (void) {
006 struct rlimit r = { 0, 0 };
007 if (setrlimit(RLIMIT_CORE, &r) != 0)
008 exit(1);
009 }
010
011 static void set_minimal_env (void) {
012 extern char **environ;
013 static char **ne = NULL;
014
015 ne = malloc(3 * sizeof(char *) + sizeof(NIFS) + sizeof(NPATH));
016 /* Umgebungsvariablen setzen */
017 ne[0] = (char *)&(ne[3]);
018 memcpy(ne[0], NIFS, sizeof(NIFS));
019 ne[1] = ne[0] + sizeof(NIFS);
020 memcpy(ne[1], NPATH, sizeof(NPATH));
021 ne[2] = NULL;
022 /* Alte Umgebung ersetzen */
023 environ = ne;
024 }
025
026 static void close_descriptors (void) {
027 int nd;
028
029 /* Nur Linux, sonst getdtablesize() */
030 if ((nd = sysconf(_SC_OPEN_MAX)) < 0)
031 exit(1);
032 while (--nd > 2)
033 close(nd);
034 }
035
036 static void open_stdfiles (void) {
037 struct stat buf;
038 FILE *f[3];
039 char *m[3] = { "rb", "wb", "wb" };
040 int i;
041
042 f[0] = stdin;
043 f[1] = stdout;
044 f[2] = stderr;
045 for (i = 0; i < 3; i++) {
046 if (fstat(i, &buf) == 0)
047 continue;
048 if (errno != EBADF)
049 exit(1);
050 if (freopen(_PATH_DEVNULL, m[i], f[i]) != f[i])
051 exit(1);
052 }
053 }
054
055 static void reset_sighandlers (void) {
056 int i;
057
058 for (i = 1; i <= NSIG; i++)
059 signal(i, SIG_DFL);
060 }
061
062 /* Sicherere Version folgt in Folge 2! */
063 static void change_workdir (char *path) {
064 if (chdir(path) != 0)
065 exit(1);
066 }
067
068 static void safe_chroot (char *path) {
069 /* Erst alle Deskriptoren schließen: open_stdfiles() */
070 if (chroot(path) != 0)
071 exit (1);
072 if (chdir("/") != 0)
073 exit(1);
074 /* Rootrechte abgeben: set_credentials() */
075 }
076
077 static void set_credentials (uid_t uid, gid_t gid) {
078 /* Nur Root darf setgroups aufrufen */
079 if (geteuid() == 0 && setgroups(1, &gid) != 0)
080 exit(1);
081 /* Nur mit Linux: */
082 if (setregid(gid, gid) != 0)
083 exit(1);
084 if (setreuid(uid, uid) != 0)
085 exit(1);
086 }
087
088 int main(void) {
089 /* Die Reihenfolge ist wichtig! */
090 disable_core_dumps();
091 reset_sighandlers();
092 umask(0077);
093 set_minimal_env();
094 close_descriptors();
095 open_stdfiles();
096 safe_chroot("/chroot");
097 change_workdir("/path/workdir");
098 set_credentials(geteuid(), getegid());
099
100 /* ... */
101 return 0;
102 }
|
Falle 7: Signalbehandlung
Am einfachsten kommunizieren Programme über Signale. Trifft ein Signal (als 5-Bit-Wert kodiert) bei einem Prozess ein, löst das Betriebssystem eine Aktion aus. Die meisten Signale darf das Programm wahlweise ignorieren, bei der Default-Reaktion bleiben (Programm abbrechen, Core Dump erzeugen) oder einen eigenen Signalhandler installieren, bei manchen Signalen erzwingt Linux aber das Ende des Prozesses.
Linux unterscheidet 31 Signale (siehe »man 7 signal«), die vom Betriebssystem stammen oder von einem anderen Prozess, der dem gleichen Benutzer oder Root gehört. Durch die Saved UID wird der Sachverhalt noch komplexer (»man 2 kill«). Wie er auf Signale reagiert, erbt ein Prozess von seinem Vaterprozess. Hatte der Vater eine eigene Signalhandler-Funktion implementiert, kehrt der Sohn zur Default-Reaktion zurück. (Allerdings nimmt »execve()« eine Sonderrolle ein: »man 2 execve«).
Sicher am Ende
Sichere Programme schreiben ist schwer. Die obigen Ausführungen helfen dabei, die Softwarewelt ein klein wenig sicherer zu gestalten. Auch Gate Guardian will dazu seinen Teil beitragen. Die nächste Folge dieser kleinen Artikelreihe beschreibt, was mit Dateien so alles schief gehen kann. (fjl)
|
Infos |
|---|
|
[1] John Viega und Matt Messier, “Secure Programming Cookbook for C an C++”: O\’Reilly 2003, [http://www.secureprogramming.com] [2] David A. Wheeler, “Secure Programming for Linux and Unix HOWTO”: [http://www.dwheeler.com/secure-programs/] [3] Steve Grubb, Env_audit: [http://www.web-insights.net/env_audit/] [4] Dominik Vogt, Gate Guardian: [http://sourceforge.net/projects/gateguardian/] [5] Super, das Programm für Admin-Aufgaben: [http://freshmeat.net/projects/super/] [6] Sudo: [http://www.courtesan.com/sudo/] [7] Mark Vogelsberger, “InSecurity News”: Linux-Magazin 11/04, S. 22, Originalmeldung: [http://www.securitytracker.com/alerts/2004/Sep/1011205.html] [8] Zsh: [http://zsh.sunsite.dk] [9] Chroot-Login-HOWTO: [http://www.tjw.org/chroot-login-HOWTO/] [10] Dirk P., “Insel-Hüpfer – Sicherheitslücken bei Hosting-Providern”: Linux-Magazin 10/2003, S. 56 [11] Quellen zum Artikel: [ftp://ftp.linux-magazin.de/pub/listings/magazin/2005/02/Sec-Prog] |
|
Der Autor |
|---|
|
Dipl.-Math. Dominik Vogt ist langjähriger Software-Entwickler und Systemadministrator. Zurzeit arbeitet er als freiberuflicher EDV-Berater mit Schwerpunkt Softwaresicherheit. In seiner Freizeit werkelt er am Windowmanager Fvwm. |





