Od korisnika se zahtjeva da sastavi tim od 11 igrača. Na ponudi su mu 3 opcije: 1 → dodaj igrača 2 → obrisi igrača 3 → završi (samo ako tim ima 11 igrača)
Odabir opcija se nalazi u beskonačnoj petlji.
Bitne funkcije koje se koriste su printTim, dodajIgraca, urediIgraca i obrisiIgraca.
void dodajIgraca(struct igrac **tim){ unsigned int poz = pozicija(); tim[poz] = malloc(sizeof(struct igrac)); urediIgraca(tim,poz); printf("Igrač dodan na poziciji %d\n",poz+1); }
Funkcija dodajIgraca interno poziva urediIgraca.
void urediIgraca(struct igrac **tim, unsigned int poz){ char a; while((a=getchar())!='\n' && a!=EOF); printf("Dodijeli ime igraču na poziciji %d:\n",poz+1); scanf("%" STR2(NAZIV_L) "s",tim[poz]->naziv); }
Dodaj igrača sadrži buffer overflow ranjivost. Duljina naziva jest NAZIV_L što je direktiva za duljinu 32. Scanf kao argument prima “%32s” što znači da se na offsetu 33 postavlja null bajt.
struct igrac{ char naziv[NAZIV_L]; };
Veličina strukture igrac jest 32. Zbog toga ova ranjivost nije upotrebiva. Kada se zahtjevana veličina za malloc nalazi unutar prve polovice 16 bajt alignmenta (17-24, 33-40 itd…) (izuzetak su veličine [1-8] jer je minimalna veličina dobivenog chunka 16 bajta) koristi se prev_size polje sljedećeg chunka što bi omogućilo izmjenjivanje prev_in_use bita ako je moguć off by one overflow od najvećeg broja u rasponu prve polovice (24, 40…).
Osim overflow-a, dostupan je i memory leak (u smislu zauzimanja memorije) ako se alociraju chunkovi na istoj poziciji.
void obrisiIgraca(struct igrac **tim){ unsigned int poz = pozicija(); memset(tim[poz]->naziv,0,NAZIV_L); free(tim[poz]); printf("Igrač na poziciji %d je obrisan\n",poz+1); }
Unutar obrisiIgraca se nalazi use after free ranjivost jer tim[poz] nije postavljen na NULL. Jer se opcije nalaze u beskonačnoj petlji time je moguć i double-free. Osim toga, pojavlja se dodatna ranjivost unutar printTim funkcije.
void printTim(struct igrac **tim){ printf("\n\nTim:\n"); for (int i = 0;i<11;i++){ printf("\t%d. %s\n",i+1,tim[i]!=NULL?tim[i]->naziv:""); } printf("\n\n"); }
Ako je tim[i] != NULL printa se naziv, a ako nije printa se “”, tj. prazan string. Unutar funkcije obrisiIgraca struktura se memseta na \0 što znači da će na prvom free-u printTim isprintati “” u oba slučaja. Zbog verzije libc-a ovaj leak je donekle zamaskiran jer je tcache tail (s obzirom da je veličina strukture 32+16 → tcache raspon) uvijek NULL jer je zadnji element. Na novijim verzijama bi se zbog safe linkage-a odmah dobio semi heap leak s 1/16 randomiziranosti. Kada se u tcacheu nalazi više chunkova output funkcije više nije isti jer se na next_chunk polju nalazi pointer na sljedeći chunk (osim na zadnjem).
Dakle primitivi su UAF→double free i heap leak (te memory leak).
Double free je ranjivost koja je moguća samo na single linked listama. Ideja je sljedeće:
Tcache bin -> NULL (1. free) Tcache bin -> chunk a (2. free) Tcache bin -> chunk a -> chunk a -> chunk a ... (beskonačno)
Kada chunk pointa na samog sebe dobije se beskonačna petlja. To znači da je moguće alocirati chunk “a” sa sljedećim malloc-om, a da tcache bin i dalje pointa na njega. Zatim, ako je moguće arbitrarno izmijeniti next_chunk pointer dobivenog chunka (što je u zadatku moguće unosom naziva igraća) tcache bin će pokazivati na chunk “a” koji pokazuje na proizvoljnu adresu. To znači da nakon 2 poziva malloca s veličinom chunka “a” dobiti će se chunk na proizvoljnoj adresi. S obzirom da je moguće upisivati arbitrarnu vrijednost u taj chunk, tok programa se može izmijeniti (got, malloc hook itd…).
Ovaj zadatak moguće je riješiti na više načina s obzirom na koji način će se tok programa eksploitati. Datoteka nije PIE, što znači da je adresa got-a dostupna. To bi bio najlakši način jer adresa libc-a nije potrebna. U ovome writeupu riješenje će ići težim putem radi učenja.
S obzirom na verziju libc-a, dostupan je primitiv izmjene flow-a preko malloc hooka. Također je moguće izmijeniti flow prepisivanjem got-a libc-a.
Najprije je potrebno dobiti libc leak. Obično se dobije preko unsorted bina ili neke druge double-linked liste. Kako bi se dobio chunk koji nakon free-a ide u unsorted bin mora se osloboditi chunk koji nije unutar tcache raspona (veličina s headerom > 0x410). U teoriji bi se moglo i da je u rasponu tcache-a, ali da nije u rasponu fastbina, no za to bi bilo potrebno alocirati barem 8 chunkova s tom veličinom te je zbog toga prva opcija lakša.
Kako bi se krivotvorio chunk s veličinom 0x420 potrebno je alocirati 22 chunka, izmijeniti size header prvog chunka i zatim ga osloboditi. Također, da završi u unsorted binu ne smije se konsolidirati s top chunkom zbog čega je potrebno staviti jedan chunk između njih (alociranje 23 chunka umjesto 22).
Dakle, na nekoj poziciji tima (npr. 1.) se alocira chunk, a na drugoj poziciji tima (npr. 2.) se 22 puta ponovno alocira chunk. Zatim se iskoristi double-free ranjivost oslobađanjem chunka na prvoj poziciji 2 puta (potrebno je 3 puta zbog internog brojanja chunkova za tcache binove, tj. ako je broj negativan smatra se da se tcache ne koristi zbog čega se gleda fastbin → free inkrementacija, malloc dekrementacija). Pošto je chunk i dalje unutar tima, na ispisu je dostupan heap leak. Na trećoj poziciji se alocira chunk i za naziv stavi adresa prev_size headera prvog chunka. Ponovno se na trećoj poziciji dva puta alocira chunk kako bi se dobio chunk na adresi prev_size headera prvog chunka. Za naziv se na offsetu 8 doda 0x421 (prev_in_use bit mora biti postavljen na 1 kako bi se izbjegla konsolidacija). Zatim se prvi chunk oslobodi i adresa main arene je na ispisu. Ponovno se iskoristi double free ranjivost (npr. chunk na drugoj poziciji) te alocira chunk s nazivom postavljenim na adresu malloc hooka. Dva puta se alocira chunk te se za naziv stavi adresa one gadgeta. Na sljedećem pozivu malloc-a pozvati će se shell.