Aus Linux-Magazin 02/2005

Workshop: Sicheres Programmieren für Administratoren - Folge 1

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.

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
Env_audit-Ausgabe

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
Sudo

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 Umgebung

Env-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
löschen

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
initialisieren

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.

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben