* 9 *

Software-sikkerhet I

Ukens faktum

Internet banker bruker et autentiseringssystem basert på engangspassord, ofte kallt signaturer (digipass/kalkulator). Disse genererer passord basert på en hemmelig nøkkel og klokkeslett. Gyligheten av nøklene går ut etter ca. 15 minutter.

Kapittel 8 Gollmann
Vi har så langt sett på mekanismer og modeller for sikkerhet fra et passivt synspunkt. Vi har betraktet en del av problemene med design av sikre systemer, og nå går vi over til problemene vi har som softwareutviklere. Eksemplene her er basert på POSIX (standardiserte Unix) systemer, men sakene er like viktige i andre operativsystemer. Det er viktig å innse at, selv om mange av de spesifikke tilfellene bare er historie, kommer disse probleme til å gjenoppstå dersom vi ikke lærer av feilene.

Software-sikkerhetsdesign

Gollmann poengterer at det fins tre forbannelser i sikkerhet. Disse har generelt med kvalitetssikring å gjøre. La oss begynne med litt folkvett som er direkte relevant til software-sikkerhet.

Formalisme er vår venn

Sikker kommunikasjon betyr det å sikre integriteten og forståelsen av en beskjed. Protokoller er bra til dette formålet. En protokoll er en formell eller standardisert oppførsel, et slags skjema eller kommunikasjonsbyråkrati. I programmering, f. eks. fins det en protokoll for å sende informasjon mellom prosedyrer ved hjelp av parametere:
function(i,j);

...


void function (int a, int b)

{
}
I C var det mulig å sende et annet antall parametere enn det som var korrekt for funksjonen. Dette var dårlig sikkerhet. I ANSI C og i C++ bruker man funksjon-prototyper for å deklarere antallet parametere i protokollen. Protokollen tillater kompilatoren å kontrollere at ingen sløve feil har oppstått. Resultatet er et sikrere og mer pålitelig program.

Et annet eksempel på en protokoll er FTP protokollen (se man ftpd) som består av en strøm med kommandoer, avsluttet med end-of-line, som i et shell. Alle tolkete shell-grensesnitt er forøvrig implisitte protokoller.

Hvor mye kontroll bør spesifiseres i en protokoll? Hvor stringent bør den være? Dersom vi er veldig stringente er det få sjanser for at noe kan gå galt på grunn av tilfeldigheter. Dersom vi ikke kontrollerer integriteten til protokollen er det mulig å misbruke den.
Protokoller utgjør en disiplin som må opprettholdes av software. Dette krever også disiplin fra programmerere. Fordelen med å bruke protokoller er at det er mulig å få en maskin til å sjekke om man har vært konsekvent.

Funskjonsprotokollen ovenfor avslutter ved å returnere en verdi. Returverdien signaliserer ofte om funskjonen lyktes eller mislyktes. Disse verdiene sender viktig informasjon tilbake til foreldrefunskjonen og bør sjekkes! Programmer bør alltid feile på en kontrollert og sikker måte.

I Unix returnerer de fleste systemkall verdien -1 dersom de ikke lykkes. Man kan da bruke perror() eller strerr() for å avsløre grunnen til problemet. f. eks.

if (chmod("/dir/newfile",0600) == -1)
   {
   perror("chmod");
   return error;
   }

Tvetydighet er vår fiende

Hver gang input eller output er tvetydig får vi et sikkerhetsproblem.

PATH angrep

Når ett program starter et annet program bør man alltid oppgi den nøyaktige banen til programmet. Noen programmer bruker PATH variablen til å søke etter programmer, men dette betyr at brukeres tilfeldige shelloppsett velger hvilket program som blir kjørt. Ved å legge et Trojansk hestprogram i PATH kan et hvilket som helst program eksekveres . f.eks.
#!/usr/bin/perl
# 

# ....

system("ls");  # should be system("/bin/ls");
Det samme gjelder popen() eller en hvilken som helst kommando som benytter et shell. Funksjoner som utfører kommandoer (barneprosesser) kan klassifiseres i to typer: de som bruker et shell for å starte programmer og de som ikke gjør det. Sikre barneprosesser unngår bruk av shell. (Mer om dette neste uke.)

Selv om vi unngår bruk av shell har vi ikke tettet alle mulige hull. Et annet eksempel er et såkalt IFS angrep. IFS variablen avgjør hvilke tegn som tolkes som whitespace i Bourne shell. Navnet står for Internal Field Separators. Anta at vi setter denne variablen slik at den innholder forward slash tegnet: (Bourne Shell og Bash)

IFS="/ \t\n"; export IFS
PATH=".:$PATH"; export PATH
Nå kaller vi et hvilket som helst program som bruker PATH fra Bourne shell, f.eks. system() kallet, eller popen(). Dette tolkes nå slik:

system("/bin/mail root");   --->  system(" bin mail root");

noe som fører til at kommandoen bin i den nåværende katalogen til brukeren kjøres. Det er da trivielt å lage et program som heter bin som plasseres rundt omkring, f.eks. på /tmp der ikke-så-smarte systemadmin tester ting. IFS bugen ser ut til å være fikset nå i de fleste moderne OS.
System administratorens PATH variabel bør aldri innholde '.', dvs. det directoryet man er i.
Den bør begrenses til et fåtall directoryer man har spesielt tillit til.

Filrettigheter

Når vi oppretter nye filer må vi være forsiktig med hva slags rettigheter de får. Rettighetene til nye filer bestemmes ved å kombinere verdien til umask variablen med den verdien som er implisitt i programmet (Nye filer får 666, nye directories får 777 av open()). Verdier kan også settes eksplisitt. For eksempel,
chmod ("/dir/myfile",0644)
Programmer arver verdien til umask fra den prosessen som starter dem. Dette kan føre til problemer for systemkall som oppretter filer. Se for eksempel på denne C koden (se oppgaven denne uke).

umask(0);

if ((fp = fopen("newfile","w")) == NULL)
   {
   perror("fopen");
   return error;
   }

/* What permissions does newfile have now? */

chmod("newfile",0755);

/* What permissions does newfile have now? */

fclose(fp);
Følgende skjer. Første gangen vi kjører eksisterer ikke filen: Filen opprettes med mode 666 (read/write for alle). chmod() kallet setter den til 755. Neste gang finnes filen fra før og den arver rettighetene 755 uten diskusjon. Nå forandrer vi umask til 077 ovenfor:
umask(077);
Dersom vi sletter filen og begynner pånytt, opprettes filen først med 0600 og så blir den endret til 0755.

Vi ser hvordan attributtarv kan være et farlig problem. Rettighetene på filene er tvetydige med mindre vi eksplisitt setter dem. En bug i Solaris/System V Unix før solaris 2.6 gjorde at prosesser som ftpd kjørte med umask 0. Det betydde at alle private filer som ble lastet ned fikk rettigheter 666, skrivbare for alle.

Buffer-overflow

Bufferhåndtering av inputstrømmer er en særdeles skummel sak. Dersom det ikke gjøres riktig fra begynnelsen av, ved hjelp av en sikker standard, dukker problemene opp om og om igjen. Mange programmerer (inkludert meg) har brent seg på dette. Det samme gjelder når vi manipulerer strenger i minnet på en hvilken som helst måte.

Å stole på input

Her er et eksempel fra en tidlig versjon av Mosaic. For å håndtere telnet sesjoner, skrev programmererne simphelten
sprintf(buf,"telnet %s",url);
system(buf);
Dette taklet ikke spesielt bra URLer på formen:
telnet://host.example.com;rm -rf *
Selv det å sjekke etter semi-colon var ikke nok, da system() bruker et shell hvor logiske uttrykk evalueres:
telnet://host.example.com&&rm -rf *
Dette gjelder generelt programmer som kjører andre programmer som barneprosesser, f.eks. når man mailer advarsler til brukere:
sprintf (command,"/usr/bin/mail %s",mailaddress);
system(command);
Dersom vi setter mailaddressen til someone;rm -rf * så har vi det samme problemet. (Tenk også på buffer overflow eksemplene i ukeoppgavene.) Den eneste måten å unngå dette problemet på er å begrense privilegiene til en prosess slik at den ikke kan gjøre noe skade. Dette skal vi se på neste uke.

Lignende saker kan brukes for å angripe web-servere hvor tilfeldige server-side kommandoer kan kjøres, eller hvor uforstikig-skrevne CGI programmer kjøres. Vi må aldri stole på input som vi kommer til å gjøre noe viktig med.

Funksjoner med særproblemer

gets()

Funskjonen gets() fra C biblioteket henter en streng fra standard input og legger den i et buffer:
char buffer[1024];

gets(buffer);
Det er ingen grensekontroll i funksjonen. Brukeren kan taste inn så mye som han/hun vil og bufferet vil renne over!

scanf() familien

scanf() funskjonsfamilien (scanf,fscanf and sscanf) er et fantastisk sett med kraftige verktøy for inputbehandling. I stil med andre kraftige verktøy kan også disse føre til kraftige tabber. Syntaksen er slik:
int i;
long L;
char ch;
char buffer[1024];

scanf ("%d %ld %c %s",&i,&L,&ch,buffer);  /* for instance */
Programmerere må huske på at scanf trenger pekere til variabler som argumenter, ikke selve variablene. Ingen kontroll på dette tas av kompilatoren. Skrivefeil kan føre til alvorlige minnekorrupsjon. I et moderne operativsystem med minnebeskytelse vil dette normalt føre til en fatal feil, slik at problemet oppdages. I DOS, MacIntosh osv kan det bli minnekorrupsjon uten varsel!

I BSD 4.3 og andre tidligere systemer var det en del bugs i implementasjonen av scanf som gjorde at funksjonen nødvendigvis måtte betraktes som farlig. Dette gjelder ikke nye implementasjoner, men det er fortsatt mange ting man kan snuble i. Den nye MacOS X er basert på BSD 4.3.

La oss se på noen eksempler. Her er en feil som kompilatoren ikke kan finne. Den kan føre til en stack overflow eller korrupsjon av en annen variabel avhengig av byte-rekkefølgen på maskinen. Den leser åtte bytes inn i en variabel på størrelse en byte.

char ch;

scanf("%ld",&ch);   /* Non detectable error by compiler */
Det fins ingenting man kan gjøre for å finne denslags feil, bortsett fra å være meget forsiktig. Dette er et problem siden mennesker ikke er flinke til å se feil i sitt eget arbeid.

Vi må huske & symbolet:

int i;

scanf("%d",&i);    /* Correct */

scanf("%d",i);     /* Wrong! */
I det siste eksemplet stoler scanf på at i er en peker og den prøver å bruke verdien som variablen innholder som en adresse (peker til) hvor den skal lagre de 4 bytes.

Når vi leser strenger med scanf, trenger vi å begrense hvor mye som skal leses inn i bufferet. Dette er mulig med scanf, men det er ikke lett å få til med C++ strengfunksjonenen. F.eks.


char buffer[1024];

scanf("%1023s",buffer);

For å unngå flere datastrømfeil etter hverandre må vi rydde bort objekter i inputen som ikke passer med programmets ønsker, (se oppgaven fra forrige uke).
int var = stupidvalue;

scanf("%d",&var)

if (var == stupidvalue)
   {
   /* recover */
   }
else
   {
   }

Dersom scanf ikke lykkes i å finne et objekt som matcher det som var ønsket i kontrollstrengen blir ingenting lest fra inputstrømmen. I dette tilfellet passer det ofte å kaste all input fram til neste linje:
int var = stupidvalue;

scanf("%d",&var)

if (var == stupidvalue)
   {
   while (fgetc(stdin) != '\n') // purge
      {
      }
   }
else
   {
   }


Skjult avhengighet

Det fins få programmer idag som ikke stoler på bruk av biblioteker. Stort sett alle Unix varianter avhenger av libc.so og alle Windows programmer trenger sine DLLer. Dersom disse bibliotekene erstattes med en Trojansk hest kan alt gå galt.

Unix ser etter shared libraries i kataloger som står i environmentvariablen LD_LIBRARY_PATH. Anta at en bruker setter denne variablen til å inkludere et directory som er skrivbar for alle

setenv LD_LIBRARY_PATH /tmp:$LD_LIBRARY_PATH
eller brukeren's hjemmekatalog:
setenv  LD_LIBRARY_PATH .:$LD_LIBRARY_PATH
Det er nå trivielt å installere en Trojansk hest som leses i steden for det viktige biblioteket. Dersom det er administratorkontoen som kjører programmet er systemet kompromittert. Windows sikrer ikke DLLer, slik at det er trivielt å erstatte dem med Trojanske hester.
Sikre applikasjoner kan linkes statisk med sikre biblioteker for å unngå å være avhengig av et shared library.


Ukens tanke

Strenghåndtering har ertstattet console input i nesten alle applikasjoner idag, da grafiske brukergrensesnitt ikke støtter konsollbegrepet. Strenghåndtering er derfor en av de viktigste ting en kan lære seg!

Back