* 10 *

Softwaresikkerhet II

Ukens faktum

Java sikkerhetsmodellen kalles en sandbox modell. Ideen er å sette alt som skjer inn i en sikker `wrapper' eller boks, slik at ingen brukere har mulighet til å gjøre noe farlig.

Kapittel 8,11 Gollmann

Race conditions

Kappløp med tiden. Flerbrukersystemer kan lures til å lese falsk informasjon eller å utføre feilaktige oppgaver på grunn av context switching.

USAs president skriver under på papirer i det ovale rom. Når telefonen ringer distraheres presidenten med en annen oppgave. Mens presidenten er opptatt på telefon bytter hans ondsinnete rådgiver papirene slik at presidenten skriver under på papirer som sier at rådgiveren er den nye presidenten, med øyeblikkelig virkning.

James Bond henger fra en tråd over Brainiac Supercomputer Weapons Control Centre til Specters Hemmelige Death Ray. Han venter på sikkerhetskameraets bevegelser. Mens kameraet ser i en annen retning daler han ned og omprogrammerer våpenet slik at det kommer til å tilintetgjøre Specters hovedbase. Så er det akkurat nok tid til å kysse supermodellen og klatre opp igjen før kameraet ser ham.

Disse scenarioer heter racer eller race conditions, fordi det er en konkurranse mot tid til å utføre noe lureri mens systemet ikke ser på.

Det samme kan skje med prosesser i flerprosesssystemer. Dersom en prosess (A) avbrytes får andre prosesser (B,C...) en sjanse til å forandre noe for A mens A er i dvale. Vanligvis har ikke andre prosesser rettigheter til å forandre noe til en annen prosess, men dersom vi er sløve med tilgangskontrol så er det mulig. Race conditions er en vanlig form for angrep mot software som avhenger av offentlig tilgjengelige kommunikasjonskanaler. F. eks.

Obs at et offentlig skrivbart område fungerer som en covert kanal for å sende informasjon utenom sikkerhetskontroller.

Begrense privilegier

Du er kanskje allerede lei av å høre det fundamentale sikkerhetsprinsippet:
For å kunne ha sikkerhet må vi kunne begrense privilegier.
Ideen er så enkel, men allikevel så sentral for sikkerhet, at den ikke kan understrekes nok. La oss nå se hvilke muligheter vi har for å begrense privilegier i softwaresystemer:

Ikke-privilegert brukerID

Operativsystemer som oppfyller Orange Book C2 kravene gir brukere unike identiteter (brukernavn og UID/SID nummere) og tillater frivillig tilgangskontroll (discretionary access control eller DAC). Dette begrenser mulighetene for vanlige brukere til å få tilgang til vilkårlige ressurser. Men mange systemprosesser startes med fulle rettigheter uten grunn og dette kan være farlig. Moderne softwareservere bytter identitet slik at de jobber med en UID som ikke har spesielle privilegier.

Unix Apache WWW serveren, f. eks. startes av root brukeren for å koble seg opp mot priviligert port nummer 80, men like etterpå bytter den UID til en ikke-priviligert ID som heter www. På den måten har den ikke lenger lese/skrive tilgang til filer som ikke er ment for Web-publisering. MySQL of LPRNG serverne gjør det samme.

En tommelregel for softwaresystemer er:

Prosesser som ikke trenger å kjøre med privileger bør gi opp sine privilegier så fort som mulig.
I POSIX/Unix modellen har hver prosess en effektiv og en reell UID. Den reelle UIDen er det høyeste sikkerhetsnivået som kan oppnås av prosessen (tenk på BLP modellen). Den effektive UIDen er det nåværende sikkerhetsnivået. Begge begreper trengs. En priviligert prosess kan, f. eks. sette begge sine UIDer til en ikke-priviligert bruker ID ved hjelp av systemkallet:
setuid(non_priv_uid);
Men etter at dette kallet er eksekvert kan ikke prosessen få tilbake dens opprinnelige privilegier, fordi den ikke lenger har privilegier til å kunne gjøre det! For software som trenger å bytte tilbake igjen bruker vi:
priv_uid = getuid();                    /* Normally root */

if (setreuid(-1,non_priv_uid) == -1)   // setreuid(real,eff)
   {
   perror("seteuid");
   return false;
   }

 // Non-privileged work

if (setreuid(-1,priv_uid) == -1)
   {
   perror("seteuid");
   exit(error);
   }
Dette kallet setter kun den effektive brukerID, i motsetning til setuid() som setter begge.

Sandboxing

For best mulig sikkerhet kunne man tenke seg å låse en prosess inn i en sikker boks. En måte å gjøre dette på er å bruke funksjonen chroot som låser en prosess inn i et undertre av filsystemet.

                        root

                       /     \
                     home    /\
                     /  \
                    /   --ftp--
                   /   |       |
                  /\   |       |
                 /\     -------

Anonym FTP gjør dette for eksempel. Brukere kan ikke se filer eller kataloger utenfor FTP området. Dette bør kombineres med en forandring av UID da root kan komme utenfor en slik sandbox ved hjelp av fchroot.
  1. Lag en privat katalog privatdir.
  2. Pass på at det ikke fins noen setuid root programmer under privatdir.
  3. Pass på at det ikke er noen linker som gir priviligert tilgang til minne, f.eks. /dev/kmem.
  4. Gjør chroot(privatedir);
  5. Gjør setuid() til en ikke-priviligert bruker.
Java bruker denne fremgangsmåten for å begrense mulighetene til brukere som kjører applets. Weben i seg selv prøver å lage en minimal sandboks ved å begrense UIDen til webprosesser. Java går enda lenger ved å bruke elementer fra objektorientering, som vi diskuterte i en tidligere forelesning, for å begrense funksjonalitet. Web browsere har en lignende sikkerhetspolitikk, men dette er noe de har valgt å gjøre (ofte basert på smertefull erfaring), det er ikke en del av designet.

exec functions, system() and popen()

Funksjonsfamilien exec kjører barneprosesser ved å bytte en nåværende prosess med en ny en. Det fins mange versjoner av exec funksjonene som fungerer på litt forskjellige måter. De kan klassifiseres i grupper som Se man execlp og man execve for detaljene. En diskusjon om barneprosesser må inkludere en diskusjon om avhengigheter og arv av attributter, som vi allerede har identifisert som problemområder. Funksjonene execlp og execvp bruker shell for å starte programmer. De gjør eksekveringen av programmer avhengig av verdien i PATH variablen og andre environment variabler. execv() er sikrere da den ikke roter bort i slike avhengigheter.

Funksjonen system(string) gir kommandoen i string til et shell for eksekvering. Den er et bekvemmelig grensesnitt mot exec-funksjonene. Men som med alle bekvemmeligheter kompromitterer den sikkerheten ved å åpne for shell-basert angrep: f.eks.

 system("/bin/ls -a | /bin/grep .");
På grunn av de avhengighetene den medfører må denne funksjonen betraktes som nødvendigvis usikker.

Standard implementasjonen av popen() har lignende problemer. Denne funksjonen åpner en pipe (rør) til en ny prosess for å eksekvere en kommando og lese outputen som en filstrøm. f.eks.

FILE *pp;
char line[1024];

pp = popen("/bin/ls -a","r");

while (!feof(pp))
   {
   fgets(line,1023,pp);

   printf("comm: %s\n",line);
   }

pclose(pp);
Også denne funksjonen starter et shell for å tolke kommandoer. En sikrere versjon av kommandoen som ikke starter et shell ble skrevet for cfengine. cfpopen() setter opp kommandoen for direkte eksekvering. Merk at dersom ingen spesifikk bane til kommandoen spesifiseres, prøver denne funksjonen fortsatt å åpne kommandoen i directoryet hvor foreldreprogrammet ble startet. Komplette baner bør alltid spesifiseres for å unngå problemer med Trojanske hester.

Obs at når et shell eskekveres betyr det at shelloppstartsfiler leses. Disse kan i prinsippet innholde vilkårlige kommandoer!

Spoofing races og midlertidige filer: symbolic link troubles

Unix filesystemer innførte symbolic links, eller aliaser til filer. Priviligerte programmer som uforsiktig åpner filer for å skrive kan lures til å ødelegge viktige systemfiler. Anta f.eks. at vi skriver et program som åpner en fil og skriver til den uten å kontrollere om filen fins fra før. Vi trenger ikke å gjøre noe mer avansert enn å lage en symbolic link fra filen vi skulle åpne til /etc/passwd eller et viktig bibliotek, og disse filene vil erstattes med det som vi skriver. Dette kan ødelegge systemet.

Dette gjelder særlig software som åpner midlertidige filer i offentlig skrivbare kataloger, for da kan en hvilken som helst bruker lage en link til /etc/passwd in /tmp. Men det samme gjelder å skrive til filer i brukeres hjemmeområder. Med default oppsett vil følgende ødelegge en Unix database server.

cd /tmp
ln -s /etc/passwd /tmp/mysql.socket
su
mysqld &
For å åpne en midlertidig fil i et farlig område på sikkert vis må vi først kontrollere at: Hvorfor trenger vi det siste? Jo, fordi det kan fortsatt oppstå en race mellom prosesser mellom det å slette filen og å åpne den nye. Hvis prosessen ble stoppet mellom disse kallene kunne en annen prosess legge inn en link. Her er en riktig metode for å åpne en fil for å skrive:
int fd;
char *filename;

unlink(new);  // delete any existing object

 // Any context switch here?
 
if ((fd = open(filename,O_WRONLY|O_CREAT|O_TRUNC|O_EXCL, 0600)) == -1)
   {
   perror("open");
   close(sd);
   unlink(new);
   return false;
   }

Vi fjerner først en eventuell fil/link som kan ha vært der fra før og så åpner vi filen med flagget O_EXCL som får open til å feile dersom objektet fins fra før. Dersom en race skulle oppstå og noen har klart å linke navnet til en annen fil vil kallet feile istedenfor å gjøre noe farlig.

Det umulige problemet: tillit omkring identitet og tilgangskontroll

La oss avslutte denne ukens forelesning med en deprimerende tanke. Hvis du ikke har innsett dette ennå bør du nå innse at alt innen sikkerhet har med tillit å gjøre.
Det er umulig å virkelig verifisere identiteten til en bruker eller et individ.
Når vi lager protokoller for å koble klienter opp mot tjenester er det viktig å identifisere brukeres identiteter. Det samme gjelder egentlig identifisering av brukere til andre prosesser på den samme maskinen, men vanligvis stoler vi på brukeres identitet på en lokal maskin fordi IDen styres av kjernen, som er en prosess vi stoler på (dersom vi ikke har tillit til kjernen kan vi bare gi opp). På et eller annet vis må vi også etablere identitet når vi kommuniserer mellom forskjellige kjerner på tvers av maskiner. For å innse hvor vanskelig dette problemet er, la oss tenke oss noen scenarioer av varierende naivitet:
  1. Vi påstår vår egen identitet ved å sende navnet som en del av protokollen: f.eks. "LOGIN mark UID 56". Dette er opplagt utilstrekkelig. Hvem som helst kan skrive et program som sender en falsk identitet til serveren. Det ville vært absurd å basere tilgangskontroll på en påstand.
  2. Vi kan prøve å bekrefte identiteten ved hjelp av en felles hemmelighet: ved å bruke passord eller offentlig/privat nøkkel par blir det lettere å stole på at en påstått identitet er autentisk. Problemet nå er det at passord kan gjettes og nøkler kan forfalskes. Hvordan vet vi at den personen som satte opp passordet eller nøkkelen er den samme personen som vi ønsket å gi tilgang til? Tenk på PGP: hvem som helst kan lage et nøkkelpar som påstås å tilhøre USAs president, men hvordan vet vi at det er autentisk? Problemet kan ikke løses uten tillit. Vi kan f.eks. kreve at personen møter opp for å få sitt passord, men identitet kan også forfalskes i den fysiske verden. Vi kan aldri vite med 100% sikkerhet.
  3. Vi kan forsøke å ta kontakt med klientmaskinens kjerne for å få bekreftet påstanden om indentiteten.: men da må vi stole på mekanismen som bekrefter identiteten, f.eks. pidentd protokollen.
Uansett hvor hardt vi jobber for å autentisere identitet fins det alltid et divergerende tillitsproblem. Vi burde slite for å minimere behovet for implisitt tillit, men problemet blir aldri borte.

Oppsummering

Tommelreglene for softwaresikkerhet:
  • Lag fornuftige brukergrensesnitt. Gjør det vanskelig å utføre farlige operasjoner.
  • Begrens privilegier: prøv å minimere brukerens skadevirkning i enhver anledning.
  • Husk at software har implisitte avhengigheter. Hva er de? Hvordan kan vi beskytte oss mot utnyttelse?
  • Jobb hardt for å autentisere brukere dersom du trenger identitet i software. Bruk etablerte metoder for autentisering og pass på at de er vanskelige å forfalske.
  • Unngå bruk av for mange lokale variabler, de kan fylle opp en prosess/thread stack. (Stacken har en endelig begrenset størrelse.)

  • Bruk sikre protokoller, utviklingsverktøy som gir god sikkerhet og pass alltid på advarsler fra kompilatoren.
  • Dersom software feiler, pass på at den feiler trygt (dvs. pass på at det fins sikre defaultverdier og at en feil ikke setter systemet i en farlig tilstand.)
  • Ikke la tilfeldigheter bestemme noe. Spesifiser alle detaljene eksplisitt, dvs enhver PATH til en kommando. Link programmer statisk for å unngå biblioteksforfalskning. Administratorens PATH bør ikke innholde ".".
  • Skriv aldri til filer på en del av et filsystem som ikke er helt privat, uten å passe på at objektet ikke er en link til noe annet.
  • Kontroller alltid arraygrenser.
  • Begrens alltid hvor mye input som leses inn i et buffer for å unngå overflow. Bruk aldri gets() fra C biblioteket eller iostream ">>" operatoren i C++)
  • Sjekk innholdet i input for skjulte kommandoer og tåpelige verdier.
  • Bruk strncpy() instedenfor strcpy() når du ikke vet strenglengder. Bruk sprintf med begrenset streng lengde %.xxxs
  • Bruk programmer som Purify/Electric Fence for å debugge.

Back