Primjer - Zadatak s hacknite platforme Format

Zadatak Format je potrebno riješiti putem format string read/write ranjivosti.

      char buf[100] = {0};
      printf("Primjer 1) 523\n");
      fgets(buf,sizeof(buf),stdin);
      printf(buf,523);
      printf("Primjer 2) \"Volim formatiranje\"\n");
      fgets(buf,sizeof(buf),stdin);
      printf(buf,"Volim formatiranje");
      printf("Primjer 3) \'c\'\n");
      fgets(buf,sizeof(buf),stdin);
      printf(buf,'c');

Nakon upisa unosa u buffer ispisuje se formatirana vrijednost te se postupak ponavlja tri puta. Također, očito je da je ciljana funkcija koju je potrebno pozvati getFlag().

      void getFlag(){
              int fd = open("./flag.txt",0,0);
              char buf[100];
              int r = read(fd,buf,sizeof(buf));
              write(1,buf,r);
      }

Kako bi se riješio zadatak, najprije je potrebno dobiti stack leak na prvoj iteraciji unosa i ispisa. Primjer jednog takvog payloada jest “%25$p”. Ovaj payload navodi printfu da ispiše 25. argument u obliku pointera, umjesto svih koristeći %p%p%p…

Na slici je prikaz stacka prije ispisa printfa. Cilj jest dobiti stack address leak (obojeno u ljubičasto). Prva adresa koja tome odgovara je na offsetu 20 od vrha stacka (tj. rsp-a) prikazano crvenom strelicom. Jedan offset odgovara 8 bajtova pošto je to zadana veličina argumenta %p. Razlog zašto se upisuje 25 umjesto 20 jest zbog linux calling konvencije. Prvih 5 argumenata se uvijek nalaze u registrima. Stoga, tek nakon 5. argumenta se vrijednost krenu uzimati s vrha stoga.

Jednom kada se dobije stack leak, sve što je preostalo jest izmijeniti return adresu na stacku. Kako bi se dobila adresa return adrese na stacku, potrebno je oduzeti offset od leaka. Za 25. argument to je 0x110 (dobiveno pregledom memorije debuggingom, razlika između dobivene adrese i adrese na kojoj je vrijednost return adrese).

      #0x00000000004011f6 - adresa getFlag funkcije

Zadnje sto je potrebno napraviti jest izmijeniti return adresu kako bi se skočilo na adresu getFlag funkcije. Printf nudi funkcionalnost pisanja s pomoću %n placeholdera. Za argument prima adresu na koju piše broj ispisanih karaktera do tog trenutka.

Primjer takvog payloada jest: “%10$n%64x%11$hn%4534x%12$hnaaaaa” + stack_ret_addr+4 + stack_ret_addr+2 + stack_ret_addr.

%n jest upis integer vrijednosti (4 bajta), a %hn shorta (2 bajta). Dijelovi payloada su sljedeći: %10$n, %64x, %11$hn, %4534x, %12$hn, aaaaa i stack_ret adrese.

Cilj jest upisati adresu getFlag funkcije, 0x00000000004011f6. Format adresa je u LSB obliku (manje vrijednosti dolaze prije). Prvo se upisuje vrijednost 0 na gornja 4 bajta s prvim dijelom payloada, %10$n. To tad printf nije imao nikakav ispis zbog čega se upisuje 0. Zatim se uz pomoć %64x ispisuju 64 razmaka. 64 u hex obliku jest 0x40. Ta vrijednost se zatim upisuje s %11$hn (%hn, dakle 2 bajta). Na kraju, ispisuje se dodatnih 4534 razmaka (plus prethodnih 64) kako bi se dobila vrijednost 0x11f6 za sveukupni broj ispisanih znakova. Ta vrijednost se upisuje s %12$hn.

Dakle, redom su upisane vrijednosti 0x00000000, 0x0040 i 0x11f6 na adrese stack_ret_addr+4, stack_ret_addr+2 i stack_ret_addr što odgovara adresi getFlag funkcije u LSB obliku. Kako bi se ispravni argumenti uzimali s vrha potrebno je napraviti ispravan alignment. U ovom primjeru, duljina stringa “%10$n%64x%11$hn%4534x%12$hnaaaaa” jest 32 bajta. Jer %n i %hn uzimaju pointere ako argumente, na samom stacku se nalaze 4 argumenta samo kroz payload. Uzimajući u obzir 5 argumenata danih kroz registre, stack_ret_addr+4 argument se nalaze točno na poziciji 10. argumenta. Zbog toga %10$n uzima 10. argument (isto vrijedi za ostale). To jest funkcija niza “aaaaa” na kraju stringa, služi za poravnavanje. Generalno ga je lakše stavljat na kraj niza kako bi računica za broj ispisanih znakova bila lakša (jer ispis tih znakova dolazi nakon upisivanja kroz %n ili %hn).

Za ručno sastavljanja payloada, najprije se odrede dijelovi adrese i poredaju rastući. Najviših 4 bajta adrese getFlag funkcije su 0, zbog čega taj dio dolazi kao prvi argument. Ovisno o payloadu, poredak se može izmijeniti. Također, dobra je praksa podijeliti vrijednosti na short umjesto int jer ispis razmaka traje kraće. Umjesto ispisivanja 0x004011f6 znakova ispiše se najprije 0x40 i zatim 0x11f6.

Jednom kada se sastavi kostur payloada (poredak argumenata, ispis razmaka s %x i upis vrijednosti kroz %hn ili %n) potrebno je igrati se s poravnanjem. Odredi se pozicija prvog argumenta koji se nalazi iza trenutačne duljine stringa, npr. ako je duljina trenutačnog stringa 34 sljedeći višekratnih broja 8 jest 40 što znači da će prvi argument biti na poziciji 11 itd…

Napomena, placeholder %nx gdje je n neki broj ispisuje hex vrijednost duljine n. Ako je duljina hex vrijednosti manja od n prazna mjesta će se popuniti s razmakom. Ako je veća, (jer je n manji od 8) ispis će biti dulji od n. Generalno će n-ovi će biti brojevi veći od 8 pa se ne treba brinuti o tome, no ako nije to može utjecati na payload.

Više o format string napadima ovdje.

Pwntools

Automatiziranje exploita se može postići uz pomoć pwntoolsa. Objektu FmtStr se proslijedi funkcija za slanje i primanje payloada. Zatim se zabilježi na koju adresu se želi upisati proizvoljna vrijednost i na kraju se funkcija izvrši.

Na primjeru zadatka, slanje zadnjeg posljednjeg payloada bi izgledao na sljedeći način:

      format_string = FmtStr (execute_fmt=send_payload)
      format_string.write(stack_ret_addr,0x00000000004011f6)
      format_string.execute_writes()

Uz automatizirano exploitanje, mogu se kreirati payloadi itd… Više se može pročitati ovdje.