==== Fantasy football ====
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.