Veiligheidslekken vermijden bij het programmeren - Deel 5 : "Race Conditions"

ArticleCategory:

Software Development

AuthorImage:

[image of the authors]

TranslationInfo:

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

fr to en Georges Tarbouriech

en to en Lorne Bailey

en to nl Hendrik-Jan Heins

AboutTheAuthor:

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.

Abstract:

Dit vijfde artikel van deze serie is gericht op veiligheidslekken gerelateerd aan multi-tasken. Een zogenaamde "race condition" ontstaat wanneer verschillende processen tegelijk dezelfde bronnen gebruiken (bestanden, apparaten of geheugen) en ieder proces meent de exclusieve rechten op deze bron te hebben. Dit leidt tot moeilijk op te sporen bugs en veiligheidsgaten die de totale systeemveiligheid kunnen schaden.

ArticleIllustration:[illustration]

[article illustration]

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

Inleiding

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.

Eerste voorbeeld

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
#

Laten we meer realistisch te werk gaan

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.

Mogelijke verbeteringen

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.

Richtlijnen

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.

Race conditions met betrekking tot de bestandsinhoud

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.

Tijdelijke bestanden

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:

  1. Unieke naam creatie (random) ;
  2. Het openen van het bestand door middel van O_CREAT | O_EXCL, met de meest meest restrictieve rechten;
  3. controle en correcte reactie op het resultaat op het moment dat het bestand geopend wordt (hetzij "retry" of "quit").

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.

Conclusie

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.

Links