8. Preljev integera
Preljev integera su propusti vrlo visokog stupnja sigurnosti koji se najčešće pronalaze u Open source kodovima. Primjer takvih propusta nađen je u OpenSSH, Snort, Apache, SUN RPC XDR bibliotekama, i nađeni su još u raznim jezgrinim implementacijama operacijskih sustava. Preljev integera je teže za otkriti negoli preljev spremnika, implicitno greške koje se javljaju kod rada s integer varijablama su manje shvaćene od strane programera te se zbog toga lakše dogodi takva greška a da je nitko nije svjestan.
Osim toga, gotovo niti jedan analizator izvornog koda ne pokušava detektirati preljev integera. Većina analizatora izvornog koda imaju implementirane samo osnovne usporedbe za listu LIBC funkcija koje imaju neke sigurnosne probleme u implementaciji. Mada funkcije za alokaciju memorije su jako dobra početna pozicija za traženje preljeva integera, jer takvi propusti nisu vezani uz bilo koju LIBC funkciju.
8.1 Integer wrapping
Integer wrapping se javlja kada se veliki broj uveća do točke gdje se dosegne maksimum i prebaci se na nulu, i ako se dalje uvećava , postaje mali broj. Osim toga, integer wrapping se isto taklo javlja kada se mali broj umanjuje do točke gdje se prebaci i poprimi vrijednost nule, a ako se dalje umanjuje, postaje sve veći broj. Sljedeći primjer Integer wrapping upućuje na malloc(), ali nije problem u LIBC malloc() funkciji za alokaciju memorije. Prije Integer wrappinga dolazi do dostizanja maksimuma veličine cijelog broja i zatim se prebacuju na nulu ili na mali broj. Treba imati na umu da Integer wrapping se isto tako može javiti kada se integer umanjuje tj. oduzima ili dijeli i dosegne vrijednost nula ili dosegne veliku pozitivnu vrijednost. Primjer 8.1 pokazuje Integer wrapping pri zbrajanju:
Primjer 8.1 Integer Wrapping kod zbrajanja
#include <stdio.h> int main(void) // inicijalizacija varijabli buf = (char *)malloc(length1+length2); //linija 13 printf("length1: %x\tlength2: %x\ttotal: %x\tbuf: %s\n", length1, length2,length1+length2, buf); for(i=0; i<length1; i++) buf[i] = 0x41; // linija 20 printf("length1: %x\tlength2: %x\ttotal: %x\tbuf: %s\n", length1, length2,length1+length2, buf); return 0; |
Analiza
U linijama 10 i 11, varijable length se inicijalizirane. U liniji 13, dva cijela broja su zbrojena zajedno kako bi se dobila ukupna duljina spremnika, neposredno prije alokacije memorije za spremnika. length1 varijabla ima vrijednost 0xffffffff, što predstavlja gornjih 32-bit unsigned integer vrijednosti u heksadecimalnom formatu. Kada je "1", pohranjen u length2, i pribrojen length1,veličina spremnika izračunata za poziv malloc() funkcije u liniji 13 postaje nula. To je zato jer 0xffffffff+1 je 0x100000000, što uzrokuje prebacivanje natrag na 0x00000000 (0x0 ili nula), što uzrokuje Integer wrapping. Veličina alocirane memorije za spremnik (buf ) je nula. U liniji 20, for petlja pokušava pisati vrijednost 0x41 ( slovo 'A' u heksadecimalnoj notaciji) povećavajući brojač sve do kraja spremnika ( ne vodi se račun o length2, zato što length2 služi za jedan oktet NULL ). U liniji 23, zadnji oktet spremnika je postavljen na NULL. Ako se ovaj kod direktno prevede, on se srušiti pri pokretanju. Srušit će se zato je spremnik je postavljen na nulu, k tome 4294967295 (0xffffffff u hex) slovo 'A' pokušava biti zapisano u spremnik veličine nula. Varijable length1 i length2 mogu biti promijenjene, stoga length1 je 0xfffffffe i
length2 je 0x2 kako bi postigle isto ponašanje, ili length1 može biti postavljena na 0x5 i length2 na 0x1 za postizanje potpuno istog ponašanja. Primjer 8.1 se čini neprimjenjiv i teško ostvariv za korisničku interakciju i odmah će se program srušiti pri svom pokretanju. Pa ipak, ovaj primjer prikazuje kritične točke Integer wrapping propusta i zrcali stvarne propuste koji se mogu naći u stvarnom svijetu. Na primjer, malloc() poziv u liniji 13 je obično viđen kao buf = (char*)malloc(length1+1). Vrijednost "1" u ovom slučaju je upotrebljena da se zauzme jedan oktet za NULL karakter. Na taj način svi znakovni nizovi su zaključeni s NULL stringom, a to je dobra obrambena navika kod programiranja, koja ako se ignorira, može vrlo vjerojatno voditi do preljeva stoga ili preljeva gomile. Osim toga, length1, u stvarnim aplikacijama, očito neće biti postavljena na vrijednost 0xffffffff, kao u ovom primjeru. Normalno, u sličnoj ranjivoj aplikaciji, length1 će biti postavljen na vrijednost koja je izračunata pri samom unosu. Program će imati ovakvu logičku grešku zato što programer pretpostavlja da će se unijeti "normalna" vrijednost u program za veličinu spremnika, a ne neka pretjerano velika vrijednost kao recimo 4294967295 (u dekatskoj notaciji). Treba imati na umu da korisnički unos može biti bilo što, od environment varijabli, ulaznih argumenata u program, opcija za konfiguraciju, broja paketa poslanih aplikaciji, polja u mrežnim protokolima, ili bilo što drugo. Kako bi se popravila ovakva vrsta problema, duljina se apsolutno mora unosit kao korisnički ulaz, i nakon toga treba vršiti provjeru ulazne vrijednosti da ne bi bila ulazna vrijednost manja ili veća od dozvoljene duljine. Propust Integer wrapping-a izazvan množenjem u primjeru 8.2 je vrlo sličan primjeru problema s Integer wrapping-om izazvanim zbrajanjem.
Primjer 8.2 Integer Wrapping kod množenja
#include <stdio.h> int main(void) length1 = 0x33333333; buf = (char *)malloc((length1*length2)+1); printf("length1: %x\tlength2: %x\ttotal: %x\tbuf: %s\n", length1, length2,(length1*length2)+1, buf); for(i=0; i<(length1*length2); i++) buf[i] = 0x41; // linija 20 buf[i] = 0x0; printf("length1: %x\tlength2: %x\ttotal: %x\tbuf: %s\n", length1, length2,(length1*length2)+1, buf); return 0; |
Analiza
Veličine dva spremnika (length1 i length2) su pomnožene zajedno kako bi se dobila veličina spremnika i uvećana za jedan (to jedno mjesto je za NULL karakter).Gornji dio 32-bita unsigned integer vrijednosti prije wrapping-a nego poprime vrijednost nula je 0xffffffff. U tom slučaju, length2 će biti tvrdo kodirana vrijednost u aplikaciji. Stoga, veličina spremnika će se prebaciti na nulu, length1 mora biti postavljena na najmanje 0x33333333 zato što 0x33333333 pomnožen s 5 je 0xffffffff. Aplikacija zatim pribroji jedan za NULL string i kako je uvećavanje cijelog broja preveliko, on se prebaci na nulu, a kao rezultat, alocirat će se nula okteta za veličinu spremnika. Kasnije, u liniji 20 programa, kada for petlja pokuša pisati u spremnik veličine nula, program će se srušiti. Propust Integer wrapping izazvan množenjem, koji je prikazan u primjeru 8.3 i 8.4, je jako sličan propustu koji je pronađen u vrlo poznatom programu OpenSSH.
8.2 Zaobilaženje provjere veličine
Provjera veličine je više puta upotrebljena u kodu kako bi osigurala pouzdano izvršavanje ili blokirala unos ako je veličina cijelog broja veća ili manja od zadane varijable ili spremnika. Osim toga, programeri katkad koriste provjeru veličine kako bi se zaštitili od Integer wrapping propusta što je opisano u tekstu prije. Provjera veličine se obično javlja kad je varijabla postavljena na maksimalnu vrijednost koju može primiti ili veličinu spremnika, kako bi se osiguralo da korisnik ne bi kojim slučajem prekoračio dozvoljene granice i na taj način onesposobio program. Ova taktika služi kao anti-overflow zaštita. Na žalost za programera koji se brani, kada je i slično, ili veće, znakovni tip broja ima sigurnosnu implementaciju i zahtjeva određeni kod ili provjeru.
U primjeru 8.3, vidi se jednostavan primjer kako provjera veličine može zaštititi program, i mnogo važnije, kako zaobići provjeru veličine koristeći prekrivanje integer-a (engl. integer wrapping)
Primjer 8.3 Zaobilaženje provjere nepredznačnog tipa pomoću Integer Wrapping-a
#include <stdio.h> int main(void) num = 0xffffffff; // linija 6 if(num > 512) // linija 10 return 0; |
Analiza
Za liniju 6, može se misliti da na nju može utjecati korisnik. Linija 10 je tvrdo zakodirana provjera veličine, a predstavlja test ulaznog broja. Linija 10 provjerava dali je zahtijevani broj (uvećan za jedan) veći od 512, u ovom slučaju, broj je u stvari (po liniji 6) 4294967295. Očito, ovaj broj puno veći od 512, ali kad se uveća za jedan, on se vrati na nulu i tako zaobiđe liniju koda za provjeru veličine. Integer wrapping se ne mora nužno javljati kako bi se zaobišla provjera veličine, ni dovoditi u pitanje da je integer unsigned. Više puta, u stvarnom svijetu zaobilaženje provjere veličine upliće signed integer. Primjer 8.4 demonstrira zaobilažene zaštite za predznačni cijeli broj (engl. signed integer).
Primjer 8.4 Zaobilaženje predznačnog tipa bez Integer Wrapping-a
#include <stdio.h> #define BUFSIZE 1024 int main(int argc, char *argv[]) if(argc != 3) return -1; strncpy(inputbuf, argv[2], BUFSIZE-1); printf("num: %x\tinputbuf: %s\n", num, inputbuf); if(num > limit) return 0; |
Analiza
Po standardnim postavkama, svi cijeli brojevi su predznačni, osim ako to nije eksplicitno označeno. Pa ipak, treba biti svjestan da se može javiti tiha promjena tipa. Kako bi zaobišli provjeru veličine u označenoj liniji u gornjem primjeru, sve što se treba učiniti je da se ubaci negativni broj kao prvi argument komandne linije Unix programa. Na primjer, pokušajmo pokrenuti:
$ gcc -o example example.c
|
U ovom slučaju, punjenje znakom 'A' neće doseći kraj spremnika zato što negativni 200 će zaobići provjeru veličine označenoj liniji primjera 8.4, i preljev gomile će se javiti kada memcpy() pokuša pisati preko granica spremnika.
8.3 Ostali Intger propusti
Preljev integera je isto tako moguć u slučaju kada uspoređujemo 16-bitni cijeli broj s 32- bitnim cijelim brojem, bilo svjesno ili nesvjesno. Ovakva vrsta greške, između ostalog, je puno manje pronalažena u softveru zato što je veća vjerojatnost da će biti pronađen od strane sigurnosnih pretraživača ili krajnjeg korisnika. Kada se radi s UNICODE znakovima ili se implementiraju win32 funkcije za rad s širim skupom znakova, veličine spremnika i cjelobrojne veličine moraju biti pravilno izračunate. Propusti prekrivanja integera ( engl. Integer wrapping ) predstavljaju problem koji se temelji na unsigned 32-bit integeru, problem dinamičkog prikrivanja integera se može primijeniti na signed integers, short integers, 64-bit integers, i druge brojčane tipove. Tipično, za cjelobrojne propuste da vode do scenarija za iskorištavanje, što obično završi kao preljev gomile ili stoga, zlonamjerni krajnji korisnik mora imati direktnu ili indirektnu kontrolu nad specifikatorom duljine, kako bi mogao ubaciti neočekivani cijeli broj kao argument komandne linije, što je naravno moguće. Vrlo vjerojatno, program će pročitati cijeli broj od korisnika kako bi izračunao duljinu podatka unesenog ili poslanog od strane korisnika, ili broj puta koliko je poslan, suprotno od aplikacije jednostavno se mijenja broj od strane korisnika.