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

ArticleCategory:

Software Development

AuthorImage:

[image of the authors]

TranslationInfo:

Original in fr Frédéric Raynal, Christophe Blaess, Christophe Grenier

fr to en Georges Tarbouriech

en to en Lorne Bailey

en to de Guido Socher

AboutTheAuthor:

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.

Abstract:

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.

ArticleIllustration:[illustration]

[article illustration]

ArticleBody:[The real article: put the text and html-codes here]

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