|
|
Dieser Artikel ist verfübar in: English Castellano Deutsch Francais Nederlands Portugues Russian Turkce |
von Frédéric Raynal, Christophe Blaess, Christophe Grenier Ü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: |
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.
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.
|
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:
|
2001-09-02, generated by lfparser version 2.17