original in fr Frédéric Raynal, Christophe Blaess, Christophe Grenier
fr to en Georges Tarbouriech
en to en Lorne Bailey
en to nl Hendrik-Jan Heins
Christophe Blaess is een onafhankelijke luchtvaart ingenieur. Hij is een Linux fan en werkt veel met dit systeem. Hij coordineert de vertaling van de man pages zoals die te vinden zijn op de site van het Linux Documentation Project.
Christophe Grenier is een 5e jaars student aan de ESIEA, hij werkt daar ook als systeembeheerder. Hij is gek van computer beveiligingssystemen.
Frédéric Raynal gebruikt Linux nu al jaren omdat het niet vervuilend is, niet opgepept wordt met hormonen, MSG of beendermeel... maar alleen met bloed, zweet, tranen en kennis.
De algemene definitie van "race conditions" is de volgende: een proces wil exclusieve rechten op een systeembron. Dit proces controleert of er geen ander proces is dat de bron al gebruikt en gebruikt de bron dan wanneer nodig. De "race condition" treedt op op het moment dat een ander proces de bron vertraagd probeert aan te spreken tussen het moment van checken door het eerste proces en voordat het eerste proces de bron daadwerkelijk over heeft genomen. De hierna optredende effecten kunnen varieren. Het klassieke voorbeeld hiervan in OS theorie is het vastlopen van beide processen. Maar meestal leidt het tot een fout binnen een applicatie of zelfs tot een veiligheidsgat wanneer een proces per ongeluk bevoordeeld wordt door de rechten van een ander proces.
Wat we tot nu toe een "bron" hebben genoemd kan verschillende vormen hebben. Hier focussen we met name op "race conditions" die ontdekt en gecorrigeerd worden door de Linux kernel zelf die te maken hebben met gelijktijdige toegang tot geheugengebieden. In dit artikel focussen we op systeem applicaties met als bronnen de bestandssysteem nodes. Dit gaat niet alleen over gewone bestanden maar ook om directe toegang tot apparaten door speciale koppelpunten in de /dev/ directory.
Aanvallen die bedoeld zijn om de systeemveiligheid in gevaar te brengen worden meestal gedaan op "Set-UID" applicaties omdat de aanvaller dan kan profiteren van het feit dat hij de eigenaar van het betreffende uitvoerbare bestand wordt. Echter staan "race conditions" de uitvoering van "aangepaste" code meestal niet toe, dit in tegenstelling tot eerder bediscussieerde veiligheidsgaten (zoals buffer overflow, format strings etc.), "race conditions" staat over het algemeen de uitvoering van aangepaste code niet toe. Maar er wordt gebruik gemaakt van de bronnen van een programma terwijl het draait. Dit type aanval kan ook worden gericht tegen "normale" utilities ( die niet werken met "Set-UID"), de kraker legt een hinderlaag voor een andere gebruiker, liefst de root, om op het moment dat die een bepaalde applicatie draait de bronnen van die applicatie de openen. Dit geldt bijvoorbeeld voor het schrijven van een bestand ( zoals: ~/.rhost met daarin de string "+ +", dit staat directe toegang toe vanaf iedere machine en zonder wachtwoord), of voor het lezen van een vertrouwelijk bestand (gevoelige commerciele gegevens, medische gegevens, wachtwoord bestanden, coderings sleutels, etc).
In tegenstelling tot de veiligheidsgaten die besproken zijn in onze voorgaande artikelen, is dit veiligheidsgat gerelateerd aan iedere applicatie en niet alleen aan "Set-UID" utilities en systeem servers of deamons.
Laten we eens kijken naar het gedrag van een "Set-UID" programma dat bepaalde gegevens in een bestand van een bepaalde gebruiker wil bewaren. We zouden bijvoorbeeld kunnen kijken naar een mail transport pakket zoals sendmail. Laten we er vanuit gaan dat de gebruiker de naam van een back-up bestand mag geven en een bericht mag schrijven in dat bestand. Dit is onder bepaalde omstandigheden noodzakelijk en logisch. De applicatie moet dan controleren of het bestand daadwerkelijk van de gebruiker is die het programma opstartte. Het zal ook controleren of het bestand geen symbolische koppeling naar een systeembestand. Vergeet niet dat het bestand "Set-UID root" is, hierdoor mag het ieder bestand op de machine veranderen. Op de hier beschreven manier zal het programma de identiteit van de eigenaar van het bestand vergelijken met de eigenaar z'n eigen UID. Laten we iets als het volgende schrijven:
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 }
Zoals we in het eerste artikel al hebben uitgelegd, zou het beter zijn wanneer een "Set-UID" applicatie tijdelijk z'n privileges los zou laten en het bestand zou openen met behulp van de UID van de betreffende gebruiker. In feite werkt de hierboven uitgelegde situatie als een deamon, het verleent diensten aan iedere gebruiker. Het draait altijd onder de root ID, maar het zou moeten checken met de UID in plaats van de eigen UID. Maar we houden het geschetste systeem aan, ook al is het niet geheel realistisch, aangezien het het probleem eenvoudiger voorstelt terwijl er gemakkelijk "gebruikt gemaakt" kan worden van het veiligheidsgat.
Zoals we nu kunnen zien, begint het programma met de benodigde controles, dus
controleren of het bestand bestaat, of het van de gebruiker is en of het een
normaal bestand is. Vervolgens opent het het bestand en schrijft het bericht.
Dat is waar het veiligheidsgat ontstaat! Of, om exacter te zijn, het ontstaat
in de tijd tussen het lezen van de bestandseigenschappen met stat()
en het
openen met fopen()
. Deze tijdsspanne is vaak extreem kort, maar een
aanvaller kan er gebruik van maken om de bestandseigenschappen te veranderen.
Om de aanval nog makkelijker te maken, bouwen we een regel in die er voor zorgt
dat het proces rust tussen twee operaties, waardoor er meer tijd is om de
veranderingen te realiseren. Regel 30 wordt veranderd (voorheen was deze leeg):
30 sleep (20);
Dit moet u eerst geimplementeerd worden, maak de applicatie "Set-UID root".
En, let op, dit is belangrijk, maak een back-up van het wachtwoord bestand
/etc/shadow
:
$ 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 $
Nu is alles klaar voor de aanval. We beginnen in een directory waarvan wij de eigenaar zijn. We hebben een "Set-UID" utility (hier is dat dus ex_01) met een veiligheidsgat and we willen de eerste regel met betrekking tot de root login in het wachtwoord-bestand /etc/shadow vervangen door een regel met een leeg wachtwoord.
Eerst maken we een "fic" bestand dat van ons is:
$ rm -f fic $ touch fic
Vervolgens draaien we onze applicatie in de achtergrond "om 'em voor te zijn". We vragen de applicatie om een string in dat bestand weg te schrijven. Hij controleert wat gecontroleerd moet worden en slaapt een tijdje voordat hij daadwerkelijk het bestand opent.
$ ./ex_01 fic "root::1:99999:::::" & [1] 4426
De inhoud van de root regel komt van de shadow (5) man pagina's, het belangrijkste is het lege tweede veld (geen wachtwoord). Zolang het proces slaapt, dit is ongeveer 20 seconden, kunnen we het "fic" bestand verwijderen en het vervangen door een link (symbolisch of fysiek, dat maakt niet uit) naar het /etc/shadow bestand. Onthoud wel dat iedere gebruiker een link naar een bestand in een directory die van hem is kan maken, zelfs als hij de inhoud niet kan lezen, (dit kan ook altijd in /tmp, verderop meer hierover). Het is echter niet mogelijk om een copie te maken van zo'n bestand, omdat dat de volledige leesrechten vereist.
$ rm -f fic $ ln -s /etc/shadow ./fic
Vervolgens vragen we de shell om het ex_01 proces terug te plaatsen naar de voorgrond met het "fg" commando and wachten we tot het geeindigd is:
$ fg ./ex_01 fic "root::1:99999:::::" Write Ok $
Voil� ! Dat was het, het /etc/shadow bestand bevat slechts een regel die aangeeft dat root geen wachtwoord heeft. Je gelooft het niet?
$ su # whoami root # cat /etc/shadow root::1:99999::::: #
Om ons experiment af te ronden zetten we het oude wachtwoord-bestand terug:
# cp /etc/shadow.bak /etc/shadow cp: replace `/etc/shadow'? y #
We hebben met succes gebruik kunnen maken van een "race condition" in een "Set-UID" utility. Het is natuurlijk wel zo dat dit programma erg behulpzaam was door 20 seconden te wachten om ons de tijd te geven om het bestand te veranderen. In een echte applicatie bestaat de "race condition" slechts een korte tijd. Hoe kunnen we daar gebruik van maken?
Normaal gesproken maakt een kraker gebruik van een "brute force" aanval, waarbij er honderden, duizenden of zelfs tienduizenden pogingen gedaan worden met behulp van scripts om dit proces te automatiseren. Het is mogelijk om de kansen op het succesvol exploiteren van een veiligheidsgat te vergroten met behulp van enkele trucs die het tijdsgat tussen de twee operaties die het programma moet uitvoeren vergroot. Het plan is om het doelproces te vertragen om zo gemakkelijker het bestand te kunnen modificeren. Verschillende benaderingen kunnen ons helpen om dit doel te bereiken:
Ieder van deze methodes, hoewel saai en repetatief, maakt het mogelijk om gebruik te maken van veiligheidsgaten gabaseerd op "race conditions" en ze zijn absoluut bruikbaar! Laten we nu proberen de meest effectieve oplossingen te vinden.
Het probleem dat hierboven besproken is, gaat uit van de mogelijkheid om de karakteristieken van een object te veranderen in het tijdsgat tussen twee operaties, terwijl de hele actie zo continu doorloopt als mogelijk is. In de voorgaande situatie, had de verandering niets te maken met het bestand zelf. Trouwens, dit zou een vrij lastige actie geweest zijn als deze uitgevoerd zou worden door een gewone gebruiker, aangezien deze modificaties aan het /etc/shadow bestand niet zomaar gemaakt mogen worden. De veranderingen zijn in feite gebaseerd op de koppeling tussen de bestaande bestandsnode in de "nametree" en het fysieke bestand zelf.Onthoud hierbij wel dat de meeste systeemcommando's (zoals rm, mv, ln, etc.) werken met de bestandsnaam, niet de bestandsinhoud. Zelfs wanneer je een bestand verwijdert (met behulp van "rm" en het "unlink ()" commando, de inhoud is nu echt verwijderd op het moment dat de laatste fysieke link, het laatste referentiepunt, verwijderd is.
De fout die gemaakt is in het bovenstaande programma is dat de associatie tussen
de bestandsnaam en de bestandsinhoud onveranderlijk zijn, of tenminste
onderanderd, tussen de "stat()" en de "fopen()" operatie.
Met andere woorden: het voorbeeld van een fysieke koppeling zou voldoende moeten zijn
om aan te tonen dat deze koppeling helemaal geen permanente koppeling is.
Laten we een voorbeeld uitwerken met dit type koppeling. In een diretory, waarvan wij
de eigenaar zijn, maken we een nieuwe koppeling naar een systeembestand.
We veranderen de bestandseigenaar en de toegangsinstellingen van het bestand niet.
Met het ln
commando met de -f
optie wordt de creatie van het bestand geforceerd, zelfs wanneer er al een bestand met die naam bestaat:
$ 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 $
De opdracht /bin/ls
met de -i
optie laat het nummer van de inode
aan het begin van de regel zien. Zoals we nu kunnen zien, wijst de zelfde naam naar twee
fysiek verschillende inodes.
Maar eigenlijk willen we dat de functies die de toegang tot een bestand conroleren
altijd naar dezelfde inhoudsgegevens en dezelfde inode wijzen. En dat is mogelijk!
De kernel zelf houdt automatisch deze associaties bij wanneer hij ons een bestands
"descriptor" presenteert. Op het moment dat we een bestand openen om de inhoud te lezen,
met het commando open()
geeft een systeemoproep een getalswaarde, dat is de "descriptor", en associeert het
met het fysieke bestand met behulp van een interne tabel. En alles wat we hierna lezen
is onderdeel van deze bestandsinhoud. Het maakt niet uit wat er onderwijl gebeurt met
de bestandsnaam tijdens de bestands-open operatie.
Het bovenstaande moet benadrukt worden: Zodra een bestand geopend is, maakt het niet uit
wat er gebeurd met de bestandsnaam, ook het verwijderen ervan maakt niet uit voor de bestandsinhoud
Zolang er maar een proces draait dat een "descriptor" voor het bestand aangeeft,
wordt de inhoud van het bestand niet verwijderd van de schijf, zelfs niet als
de bestandsnaam verdwijnt uit de directory waar hij stond.
De kernel beheert de associatie van het bestand tussen het
open()
commando en het close()
commando
door het bestand te voorzien van een "descriptor" die blijft bestaan tot
het proces beeindigd wordt.
Dus dat is onze oplossing! We kunnen het bestand openen en dan de togeangsrechten controleren
door de "descriptor" karakteristieken te bekijken en niet de bestandsnaam karakteristieken.
Dit kan worden gedaan met behulp van het fstat()
commando (het werkt net zo als
het stat()
commando), maar het controleert de bestands-"descriptor" en niet
het pad. Om de inhoud van het bestand te bekijken met behulp van de "descriptor" kunnen we
het fdopen()
commando gebruiken ( dit werkt net zo als het fopen()
commando), maar het maakt gebruik van de "descriptor" in plaats van de bestandsnaam.
En dus ziet het programma er nu als volgt uit:
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 }
Nu kan niets, na regel 20, het gedrag van ons programma beinvloeden (dus ook niet: verwijderen, hernoemen, koppelen); de inhoud van het originele, fysieke, bestand blijft behouden.
Wanneer je een bestand verandert is het van belang om je er van te verzekeren dat de associatie tussen de representatie van de inhoud en de echte inhoud gelijk is. We gebruiken bij voorkeur de volgende commando's om het fysieke bestand te veranderen met behulp van de geactiveerde "descriptor" in plaats van met het pad en de naamsgegevens van het bestand:
Systeem opdracht | Gebruik |
fchdir (int fd) |
gaat naar de directory die fd aangeeft. |
fchmod (int fd, mode_t mode) |
Verandert de toegangsrechten van het bestand. |
fchown (int fd, uid_t uid, gid_t gif) |
Verandert de bestandseigenaar. |
fstat (int fd, struct stat * st) |
Raadpleegt de informatie die opgeslagen is in de inode van het fysieke bestand. |
ftruncate (int fd, off_t length) |
Trunkeert een bestaand bestand. |
fdopen (int fd, char * mode) |
Initialiseert IO vanuit een geopende "descriptor". Het is een stdio bibliotheek-routine, geen commando. |
Nu moet natuurlijk het bestand worden geopend in de gewenste modus, dit kan
met het commando open()
(Vergeet het derde argument niet wanneer je een nieuw bestand maakt).
Meer over open()
later wanneer we het tijdelijke bestandsprobleem bespreken.
We wijzen er met klem op dat het zeer belangrijk is om de feedback die
de commando's genereren te controleren.
Om een voorbeeld te geven, dat welliswaar niets te maken heeft met "race conditions",
de problemen die gevonden kunnen worden in oude /bin/login
implementaties
doordat er geen foute code controle plaatsvindt. Deze applicatie
voorzag in een automatische "root" toegang wanneer het /etc/passwd
niet gevonden werd. Dit lijkt een acceptabele oplossing wanneer we over een
beschadigd bestandssysteem praten. Echter controleren of het onmogelijk was
om het bestand te openen in plaats van controleren of het bestand echt bestaat
is minder acceptabel. Het aanroepen van /bin/login
na het openen
van het maximaal toegestane aantal "descriptors" geeft iedere gebruiker toegang
tot het systeem als "root".....Laten we deze uitweiding beeindigen met benadrukken
hoe belangrijk het is om te controleren, en dan niet alleen de systeemmeldingen
"succes" of "error", maar de errorcodes zelf ook en dan wel voordat je iets gaat
veranderen aan de veiligheidsinstellingen van het systeem.
Eeen programma dat betrekking heeft op systeemveiligheid zou niet moeten vertrouwen op exclusieve toegang tot de inhoud van een bestand. Het is belangrijk om de risico's van een "race condition" in dat zelfde bestand goed te beheren en beheersen. Het grootste gevaar komt van een gebruiker die meerdere versies van een Set-UID root applicatie tegelijk draait of met een en dezelfde deamon meerdere connecties tegelijk initieert, in de hoop een "race condition" te genereren, om op dat moment een systeembestand op een ongewone wijze te veranderen.
Om te vermijden dat het programma gevoelig is voor dit soort situaties, is het noodzakelijk om een exclusief toegangsmechanisme tot de inhoud van het bestand in te stellen. Dit is hetzelfde probleem als het probleem in databases gevonden kan worden wanneer verschillende gebruikers gelijktijdig bestandsinhoud mogen opvragen of veranderen. Het principe van "file locking" kan dit probleem opgelost worden.
Wanneer een proces naar een bestand wil schrijven, vraagt het de kernel om het bestand, of een deel ervan, te "locken". Zolang het proces het "lock" in stand houdt, kan geen enkel ander proces het betreffende bestand, of deel ervan, "locken". Een proces kan op dezelfde manier een "lock" aanvragen voordat het de inhoud van een bestand leest om te voorkomen dat de inhoud niet veranderd wordt tijdens de "lock" situatie.
Het systeem is in feite nog slimmer dan dat: de kernel maakt een onderscheid tussen het type "lock" dat nodig is voor het lezen van een bestand en het type dat nodig is voor het schrijven van een bestand. Meerdere processen kunnen gelijktijdig een lees-"lock" op een bestand hebben, omdat geen ervan de inhoud van het bestand zal veranderen. Maar bij schrijven kan maar een proces een "lock" op een bestand hebben, een ander proces kan dan geen "lock" voor schrijven krijgen, maar ook geen "lock" voor lezen.
Er zijn twee "lock"-types (die over het algemeen niet compatible met elkaar zijn).
De eerste komt van BSD en maakt gebruik van de flock()
operatie.
Het eerste argument hierbij is de is de "descriptor" van het bestand waarin je
exclusieve toegangwilt hebben en het tweede argument is een symbolische constante
die de uit te voeren operatie representeert. De opdracht kan verschillende waardes
hebben: LOCK_SH
(een lees-"lock"), LOCK_EX
(een schrijf-"lock")
LOCK_UN
(het opheffen van het "lock").
Het systeem blokkeert alle andere aanvragen zolang het proces het bestand beheert.
Echter, je kan een binaire OR |
van de LOCK_NB
constante
uitvoeren, zodat het proces met een foutmelding stopt en niet "gelocked" blijft.
Het tweede "lock"-type komt van System V en vertrouwt op de fcntl()
systeem operatie, echter deze heeft een wat lastige aanroep.
Er is een bibliotheekfunctie fcntl()
, die veel lijkt op de systeem
operatie, maar deze is veel trager. Het eerste argument van fcntl()
is de "descriptor" die het bestand "locked". Het tweede argument representeert de
operatie die uitgevoerd moet worden: F_SETLK
en F_SETLKW
beheren een "lock", waarbij het tweede commando geblokkeerd blijft tot deze mogelijk
wordt, terwijl de eerste meteen output genereert als er iets mis gaat.
F_GETLK
vraagt de "lock"-staat aan van een bestand (dat niet te gebruiken
is door de op dat moment draaiende applicaties). Het derde argument is een "pointer"
naar een variabele van het struct flock
type, dat het "lock" beschrijft.
De belangrijke onderdelen voor de flock
structuur zijn de volgenden:
Naam | Type | Betekenis |
l_type |
int |
Verwachte actie:
F_RDLCK (om te "locken" voor een leesactie),
F_WRLCK (om te "locken" voor een schrijfactie) en
F_UNLCK (om het "lock" te verwijderen). |
l_whence |
int |
l_start Veld oorsprong (meestal
SEEK_SET ). |
l_start |
off_t |
Positie van het begin van het "lock" (meestal 0). |
l_len |
off_t |
Lengte van het "lock", 0 voor het einde van het bestand. |
We kunnen nu zien dat fcntl()
gedeeltes van een bestand kan "locken",
maar het kan nog veel meer als je het vergelijkt met het flock()
commando.
Laten we eens kijken naar een klein programma dat vraagt om een "lock" om een bepaald
bestand te lezen dat gedefinieerd kan worden met een argument, bovendien wacht het
op de gebruiker die op de "enter" toets moet drukken voordat het eindigt (en dan dus
ook de "locks" opent).
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, "Druk op de "enter" toets om het "lock" te openen\n"); 33 fgets (buffer, 2, stdin); 34 exit(EXIT_SUCCESS); 35 }
We draaien dit programma eerst via een console, dat ziet er als volgt uit:
$ cc -Wall ex_03.c -o ex_03 $ ./ex_03 myfile Druk op de "enter" toets om het "lock" te openen>En dan met een tweede console...
$ ./ex_03 myfile Can't lock myfile $Op het moment dat je op
Enter
drukt in de eerste console, wordt het "lock" geopend
Met dit "locking" mechanisme kan je "race conditions" in directories en printer
wachtrijen, zoals de lpd
daemon, voorkomen door gebruik te maken van
flock()
om een "lock" te zetten op het /var/lock/subsys/lpd
bestand, waardoor er slechts een proces tegelijk toegang heeft.
Je kan de toegang tot een systeembestand, zoals /etc/passwd
, op een
veilige manier beheren, wanneer je gebruik maakt van fcntl()
met behulp
van de pam bibliotheek wanneer je gebruikersgegevens verandert.
Helaas echter beveiligt het bovenstaande alleen tegen storingen van applicaties die zich correct gedragen, dus eerst de kernel vragen om de juiste manier van toegang voordat ze lezen of schrijven naar een belangrijk systeembestand. We gaan het nu hebben over een "cooperatief lock", dat de vatbaarheid van een applicatie aantoont met betrekking tot de toegang tot gegevens. Helaas kan een slecht geschreven programma ook de inhoud van een bestand vervangen, zelfs als een ander proces, dat zich wel correct gedraagt, een schrijf-"lock" heeft op het betreffende bestand. Hier volgt een voorbeeld. We schrijven een paar letters in een bestand en we "locken" het bet behulp van het eerder gemaakte programma:
$ echo "FIRST" > myfile $ ./ex_03 myfile Press Enter to release the lock(s)>Vanaf een andere console kunnen we het bestand veranderen:
$ echo "SECOND" > myfile $Terug naar de eerste console, waar we de "schade" opnemen:
(Enter) $ cat myfile SECOND $
Om dit probleem op te kunnen lossen geeft de Linux kernel de systeem administrator
een "locking" mechanisme dat van System V komt. Daardoor is het alleen te gebruiken
met fcntl()
"locks" en niet met flock()
"locks".
De systeem administrator kan de kernel vertellen dat de fcntl()
"locks"
strict zijn, door gebruik te maken van een speciale comninatie van toegangsrechten.
Hierna kan geen enkel ander proces een bestand dat "gelocked" is voor schrijven nog openen
op zelf naar te schrijven, zelfs niet als root.
Deze specifieke combinatie maakt gebruik van de Set-GID bit terwijl de
bit die uitvoering toestaat verwijderd is van de groep. Dit kan door middel van
het volgende commando gerealiseerd worden:
$ chmod g+s-x myfile $Dit is echter niet genoeg. Om een bestand automatisch gebruik te laten maken van strikte cooperatieve "locks", moet het mandatory attribuut geactiveerd zijn op de partitie waar het bestand gevonden kan worden. Over het algemeen moet je het
/etc/fstab
bestand wijzigen en het mand
commando
toevoegen in de vierde kolom, of je moet het volgende commando intypen:
# mount /dev/hda5 on / type ext2 (rw) [...] # mount / -o remount,mand # mount /dev/hda5 on / type ext2 (rw,mand) [...] #Nu kunnen we controleren dat een verandering vanaf een andere console niet mogelijk is:
$ ./ex_03 myfile Press Enter to release the lock(s)>Vanaf een andere console:
$ echo "THIRD" > myfile bash: myfile: Resource temporarily not available $En terug naar de eerste console:
(Enter) $ cat myfile SECOND $
De administrator en niet de programmeur moet de beslissing om strike bestands "locks"
te gebruiken, nemen ( bijvoorbeeld op /etc/passwd
, of /etc/shadow
).
De programmeur moet de manier waarop de gegevens worden geopend controleren,
daardoor is hij ervan verzekerd dat zijn applicatie gegevens op een correcte, samenhangende,
manier benadert, wanneer de applicatie leest en dat de applicatie geen gevaar is voor
andere processen wanneer hij iets wegschrijft, zolang de omgeving correct is onderhouden door
de administrator.
Vaak moet een programma tijdelijk gegevens wegschrijven in een extern bestand.
Doorgaans wordt er een record in het midden van een sequentieel geordend bestand
aangelegd, dit houdt dan ook in dat we een copy maken van het originele bestand
in een tijdelijk bestand,
The most usual case is inserting a record in the middle of a sequential ordered
file, which implies that we make a copy of the original file in a temporary file,
while adding new information. Next the unlink()
system call removes
the original file and rename()
renames the temporary file to
replace the previous one.
Het openen van een standaard bestand is, mits niet goed uitgevoerd, vaak het begin van een "race condition" die wordt opgestart door een gebruiker met kwade bedoelingen. Veiligheidslekken gebaseerd op tijdelijke bestanden zijn recentelijk aangetroffen in applicaties als bijvoorbeeld Apache, Linuxconf, getty_ps, wu-ftpd, rdist, gpm, inn, etc. Onthoud een paar principes om dit soort problemen te vermijden.
Gewoonlijk worden tijdelijke bestanden gemaakt in de /tmp
directory.
Hierdoor weet de systeembeheerder waar de tijdelijke data te vinden is. Het is daardoor
ook mogelijk om een proramma te schrijven dat de directory periodiek opschoont,
door gebruik te maken van cron
, door het gebruik van een losse partitie
die geformateerd wordt tijdens een boot etc.
Normaal gesproken definieert de systeembeheerder de plaats die gereserveerd is voor
tijdelijke bestanden in de <paths.h
> en <stdio.h
>
bestanden in de symbolische constantes definities _PATH_TMP
en
P_tmpdir
. Het is dan ook niet verstandig om een andere standaard
directory in te stellen dan /tmp
, aangezien dat betekent dat je dan
ook iedere applicatie opnieuw moet compileren, inclusief de C bibliotheek.
In de GlibC routine is echter wel aan te geven waar naartoe wordt geschreven
met behulp van de TMPDIR
omgevingsvariabele.
Zo kan een gebruiker instellen dat tijdelijke bestanden naar een directory die van hem
is, worden weggeschreven in plaats van in /tmp
. Dit is soms verplicht,
bijvoorbeeld wanneer de partitie die is toegewezen aan /tmp
te klein is
om een applicatie te draaien die veel tijdelijke betsandsruimte nodig heeft.
De /tmp
systeem directory is iets speciaals door z'n toegangsrechten:
$ ls -ld /tmp drwxrwxrwt 7 root root 31744 Feb 14 09:47 /tmp $
De zogenaamde Sticky-Bit die gerepresenteerd wordt door de letter t
aan het einde of de octale 01000 modus, heeft een speciale betekenis op het moment dat
deze wordt toegepast op een directory: alleen de eigenaar van de directory (root )
en de eigenaar van een bestand in deze directory kunnen dit bestand verwijderen.
De directory heeft volledige schrijftoegang, iedere gebruiker kan zijn bestanden er in
wegschrijven, en er zeker van zijn dat ze beveiligd zijn - tenminste, tot aan de volgende
schoonmaakoperatie uitgevoerd door de systeembeheerder.
Ondanks dit alles, kan het gebruik van tijdelijke opslag een paar problemen geven.
Laten we beginnen met een alledaagse mogelijkheid, een "Set-UID" root
applicatie die communiceert met de gebruiker. Bijvoorbeeld een mail programma.
Als dit proces de opdracht krijgt om onmiddelijk te eindigen, bijvoorbeeld
SIGTERM of SIGQUIT tijdens een shutdown procedure,
dan kan hij proberen de al geschreven maar nog niet verzonden mail "on the fly"
te bewaren. Bij oude versies werd dit gedaan in het /tmp/dead.letter
bestand. Daarna hoefde de gebruiker alleen nog maar een fysieke link te maken naar
het /etc/passwd
bestand (dit omdat hij niet mag schrijven in/tmp
)
met de naam dead.letter
voor de mailer (die draait onder UID root)
om de inhoud van het nog niet afgemaakte mailtje te kunnen veranderen
(toevallig bevat dit ook de volgende regel: "root::1:99999:::::
").
Het eerste probleem met dit soort gedrag is het feit dat de bestandsnaam
te voorspellen is. Je kan door een keer naar zo'n applicatie te kijken
deduceren dat het de bestandsnaam /tmp/dead.letter
zal gebruiken.
Daarom is de eerste stap het gebruik van een bestandsnaam die per draaiend programma
wordt gedefinieerd. Er zijn meerdere bibliotheekfuncties verkrijgbaar die ons
kunnen voorzien van een tijdelijke bestandsnaam.
Laten we er van uit gaan dat we zo'n functie hebben die een unieke naam voor ons tijdelijke bestand genereert. Echter aangezien gratis software zoals dit verkrijgbaar is met broncode en al (ook voor de C bibliotheek), is de bestandsnaam echter nog steeds te voorzien, ook al is dat nog vrij lastig. Een kraker kan een symbolische link creeren naar de bestandsnaam die de C bibliotheek genereert. Onze eerste reactie is dan om te controleren of het bestand bestaat voor het te openen. Naief als we zijn, zouden we iets als het volgende kunnen schrijven:
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); ...
Dit is duidelijk een typisch geval van een "race condition", waarbij
een veiligheidsgat geopend wordt dankzij de gebruiker waardoor het
mogelijk wordt om een link naar /etc/passwd
te leggen
tussen de eerste en tweede open()
actie.
Deze twee operaties boeten gekoppeld uitgevoerd worden, zonder dat er tussen
de twee commando's veranderingen aangebracht kunnen worden. Dit is mogelijk
door een specifieke optie van de open()
systeem opdracht.
Deze specifieke opdracht wordt O_EXCL genoemd en wordt gebruikt
in combinatie met O_CREAT. Deze optie zorgt ervoor dat de open()
actie niet uitgevoerd kan worden als het bestand al bestaat, maar deze controle
of het bestand al bestaat wordt direct gekoppeld aan de creatie van het bestand.
Trouwens, de 'x
' Gnu extensie voor het openen van de fopen()
functie, vereist een exclusieve bestandsaanmaak, die een foutmelding genereert als
het bestand al bestaat:
FILE * fp; if ((fp = fopen (filename, "r+x")) == NULL) { perror ("Can't create the file."); exit (EXIT_FAILURE); }
Toegang tot de tijdelijke bestanden is ook belangrijk. Als je vertrouwelijke informatie in ee 644-bestand (lees/schrijfrechten voor de eigenaar, leesrechten voor de rest van de wereld) kan dat ook wat problemen opleveren:
#include <sys/types.h> #include <sys/stat.h> mode_t umask(mode_t mask);Bovenstaande functies staan het toe om de toegangsrechten van een bestand te bepalen op het moment van de creatie. Dan opent het bestand in modus 600 (lees/schrijfrechten voor de eigenaar, geen rechten voor de anderen) na een
umask(077)
opdracht.
Normaal gesproken wordt een tijdelijk bestand in drie stappen gemaakt:
O_CREAT | O_EXCL
, met de meest
meest restrictieve rechten;
Hoe moet een tijdelijk bestand worden gemaakt?
#include <stdio.h> char *tmpnam(char *s); char *tempnam(const char *dir, const char *prefix);Deze funties geven pointers naar random gecreerde namen.
De functie accepteert eerst een NULL
argument, daarna retourneert hij een
statisch buffer adres. De inhoud zal worden verander tijdens de volgende aanroep
tmpnam(NULL)
. Als het argument een al gealloceerde string is, dan
wordt de naam hier gecopieerd, en dat vereist een string van tenminste
L-tmpnam
bytes. Wees voorzichtig met buffer-overflows!
De man
pagina vertelt meer over problemen wanneer de functie wordt
gebruikt met een a NULL
parameter, wanneer _POSIX_THREADS
of
_POSIX_THREAD_SAFE_FUNCTIONS
zijn gedefinieerd.
De tempnam()
functie geeft een pointer naar een string. De dir
directory moet "passend" zijn (de man
pagina omschrijft wat er bedoeld wordt
met "passend"). Deze functie controleert of het bestand niet bestaat voordat het de naam retourneert.
Echter, de man
pagina's raden dit gebruik af, aangezien "passend" een varierende betekenis
kan hebben, afhankelijk van de functie-implementatie.
Hierbij melden we wat Genome aanraadt met betrekking tot gebruik:
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);De loop die hier gebruikt wordt, verkleint de risico's, maar genereert tevens nieuwe risico's. Wat er nu zou gebeuren wanneer je een tijdelijk bestand wilt maken op een volle tijdelijke partitie of het maximum aantal bestanden dat in een keer beschikbaar is, gehaald is...
#include <stdio.h> FILE *tmpfile (void);Deze functie maakt een unieke bestandsnaam en opent deze. Dit bestand wordt automatisch verwijderd als het gesloten wordt.
Met GlibC-2.1.3 is het onderstaande mogelijk. Deze functie gebruikt een mechanisme dat lijkt op
tmpnam()
om een bestandsnaam te genereren, en het opent de bijbehorende
"descriptor". Het bestand wordt daarna verwijderd, maar Linux verwijdert het pas echt op
het moment dat geen enkele bron het meer gebruikt, dat is het moment waarop de
bestands "descriptor" wordt losgelaten met behulp van een close()
systeem opdracht.
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 */
De eenvoudigste gevallen hebben geen verandering aan bestandsnaam nodig,
net zo min als overdracht aan een ander proces, maar alleen opslagruimte
en de mogelijkheid om gegevens opnieuw uit te lezen uit de tijdelijke ruimte.
Daarom hebben we de bestandsnaam van het tijdelijke bestand niet nodig, alleen
de inhoud is van belang.
De tmpfile()
functie doet dit.
De man
pagina's zeggen hier niets over, maar de "Secure-Programs-HOWTO" raadt
dit niet aan. Volgens de auteur garanderen de specificaties niet de creatie van het bestand
en hij is tot nu toe niet in staat geweest om iedere implementatie te controleren.
Ondanks deze reserveringen, is dit de meest effectieve functie.
#include <stdlib.h> char *mktemp(char *template); int mkstemp(char *template);Tenslotte de bovenstaande functies om een unieke bestandsnaam te creeren vanaf een sjabloon van een string die eindigt met"
XXXXXX
".
Deze "x-en" worden vervangen om een unieke bestandsnaam te verkrijgen.
Volgens sommige versies is alleen de laatste "x" random, mktemp()
vervangt de eerste vijf "x-en" met hetProcess ID (PID) ...,
dit maakt de naam vrij makkelijk te raden. Sommige versies staan meer dan
zes "x-en" toe.
mkstemp()
is de aangeraden functie in de
"Secure-Programs-HOWTO". Hieronder volgt de methode:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> void failure(msg) { fprintf(stderr, "%s\n", msg); exit(1); } /* * Maakt een tijdelijk bestand en laat het zien. * Deze routine verwijdert de bestandsnaam van het bestandssysteem, waardoor * het niet meer verschijnt op het moment dat de inhoud van de directory wordt opgevraagd. */ FILE *create_tempfile(char *temp_filename_pattern) { int temp_fd; mode_t old_mode; FILE *temp_file; /* Maakt een bestand met beperkte toegang */ 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; }
Deze functies laten de problemen met betrekking tot abstractie en portabiliteit zien.
In zoverre dat de standaard bibliotheekfuncties geacht worden bepaalde features te
leveren (abstractie)...., maar de manier waarop deze geimplementeerd dienen te
worden is afhankelijk van het systeem (portabiliteit).
Bijvoorbeeld de tmpfile()
functie die een tijdelijk bestand opent
op verschillende manieren (sommige versies maken geen gebruik van O_EXCL
).
Of mkstemp()
dat een variabel aantal "x-en" afhandelt, afhankelijk van de
implementatie.
We zijn door de meeste beveiligingsproblemen met betrekking tot "race conditions" aan
bronnnen heen geraced. Onthoud dat je nooit mag aannemen dat twee achtereenvolgende operaties
altijd direct na elkaar worden uitgevoerd door de processor, tenzij de kernel ze beheert.
Als "race conditions" veiligheidslekken genereren, moet je niet het probleem oplossen door
op andere bronnen te vertrouwen, zoals gedeelde variabelen tussen "threads" of gedeelde
geheugensegmenten door gebruik te maken van shmget()
.
Selectiemechanismen (zoals bijvoorbeeld "semaphore") moeten worden gebruikt om moeilijk te
vinden bugs te vermijden.