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
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, ...)
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 #
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:
nice -n 20
reduzieren.while
(1);
)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.
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 ...
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.
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:
O_CREAT | O_EXCL
, und einer
umask von 077;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.
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.