[LinuxFocus-icon]
Home  |  Plan  |  Index  |  Suchen

Nachrichten | Archiv | Links | Über uns
Dieser Artikel ist verfübar in: English  Castellano  Deutsch  Francais  Nederlands  Portugues  Turkce  
convert to palmConvert to GutenPalm
or to PalmDoc

[image of the authors]
von

Über den Autor:
Christophe Blaess ist ein unabhängiger Flugzeugingenieur. Er ist ein Linux-Fan, und erledigt den Großteil seiner Arbeit auf diesem System. Er koordiniert die Übersetzung der man-Pages des Linux Documentation Projects (LDP).

Christophe Grenier studiert im 5.Jahr am ESIEA, wo er auch als Sysadmin arbeitet. Er interessiert sich besonders für Computersicherheit.

Frédéric Raynal benutzt Linux seit vielen Jahren, weil es nicht verseucht ist mit Fetten, frei ist von künstlichen Hormonen und ohne BSE .... es enthält nur den Schweiß ehrlicher Leute und einige Tricks.
Inhalt:

 

Vermeiden von Sicherheitslöchern beim Entwickeln einer Applikation - Teil 5: Race Conditions

[article illustration]

Zusammenfassung:

Dieser fünfte Artikel in unserer Serie befaßt sich mit Problemen, die mit der Multitaskingfähigkeit des Betriebssystems zusammenhängen. Eine Race Condition kann im Deutschen als Lauf(zeit)bedingung, Konkurrenzsituation bezeichnet werden, aber dann weiß eigentlich niemand was gemeint ist. Race condition ist auch im Deutschen ein gängiger Fachbegriff. Man versteht darunter eine Situation in der verschiedene Prozesse auf dieselben Geräte (Dateien, Hardware, Speicher..) zugreifen und dabei nicht berücksichtigen, daß ein anderer Prozeß diese zur gleichen Zeit bearbeiten könnte. Dieses Verhalten führt zu sehr schwer auffindbaren Fehlern, die die Sicherheit des gesammten Systems kompromittieren können.



 

Einführung

Das Prinzip einer Race Condition ist wie folgt: Ein Prozeß möchte Exklusivrechte für einen Teil des Systems haben. Er überprüft, daß noch kein anderer Prozeß mit diesem Teil des Systems arbeitet, danach bearbeitet er diesen Teil des Systems. Die Race Condition tritt auf, wenn ein anderer Prozeß versucht, in dem kurzen Intervall, in dem der erste Prozeß geprüft hat, daß niemand darauf zugreift, aber den Teil noch nicht für sich reserviert hat, auf dasselbe Teil zuzugreifen. Das Ergebnis kann sehr unterschiedlich sein. Der klassische Fall aus der Betriebssystemtheorie ist ein deadlock für beide Prozesse, das heißt, jeder Prozeß wartet auf den anderen und nichts passiert. Viel häufiger führt es zu "nicht reproduzierbarem" Fehlverhalten des Systems. Ausschalten, wieder einschalten und es geht plötzlich. Viel schlimmer ist, daß sich daraus ein Sicherheitsproblem ergeben kann.

Race Conditions werden oft im Kernel selbst gefunden und behoben und es handelt sich dabei meist um Probleme beim Zugriff auf Speicher. In diesem Artikel werden wir jedoch mehr auf Race Conditions beim Zugriff auf Dateien (Filesystem Nodes) eingehen. Das betrifft nicht nur normale Dateien, sondern auch Device Dateien aus /dev/.

Im allgemeinen werden immer Set-UID Programme angegriffen, wenn versucht wird, die Systemsicherheit zu kompromittieren. Das liegt daran, daß der Angreifer dann die Privilegien der Set-UID Applikation erben kann. Jedoch erlaubt im Gegensatz zu früher besprochenen Sicherheitslöchern (buffer overflow, format strings...), die Race Conditions es nicht, fremden Code auszuführen. Der Angriff kann auch gegen normale Programme (nicht Set-UID) laufen. Der Angreifer lauert einem anderen Benutzer auf (oft dem User root) und versucht auf Dateien zuzugreifen, die sonst nur root lesen und schreiben kann. Schafft man es z.B ein "+ +" in die Datei ~/.rhost zu schreiben, dann kann man sich auf dem Rechner von einem anderen Rechner aus ohne Passwort einloggen. Man kann auch geheime Dateien lesen (sensitive kommerzielle Daten, medizinische Daten, Passwort Datei, ...)

 

Ein erstes Beispiel

Betrachten wir das Verhalten eines Set-UID Programmes, das Daten in eine Datei schreiben muß die einem Benutzer gehört. Dieses ist z.B bei dem Mail Transport Programm sendmail der Fall. Die Applikation muß prüfen, ob die Datei auch wirklich dem Benutzer gehört und es kein Verweis (symlink) auf eine Systemdatei ist. Wir sollten nicht vergessen, das das Programm mit Set-UID root läuft und damit jede beliebige Datei auf dem Rechner modifizieren könnte. Diese checks machen also Sinn. Unser Programm könnte z.B so aussehen:

1     /* ex_01.c */
2     #include <stdio.h>
3     #include <stdlib.h>
4     #include <unistd.h>
5     #include <sys/stat.h>
6     #include <sys/types.h>
7
8     int
9     main (int argc, char * argv [])
10    {
11        struct stat st;
12        FILE * fp;
13
14        if (argc != 3) {
15            fprintf (stderr, "usage : %s file message\n", argv [0]);
16            exit(EXIT_FAILURE);
17        }
18        if (stat (argv [1], & st) < 0) {
19            fprintf (stderr, "can't find %s\n", argv [1]);
20            exit(EXIT_FAILURE);
21        }
22        if (st . st_uid != getuid ()) {
23            fprintf (stderr, "not the owner of %s \n", argv [1]);
24            exit(EXIT_FAILURE);
25        }
26        if (! S_ISREG (st . st_mode)) {
27            fprintf (stderr, "%s is not a normal file\n", argv[1]);
28            exit(EXIT_FAILURE);
29        }
30
31        if ((fp = fopen (argv [1], "w")) == NULL) {
32            fprintf (stderr, "Can't open\n");
33            exit(EXIT_FAILURE);
34        }
35        fprintf (fp, "%s\n", argv [2]);
36        fclose (fp);
37        fprintf (stderr, "Write Ok\n");
38        exit(EXIT_SUCCESS);
39    }

Wie wir in dem ersten Artikel erklärt haben, wäre es besser für die Set-UID Applikation zeitweise die Privilegien aufzugeben und die Dateien unter der Identität des Benutzers zu öffnen. Wir bleiben jedoch bei unserem Beispiel, da es dann leichter ist, das Problem Race Condition zu verstehen.

Wie wir sehen, führt das Programm alle nötigen checks durch. Als nächstes öffnet es die Datei und schreibt einen kurzen Text. Da liegt das Sicherheitsproblem. Oder genauer gesagt, liegt es in dem Zeitintervall zwischen den stat() und dem fopen(). Diese Zeit ist extrem kurz, aber nicht Null. Um den Angriff zu Testzwecken für uns einfacher zu machen, erhöhen wir den Zeitraum etwas und fügen ein sleep ein. In Zeile 30 schreiben wir:

30        sleep (20);

Hier ist der Probelauf: Wir setzen das Programm auf Set-UID root und machen eine Sicherheitskopie der Passwort Datei /etc/shadow ( sehr wichtig):

$ cc ex_01.c -Wall -o ex_01
$ su
Password:
# cp /etc/shadow /etc/shadow.bak
# chown root.root ex_01
# chmod +s ex_01
# exit
$ ls -l ex_01
-rwsrwsr-x 1 root  root    15454 Jan 30 14:14 ex_01
$

Alles ist fertig für den Angriff. Wir sind in einem Verzeichnis, das uns gehört, wir haben eine Set-UID root Utility (hier ex_01) mit einem Sicherheitsloch und wir würden gerne den Eintrag für root in der Datei /etc/shadow durch ein leeres Password ersetzen.

Zuerst erzeugen wir eine Datei namens fic, die uns gehört:

$ rm -f fic
$ touch fic

Als nächstes starten wir unser Programm in den Hintergrund (&) und bitten es einen String in die Datei fic zu schreiben. Das Programm führt seine checks durch und schläft dann, bevor es wirklich auf die Datei zugreift.

$ ./ex_01 fic "root::1:99999:::::" &
[1] 4426

Diesen String hier haben wir in der shadow(5) man page nachgelesen. Das zweite Feld ist leer (kein Password). Solange der Prozess schläft, wir haben ca. 20 Sekunden Zeit, löschen wir die Datei fic und ersetzen sie durch einen Link auf /etc/shadow. Wir wir wissen, können wir einen Link erzeugen, da uns das Verzeichnis in dem fic liegt für uns schreibar ist. Dieses ist auch dann der Fall, wenn wir das Ziel des Links, die Datei /etc/shadow, nicht lesen können. Es ist jedoch nicht möglich, eine Kopie von /etc/shadow zu machen.

$ rm -f fic
$ ln -s /etc/shadow ./fic

Nun bitten wir die shell den ex_01 Prozess wieder in den Vordergrund zu holen, in dem wir fg eingeben und warten, bis der Prozess fertig ist.

$ fg
./ex_01 fic "root::1:99999:::::"
Write Ok
$

Voilà ! Es ist geschehen. Die Datei /etc/shadow enthält jetzt genau eine Zeile und dort steht, daß root kein Password hat. Du glaubst es nicht?

$ su
# whoami
root
# cat /etc/shadow
root::1:99999:::::
#

Wir beenden das Experiment, indem wir die Sicherheitskopie der Datei /etc/shadow wieder zurückspielen:

# cp /etc/shadow.bak /etc/shadow
cp: replace `/etc/shadow'? y
#
 

Etwas realistischer

Wir haben es geschafft, eine Race Condition in einem Set-UID root Programm auszunutzen. Natürlich war das Programm sehr hilfsbereit und wartete 20 Sekunden. In einer echten Applikation ist das nur ein extern kurzer Zeitraum. Wie können wir dann die Race Condition ausnutzen?

Normalerweise probiert es der Angreifer einfach 100, 1000, vielleicht 10000 mal und automatisiert die Sache mit Scripten. Man kann außerdem versuchen, das Programm langsamer zu machen:

 

Mögliche Verbesserungen

Das Sicherheitsproblem entsteht aus dem Zeitabstand zwischen dem Prüfen der Datei und dem Öffnen der Datei zum Schreiben. Ein normaler Benutzer könnte die Datei weder lesen noch schreiben, die Datei /etc/shadow selbst hat also nichts mit dem Problem zu tun. Die meisten Systembefehle (rm, mv, ln, u.s.w.) benutzen einen Dateinamen, um auf einen file node im Dateisystem zuzugreifen. Eine Datei wird aber wirklich nur gelöscht (rm, unlink() system call), wenn der letzte Verweis auf eine Datei gelöscht ist. Das wiederum hat nichts mit dem Namen der Datei zu tun.

Der Fehler in dem Programm ist die Annahme, daß die Assoziation zwischen dem Dateiinhalt und dem Namen konstant sei zwischen dem ersten stat() und dem fopen(). Das Beispiel eines hardlinks sollte reichen, um zu zeigen, daß die Assoziation zwischen Name und physikalischer Datei nicht permanent ist. In einem Verzeichnis, das uns gehört, erzeugen wir einen neuen Verweis (link) auf eine Systemdatei. Natürlich bleiben Eigentümer und Dateirechte erhalten:

$ ln -f /etc/fstab ./myfile
$ ls -il /etc/fstab myfile
8570 -rw-r--r--   2 root  root  716 Jan 25 19:07 /etc/fstab
8570 -rw-r--r--   2 root  root  716 Jan 25 19:07 myfile
$ cat myfile
/dev/hda5   /                 ext2    defaults,mand   1 1
/dev/hda6   swap              swap    defaults        0 0
/dev/fd0    /mnt/floppy       vfat    noauto,user     0 0
/dev/hdc    /mnt/cdrom        iso9660 noauto,ro,user  0 0
/dev/hda1   /mnt/dos          vfat    noauto,user     0 0
/dev/hda7   /mnt/audio        vfat    noauto,user     0 0
/dev/hda8   /home/ccb/annexe  ext2    noauto,user     0 0
none        /dev/pts          devpts  gid=5,mode=620  0 0
none        /proc             proc    defaults        0 0
$ ln -f /etc/host.conf ./myfile
$ ls -il /etc/host.conf myfile
8198 -rw-r--r--   2 root  root   26 Mar 11  2000 /etc/host.conf
8198 -rw-r--r--   2 root  root   26 Mar 11  2000 myfile
$ cat myfile
order hosts,bind
multi on
$

Der Befehl /bin/ls -i zeigt die Dateisystem inode number am Anfang der Zeile.

Was wir also brauchen, sind Funktionen, die die Zugriffsrechte prüfen und nicht den Namen der Datei benutzen, sondern die inode Nummer. Das ist möglich. Der Kernel selbst managed diese Assoziation, wenn er uns einen Filedescriptor gibt. Wenn wir eine Datei zum Lesen öffnen, gibt der open() Aufruf einen Integer Wert zurück. Dieser Wert wird in einer internen Tabelle verwaltet und zeigt immer auf denselben Inhalt, egal was mit dem Namen der Datei passiert, während wir die Datei lesen.

Um das nochmal zu betonen: Sobald eine Datei geöffnet wird, hat jede Operation, die mit dem Dateinamen arbeitet, keinen Effekt mehr. Selbst wenn jemand die Datei (den Namen) löscht, sorgt der Kernel dafür, das wir sie in Ruhe zu Ende lesen dürfen. Der Kernel erhält also die Assoziation zwischen Inhalt und dem Filedescriptor, den wir mit dem open() system call erhalten haben, bis wir den Filedescriptor mit close() wieder freigeben oder unser Programm beenden.

Da haben wir die Lösung! Beim Check der Rechte und Dateieigentümer benutzen wir den Filedescriptor und nicht den Namen. Der System Call ist dann fstat() Anstelle von stat() und fdopen() benutzen wir, wenn wir die Datei lesen möchten. Damit sieht unser Programm so aus:

1    /* ex_02.c */
2    #include <fcntl.h>
3    #include <stdio.h>
4    #include <stdlib.h>
5    #include <unistd.h>
6    #include <sys/stat.h>
7    #include <sys/types.h>
8
9     int
10    main (int argc, char * argv [])
11    {
12        struct stat st;
13        int fd;
14        FILE * fp;
15
16        if (argc != 3) {
17            fprintf (stderr, "usage : %s file message\n", argv [0]);
18            exit(EXIT_FAILURE);
19        }
20        if ((fd = open (argv [1], O_WRONLY, 0)) < 0) {
21            fprintf (stderr, "Can't open %s\n", argv [1]);
22            exit(EXIT_FAILURE);
23        }
24        fstat (fd, & st);
25        if (st . st_uid != getuid ()) {
26            fprintf (stderr, "%s not owner !\n", argv [1]);
27            exit(EXIT_FAILURE);
28        }
29        if (! S_ISREG (st . st_mode)) {
30            fprintf (stderr, "%s not a normal file\n", argv[1]);
31            exit(EXIT_FAILURE);
32        }
33        if ((fp = fdopen (fd, "w")) == NULL) {
34            fprintf (stderr, "Can't open\n");
35            exit(EXIT_FAILURE);
36        }
37        fprintf (fp, "%s", argv [2]);
38        fclose (fp);
39        fprintf (stderr, "Write Ok\n");
40        exit(EXIT_SUCCESS);
41    }

Dieses Mal wird nach Zeile 20 kein Verändern des Dateinamens (löschen, umbenennen, Link setzen) Einfluß auf das Programm haben.

 

Richtlinien

Wenn man eine Datei verändert, ist es wichtig, sicherzustellen, daß die Assoziation zwischen interner Darstellung im Programm und dem wirklichem Inhalt konstant bleibt. Man sollte folgende Befehle benutzen und nicht ihre Äquivalente, die nur mit dem Dateinamen arbeiten:

System call Use
fchdir (int fd) Geht in das Verzeichnis, das durch fd repräsentiert wird.
fchmod (int fd, mode_t mode) Ändert die Dateizugriffsrechte.
fchown (int fd, uid_t uid, gid_t gif) Ändert den Dateieigentümer.
fstat (int fd, struct stat * st) Liest verschiedene Parameter, die die physikalische Datei beschreiben.
ftruncate (int fd, off_t length) Schneidet eine Datei ab.
fdopen (int fd, char * mode) Inizialisiert die Ein- Ausgabe einer schon geöffneten Datei. Es ist eine stdio Bibliotheksroutine und kein system call.

Natürlich muß man die Datei in dem gewünschten Mode öffnen, wenn man open() aufruft.

Es ist wichtig, die Rückgabewerte der Systemcalls zu prüfen. Das hat nichts mit Race Conditions zu tun, kann aber auch zu Sicherheitsproblemen führen. Eine ältere Implementation von /bin/login führte zu einem Sicherheitsproblem, weil ein Fehlercode nicht geprüft wurde. Login gab automatisch root Rechte frei, wenn die Datei /etc/passwd nicht gefunden wurde. Das Verhalten mag hilfreich bei einem beschädigten Dateisystem sein, wenn dadurch /etc/passwd nicht lesbar ist, es ist aber auch ein Sicherheitsloch. Nachdem die maximale Anzahl möglicher geöffneter Filedescriptoren geöffnet war, mußte man nur /bin/login aufrufen und man war ... root ...

 

Race Conditions im Inhalt einer Datei

Ein Programm bei dem es um Systemsicherheit geht, sollte sich nicht auf exklusive Zugriffsrechte verlassen. Das Hauptproblem entsteht, wenn ein Benutzer mehrere Instanzen eines Set-UID root Programmes laufen läßt.

Um die Probleme zu vermeiden, sollte man einen Exklusiv Zugriffsmechanismus für Dateien benutzen. Ähnliche Mechanismen findet man in Datenbanken, wenn mehrere Benutzer eine Tabelle modifizieren. Man bezeichnet das als Locking.

Wenn ein Prozess Daten exklusiv schreiben/lesen möchte, dann muß er den Kernel bitten, die ganze Datei oder Teile davon zu locken. Solange der Prozess dann im Besitz des Locks (Schloß) ist, kann kein anderer Prozess ein Lock erhalten oder zumindest kein Lock für denselben Teil der Datei.

Es gibt unterschiedliche Locks für Prozesse, die nur schreiben oder nur lesen möchten. Viele Prozesse können ein Lock zum Lesen besitzen, aber nur einer kann eines zum Schreiben haben.

Es gibt zwei unterschiedliche Lock Mechanismen, die nicht kompatibel zueinander sind. Das eine kommt von BSD und benutzt den Systemcall flock(). Das erste Argument für flock ist ein Filedescriptor der Datei, auf die man zugreifen möchte. Das zweite Argument ist eine symbolische Konstante, die folgende Werte haben kann: LOCK_SH (Lock zum Lesen), LOCK_EX (Lock zum Schreiben). Zusätzlich kann man diese Konstanten über ein binäres oder (|) mit LOCK_NB verknüpfen, um zu bestimmern, ob der eigene Prozess blocken (=warten) soll, bis das Lock frei ist, oder ob der flock() mit einem Fehlercode zurückkommen soll, falls das Lock nicht verfügbar ist.

Der zweite Typ von Lock kommt aus System V und benutzt den fcntl() Systemcall, dessen Aufruf etwas komplizierter ist. Es gibt eine Bibliotheksfunktion lockf(), die den fcntl() Aufruf benutzt, jedoch nicht so schnell ist wie die ursprüngliche fcntl() Funktion. Das erste Argument für fcntl() ist ein Filedescriptor. Das zweite repräsentiert die Operation, die ausgeführt werden soll: F_SETLK und F_SETLKW. F_SETLKW wartet bis das Lock erhalten werden kann wohingegen die andere mit einem Fehlercode zurückkommt. Mit F_GETLK kann man den Zustand des Locks abfragen. Das dritte Argument ist ein Pointer auf struct flock der das Lock beschreibet:

Name Typ Bedeutung
l_type int Was zu tun ist : F_RDLCK (lock zum Lesen), F_WRLCK (lock zum Schreiben) und F_UNLCK (lock freigeben).
l_whence int l_start = Field origin (normalerweise SEEK_SET).
l_start off_t Position, bei der das Lock beginnt (normalerweise 0).
l_len off_t Länge des Locks. 0 = bis zum Ende der Datei

Wie wir sehen, kann fcntl() auch Teile einer Datei locken. Hier ist ein kleines Beispielprogramm, das eine Datei lockt und dann den Benutzer bittet, Return zu drücken und das Lock wieder frei gibt.

1    /* ex_03.c */
2    #include <fcntl.h>
3    #include <stdio.h>
4    #include <stdlib.h>
5    #include <sys/stat.h>
6    #include <sys/types.h>
7    #include <unistd.h>
8
9    int
10   main (int argc, char * argv [])
11   {
12     int i;
13     int fd;
14     char buffer [2];
15     struct flock lock;
16
17     for (i = 1; i < argc; i ++) {
18       fd = open (argv [i], O_RDWR | O_CREAT, 0644);
19       if (fd < 0) {
20         fprintf (stderr, "Can't open %s\n", argv [i]);
21         exit(EXIT_FAILURE);
22       }
23       lock . l_type = F_WRLCK;
24       lock . l_whence = SEEK_SET;
25       lock . l_start = 0;
26       lock . l_len = 0;
27       if (fcntl (fd, F_SETLK, & lock) < 0) {
28         fprintf (stderr, "Can't lock %s\n", argv [i]);
29         exit(EXIT_FAILURE);
30       }
31     }
32     fprintf (stdout, "Press Enter to release the lock(s)\n");
33     fgets (buffer, 2, stdin);
34     exit(EXIT_SUCCESS);
35   }

Wir starten das Programm aus dem ersten xterm Fenster, wo es dann auf die Eingabe wartet.

$ cc -Wall ex_03.c -o ex_03
$ ./ex_03 myfile
Press Enter to release the lock(s)
>in dem zweiten xterm Fenster...
    $ ./ex_03 myfile
    Can't lock myfile
    $
Wenn wir Enter in dem ersten Xterm Fenster drücken, geben wir das Lock frei.

Mit diesem Mechanismus kann man Race Conditions verhindern. Der lpd daemon benutzt ein flock() lock auf /var/lock/subsys/lpd, um zu erreichen, daß nur eine Instanz von lpd läuft. Die pam library benutzt fcntl(), um /etc/passwd zu lesen.

Leider schützt dieser Mechanismus nur vor Applikationen, die sich korrekt verhalten. Das heißt, sie fragen den Kernel zuerst nach einem Lock, bevor sie wichtige Daten lesen oder schreiben. Wir sprechen hier von sogenannten kooperativen Locks. Ein schlecht geschriebenes Programm kann die Datei immer noch änderen selbst, wenn ein gutes Programm ein Lock für die Datei besitzt. Hier ist ein Beispiel. Wir schreiben ein paar Zeichen in eine Datei, die gelockt ist:

$ echo "FIRST" > myfile
$ ./ex_03 myfile
Press Enter to release the lock(s)
>In dem anderem Xterm ändern wir die Datei einfach :
    $ echo "SECOND" > myfile
    $
Zurück in dem ersten xterm überprüfen wir den Schaden:
(Enter)
$ cat myfile
SECOND
$

Um dieses Problem zu lösen, bietet der Linux Kernel dem Sysadmin noch einen weiteren Mechanismus, der das Problem löst. Er kommt aus System V und kann deshalb nur mit fcntl() und nicht mit flock() benutzt werden. Der Systemadministrator kann dem Kernel sagen, daß die fcntl() locks streng sind. Das geht mit einer bestimmten Set-GID Bit Kombination, bei der das X-Bit entfernt ist für die Gruppe. Gesetzt wird das über chmod:

$ chmod g+s-x myfile
$
Das ist jedoch noch nicht genug. Zusätzlich muß man sicherstellen, daß das mandatory Attribut für die Partition aktiviert ist, in der sich die Datei befindet. Normalerweise muß man dazu den /etc/fstab Eintrag ändern und die mand Option in der vierten Spalte einfügen oder die Option dem Kommando mount direkt übergeben:
# mount
/dev/hda5 on / type ext2 (rw)
[...]
# mount / -o remount,mand
# mount
/dev/hda5 on / type ext2 (rw,mand)
[...]
#
Nun probieren wir das nochmal:
$ ./ex_03 myfile
Press Enter to release the lock(s)
>aus dem zweiten xterm ...:
    $ echo "THIRD" > myfile
    bash: myfile: Resource temporarily not available
    $

Der Systemadministrator und nicht der Programmierer entscheidet, ob Locks streng sind für bestimmte Dateien (z.B. /etc/passwd, oder /etc/shadow). Der Programmierer muß kontrollieren, wann auf die Daten zugegriffen werden soll und locks richtig handhaben.

 

Temporäre Dateien

Sehr oft besteht die Notwendigkeit, in einem Programm Daten temporär in eine Datei zu speichern. Wenn man z.B in der Mitte einer Datei etwas einfügen möchte liest man das Original und schreibt die entsprechend geänderten Daten in eine temporäre Datei. Anschließend kann man das Original löschen (unlink()) und die temporäre Datei in die Original Datei umbenennen (rename()).

Das Öffnen einer temporären Datei, wenn falsch angelegt, ist oft der Startpunkt einer Race Condition, die von einem boshaften Benutzer ausgenutzt werden kann. Sicherheitslöcher basierend auf temporären Dateien wurden kürzlich in Programmen wie Apache, Linuxconf, getty_ps, wu-ftpd, rdist, gpm, inn, etc... entdeckt. Es gibt einige Regeln, die man beachten muß, um solche Probleme zu vermeiden.

Temporäre Dateien werden im allgemeinen in /tmp erzeugt. Der Systemadministrator kann dann periodisch ein Programm (mit Hilfe von crontab) laufen lassen, das alte temporären Dateien löscht. Das Verzeichnis für temporäre Dateien ist in <paths.h> und <stdio.h> festgelegt über die symbolischen Konstanten _PATH_TMP und P_tmpdir. GlibC erlaubt es auch über die Environment Variable TMPDIR festzulegen, wo temporäre Dateien geschrieben werden sollen.

Das Verzeichnis /tmp ist etwas besonderes wegen seiner speziellen Zugriffsrechte:

$ ls -ld /tmp
drwxrwxrwt 7 root  root    31744 Feb 14 09:47 /tmp
$

Das Sticky-Bit hier als t dargestellt, oktal 01000, hat eine besondere Bedeutung, wenn es auf Verzeichnisse angewendet wird: Nur der Eigentümer (root) des Verzeichnisses und der Eigentümer der Datei können Dateien löschen, da das Verzeichnis aber ansonsten volle Schreibrechte hat, kann jeder dort schreiben.

Trotzdem kann es hier zu Problemen kommen. Nehmen wir z.B ein Mail Transport Programm. Wenn es ein Signal SIGTERM oder SIGQUIT während des shutdown des Rechners erhält, kann es versuchen, Dateien schnell zu speichern. In älteren Programmen wurde das in /tmp/dead.letter gemacht. Ein böswilliger Benutzer brauchte nur einen Link in /tmp mit dem Namen dead.letter zu erzeugen und diesen auf /etc/passwd zeigen zu lassen. Da das Mail Transport Programm mit root Rechten läuft, schrieb es die noch nicht fertige Mail, die zufällig die Zeile "root::1:99999:::::" enthielt in /etc/passwd.

Das erste Problem ist der vorhersehbare Name. Man braucht solch eine Applikation nur einmal zu beobachten und man weiß, daß die Datei /tmp/dead.letter heißen wird. Der erste Schritt ist daher, einen Namen zu benutzen, der nicht konstant ist. Verschiedene Bibliotheksfunktionen sind dazu in der Lage.

Jetzt ist die Sache jedoch nur schwieriger geworden. Der Name wird immer noch berechenbar sein, speziell wenn der Sourcecode der Bibliotheksfunktionen vorliegt und man studieren kann, wie der Name erzeugt wird (z.B. PID + Zeit). Man muß also prüfen, ob die Datei schon vorhanden ist. Naiverweise könnte man folgendes schreiben:

  if ((fd = open (filename, O_RDWR)) != -1) {
    fprintf (stderr, "%s already exists\n", filename);
    exit(EXIT_FAILURE);
  }
  fd = open (filename, O_RDWR | O_CREAT, 0644);
  ...

Offensichtlich ist das eine typische Race Condition, da die Zeit zwischen den zwei open Aufrufen nie null ist. Das Überprüfen der Existenz der Datei und das Öffnen muß atomar sein. Das ist möglich, wenn man open() mit den Optionen O_EXCL und O_CREAT benutzt. Damit schlägt open() fehl, wenn die Datei schon existiert, aber der Check der Existenz ist atomar an ihr Erzeugen gebunden.

Übrigens bietet die Option-'x' in der Gnu Erweiterung von fopen() die gleichen Möglichkeiten atomar zu testen und eine Datei zu erzeugen:

  FILE * fp;

  if ((fp = fopen (filename, "r+x")) == NULL) {
    perror ("Can't create the file.");
    exit (EXIT_FAILURE);
  }


Die Rechte der temporären Datei sind auch sehr wichtig. Wenn man geheime Daten in eine Datei mit Mode 644 (lesen für alle) schreibt, kann jeder sehen, was darin steht. Mit der umask Funktion kann man festlegen, welche Rechte eine Datei beim Erzeugen erhält.

    #include <sys/types.h>
    #include <sys/stat.h>

        mode_t umask(mode_t mask);
Mit umask(077) wird die Datei im Mode 600 erzeugt und nur der Eigentümer kann lesen und schreiben.

Normalerweise sind 3 Schritte zum erzeugen temporärer Dateien nötig:

  1. neuer und zufälliger Name;
  2. Öffnen mit O_CREAT | O_EXCL, und einer umask von 077;
  3. Überprüfen des Rückgabewertes von open.

Wie erzeugt man nun einen temporären Namen? Die Funktionen

      #include <stdio.h>

      char *tmpnam(char *s);
      char *tempnam(const char *dir, const char *prefix);

geben einen Pointer auf einen zufällig erzeugten temporären Namen zurück.

Die erste Funktion akzeptiert ein NULL Argument und gibt dann eine Adresse eines statischen Buffers zurück, in dem der Name steht. Sein Inhalt wird sich beim nächsten Aufruf von tmpnam(NULL) wieder ändern. Wenn man tmpnam die Adresse eines schon allokierten Strings gibt, dann wird der Name dahin kopiert. Das erfordert eine Stringlänge von mindestens L-tmpnam Bytes. Vorsicht mit buffer overflows! Die manpage sagt einiges zu Problemen, wenn die Funktion mit NULL Argument benutzt wird und gleichzeitig _POSIX_THREADS oder _POSIX_THREAD_SAFE_FUNCTIONS definiert sind.

Die tempnam(dir,prefix) Funktion gibt einen Pointer auf einen String zurück. Dabei muß dir ein geeignetes Verzeichnis sein (die manpage beschreibt was "geeignetes" meint). Die Funktion überprüft auch, daß der Name nicht existiert, bevor sie ihn zurück gibt, aber die manpage sagt, daß man sich (wegen Race Conditions) darauf nicht verlassen sollte. Das Gnome Projekt empfiehlt die Funktion so zu benutzen:

  char *filename;
  int fd;

  do {
    filename = tempnam (NULL, "foo");
    fd = open (filename, O_CREAT | O_EXCL | O_TRUNC | O_RDWR, 0600);
    free (filename);
  } while (fd == -1);
Die hier benutzte Schleife reduziert das Risiko, erzeugt aber neue Probleme. Was passiert, wenn das Dateisystem voll ist oder schon die maximale Anzahl geöffneter Dateien erreicht ist...

Die Funktion

       #include <stdio.h>

       FILE *tmpfile (void);
erzeugt einen neuen Namen und öffnet die Datei. Sie wird automatisch beim Schließen gelöscht.

In GlibC-2.1.3 benutzt diese Funktion einen ähnlichen Mechanismus wie tmpnam().

  FILE * fp_tmp;

  if ((fp_tmp = tmpfile()) == NULL) {
    fprintf (stderr, "Can't create a temporary file\n");
    exit (EXIT_FAILURE);
  }

  /* ... use of the temporary file ... */

  fclose (fp_tmp);  /* real deletion from the system */

Im Normalfall braucht man nicht wissen, wo die Datei erzeugt wird und was der Name ist. Hier ist tmpfile() genau richtig.

Die man Page sagt nichts, aber das Secure-Programs-HOWTO empfiehlt die Funktion nicht. Der Autor meint, daß die Spezifikation nicht garantiert, daß die Datei erzeugt wird und er konnte bisher nicht alle Implementationen überprüfen. Trotzdem ist diese Funktion die effizienteste.

Zuletzt noch:

       #include <stdlib.h>

       char *mktemp(char *template);
       int mkstemp(char *template);
Diese Funktion erzeugt einen eindeutigen Namen basierend auf einem vorgegebenen String, der in "XXXXXX" enden muß. Diese X werden dann durch neue und eindeutige Buchstaben und Zahlenkombinationen ersetzt.

mktemp() ersetzt die ersten 5 X mit der Process ID (PID) und nur das letzte X ist zufällig. Einige Versionen erlauben mehr als 6 X.

mkstemp() ist die empfohlene Funktion in der Secure-Programs-HOWTO:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>

 void failure(msg) {
  fprintf(stderr, "%s\n", msg);
  exit(1);
 }

/*
 * Creates a temporary file and returns it.
 * This routine removes the filename from the filesystem thus
 * it doesn't appear anymore when listing the directory.
 */
FILE *create_tempfile(char *temp_filename_pattern)
{
  int temp_fd;
  mode_t old_mode;
  FILE *temp_file;

  /* Create file with restrictive permissions */
  old_mode = umask(077);
  temp_fd = mkstemp(temp_filename_pattern);
  (void) umask(old_mode);
  if (temp_fd == -1) {
    failure("Couldn't open temporary file");
  }
  if (!(temp_file = fdopen(temp_fd, "w+b"))) {
    failure("Couldn't create temporary file's file descriptor");
  }
  if (unlink(temp_filename_pattern) == -1) {
    failure("Couldn't unlink temporary file");
  }
  return temp_file;
}

Diese Funktionen zeigen die Probleme von Portierbarkeit und Abstraktion. Standard Bibliotheksfunktionen sollten gewisse "Features" zur Verfügung stellen (Abstraktion) ... aber die Art wie sie implementiert sind, variiert von System zu System (Portierbarkeit). Die Funktion tmpfile() öffnet z.B temporäre Dateien auf verschiedene Art. Einige Versionen benutzen O_EXCL nicht. mkstemp() nimmt eine unterschiedliche Anzahl von 'X', je nach Implementation.

 

Zusammenfassung

Race Conditions haben immer eine Ursache: Zwei abhängige Operationen sind nicht atomar. Man darf niemals annehmen, daß aufeinander folgende Anweisungen auch wirklich in dieser Reihenfolge in der CPU bearbeitet werden. Das ist so, weil in einem Multitaskingsystem mehrere Dinge gleichzeitig geschehen. Wenn Race Conditions Sicherheitsprobleme nachsichziehen, so muß man erst recht bei threads und shared variables , shared memory segments mit shmget() aufpassen. Hier sind auch locks wie z.B semaphores nötig, um schwer zu findende Fehler zu vermeiden.

 

Links


 

Talkback für diesen Artikel

Jeder Artikel hat seine eigene Seite für Kommentare und Rückmeldungen. Auf dieser Seite kann jeder eigene Kommentare abgeben und die Kommentare anderer Leser sehen:
 Talkback Seite 

Der LinuxFocus Redaktion schreiben
© Frédéric Raynal, Christophe Blaess, Christophe Grenier, FDL
LinuxFocus.org

Einen Fehler melden oder einen Kommentar an LinuxFocus schicken
Autoren und Übersetzer:
fr -> --
fr -> en
en -> en
en -> de

2001-09-02, generated by lfparser version 2.17