De Java security architectuur op embedded systemen Chris Vanden Berghe 3 mei 2001
Dankwoord Allereerst bedank ik mijn promotor, Prof. Dr. ir. Frank Piessens, voor zijn bereidheid om deze thesis te begeleiden. Ik apprecieer zowel de steun, als de kritische bedenkingen die hij mij bezorgde. Mijn begeleider bij Acunia, ir. Vincent Nollet, wil ik in de eerste plaats bedanken voor het boeiende onderwerp waarmee hij mij in contact heeft gebracht. Tevens ben ik hem dankbaar voor de talrijke interessante gesprekken, en zijn feedback op de code. Gregor wens ik te bedanken voor de vele uren die hij gespendeerd heeft aan het nalezen van deze tekst. Ik kan me geen betere vriend voorstellen. Tenslotte zijn er nog mijn ouders, die ik dankbaar ben omdat ze me steunen door dik en dun, en omdat ze me de kans hebben gegeven om te studeren.
1
Inhoudsopgave 1 Inleiding 1.1 Situering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Overzicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Voertuigentelematica 2.1 Wat is voertuigentelematica?[15] . . . . . . 2.2 Evolutie van de voertuigentelematica[2] . . 2.3 Acunia Open Telematica Platform[2][14][15] 2.4 Bedenkingen omtrent beveiliging[7][8][12] .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
6 6 7 9 9 10 11 13
3 De Java Virtuele Machine 16 3.1 Java architectuur[11] . . . . . . . . . . . . . . . . . . . . . . . 16 3.2 De logische structuur van de JVM[4] . . . . . . . . . . . . . . 18 3.2.1 De methodenruimte . . . . . . . . . . . . . . . . . . . 18 3.2.2 De objectenruimte . . . . . . . . . . . . . . . . . . . . 19 3.2.3 De Java virtuele machine stapel . . . . . . . . . . . . . 19 3.2.4 De native method stack . . . . . . . . . . . . . . . . . 20 3.3 De instructieset[4][11] . . . . . . . . . . . . . . . . . . . . . . 21 3.3.1 Argumenten en operanden . . . . . . . . . . . . . . . . 21 3.3.2 Data types . . . . . . . . . . . . . . . . . . . . . . . . 21 3.3.3 Instructie categorie¨en . . . . . . . . . . . . . . . . . . 22 3.3.4 Methode aanroepen . . . . . . . . . . . . . . . . . . . 23 3.3.5 Behandeling van uitzonderingen en finally constructie 24 3.3.6 Creatie en manipulatie van objecten . . . . . . . . . . 26 3.4 Java en embedded systemen[3][12][19] . . . . . . . . . . . . . 26 3.4.1 Voordelen van Java . . . . . . . . . . . . . . . . . . . . 26 3.4.2 Nadelen van Java . . . . . . . . . . . . . . . . . . . . . 28 4 Java beveiliging voor mobiele code 4.1 Evolutie van mobiele code[12] . . . . . 4.2 Gevaren van mobiele code[7][8][12][13] 4.2.1 Schending van integriteit . . . 4.2.2 Schending van de privacy . . .
2
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
31 31 32 33 33
4.3
4.4 4.5
4.2.3 Beschikbaarheids aanvallen . . . . . . . . . . . . . . . 34 Het oorspronkelijke Java beveiligingsmodel: de zandbak[8][12] 34 4.3.1 De verifier . . . . . . . . . . . . . . . . . . . . . . . . . 36 4.3.2 De class loader . . . . . . . . . . . . . . . . . . . . . . 37 4.3.3 De security manager . . . . . . . . . . . . . . . . . . . 39 Een eerste stap naar een minder restrictief beveligingsmodel: JDK 1.1[8][12] . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 Een volledig configureerbaar beveiligingsmodel: het Java 2 Platform[8][12] . . . . . . . . . . . . . . . . . . . . . . . . . . 44 4.5.1 Waarom dit nieuwe beveiligingsmodel? . . . . . . . . . 44 4.5.2 De nieuwe en aangepaste onderdelen . . . . . . . . . . 45 4.5.3 Samengevat . . . . . . . . . . . . . . . . . . . . . . . . 50
5 De verifier 5.1 Het laadproces[11] . . . . . . . . . . . . . . . . . 5.1.1 Laden . . . . . . . . . . . . . . . . . . . . 5.1.2 Linken . . . . . . . . . . . . . . . . . . . . 5.1.3 Initialisatie . . . . . . . . . . . . . . . . . 5.2 De eerste verificatie stap[4][6][11][23] . . . . . . . 5.3 De tweede verificatie stap[4][6][11][23] . . . . . . 5.4 De derde verificatie stap[4][11][23] . . . . . . . . . 5.4.1 Control-flow analyse . . . . . . . . . . . . 5.4.2 Data-flow analyse . . . . . . . . . . . . . . 5.4.3 Uitbreidingen op het data-flow algoritme 5.5 De vierde verificatie stap[4][11][23] . . . . . . . . 5.6 Invloed op de beveiliging[4][12] . . . . . . . . . . 5.7 Faalt enkel gevaarlijke code voor de verifier?[4] . 6 Implementatie 6.1 Inleiding . . . . . . . . . . . . 6.2 Overzicht van het programma 6.3 De eerste verificatie stap . . . 6.4 De tweede verificatie stap . . 6.5 Control-flow analyse[1] . . . . 6.5.1 Control-flow graphs . 6.5.2 Algoritme . . . . . . . 6.5.3 Voorbeeld . . . . . . . 6.6 Data-flow analyse . . . . . . . 6.6.1 Aangepast algoritme . 6.6.2 Voordelen . . . . . . . 6.6.3 Implementatie . . . . 6.7 Acunia . . . . . . . . . . . . . 6.8 Pre-verificatie[18] . . . . . . . 6.9 Testen[18] . . . . . . . . . . . 3
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . .
52 52 52 53 54 54 59 60 61 63 70 72 72 74
. . . . . . . . . . . . . . .
75 75 77 80 81 81 82 82 85 85 85 87 88 90 91 92
7 Besluit
95
4
Lijst van figuren 2.1
Acunia’s open telematica concept . . . . . . . . . . . . . . . .
12
4.1 4.2 4.3 4.4 4.5 4.6
Het beveiligingsmodel in JDK 1.0 Het beveiligingsmodel in JDK 1.1 Policy . . . . . . . . . . . . . . . Protection Domains . . . . . . . Laden van klassen via delegatie . Het beveiligingsmodel in JDK 1.2
. . . . . .
36 42 46 47 48 50
5.1 5.2
De initi¨ele operanden stapel en lokale variabelen. . . . . . . . De operanden stapel en lokale variabelen na uitvoering van de ifne instructie. . . . . . . . . . . . . . . . . . . . . . . . . De operanden stapel en lokale variabelen aan het einde van de data-flow analyse. . . . . . . . . . . . . . . . . . . . . . . . De initi¨ele operanden stapel en lokale variabelen. . . . . . . . De operanden stapel en lokale variabelen na uitvoering van de goto instructie. . . . . . . . . . . . . . . . . . . . . . . . . Klasse hi¨erarchie . . . . . . . . . . . . . . . . . . . . . . . . . De operanden stapel en lokale variabelen aan het einde van de data-flow analyse. . . . . . . . . . . . . . . . . . . . . . . .
66
Control-flow graph . . . . . . . . . . . . . . . . . . . . . . . . Opeenvolgende stappen bij het aanmaken van de control-flow graph . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vergelijking van het aantal bytes en basic blocks voor ca. 3000 methodes . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
82
5.3 5.4 5.5 5.6 5.7 6.1 6.2 6.3
5
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
66 67 68 69 69 70
86 88
Hoofdstuk 1
Inleiding 1.1
Situering
In onze huidige maatschappij, die volledig draait rond kennis en informatie, zijn netwerken niet meer weg te denken. Informatie dient overal en altijd beschikbaar te zijn. Momenteel is de computer daarbij nog ons belangrijkste hulpmiddel, maar dit belooft snel te veranderen ten voordele van allerlei consumentenelektronica. Verwacht wordt dat er zich over enkele jaren meer mensen met een GSM of zakcomputer op het Internet zullen begeven, dan met een gewone computer. Gelijktijdig met de nood aan informatie, neemt ook de nood aan beveiliging van informatie toe. Java werpt zich op als een platform dat informatie overal en altijd op een veilige manier beschikbaar maakt. Het is voornamelijk dit veiligheidsapect dat we in deze thesis zullen belichten. Daartoe bekijken we eerst de Java beveiligingsarchitectuur in zijn geheel. Deze architectuur is opgebouwd uit een aantal onderdelen die gezamenlijk voor de veiligheid instaan. Vervolgens gaan we een van deze onderdelen, met name de verifier, grondiger bestuderen. Wat de taak van de verifier is, bespreken we verder. De studie van de verifier zal gebeuren uit het oogpunt van embedded systemen. Het begrip ‘embedded systeem’ is heel ruim, en de grens tussen wat we wel of niet tot deze categorie rekenen zeer vaag. Een eigenschap die veel embedded systemen gemeenschappelijk hebben, is de beperkte beschikbaarheid van systeembronnen. Het zal dan ook in dit kader zijn dat we de verifier bekijken. Om deze studie een concrete toepassing te geven, hebben we ons toegespitst op een bestaand systeem, met name het Acunia Open Telematica Platform.
6
Telematica is het gebruik van informatica voor gegevensverwerking en besturing, waarbij de fysische lokatie van de gegevens en het te bestuderen systeem niet van belang zijn. Acunia richt zich op telematica toepassingen voor voertuigen. Op deze manier brengen zij het ‘overal en altijd beschikbaar zijn’ van informatie een stap dichterbij. Het is een zeer interessant platform voor deze studie, omdat het volledig op Java gebaseerd is en de beveiligingsvereisten, onder andere door de integratie met vitale delen van de wagen, bijzonder hoog liggen. Als praktisch resultaat van deze thesis stellen we de implementatie van een geheugenvriendelijke verifier voorop. Het primaire doelplatform van deze verifier is het Acunia platform, maar we wensen ook de mogelijkheid om deze als een alleenstaande applicatie te gebruiken. In hoofdstuk 6 gaan we uitgebreid in op deze implementatie.
1.2
Overzicht
We zullen nu kort de volgende hoofdstukken uit deze thesistekst toelichten. Hoofdstuk 2: Voertuigentelematica De bedoeling van dit hoofdstuk is om de lezer van enige achtergrond te voorzien over voertuigentelematica in het algemeen en het Acunia platform in het bijzonder. We gaan ook in op de veiligheidsaspecten gerelateerd aan deze technologie. Hoofdstuk 3: De Java Virtuele Machine We leggen hier kort enkele begrippen van de Java virtuele machine uit. We benadrukken hierbij de aspecten die relevant zijn voor de verifier. We gaan ook na in hoeverre Java geschikt is als platform voor embedded systemen. Hoofdstuk 4: Java beveiliging voor mobiele code We bespreken hier het begrip mobiele code, en de gevaren die eraan verbonden zijn. Java biedt enkele technieken die het mogelijk maken om mobiele code op een veilige manier te gebruiken. We bespreken deze technieken, gezamenlijk de Java beveiligingsarchitectuur genaamd, en hun evolutie sinds het ontstaan van Java. Hoofdstuk 5: De verifier In dit hoofdstuk belichten we de verifier als een van de onderdelen van de Java beveiligingsarchitectuur, meer gedetailleerd. We overlopen de taak van
7
de verifier doorheen het laden van een klasse, de verschillende stappen van het verificatie algoritme en hun relevantie voor de geboden beveiliging. Hoofdstuk 6: Implementatie Als praktisch resultaat van deze thesis is de implementatie van een geheugenvriendelijke verifier vooropgesteld. In hoofdstuk 6 gaan we de kenmerken van deze verifier na, en lichten we het aangepaste algoritme toe. Hoofdstuk 7: Besluit Tenslotte is er nog het algemene besluit, waar we het afgeleverde resultaat kritisch bekijken en mogelijke verbeteringen of uitbreidingen suggereren.
8
Hoofdstuk 2
Voertuigentelematica 2.1
Wat is voertuigentelematica?[15]
Voertuigentelematica1 is de technologie die informatica-toepassingen in voertuigen introduceert en via diverse telecommunicatietoepassingen een schier onbeperkt aantal diensten ontsluit. Het vormt als het ware een brug tussen de service provider en de gebruiker van de diensten. Mogelijke toepassingen van deze informatie- en assistentiediensten zijn: Lokalisatie van voertuigen: maakt het mogelijk om gestolen voertuigen terug te vinden, of een wagenpark effici¨ent te beheren. Navigatiehulp: gebruikmakend van recente elektronische wegenkaarten, kan men navigatiehulp bekomen die weers- en verkeersinformatie incorporeert. Ook toeristische informatie kan op deze wijze opgevraagd worden. Mobiele communicatie: diensten als e-mail, internet toegang, instant messaging, . . . kunnen aan de inzittenden aangeboden worden. Verkeers management: door real-time informatie te verzamelen van een groot aantal voertuigen, kunnen verkeersproblemen nauwkeurig gedetecteerd, geanalyseerd en eventueel zelfs voorspeld worden. Veiligheid: automatische snelheidsaanpassing afhankelijk van de verkeerssituatie en weersomstandigheden. Identificatie: voertuigen kunnen op een eenvoudige ´en correcte wijze ge¨ıdentificeerd worden. 1
Engelse benaming: vehicle telematics (Europa), intelligent transportation systems (Verenigde Staten)
9
Interne diagnostiek: maakt detectie van technische problemen vanop afstand mogelijk. Elektronische betalingen: automatische betaling van verkeersbelasting, tol en parking. Flexibele verzekeringen: verzekeringspremies kunnen periodiek aangepast worden aan het profiel van de bestuurder. Dit profiel kan factoren omvatten zoals het rijgedrag, en de omvang en aard van de gemaakte verplaatsingen. Dit moet uiteraard met expliciete instemming van de bestuurder in kwestie gebeuren, en met in acht name van zijn privacy.
2.2
Evolutie van de voertuigentelematica[2]
In de evolutie van de voertuigentelematica worden drie generaties onderscheiden: Eerste generatie Deze systemen zijn rond 1995 ge¨ıntroduceerd. Elke telematicatoepassing wordt gerealiseerd door een autonoom systeem. Er is nog geen sprake van onderling compatibele hardware, waardoor de hardware componenten voor elke dienst afzonderlijk voorzien moeten worden. Dit maakt een eerste generatie systeem bijzonder duur. In het voorbeeld van een after-theft systeem betekent dit bijvoorbeeld dat het communicatiemiddel (bv. een GSM2 ) en het lokalisatiemiddel (GPS3 ) onder ideale omstandigheden nooit gebruikt zullen worden. Tweede generatie Systemen van de tweede generatie lossen dit probleem op door meerdere diensten te integreren in ´e´en toestel. De beschikbare hardware wordt op deze manier beter benut, zodat de kosten gedrukt worden. Deze toestellen bieden onder andere toeristische- en verkeersinformatie, navigatie hulp en after-theft systemen aan. Het toevoegen van nieuwe diensten is echter moeilijk, omdat de bestaande tweede generatie systemen gepatendeerde producten zijn. Derde generatie Systemen van de derde generatie gaan nog een stap verder. Zij bieden een platform, met een breed spectrum aan telecommunicatie middelen voor 2 3
Global System for Mobile communications Global Positioning System
10
zowel bidirectionele (bv. GPRS4 , UMTS5 , Bluetooth6 ) als unidirectionele communicatie (bv. DAB7 ), waarop allerlei diensten kunnen aangeboden worden. Wil de bestuurder van een nieuwe dienst gebruik maken, dan kan hij die automatisch laten installeren. Er wordt ook gestreefd naar compatibiliteit met bestaande hardware standaarden. Dit maakt het toevoegen van extra hardware goedkoop, eenvoudig en gestandaardiseerd. Het Acunia Open Telematica Platform is een voorbeeld van een derde generatie voertuigentelematica systeem.
2.3
Acunia Open Telematica Platform[2][14][15]
Acunia is in 1996 onder de naam SmartMove opgericht als een spin-off van de onderzoeksinstelling IMEC8 . De oorspronkelijke doelstelling was het ontwikkelen van een after-theft systeem dat, gebruik makend van pagingtechnologie, gestolen wagens kan opsporen en vanop afstand stilleggen. Uit de ervaring die men hierbij opdeed, en het besef dat voertuigentelematica heel wat meer mogelijkheden biedt, onstond het idee om een open standaard voor voertuigentelematica te cre¨eren. Voor de aanbieder van deze diensten is de vrije beschikbaarheid van de standaard een belangrijk gegeven, omdat dit de mogelijkheid biedt om op uniforme wijze diensten aan te bieden aan alle gebruikers. Acunia streeft naar volledige compatibiliteit met de standaard gedefinieerd door het Open Services Gateway Initiative. OSGi is een industri¨ele groepering die zich de definitie en voorspraak van een open standaard voor interconnectie van de komende generatie consumptie-elektronica tot doel stelt. De centrale OSGi component is de services gateway, een embedded server die de aanbieders van diensten via een Wide Area Network verbindt met een lokaal netwerk van gebruikers. Om deze flexibiliteit te kunnen bieden is een modulaire aanpak noodzakelijk. Het Acunia Open Telematica Platform bestaat uit volgende drie lagen: 4
General Packet Radio System Universal Mobile Telephone Service 6 Standaard voor draadloze netwerktechnologie. Genoemd naar de Deense koning Harald Bl˚ atand (908-987 AD), die Denemarken en Noorwegen verenigde, omdat deze standaard als doel had de telecom- en computerindustrie te verenigen. 7 Digital Audio Broadcast 8 Interuniversity Micro-Electronics Center 5
11
service provider
service provider
service provider
communicatie - en controle centrum publiek
netwerk
voertuig Figuur 2.1: Acunia’s open telematica concept
Een gestandaardiseerde omgeving voor het aanbieden van diensten: Acunia biedt een toolkit of API9 aan die het mogelijk maakt voor service providers om zelf diensten te ontwikkelen of pasklare diensten aan te kopen van zogenaamde application providers. Deze diensten kunnen dan probleemloos in het systeem ge¨ıntegreerd worden. Een communicatie- en controlecentrum: Het communicatie- en controlecentrum, ook wel CCC genoemd, is het centrale aanspreekpunt van het platform. Alle communicatie tussen service providers en gebruikers loopt via het CCC. Het biedt uiteenlopende administratieve diensten aan zoals logging, facturatie, beveiliging, toegangscontrole, databank management, . . . Een gestandaardiseerde on-board module: De on-board module maakt het platform compleet. Acunia ontwikkelt een hardware module, gebaseerd op de StrongARM processor, die compatibel is met verschillende standaarden voor de fysieke aansluiting van randapparatuur, waaronder USB10 en CAN11 . Door deze standaarden te ondersteunen wordt het ontwikkelen van extra randapparatuur veel goedkoper. Het 9
Application Programming Interface Universal Serial Bus 11 Controller Area Network 10
12
ontwerp van deze hardware module zal gebruikt worden als referentie, ook apparatuur van andere fabrikanten kan als on-board module fungeren. Gelijktijdig wordt een gestandaardiseerd software platform, gebaseerd op het Java 2 Platform, ontwikkeld. Ook hier is het mogelijk om voor een Java omgeving afkomstig van een andere fabrikant te kiezen. Het software platform ondersteunt diverse protocollen voor logische toegang tot randapparatuur waaronder UPnP12 en Jini. Deze protocollen zorgen voor het probleemloos toevoegen, beheren en verwijderen van hardware. De door Acunia ontwikkelde Java omgeving werd Wonka gedoopt, dit is het onderdeel waarmee ik het meest in aanraking gekomen ben.
2.4
Bedenkingen omtrent beveiliging[7][8][12]
Derde generatie telematica systemen bieden uitermate interessante perspectieven, waarvan we de re¨ele impact slechts kunnen vermoeden. Het is in dit verband aangewezen om ook eens stil te staan bij de risico’s die deze technologie met zich meebrengt, met name op het gebied van veiligheid. Beveiliging gaat over het voorkomen van ongewenste zaken. Dit is een zeer eenvoudige definitie, maar de implicaties zijn minder eenvoudig en erg afhankelijk van de situatie. Enkele vuistregels zijn echter altijd toepasbaar: Beveiliging en de menselijke factor Bij de ontwikkeling van een beveiligingsarchitectuur mag de gebruiker nooit uit het oog verloren worden. Het ideale beveiligingsconcept is volledig transparant voor de doorsnee gebruiker. Ontwikkelt men daarentegen een systeem dat te omslachtig of te complex is, dan hebben mensen de neiging om de veiligheidsmaatregelen naast zich neer te leggen. Gebuikers verplichten om regelmatig hun wachtwoord te veranderen is vanuit beveiligingsoogpunt een goed idee. Wanneer dit echter tot gevolg heeft dat de gebruiker zijn wachtwoord niet kan onthouden, en het bijvoorbeeld op een briefje aan zijn scherm hangt, mist deze maatregel duidelijk zijn doel. Voertuigentelematica richt zich tot een ruime gebruikersgroep. Deze vuistregel is hier dus zeker van toepassing, en maakt de ontwikkeling van een afdoende beveiliging niet bepaald eenvoudiger. In dit verband kan het zinnig zijn om een gedragsstudie bij zijn doelpubliek te organiseren, zodat men gevaarlijke situaties zoals in bovenstaand voorbeeld tijdig ontdekt. 12
Universal Plug and Play
13
Beveiliging is relatief tot de bedreiging Vermits volledige veiligheid meestal onmogelijk is, zal een veiligheidsarchitect voor iedere situatie moeten bepalen wat een afdoende beveiliging is. De vereisten voor de beveiliging van staatsgeheimen zijn duidelijk verschillend van die voor de gegevens van de doorsnee gebruiker. De te nemen maatregelen worden dan ook best op deze vereisten afgestemd. De veiligheidsvereisten voor een voeruigentelematica systeem kunnen moeilijk overschat worden, vermits zo’n systeem met vitale functies van de wagen ge¨ıntegreerd wordt. Als de veiligheid van het systeem gecompromitteerd wordt, kunnen talloze doemscenario’s realiteit worden. Enkele voorbeelden: het ongewenst stilleggen van wagens, omzeilen van anti - en after-theft systemen, beschadiging van wagens door installatie van foutieve firmware, . . . Ook het mobile agent concept toegepast in het telematica systeem, roept vele vragen op. Hierbij wordt code, al dan niet uit vertrouwde bron, over een publiek netwerk gedownload om lokaal op het telematica platform uitgevoerd te worden. Hoe kunnen we een misdragen van deze code voorkomen? Deze vraag trachten we te beantwoorden in hoofdstuk 4. Beveiliging mag functionaliteit niet in de weg staan Een goede beveiligingsarchitectuur heeft een slechts zeer beperkte invloed op de functionaliteit. Zo is de eenvoudigste oplossing om een auto-ongeval te vermijden, de auto altijd in de garage te laten staan. Het is duidelijk dat deze oplossing niet aanvaardbaar is, daar deze de functionaliteit van de auto te fel beperkt. We zullen nog op deze vuistregel terugkomen wanneer we het hebben over de beveiligingsarchitectuur van Java. Beveiliging is maar zo sterk als de zwakste schakel Tenslotte moet veiligheid altijd uit het oogpunt van het volledige systeem beschouwd worden. In het geval van de voertuigentelematica moeten de gebruiker, de voertuigenmodule, het communicatie netwerk, het communicatie- en controlecentrum ´en de service providers bij de ontwikkeling van de beveiligingsarchitectuur betrokken worden. Besluit Deze vuistregels maken duidelijk dat het ontwikkelen van een goede beveiligingsarchitectuur complexer is dan de hoger gegeven definitie doet vermoeden, en een grondige kennis van het systeem als geheel vereist. In het geval van de voertuigentelematica maken de zware consequenties van een falende beveiliging de situatie er niet eenvoudiger op. 14
Het beveiligingsmodel waarop Acunia steunt is grotendeels gebaseerd op de beveiligingskenmerken van het Java 2 Platform. Deze thesis heeft als doelstelling om een deelaspect van dit beveiligingsmodel uit te werken.
15
Hoofdstuk 3
De Java Virtuele Machine De betrachting van dit hoofdstuk is de lezer van enige achtergrond te voorzien omtrent de interne werking van een Java virtuele machine (JVM). We streven hier geenszins naar een volledige beschrijving1 , maar wensen toch voldoende achtergrond te verschaffen om het in hoofdstuk 5 beschreven verificatie algoritme in de juiste context te kunnen plaatsen.
3.1
Java architectuur[11]
De Java architectuur bestaat uit drie grote onderdelen: 1. de Java programmeertaal, 2. de Java Application Programming Interface, en 3. de Java Virtuele Machine. De twee laatste delen vormen samen het ‘Java platform’; vanaf versie 1.2 spreken we over het Java 2 Platform. Het Java platform biedt een uitvoeringsomgeving die onafhankelijk is van de onderliggende architectuur. Het grootste deel van de Java programma’s beschikbaar voor het Java platform zijn geschreven in de Java programmeertaal, maar dit is geen vereiste. Het Java platform kent immers slechts ´e´en formaat, dit formaat noemen we het class file formaat. Compilers die zulke class files genereren zijn beschikbaar voor diverse programmeertalen, waaronder (uiteraard) Java, maar ook Scheme, Eiffel, Python, Tcl, Ada, en anderen. Het is zelfs mogelijk om rechtstreeks, of met behulp van een assembler taal, programma’s te schrijven die door het Java platform uitgevoerd kunnen worden. 1
Daarvoor kan de ge¨ınteresseerde lezer terecht bij de offici¨ele JVM specificatie opgesteld door Sun: The Java Virtual Machine Specification, Second Edition - Tim Lindholm & Frank Yellin
16
Java is in 1995 ge¨ıntroduceerd, en kende zijn doorbraak onder de vorm van applets. De Java virtuele machine is gebaseerd op een in 1992 ontwikkeld uitvoeringsplatform voor de programmeertaal Oak, dat de wereld van embedded systemen als focus had. Java is desondanks populair geworden op de PC, maar kent nu tegelijk een expansie naar zowel kleinere als grotere systemen. Zo richt de Java 2 Micro Edition zich op allerlei consumentenelektronica, van netwerk computers tot mobiele telefoons. De Java 2 Enterprise Edition daarentegen, spitst zich toe op grotere systemen, zoals webservers. Tenslotte is er nog de Java Card Edition, bedoeld als platform voor smart cards. Ons interesseert voornamelijk de Java 2 Micro Edition, vanwege de focus op embedded systemen zoals het Acunia platform. De Java 2 Micro Edition wordt op basis van de beschikbare systeembronnen nogmaals opgedeeld in een Connected Device Configuration (CDC) en een Connected, Limited Device Configuration (CLDC). Sommige systemen, waaronder het Acunia platform, ondersteunen een volledig Java platform, waarvan de minimale vereisten in de CDC gespecifieerd zijn. Voor andere systemen, met stringentere beperkingen op het gebied van beschikbare bronnen, is de CLDC specificatie opgesteld. De minimale vereisten hiervan zijn ook haalbaar voor systemen met slechts enkele honderden kilobytes geheugen beschikbaar. CLDC systemen zijn gebaseerd op een aangepaste virtuele machine en een erg beperkte API. De offici¨ele specificatie van de Java virtuele machine wordt beschreven in ‘The Java Virtual Machine Specification’ door Tim Lindholm en Frank Yellin. De gegeven specificatie beschrijft volgende aspecten: • de logische structuur van de JVM, • de instructieset, • het class file formaat, • de verifier, en • het laadproces van klassen. De eerste twee onderdelen worden in dit hoofdstuk besproken, de andere delen komen verder in de tekst aan bod. Zo bespreken we het class file formaat en de verifier wanneer we het verificatie algoritme toelichten. Het laadproces wordt gedeeltelijk beschreven in het hoofdstuk over Java beveiliging, en ook weer gedeeltelijk wanneer we dieper ingaan op het verificatie algoritme.
17
3.2
De logische structuur van de JVM[4]
De JVM specificatie beschrijft de virtuele machine op conceptuele wijze, en laat zo een bijzonder grote implementatievrijheid. Zo is het mogelijk om een virtuele machine te maken die beter is dan de referentie implementatie van Sun; zelfs een volledig in hardware ge¨ımplementeerde JVM is mogelijk. In de eerste plaats bespreken we de werking van de virtuele machine aan de hand van de conceptuele geheugenopmaak. We onderscheiden daarbij de volgende delen: • de methodenruimte, • de objectenruimte, • de Java virtuele machine stapel, en • de native method stack .
3.2.1
De methodenruimte
De methodenruimte, of ‘method area’, is de geheugenruimte waar klassen (en interfaces) geladen door de virtuele machine in bewaard worden. Deze geheugenruimte wordt aangemaakt bij het opstarten van de virtuele machine, en is gemeenschappelijk aan alle threads. De klassen die hier worden geladen dienen als een template voor het construeren van objecten. Per klasse worden er structuren opgebouwd met een runtime constant pool, gegevens over velden en methodes, en de eigenlijke code van methodes en constructoren. Deze eigenschappen van klassen zijn statisch; ze kunnen niet meer gewijzigd worden eenmaal de klasse geladen is. Er bestaan twee soorten velden: statische en niet-statische. Voor statische velden is er slechts ´e´en exemplaar per klasse, voor niet-statische velden daarentegen is er ´e´en exemplaar per instantie van deze klassen. Iedere klasse, met java.lang.Object als uitzondering, heeft een superklasse, en geen of meerdere interfaces. Wanneer een klasse geladen wordt, worden tegelijk ook haar superklasse en al haar interfaces geladen. Om een methode aan te roepen, of om naar een variabele te verwijzen, worden symbolische namen gebruikt. Bij uitvoering moeten deze symbolische namen eerst naar concrete referenties worden omgezet. Deze omzetting gebeurt typisch bij de eerste verwijzing naar de symbolische naam. Wanneer deze concrete referentie bepaald is, dan wordt die in de runtime constant pool bewaard. Op deze manier moet een symbolische naam maar ´e´en keer omgezet worden. 18
3.2.2
De objectenruimte
In de objectenruimte, of ‘heap’ worden alle objecten bewaard. Al deze objecten zijn instanties van klassen, gedefinieerd in de methodenruimte. In alle objecten is plaats voorzien voor ieder niet-statisch veld in de bijbehorende klasse en alle superklassen. Indien een klasse en haar superklasse een veld met dezelfde naam bevatten, dan zal dit veld dus tweemaal aanwezig zijn in het object. Deze velden kunnen toch onderscheiden worden, omdat steeds de volledige naam, van de vorm ‘pakket/klasse naam/veld naam’, gebruikt wordt. Ieder object neemt een zekere hoeveelheid geheugen in, waarvan er een eindige hoeveelheid beschikbaar is. Daarom wordt het geheugen ingenomen door objecten teruggevorderd wanneer de objecten niet meer in gebruik zijn. De Java specificatie legt op dat dit automatisch moet gebeuren door een garbage collector, maar laat de keuze van het algoritme vrij. Een object wordt als ‘in gebruik’ beschouwd, als ernaar gerefereerd wordt vanuit: • de stack of een lokale variabele, • een statisch veld, • een niet statisch veld van een object dat zelf nog in gebruik is, of • de JVM zelf. In alle andere gevallen wordt het object als ongebruikt beschouwd. Speciale voorzieningen moeten getroffen worden indien een object een finalize() method heeft. In dat geval kan het ongebruikte object zichzelf immers terug ‘tot leven wekken’.
3.2.3
De Java virtuele machine stapel
Bij iedere methode aanroep wordt een nieuwe stack frame aangemaakt. Al deze stack frames, behorende tot een bepaalde thread, vormen samen een Java virtuele machine stapel, of ‘Java virtual machine stack ’. De Java virtuele machine stapel wordt aangemaakt bij creatie van een nieuwe thread. Deze geheugenruimte is enkel een logische stapel, in het fysische geheugen is er meestal geen continue allocatie. Een stack frame bevat volgende onderdelen: • de operanden stapel, • de lokale variabelen lijst, • een verwijzing naar de runtime constant pool, en 19
• de programma teller. Een stack frame wordt gebruikt om gegevens en tussenresultaten te bewaren, alsook om resultaatwaarden van methodes door te geven, uitzonderingen af te handelen en dynamisch linken mogelijk te maken. Het bovenste frame wordt het actieve frame genoemd, en bevat een momentopname van de methode in uitvoering. Wanneer deze methode een andere methode aanroept, zal een nieuw frame aangemaakt worden dat bovenaan de stapel komt. Bij het be¨eindigen van de methode wordt het actieve frame van de stapel gehaald, en wordt de oproepende methode opnieuw het actieve frame. Ieder frame bevat een lijst van variabelen die we de lokale variabelen noemen. Deze worden gebruikt om tussenresultaten te bewaren, maar ook om parameters door te geven bij een methode aanroep. De lokale variabelen van een nieuw frame bevatten de parameters en, als het geen statische methode is, een referentie naar het object waarop de methode inwerkt. De grootte van de lokale variabelen lijst wordt tijdens het compileren bepaald, en verandert niet tijdens de uitvoering. Een frame bevat ook een last-in-first-out, of LIFO, operanden stapel. Bij de aanmaak van een nieuw stack frame zal deze operanden stapel leeg zijn. De JVM voorziet instructies om gegevens tussen de lokale variabelen lijst en de operanden stapel uit te wisselen. Andere instructies werken rechtstreeks met de operanden op de stapel. De operanden stapel wordt ook gebruikt om parameters klaar te zetten voor een methode aanroep. Deze parameters worden dan in de lokale variabelen lijst van het nieuw gecre¨eerde stack frame geplaatst. De maximale grootte van de operanden stapel wordt ook weer tijdens het compileren bepaald. De programma teller is een verwijzing naar de eigenlijke code van een methode in de method stack. Normaal wordt de programmateller verhoogd met de lengte van de instructie. Control flow instructies en uitzonderingen kunnen het normale verloop van de programma teller echter wijzigen.
3.2.4
De native method stack
Sommige dingen, zoals bijvoorbeeld het aanroepen van platform specifieke hardware, kunnen niet in JVM instructies beschreven worden. Daarom biedt Java de mogelijkheid om methodes in platform specifieke code te implementeren. Deze native methods, zoals ze genoemd worden, gebruiken ook een stapel om hun status te bewaren. De JVM voorziet daarvoor de native method stack, ook wel de C stapel genoemd.
20
3.3
De instructieset[4][11]
De JVM is een virtuele computer met een eigen instructieset. We gaan hier in op het onderscheid tussen argumenten en operanden, de verschillende datatypes en instructie categorie¨en. Elke categorie in detail bespreken zou ons te ver leiden. We beperken ons daarom tot een bondige uitleg over methode aanroepen, behandeling van uitzonderingen, en object manipulatie.
3.3.1
Argumenten en operanden
Instructies bestaan uit twee delen: een uit ´e´en byte bestaande opcode en een aantal argumenten. Afhankelijk van de instructie, zijn er geen, ´e´en of meerdere, of een variabel aantal argumenten. Vermits deze argumenten in de bytecode verweven zitten, zijn ze onveranderlijk. Dikwijls verwijzen deze argumenten naar een bepaalde gegevenslocatie, zoals een plaats in de constant pool of een lokale variabele. Indien een argument uit meer dan ´e´en byte bestaat, dan worden de opeenvolgende bytes in big-endian volgorde geplaatst. Zo wordt de tekenloze 16-bit index naar een lokale variabele voorgesteld door twee bytes, byte1 en byte2; de index wordt dan berekend uit (byte1 ≪ 8) | byte2. Operanden zijn daarentegen gegevens die zich op de operanden stapel bevinden. Zo haalt de iadd instructie de twee bovenste elementen van de operanden stapel, en plaatst vervolgens de som bovenaan de stapel.
3.3.2
Data types
De JVM ondersteunt volgende datatypes: • int (32-bit integer), • long (64-bit integer), • float (32-bit drijvende komma getal), • double (64-bit drijvende komma getal), • reference, en • returnAddress. De eerste vier types komen overeen met de gelijknamige types uit de Java programmeertaal. Voor de ontbrekende numerieke types, met name boolean, byte, short en char, is er slechts een beperkte JVM ondersteuning. In de meeste gevallen worden die als int behandeld.
21
Een reference is een verwijzing naar een object in de objectenruimte. Er zijn drie types te onderscheiden: class reference, array reference, en interface reference. De null reference is een speciale reference zonder type. References in de JVM zijn gelijkaardig aan C pointers, maar er zijn toch enkele vermeldenswaardige verschillen. Zo is bijvoorbeeld rekenen met references niet mogelijk. Ook verwijzen references enkel naar objecten, en niet naar lokale variabelen, stapel elementen, . . . Het laatste type, het returnAddress, wordt enkel gebruikt door de jsr, ret en jsr w instructies. Deze instructies zijn bedoeld om de ‘finally’ constructie te implementeren. Dit type heeft geen tegenhanger in de Java programmeertaal. Int, float, reference, en returnAddress operanden worden ‘types van categorie 1’ genoemd, en nemen juist ´e´en plaats in op de operanden stapel of in de lokale variabelen. Operanden van het long of double type vormen de ‘types van categorie 2’ en nemen twee plaatsen in.
3.3.3
Instructie categorie¨ en
JVM instructies kunnen in volgende categorie¨en onderverdeeld worden: • aritmetische instructies, • laad- en bewaarinstructies, • stapel manipulaties, • methode aanroepen, • behandeling van uitzonderingen, • type conversies, • creatie en manipulatie van objecten, • control-flow instructies, • synchronisatie, en • instructies ter implementatie van de finally constructie. De mnemotechnische benaming van instructies heeft als conventie dat de eerste letter het type aangeeft waarop de instructie van toepassing is. Zo staat i voor een operatie op een int, l voor long, b voor byte, s voor short, c voor char, f voor float, d voor double en a voor reference. Een voorbeeld is de add instructie, die bestaat als iadd, ladd, fadd en dadd. Sommige operaties zijn niet typespecifiek, en hebben dan ook geen typevermelding in hun naam. 22
3.3.4
Methode aanroepen
Een methode kan met 4 verschillende instructies worden aangeroepen, afhankelijk van de aard van de methode en de klasse waartoe ze behoort. Zo onderscheiden we invokevirtual, invokeinterface, invokespecial, en invokestatic. De eerste stap van een methode aanroep, is het klaarzetten van de parameters. Zo zal voor een niet-statische methode eerst een referentie naar het object waarop de methode inwerkt op de stapel geplaatst worden. Vervolgens worden de nodige parameters op de stapel geplaatst, in de volgorde zoals gespecifieerd in de methode beschrijving. Deze methode beschrijving kan in de constant pool gevonden worden. Ter illustratie, de standaard methode beschrijving van de ‘main’ methode is ‘([Ljava/lang/String;)V’. Het deel tussen de haakjes beschrijft de parameters; hier wordt een referentie naar een rij van String objecten verwacht. Het deel na de haakjes specifieert het return type, in dit geval is dat void. Bij de volgende stap wordt een nieuwe stack frame gecre¨eerd. De lokale variabelen lijst wordt gedeeltelijke ge¨ınitialiseerd met de methode parameters die van de operanden stapel gehaald worden. De nieuwe operanden stapel is initieel leeg. Wanneer er geen uitzondering is opgetreden, zal de methode eindigen met een return instructie. Deze instructie vereist dat zich op de operanden stapel een operand van het return type van de methode bevindt. Het stack frame wordt vervolgens verwijderd, en de return waarde wordt dan op de operanden stapel van de aanroepende methode geplaatst. Het stack frame van deze methode wordt opnieuw actief, en de uitvoering hervat. De ‘normale’ manier om een methode aan te roepen is de invokevirtual instructie. De naam is afgeleid van virtual dispatch, het mechanisme gebruikt om de uit te voeren methode te selecteren. Eerst zal gekeken worden of er in het object, waarop de methode inwerkt, een methode bestaat met exact dezelfde naam en beschrijving. Indien dit het geval is, zal deze methode worden uitgevoerd. In het andere geval past men hetzelfde mechanisme toe op de superklasse. De invokespecial instructie wordt daarentegen enkel gebruikt voor het aanroepen van constructoren en methodes van de klasse zelf of haar superklasse. Hier wordt geen virtual dispatch mechanisme toegepast om de methode te selecteren, maar het is exact de gespecifieerde methode die uitgevoerd wordt. Constructoren krijgen in de JVM een speciale benaming:
voor object initialisatie en voor klasse initialisatie. 23
Tenslotte zijn er nog de invokestatic en invokeinterface instructies. Deze instructies worden respectievelijk gebruikt voor het aanroepen van statische methodes en methodes gespecifieerd in interfaces.
3.3.5
Behandeling van uitzonderingen en finally constructie
Uitzonderingen, of exceptions, worden gebruikt om een abnormale situatie te signaleren. Uitzonderingen hebben drie verschillende oorzaken: 1. De JVM merkt een synchrone abnormale uitvoeringssituatie op. Zulke situatie treedt niet op een willekeurig moment op, maar als gevolg van de uitvoering van een instructie. Voorbeelden zijn: • optreden van een fout tijdens het laad- of linkproces • te veel geheugen gealloceerd • deling door nul • ... 2. Een uitzondering werd expliciet geworpen. Dit gebeurt via de athrow instructie. 3. Er doet zich een asynchrone abnormale uitvoeringssituatie voor. Bijvoorbeeld: • Het aanroepen van de ‘stop’ methode van een bepaalde Thread of ThreadGroup. • Interne JVM error. Een uitzondering is een instantie van een subklasse van java.lang.Throwable. We zullen nu aan de hand van een klein voorbeeldje zien hoe uitzonderingen kunnen opgevangen worden in de JVM. Onderstaande methode wordt aan de rechterkant in bytecode vertaald. void tweeHandlers(){ try{ eenMethode(); } catch (EenUitzondering e) { handler(); } catch (Exception e) { handler(); } }
0: 1: 4: 5: 6: 7: 10: 11: 12: 13: 16:
24
ALOAD_0 INVOKEVIRTUAL RETURN ASTORE_1 ALOAD_0 INVOKEVIRTUAL RETURN ASTORE_1 ALOAD_0 INVOKEVIRTUAL RETURN
Bij de bytecode hoort nog een exception table: from 0 0
to 4 4
handler 5 11
exception EenUitzondering Exception
Indien ‘eenMethode()’ geen uitzondering gooit, dan zullen enkel de instructies van byte 0 tot 4 uitgevoerd worden. Indien wel een uitzondering gegooid wordt, dan zal de JVM in de exception table van voor naar achter zoeken naar een handler voor dat bepaalde stuk bytecode. De uitvoering gaat dan verder bij de eerste handler die de juiste uitzondering behandelt. Hier zal dus ‘EenUitzondering’ door instructies van byte 5 tot 10 worden behandeld; ‘Exceptions’ daarentegen hebben de instructies van byte 11 tot 16 als handler. Indien het uitzonderingsobject een instantie is van een subklasse van java.lang.Throwable, maar geen subklasse van java.lang.Exception, dan zal er geen handler gevonden worden. De JVM zal de methode dan be¨eindigen, en de uitzondering verder naar de oproepende methode gooien. Indien de oproepende methodes ook geen handler voor deze uitzondering hebben, zal de ’uncaughtException’ methode van de ThreadGroup aangeroepen worden, waarna de Thread be¨eindigd wordt. Voor de try-finally constructie gebruikt men de jsr (Jump to subroutine) en ret (return from subroutine) instructies. Een voorbeeldje maakt de constructie duidelijk. 0: 1: 4: 7: 8: 9: 12: 13: 14: 15: 16: 19:
void finallyVoorbeeld(){ try{ eenMethode(); } finally{ handler(); } }
De bijhorende exception table is als volgt: from 0
to 4
handler 8
exception any
25
ALOAD_0 INVOKEVIRTUAL JSR 14 RETURN ASTORE_1 JSR 14 ALOAD_1 ATHROW ASTORE_2 ALOAD_0 INVOKEVIRTUAL RET 2
In de eerste situatie, waar geen uitzondering gegooid wordt door de opgeroepen methode, wordt v´o´ or de return een jsr instructie ingevoegd. Deze zorgt ervoor dat de finally handler als een subroutine wordt aangeroepen. Wanneer die klaar is, zorgt de ret instructie dat de normale uitvoering vervolgd wordt. Voor situaties waar er wel een uitzondering gegooid wordt door de opgeroepen methode, wordt een extra exception handler voorzien die alle mogelijke uitzonderingen behandelt. Deze handler heeft als enige taak een jsr te maken naar de finally handler en bij terugkeer de oorspronkelijke uitzondering opnieuw te gooien. Om te kunnen bepalen waarheen moet teruggekeerd worden na de subroutine, plaats de jsr instructie een returnAddress op de operanden stapel. Dit returnAddress wordt door de subroutine in een lokale variabele bewaard; de ret instructie krijgt als argument de index mee waar het returnAddress te vinden is. In het geval van een try-catch-finally constructie wordt de situatie iets ingewikkelder, maar de toegepaste techniek blijft dezelfde.
3.3.6
Creatie en manipulatie van objecten
Objecten worden aangemaakt door de new instructie. Deze instructie zorgt ervoor dat er geheugen in de objectenruimte gealloceerd wordt, en er een referentie naar het nieuwe object op de operanden stapel geplaatst wordt. Na het uitvoeren van de new instructie is het object nog niet bruikbaar; eerst moet nog een constructor worden aangeroepen. Nadien is het object ge¨ınitialiseerd en volledig bruikbaar. Arrays worden ook als objecten beschouwd, maar de bijbehorende klassen worden door de JVM zelf gegenereerd. De instructies om een array aan te maken zijn verschillend van die van een normaal object: newarray, anewarray, en multianewarray. Tenslotte kunnen velden gemanipuleerd worden met volgende instructies: getfield, putfield, getstatic, en putstatic. De eerste twee instructies worden op niet-statische velden gebruikt, de laatste twee op statische velden.
3.4
Java en embedded systemen[3][12][19]
We gaan hier even in op de voor- en nadelen van Java, voornamelijk in het licht van embedded systemen.
3.4.1
Voordelen van Java
Java heeft verschillende interessante eigenschappen, zoals platform onafhankelijkheid, mogelijkheid tot veilige uitvoering van mobiele code, dynamisch
26
laden van klassen, ingebouwde ondersteuning voor threads en synchronisatie, . . . Hier willen we het voornamelijk hebben over die kenmerken die verband houden met de correcte werking van systemen. Correctheid en beveiliging zijn nauw met elkaar verbonden. Denken we in dit opzicht maar aan buffer overflows, die verantwoordelijk zijn voor een zeer groot percentage van de beveiligingsgaten in diverse systemen. Om een veilig systeem te ontwerpen, moeten we dus eerst een correcte werking garanderen. In de wereld van de embedded systemen is correctheid zo mogelijk nog belangrijker. Veel van de toepassingsgebieden van embedded systemen laten immers geen ruimte voor fouten, denken we hier maar aan de sturing van chemische processen of productielijnen. We overlopen nu even de kenmerken van Java die de correcte werking van programma’s bevorderen. We noemen ze de ‘beveiligingsgerelateerde kenmerken’, omdat ze een onrechtstreekse invloed hebben op de beveiliging van een systeem. Eenvoud Een van de doelstellingen van Java is ontwikkeling en onderhoud van code eenvoudiger maken. Daartoe heeft men enkele van de mogelijkheden van C en C++, die vaak tot fouten leiden en code moeilijker te begrijpen maken, uit Java geweerd. Zo zijn templates, unions, structures, en operator overloading niet in Java terug te vinden. Pointers zijn dan weer vervangen door references. Deze maatregelen leiden samen tot eenvoudigere code, en bijgevolg tot minder programmeerfouten. Type safe Een taal is type-safe indien men op gegevens enkel die operaties kan toepassen, die voor dat type van gegevens bedoeld zijn. Het is eenvoudig in te zien dat dit de correcte werking van programma’s bevordert. E´en manier om dit mogelijk te maken, is alle gegevens voorzien van een etiket met het datatype. Dit mechanisme wordt ‘dynamische type controle’ genoemd, en heeft als nadeel dat het weinig effici¨ent is. Java biedt type-safety via een ander mechanisme, namelijk ‘statische type controle’. Hierbij tracht men nog v´o´or uitvoering te bewijzen dat het programma enkel operaties zal uitvoeren op gegevens van het juiste type. Ineffici¨ente controles tijdens de uitvoering worden zo overbodig. Deze statische controle wordt door de verifier uitgevoerd als onderdeel van het laadproces van een klasse.
27
Objectgericht Java is intrinsiek een objectgerichte taal, waardoor het mogelijk is om gegevens enkel toegankelijk te maken via een goed gedefinieerde, publieke interface. Interne gegevens kunnen op deze manier privaat gehouden worden. Dit principe wordt data encapsulatie genoemd, en is een van de hoekstenen van de verder besproken Java beveiligingsarchitectuur. Data encapsulatie steunt op type-safety, vermits dit ervoor zorgt dat willekeurige toegang tot het geheugen onmogelijk is. Robuust Robuustheid heeft te maken met hoe goed een systeem reageert op fouten. Controleren of een rij niet buiten zijn grenzen wordt benaderd, is een voorbeeld van zo’n maatregel die zorgt voor een robuuster systeem. Ook de aanwezigheid van een garbage collector draagt hiertoe bij, vermits het problemen gerelateerd met dangling pointers aanpakt. Merk op dat zowel de aangehaalde controle op rijen, als de garbage collector een performantiekost hebben. Bij het afwegen van robuustheid en performantie, heeft men dus resoluut voor het eerste gekozen. Ge¨ıntegreerde behandeling van uitzonderingen Uitzonderingen zorgen voor een elegante behandeling van fouten. Een programma krijgt de mogelijkheid om de situatie te corrigeren. Indien dit niet gebeurt, wordt de thread afgesloten en krijgt de gebruiker melding van de opgetreden fout. Strikt gedefinieerd In programmeertalen zoals C, zijn datatypes en bit-operaties, zoals verschuivingen, platform afhankelijk, waardoor werking en resultaten van een programma afhankelijk zijn van het onderliggende systeem. Java voorkomt deze onverwachte situaties door een strikte definitie van de datatypes en operaties op te leggen; de resultaten van een programma zullen dus onafhankelijk zijn van het systeem waarop gewerkt wordt.
3.4.2
Nadelen van Java
Embedded systemen hebben vaak heel andere beperkingen en vereisten dan de normale PC omgeving waarop Java populair geworden is. Zo hebben embedded systemen in vele gevallen belangrijke restricties op het gebied van geheugen en beschikbare rekencapaciteit, en is real-time gegevensverwerking vaak een vereiste. We gaan hier na of Java aan deze beperkingen en vereisten voldoet.
28
Geheugenvereisten Om de geheugenvereisten van het Java platform te kennen, moeten we drie aspecten bekijken: de JVM, de API’s en de applicaties. De vereisten van de JVM zijn afhankelijk van de instructieset van de onderliggende architectuur en het al dan niet aanwezig zijn van thread support in het bestuuringssysteem. Voor systemen uit de eerder besproken CLDC categorie zijn virtuele machines ontwikkeld met minimale geheugenvereisten. Dit gaat echter wel ten koste van de performantie, zo zullen deze virtuele machines meestal niet over een Just-in-time compiler beschikken. De vereiste statische geheugencapaciteit voor de standaard API is in de grootteorde van 10 megabyte. De meeste embedded systemen voldoen niet aan deze eis, vandaar de ontwikkeling van alternatieve API’s, die enkel de strikt noodzakelijke functionaliteit bevatten. Tenslotte zijn er de geheugenvereisten voor applicaties. Hier maakt Java een goede beurt, Java programma’s zijn namelijk vaak kleiner dan hun C en C++ tegenhangers. Een eerste reden kan gevonden worden in het compacte bytecode formaat, dat vaak kleinere programma’s oplevert dan overeenkomstige machinecode. Ten tweede wordt dit ook bewerkstelligd door het dynamisch linken. Ter illustratie, een ‘HelloWorld’ programma in Java neemt minder dan 1 kilobyte in, de C++ versie meer dan 100 kilobyte. Het gebruik van deze aangepaste JVM en API maken de geheugenvereisten voor embedded systemen heel wat realistischer. Reeds met een minimum van 128 kilobyte dynamisch geheugen beschikbaar, is het mogelijk om een beperkt Java platform te gebruiken. Performantie Buiten stringente geheugenbeperkingen, heeft een embedded systeem meestal ook geen overvloed aan rekencapaciteit. Enkele typische kenmerken van Java hebben daarenboven consequenties op de performantie. Zo is er bijvoorbeeld de platform onafhankelijkheid van Java. Vermits bytecode niet rechtstreeks kan uitgevoerd worden, moet die ofwel ge¨ınterpreteerd ofwel v´o´ or uitvoering naar machinecode omgezet worden. Terwijl de eerste Java versies nog bijna 40 maal trager waren dan overeenkomstige C++ programma’s, heeft men ondertussen deze achterstand grotendeels weggewerkt. Technieken zoals Just-in-time compilers (JIT) zijn hier niet vreemd aan. Vanwege de grote geheugenvereisten die een JIT stelt, zijn deze niet of nauwelijks beschikbaar voor embedded systemen. Deze systemen kunnen dus niet profiteren van de grote performantiewinst die hiermee bereikt wordt. Ook andere kenmerken van Java hebben een negatieve invloed op de performantie. Denken we maar aan de overhead gegenereerd door de automatische garbage collector en bound checks.
29
Determinisme Determinisme houdt verband met de mogelijkheid om een taak binnen een vooraf bepaalde tijdsspanne af te werken. Een taak in real-time uitvoeren, vereist het voldoen aan vooropgestelde deadlines. Indien het onderliggende systeem niet deterministisch is, is het onmogelijk om de haalbaarheid van een deadline te garanderen. Vermits een belangrijk toepassingsgebied van embedded systemen het real-time sturen van processen is, zal deze vereiste vaak doorslaggevend zijn. Java is standaard niet deterministisch omwille van zijn garbage collector. Deze kan op ieder moment het systeem voor onbepaalde tijd onbeschikbaar maken, zodat geen maximale tijdsspanne voor een bepaalde taak kan gegarandeerd worden. Er bestaan wel alternatieve garbage collection algoritmes die hieraan trachten tegemoet te komen. Vaak wordt ook gesuggereerd om een extra, optionele instructie free toe te voegen aan de huidige Java instructies, die het gebruik van een garbage collector overbodig zou maken.
30
Hoofdstuk 4
Java beveiliging voor mobiele code Mobiele code[8] is geen fundamenteel nieuw concept – ruim gedefinieerd omvat dit alle gegevens die een systeem vanop afstand be¨ınvloeden. Dit kan bijvoorbeeld Domein Naam Server (DNS) informatie, een Remote Procedure Call of uitvoerbare code zijn. Hier gaan we het hebben over de laatste categorie, een programma dat over een netwerk wordt getransfereerd en vervolgens lokaal wordt uitgevoerd. Voorbeelden uit deze categorie zijn Postscript bestanden, JavaScript, ActiveX, Word macros en Java applets. In dit hoofdstuk gaan we dieper in op de veiligheidsaspecten van mobiele code; en hoe Java zijn claim van secure language 1 tracht waar te maken.
4.1
Evolutie van mobiele code[12]
Reeds bij het onstaan van het Internet was men het erover eens dat het downloaden en uitvoeren van willekeurige programma’s geen goed idee was. Dat hier risco’s mee verbonden zijn, is vrij duidelijk. Het programma kan een virus, een trojan horse 2 , of zelfs gewone programmeerfouten bevatten, die heel wat schade kunnen aanrichten. Archie was oorspronkelijk h´et hulpmiddel om freeware en shareware op het Internet te vinden. Uit de zoekresultaten selecteerde men de gewenste bestanden, die men vervolgens downloadde, installeerde en tenslotte uitvoerde. Het gebruik van mobiele code was dus een expliciet proces dat verschillende handelingen vereiste. 1 Een van de eerste publieke documenten over Java, gepubliceerd door Sun Microsystems, was een whitepaper die de fundamentele eigenschappen van Java in ´e´en zin trachtte samen te vatten: A simple, object-oriented, distributed, interpreted, robust, secure, architecture neutral, portable high-performance, multi-threaded, and dynamic language. 2 Een trojan horse is een programma, meestal verborgen in een ander programma, dat tracht controle over het systeem van het slachtoffer te verwerven.
31
Het onstaan van het World Wide Web in 1992 heeft de ontwikkeling van het Internet in een stroomversnelling gebracht. De grafische gebruikersomgeving maakt het ook voor minder computervaardige gebruikers mogelijk om zich op het Internet te begeven. Tegelijk werd het downloaden en uitvoeren van mobiele code een stuk eenvoudiger, ´ e´ en welgemikte muisklik en een programma wordt gedownload en uitgevoerd. Al snel groeide het besef dat er ook nadelen waren aan het oorsponkelijke concept van HTML-gebaseerde webpagina’s. De inhoud was statisch en bood weinig flexibiliteit. De oplossingen ontwikkeld om het web interactiever te maken, kunnen ruwweg in twee categorie¨en ingedeeld worden. Ten eerste hebben we de server side scripts. Dit zijn in essentie programma’s die op de webserver draaien, en dynamisch HTML pagina’s genereren, aangepast aan de specifieke invoer van een gebruiker. Voorbeelden zijn CGI3 , PHP4 en Java servlets. Ten tweede zijn er de client side scripts. Dit zijn –zoals de naam het zegt– programma’s die lokaal bij de gebruiker worden uitgevoerd. Er bestaan verscheidene alternatieven, waaronder JavaScript, ActiveX en Java applets. Client side scripts maken van iedere web gebruiker (die niet bewust gekozen heeft om de ondersteuning hiervoor uit te schakelen) een gebruiker van mobiele code, en dit zonder expliciete bevestiging. Momenteel wint push technologie duidelijk terrein. Na een eenmalige subscriptie voor een bepaalde dienst, gebeurt alles volstrekt automatisch. De meeste toepassingen van push bestaan uit automatische installatie van programma aanpassingen. Anti-virus programma’s –die zeer up-to-date moeten zijn– gebruiken deze technologie om nieuwe versies van hun virusdefinities te installeren. Uit de beschreven evolutie kan men concluderen dat mobiele code meer en meer een evidentie wordt voor de doorsnee Internet gebruiker. In vele gevallen is deze gebruiker zich zelfs niet bewust dat hij mobiele code gebruikt, en nog minder van de risico’s die hij hiermee neemt. Nochtans zijn deze gevaren re¨eel, en loont het de moeite om hier eens even bij stil te staan.
4.2
Gevaren van mobiele code[7][8][12][13]
Java is een volwaardige programmeertaal die gebruikers de mogelijkheid biedt om bestanden te lezen, wijzigen of verwijderen, connecties op te bouwen en informatie door te sturen. Een applet ontworpen om deze mogelijkheden te misbruiken kan heel wat schade aanbrengen, zoals bijvoorbeeld het wissen van bestanden, het doorsturen van confidenti¨ele informatie, . . . 3 4
Common Gateway Interface Personal Home Page Tools
32
De gevaren geassocieerd met mobiele code worden in de lectuur over computerbeveiliging onderverdeeld in een drietal (niet-disjuncte) categorie¨en: schending van integriteit, schending van privacy en beschikbaarheidsaanvallen. Bij beschermingsmaatregelen wordt er vaak een onderscheid gemaakt tussen preventie, detectie en herstelmaatregelen. We bespreken kort de verschillende categorie¨en van aanvallen, de impact op de gebruiker, en de bescherming die Java tegen elk type aanval biedt.
4.2.1
Schending van integriteit
Aanvallen uit deze categorie worden beschouwd als de ernstigste en kunnen belangrijke consequenties hebben. Denken we maar aan gewijzigde medische dossiers, examenscores of financi¨ele gegevens. Java biedt een goede preventieve beveiliging tegen dit soort aanvallen. Herinner u de algemene vuistregel: beveiliging mag functionaliteit niet in de weg staan. Terwijl in het oorspronkelijke beveiligingsmodel functionaliteit nog moest wijken voor veiligheid, is dit met recentere versies volledig rechtgezet. Voor detectie en herstel moet men naar externe hulpmiddelen grijpen. Digitale handtekeningen kunnen bijvoorbeeld gebruikt worden om te controleren of gegevens intact zijn. Hier verschuift men het probleem van detectie naar sleutelbeheer. Herstel van de gegevens kan gebeuren door het terugplaatsen van een reserve kopie of het achterwaarts uitvoeren van een transactie log. Nieuwe versies van de Java virtuele machine zullen deze functionaliteit wel ondersteunen.
4.2.2
Schending van de privacy
Alle aanvallen die de vertrouwelijkheid van gegevens schenden, horen tot deze categorie. De aard van confidenti¨ele gegevens is zeer divers. Het compromitteren van een geheime cryptografische sleutel kan –in het licht van e-commerce en internet bankieren– uiterst zwaarwichtige gevolgen hebben. Indien een bestand dat de wachtwoorden van gebruikers bevat bemachtigd wordt, kan dit zelfs leiden tot een aanval van de eerste categorie. Een aanval op de privacy bestaat uit twee delen. Ten eerste moet men zich toegang verschaffen tot de confidenti¨ele gegevens, ten tweede moet men de buitgemaakte gegevens ook nog kunnen transfereren. Java biedt een goede preventie tegen het eerste deel van deze aanval, maar bescherming tegen het tweede deel van de aanval is nagenoeg onbestaande. Java applets kunnen namelijk altijd terug de originele host contacteren, en zo de buitgemaakte gegevens doorsturen. Typerend voor dit soort aanvallen is dat ze zeer moeilijk te detecteren zijn, en herstel is vaak onmogelijk. Preventie is hier dus het belangrijkste hulpmiddel, en men kan overwegen om meerdere technieken, waaronder
33
encryptie, simultaan toe te passen.
4.2.3
Beschikbaarheids aanvallen
Deze categorie van aanvallen zijn beter bekend onder hun Engelse benaming Denial of Service attacks, en dekt een zeer ruime lading. Voorbeelden zijn: • grote hoeveelheden geheugen alloceren • de processor monopoliseren door veel threads aan te maken • duizenden venster aanmaken, die ervoor zorgen dat alle schermuitvoer verstoord wordt • vullen van de harde schijf met willekeurige gegevens • een webserver bedelven onder een groot aantal netwerk pakketjes • ... Alle aangehaalde voorbeelden hebben gemeen dat ze de normale werking van het systeem beletten. De ernst van deze categorie aanvallen is zeer afhankelijk van de situatie. Voor de doorsnee Internet gebruiker is dit meestal slechts een vervelende ervaring, en kan de situatie hersteld worden door bijvoorbeeld het systeem te herstarten. Is het besmette systeem echter een e-commerce webserver met Java servlet ondersteuning, dan zal de impact heel wat groter zijn. Herstarten van de virtuele machine kan een verlies aan inkomsten tot gevolg hebben. Java biedt tegen deze categorie aanvallen maar een zeer beperkte preventieve beveiliging; wel zijn er plannen om dit in latere versies van het Java platform te voorzien. Voor detectie moet weer naar externe hulmiddelen gegrepen worden. Momenteel is er heel wat onderzoek naar firewalls met een adaptief beleid. Deze firewall zal afwijkend gedrag opmerken en indien nodig de toegangsregels autonoom aanpassen.
4.3
Het oorspronkelijke Java beveiligingsmodel: de zandbak[8][12]
Met het oorspronkelijke Java beveiligingsmodel bedoelen we de beveiliginsmaatregelen voorzien in de eerste versie van de Java Development Kit (JDK 1.0). Dit model heeft sinds zijn introductie in JDK 1.0 een opmerkelijke evolutie doorgemaakt. De nieuwe beveiligarchitectuur was dan ook de meest in het oog springende wijziging tussen JDK 1.1 en het Java 2 Platform. We bespreken hoe Java met dit oorspronkelijk beveiligingsmodel de claim van een veilig platform voor mobiele code trachtte waar te maken, 34
en welke technieken en compromissen daarbij noodzakelijk waren. Later in dit hoofdstuk, wanneer de evolutie naar de Java 2 beveiligingsarchitectuur besproken wordt, zullen we zien hoe de principes toegepast in dit model verfijnd en uitgebreid werden met als doel aan de nadelen, verbonden met de oorspronkelijke compromissen, tegemoet te komen. Oorspronkelijk werd een belangrijk onderscheid gemaakt tussen applets en applicaties. Een ruime definitie5 van applets omvat alle Java code die zich niet op het lokale systeem bevindt, maar over een netwerk moet getransfereerd worden voor ze kan uitgevoerd worden. Programma’s die zich wel op het lokale systeem bevinden worden volgens deze definitie applicaties genoemd. Applets worden normalerwijze gedownload tijdens het laden van een webpagina, en vervolgens uitgevoerd zonder dat de gebruiker daar expliciet toestemming voor moet geven. Daar de herkomst van een applet dikwijls een raadsel is en de auteur dus niet blindelings kan vertrouwd worden, is er een mechanisme bedacht om deze onvertrouwde code toch op een veilige manier uit te voeren. Dit mechanisme bestaat erin om applets uit te voeren in een erg beperkte omgeving, de zandbak genoemd. De grenzen van deze zandbak zijn zorgvuldig gekozen zodat alle potentieel gevaarlijke mogelijkheden die Java biedt ontoegankelijk zijn voor onvertrouwde code. Het is applets bijvoorbeeld niet toegestaan om lokale bestanden te manipuleren of verbindingen met willekeurige computers op te zetten. Een volledige beschrijving van deze standaard zandbak volgt later wanneer we over de security manager spreken. Om dit zandbak beveiligingsmodel af te dwingen wordt gesteund op de beveiligingsgerelateerde karakteristieken van Java, zoals besproken in hoofdstuk 3, en op verschillende complementaire en onafscheidelijke delen die samen de veiligheidsarchitectuur vormen. We onderscheiden hier de verifier, class loader en de security manager, elk met hun specifieke taak, om ervoor te zorgen dat onvertrouwde code binnen de grenzen van de zandbak blijft. We bespreken nu uitgebreid de taak en toegepaste technieken van elk van deze delen. 5 Deze definitie is afkomstig uit Inside Java 2 Platform Security van Li Gong, en is een vereenvoudiging van de werkelijkheid in die zin dat applets zich ook op het lokale systeem kunnen bevinden, net zoals applicaties over een netwerk kunnen uitgevoerd worden. Ter beschrijving van het beveiligingsmodel is deze definitie echter zeer bruikbaar.
35
applets
applicaties
verifier
primordial class loader
class loader
security manager
niet-kritische operaties
kritische operaties
Figuur 4.1: Het beveiligingsmodel in JDK 1.0
4.3.1
De verifier
Zoals reeds vermeld, staat of valt het hele Java beveiligingsmodel met de beveiligingsgerelateerde karakteristieken van Java, zoals daar zijn type-safety en data encapsulatie. De overige delen van de veiligheidsarchitectuur, met name de class loader en de security manager, zijn namelijk zelf grotendeels in Java ge¨ımplementeerd en steunen aldus op deze karakteristieken. Het is de taak van de verifier om te controleren of onvertrouwde code deze regels volgt. Normaal gezien wordt bytecode door een compiler gegenereerd. Deze zorgt ervoor dat alle afgeleverde bytecode aan deze regels voldoet. Waarom is zulke controle dan noodzakelijk? Een eerste reden is dat we nooit zeker kunnen zijn of code die we downloaden al dan niet door een betrouwbare compiler aangemaakt is. Er bestaan handige hulpmiddelen zoals Java bytecode (dis)assemblers die het aanpassen of aanmaken van klassebestanden zonder tussenkomst van compiler mogelijk maken en dit zonder zich te moeten verdiepen in het interne bestandsformaat. Op deze manier wordt het dus eenvoudig om code aan te maken die zich niet aan de regels houdt. Een tweede reden vloeit voort uit het dynamisch laden van klassen. Wanneer een klasse verwijst naar een andere klasse, kan het voorkomen dat een
36
oudere of nieuwere versie van de deze klasse wordt geladen. Wanneer in deze versie bijvoorbeeld een methode ontbreekt of de toegangsbepalingen veranderd zijn, kan dit tot problemen leiden. Het is aan de verifier om deze problemen tijdig te detecteren en vervolgens de juiste uitzondering te gooien. De werking van de verifier is op hoog niveau beschreven in de specificatie van de Java virtuele machine, en de interne werking verschilt van implementatie tot implementatie. Het verificatieproces, zoals beschreven in de specificatie, gebeurt gedeeltelijk statisch en gedeeltelijk tijdens de uitvoering van het programma. Het zou ook mogelijk zijn om deze testen volledig tijdens de uitvoering te doen, maar dit zou onaanvaardbare consequenties met zich meebrengen op het gebied van performantie. In hoofdstuk 5 gaan we dieper in op het eigenlijke verificatie algoritme. Aan de hand van enkele voorbeelden bespreken we gedetailleerd de verschillende stappen met hun respectieve implicaties. We gaan ook na welke speciale voorzorgen genomen werden voor toepassingen met beperkt geheugen, zoals embedded systemen.
4.3.2
De class loader
E´en van de essenti¨ele eigenschappen van een platform voor mobiele code is de mogelijkheid om code dynamisch te laden. Dit is dan ook de primaire taak van een class loader. Een tweede –en uit beveiligings oogpunt zeker zo belangrijke– opdracht is het defini¨eren van de verschillende namespaces. We gaan hier dieper in op de werking van een class loader en bespreken tegelijk de relevantie voor de Java beveiligings architectuur. Dynamisch laden van klassen Zoals reeds vermeld, is de mogelijkheid om klassen dynamisch te laden een van de kenmerkende eigenschappen van Java. Het laden van mobiele code omvat zowel het lokaliseren en transfereren van de code als het vertalen van de getransfereerde code naar het interne klasse formaat van de virtuele machine. Men heeft gekozen voor een flexibel laadproces waar men zelf een aangepaste class loader voor kan defini¨eren. Deze flexibiliteit toont zich nuttig onder verschillende omstandigheden. Meestal worden applets via een HTTP verbinding over een netwerk getransfeerd onder de vorm van Java archive (JAR) bestanden. In sommige gevallen kan een alternatieve overdrachtswijze echter wenselijk zijn, bijvoorbeeld wanneer het lokale netwerk afgeschermd is door een firewall die geen HTTP verbindingen toestaat. Java biedt hier de mogelijkheid om een aangepaste class loader te installeren die hierop voorzien is. Er zijn ook om37
standigheden waarin het standaard Java bestandsformaat niet aan de eisen voldoet. Ook hier kan een aangepaste class loader een oplossing bieden. Met uitzondering van de primordial class loader zijn alle class loaders gewone Java klassen die zelf geladen worden door een andere class loader. De primordial class loader is daarentegen meestal in C geschreven, en heeft rechtstreeks toegang tot het lokale bestandssysteem. Hij laadt de klassen uit de standaard bibliotheek, waaronder de andere class loaders. Klassen die op deze manier worden geladen, worden als vertrouwd beschouwd en zijn dus niet aan een controle van de verifier onderhevig. De klassen geladen door de overige class loaders, worden automatisch onvertrouwd geacht, en dienen dus wel door een verifier gecontroleerd, alvorens geladen te worden. JDK 1.0 voorziet een abstracte klasse java.lang.ClassLoader waarvan alle andere class loaders erven. Ontwikkelaars van een browser met Java ondersteuning zullen deze abstracte klasse implementeren om een applet class loader te voorzien die in staat is applets over een netwerk te laden. Zulk een applet class loader zal altijd eerst trachten een klasse via primordial class loader te laden alvorens zelf tot het laden via een netwerkconnectie over te gaan. Dit mechanisme belet applets klassen te laden die essenti¨ele klassen van de virtuele machine zouden vervangen. Het is dus duidelijk dat dit de hele beveiligingsarchitectuur zou ondermijnen. Bovenstaand mechanisme moet dan ook zorgvuldig ge¨ımplementeerd worden bij de ontwikkeling van een aangepaste class loader. Installeren van een nieuwe class loader is een kritische operatie die enkel door vertrouwde code mag uitgevoerd worden. Defini¨ eren van namespaces De tweede taak is het defini¨eren van de verschillende namespaces. Op ieder moment kunnen er verschillende class loaders actief zijn. Code afkomstig van een verschillende locatie zal typisch ook door een andere class loader instantie geladen worden. Elk van deze klassen, zal enkel die klassen kunnen benaderen die zich in de namespace van zijn eigen class loader bevinden. Dit zijn de klassen die deze class loader zelf heeft geladen, en de klassen geladen door de primordial class loader. Een namespace is dus een beschrijving van het universum zichtbaar vanuit een bepaalde class loader. Waarom is dit nu zo belangrijk voor de beveiligingsarchitectuur? Het antwoord is tweeledig.
38
Ten eerste biedt dit een oplossing voor gevallen van class name collisions. Beschouwen we de situatie waar twee applets, afkomstig van een verschillende locatie, klassen wensen te laden die dezelfde naam dragen ´en behoren tot een pakket (package) met dezelfde naam. Indien dit, ondanks hun gelijknamigheid verschillende klassen zijn, kan dit voor een incorrecte werking en ernstige veiligheidsproblemen zorgen. Deze situatie is niet zo ver gezocht als ze op het eerste gezicht lijkt, daar in vele klassen het pakket niet gespecifieerd is, en al deze klassen per definitie tot hetzelfde pakket behoren. Hoe pakt men dit probleem aan? Voor de Java virtuele machine wordt een klasse gedefinieerd door zijn volledige naam ´en de class loader die de klasse heeft geladen. Vermits alle klassen afkomstig van een andere locatie door een verschillende class loader geladen worden, zal ook deze situatie geen name collision opleveren. Ten tweede maakt men door de strikte scheiding tussen de namespaces van applets van verschillende afkomst class spoofing onmogelijk. Dit is gelijkaardig aan bovenstaande situatie, maar hier probeert een applet opzettelijk een klasse van een ander applet te vervangen door een valse versie. Dit wordt verhinderd doordat applets van een verschillende locatie elkaar niet kunnen waarnemen. Conclusie Uit bovenstaande bespreking blijkt duidelijk dat een nieuwe class loader installeren een kritische operatie is die enkel door vertrouwde code mag uitgevoerd worden. Bij het ontwikkelen van een aangepaste class loader moet men zorgvuldig de besproken technieken volgen. Afwijking van deze regels leidt bijna zeker tot veiligheidsproblemen. Het argument om een eigen class loader te defini¨eren heeft in bijna alle gevallen betrekking tot de eerst besproken taak, met name de manier waarop code geladen wordt. Bijkomend krijgt de ontwikkelaar ook de verantwoordelijkheid over de definitie van de namespaces. Dit is niet wenselijk, omdat de geboden flexibiliteit in het geval van namespaces overbodig is, en in het verleden regelmatig tot problemen heeft geleid. Een vaak gehoorde suggestie is dan ook om deze twee taken te scheiden, zodat het ontwikkelen van een class loader kan geschieden zonder aanpassingen aan de namespace functionaliteit.
4.3.3
De security manager
De security manager vervolledigt het oorspronkelijke beveiligingsmodel. Net zoals een class loader is een security manager een gewone Java klasse, met dit verschil dat er op ieder moment maar ´e´en security manager actief kan zijn en deze niet gewijzigd kan worden zonder de virtuele machine te herstarten. 39
Het is de taak van de security manager om te beslissen wie een bepaalde kritische opdracht mag uitvoeren en wie niet. De standaard JDK 1.0 security manager zal vertrouwde code alles toestaan. Alle overige code zal geen kritische operaties mogen uitvoeren. Herinner u dat enkel code geladen door de primordial class loader als vertrouwd wordt beschouwd. Dit zwart-wit model legt verregaande beperkingen op aan applets, waardoor de functionaliteit in vele gevallen in het gedrang komt. De werking van de security manager is voor een ontwikkelaar van Java programma’s volledig transparant. De standaard Java bibliotheek voorziet voor iedere kritische operatie eerst een aanroep van de security manager. Deze zal een SecurityException gooien indien de operatie niet toesgestaan wordt. De standaard security manager controleert de volgende kritische operaties: • toegang tot threads en thread groups van andere programma’s • toegang tot het bestandssysteem • toegang tot de netwerkverbindingen • toegang tot gesloten pakketen • toegang tot verschillende systeembronnen, met name vensters, printrijen, klemborden, . . . • opstarten van andere programma’s • installatie van een nieuwe class loader • afsluiten van de virtuele machine Elk van bovenstaande controles komt overeen met ´e´en of meerdere methodes in de security manager. In totaal zijn er 29 methodes die allen beginnen met ‘check’. Zo controleert de checkRead(String file) methode of een bepaald bestand al dan niet gelezen mag worden. Soms kan het nuttig zijn om deze standaard controles uit te breiden met enkele toepassingsspecifieke controles. Hiervoor dient men een aangepaste security manager te ontwikkelen die deze extra controles implementeert. Dit is redelijk omslachtig en in vele gevallen zelfs onmogelijk, daar een security manager eens ge¨ınstalleerd, niet meer veranderd kan worden zonder de virtuele machine te herstarten.
40
4.4
Een eerste stap naar een minder restrictief beveligingsmodel: JDK 1.1[8][12]
Het oorspronkelijke beveiligingsmodel maakt een strikt onderscheid tussen applets en applicaties. Applicaties worden altijd volledig vertrouwd, waar applets als onvertrouwde code beschouwd worden. Dit onderscheid is louter arbitrair gekozen en in vele gevallen te restrictief. Applets op de intranet pagina’s van een bedrijf worden op deze manier over dezelfde kam geschoren als een applet op een willekeurige Internet website. In JDK 1.1 trekt men deze situatie recht door ook gepriviligeerde applets de mogelijkheid te geven om kritische operaties uit te voeren. Voor applicaties verandert er zeer weinig, zij worden nog steeds volledig vertrouwd. Voor applets daarentegen, wordt nu onderscheid gemaakt tussen deze afkomstig uit vertrouwde dan wel onvertrouwde bron. Applets uit de eerste categorie zullen dezelfde privileges krijgen als applicaties, en hebben dus volledige toegang tot het systeem. Voor applets uit de tweede categorie verandert er niets, zij blijven beperkt tot de eerder gedefinieerde zandbak. Applets ontsnappen de zandbak In dit beveiligingsmodel kan een applet een gepriviligeerde status krijgen op basis van zijn oorsprong, alsook het vertrouwen gehecht aan een derde partij die borg wenst te staan voor deze code. Zo kunnen bijvoorbeeld alle applets afkomstig van de intranet pagina’s van een bedrijf of gewaarborgd door de systeembeheerder, deze gepriviligeerde status krijgen. Zich borg stellen voor een applet gebeurt door dit applet van een digitale handtekening te voorzien. Deze cryptografische techniek maakt het mogelijk om achteraf na te gaan of de code werkelijk gewaarborgd is door een vertrouwd persoon, en of de code nadien niet gewijzigd is. Om zulk een digitale handtekening te genereren berekent men meestal eerst een message digest van de code. Dit is in essentie een getal bepaald door een ´e´en-wegsfunctie toe te passen op de code. Vervolgens kan men de private sleutel van een asymmetrisch sleutelpaar gebruiken om deze message digest te encrypteren, het resultaat is de digitale handtekening. Een gebruiker geeft aan dat hij code gewaarborgd door een bepaald persoon vertrouwt door de publieke sleutel van deze persoon aan zijn lijst van vertrouwde publieke sleutels toe te voegen. De authenticiteit van een digitale handtekening kan geverifieerd worden door deze te decrypteren met de publieke sleutel van de vertrouwde persoon en het resultaat te vergelijken met een zelf berekende message digest van de code.
41
vertrouwde applets signature Certificate check
applets verifier
applicaties
verifier primordial class loader
class loader
class loader
security manager
niet-kritische operaties
kritische operaties
Figuur 4.2: Het beveiligingsmodel in JDK 1.1
42
Bij het downloaden van een stuk code voorzien van een digitale handtekening, zal de class loader deze handtekening verifi¨eren en vervolgens de code als vertrouwd of onvertrouwd markeren. De code die als vertrouwd is gemarkeerd, wordt door de security manager op dezelfde wijze als applicaties behandeld. De gebruiker krijgt in dit beveiligingsmodel de volledige verantwoordelijkheid over het beheer van zijn lijst met vertrouwde publieke sleutels. Het behoeft geen betoog dat dit met de grootste zorg dient te gebeuren. De Java Cryptography Architecture en de Java Cryptography Extension Om dit nieuwe model mogelijk te maken werden de java.security pakketten ge¨ıntroduceerd. Gezamenlijk worden deze pakketten de Java Cryptography Architecture of JCA genoemd. JCA biedt een waaier van cryptografische functionaliteit aan, waaronder message digests, digitale handtekeningen en veilige generatie van willekeurige gegevens, maar geen encryptie of decryptie. Deze laatsten worden, vanwege uitvoer beperkingen opgelegd door de Verenigde Staten6 , enkel als een aparte bibliotheek, de Java Cryptography Extension of JCE, voorzien die niet exporteerbaar is naar landen buiten de Verenigde Staten. JCA bevat dus alle functionaliteit noodzakelijk om dit nieuwe beveiligingsmodel te ondersteunen. In combinatie met JCE vormt het een bibliotheek die het volledige gamma cryptografische technieken aanbiedt. Deze bibliotheek is gebaseerd op enkele interessante principes. Ten eerste is er de onafhankelijkheid van het gebruikte cryptografische algoritme. Dit wordt bereikt door cryptografische blokken te voorzien die abstractie maken van het gebruikte algoritme. Voorbeelden zijn MessageDigest, Signature, . . . Deze klassen worden service of engine klassen genoemd. Ten tweede is er de implementatie onafhankelijkheid, tot stand gebracht door de zogenaamde provider klassen. Provider klassen bieden een concrete implementatie voor de service klassen. Op deze manier kunnen alternatieve implementaties, zowel in hardware als in software, voor de verschillende algoritmes voorzien worden. Tenslotte zorgen KeyFactory klassen voor een probleemloos samenwerken van de verschillende provider klassen. Ondanks de mogelijke verschillen in de interne werking van de verschillende implementaties, kunnen sleutels gegenereerd door de ene provider op transparante wijze door andere providers gebruikt worden. 6
De Verenigde Staten beschouwen cryptografie als wapens, waardoor er strenge uitvoerbeperkingen werden oplegd. Deze beperkingen zijn niet van toepassing op cryptografische systemen voor authenticatie doeleinden, en zijn sinds Januari 2000 fel versoepeld.
43
4.5 4.5.1
Een volledig configureerbaar beveiligingsmodel: het Java 2 Platform[8][12] Waarom dit nieuwe beveiligingsmodel?
Ondanks de verbeteringen die JDK 1.1 met zich meebracht, met name de afschaffing van het strikte onderscheid tussen applets en applicaties, bleken toch nog een aantal punten voor verbetering vatbaar. Het Java 2 Platform tracht met een volledig herziene beveiligingsarchitectuur aan deze kritiek tegemoet te komen. Zwart-wit model van vertrouwen Het voornaamste punt van kritiek betreft het zwart-wit onderscheid tussen vertrouwde en onvertrouwde code. Op deze manier is het onmogelijk om ‘het principe van de minimale rechten’ altijd te eerbiedigen. Deze regel dicteert dat men moet trachten om altijd enkel die rechten toe te kennen, die strikt noodzakelijk zijn om de vooropgestelde taak tot een goed einde te brengen. Het Java 2 Platform biedt hier een oplossing door verschillende gradaties van vertrouwen mogelijk te maken. Waar een vertrouwd applet in de beveiligingsarchitectuur van JDK 1.1 nog toegang had tot alle kritische operaties, kan deze toegang nu volledig op de situatie afgestemd worden. Deze code wordt dus als het ware in een door de gebruiker zelf gedefinieerde zandbak uitgevoerd. Beveiligingsbeleid niet eenvoudig aanpasbaar In het standaard zandbak model is het beveiligingsbeleid vast met de security manager verweven. Een aanpassing van dit beveiligingsbeleid vereist dan ook een nieuwe versie van de security manager. Deze situatie is duidelijk niet ideaal. Een goed ontwerp zal altijd trachten beveiligingsbeleid en implementatie strikt van elkaar te scheiden. Een beleid is namelijk bijna per definitie aan periodieke wijzigingen onderhevig. Technieken om dit beveiligingsbeleid in praktijk om te zetten, veranderen daarentegen meestal ni´et drastisch. Door deze scheiding kan men gemakkelijk inspelen op wijzigingen in het beleid, vermits herimplementatie overbodig wordt. In de beveiligingsarchitectuur van het Java 2 Platform is deze scheiding w´el aanwezig. Gebruikers hebben nu de mogelijkheid om complexe beveiligingsregels te defini¨eren aan de hand van configuratie-bestanden. Hierbij dient vermeld dat het beschrijven van zo’n beveiligingsbeleid geen eenvoudige opgave is. Het is dan ook een goed idee om gebruikers hierin te assisteren. Browsers doen dit met behulp van grafische dialoogvensters. 44
De toegangscontrole structuur is moeilijk uitbreidbaar In het oorspronkelijk ontwerp vereiste iedere vorm van toegangscontrole een aparte check () methode in de security manager. Indien men nieuwe vormen van toegangscontrole wou toevoegen, was de ontwikkeling van een toepassingsspecifieke security manager noodzakelijk. Dit is op zijn minst omslachtig te noemen, en in vele gevallen is het installeren van een nieuwe security manager eenvoudigweg niet mogelijk. In Java 2 zijn al deze check () methodes vervangen door ´e´en methode, met name checkPermission(). Hoe dit wordt mogelijk gemaakt zullen we verder bespreken. Lokale code geniet te veel vertrouwen JDK 1.1 heeft ervoor gezorgd dat applets niet meer per definitie onvertrouwd zijn. Voor applicaties is er echter niets veranderd aan het originele concept, zij worden nog steeds in alle gevallen volledig vertrouwd. In vele gevallen is er niet genoeg reden om aan te nemen dat applicaties volledig kunnen vertrouwd worden. Zeker in het licht van JavaBeans, waarbij Java applicaties kunnen opgebouwd zijn uit verschillende componenten verspreid over het Internet, kan het nuttig zijn om dezelfde beveiligingstechnieken toe te passen zoals eerder besproken voor applets. In het nieuwe model worden applicaties op dezelfde manier behandeld als applets. Door het aanpassen van het beveiligingsbeleid, kan een gebruiker beslissen om bepaalde of alle lokale code als gedeeltelijk of volledig vertrouwd te beschouwen.
4.5.2
De nieuwe en aangepaste onderdelen
Om dit volledig configureerbare beveiligingsmodel mogelijk te maken, zijn verschillende nieuwe onderdelen ge¨ıntroduceerd; andere onderdelen, zoals daar zijn de security manager en de class loader, hebben grote veranderingen ondergaan. Het in detail bespreken van dit model zou ons te ver brengen, maar we zouden toch graag even ingaan op de belangrijkste nieuwigheden en de toegepaste technieken. De Policy klasse Een Policy klasse beschrijft in essentie een bepaald beveiligingsbeleid, met name de rechten verbonden aan code van een bepaalde afkomst. Op ieder moment kan in de Java virtuele machine slechts ´e´en Policy instantie actief zijn. Dit Policy object wordt telkens door de class loader geconsulteerd bij het laden van een nieuwe klasse.
45
Certificate Certificate Certificate
URL
Permissions
CodeBase
Policy
Figuur 4.3: Policy
De afkomst van code wordt op dezelfde manier beschreven als in JDK 1.1, met name aan de hand van zijn locatie (gespecifieerd door een URL) en een verzameling van digitale handtekeningen (onder de vorm van certificaten). Deze twee eigenschappen van klasse worden gezamenlijk in de CodeSource klasse beschreven. De rechten geassocieerd met een kritische operatie worden beschreven aan de hand van Permission objecten. Java voorziet de abstracte java.security.Permission klasse, waarvan kan ge¨erfd worden om nieuwe categorie¨en van rechten toe te voegen. Zulke Permission objecten bestaan gewoonlijk uit twee delen: een doel en een actie. Zo beschrijft ‘new FilePermission(/path/filename”, ”read”)’ de rechten geassocieerd met het lezen van een bepaald bestand. Protection Domain In de door Sun gebruikte terminologie, betekent een protection domain niets meer dan een verzameling van klassen met de zelfde CodeSource. Vermits al deze klassen afkomstig zijn van dezelfde locatie en getekend zijn met dezelfde certificaten, bezitten deze klassen ook dezelfde rechten. Deze rechten worden dus niet rechtstreeks aan klassen en objecten toegekend, maar via de tussenstap van een protection domain. E´en protection domain is speciaal, met name het system domain. Dit protection domain bevat alle code geladen door de Primordial Class Loader, en heeft speciale privileges. Security Manager De opvallendste wijziging doorgevoerd aan de security manager, is de hoger vermelde invoering van de checkPermission() methode. Deze methode vervangt de talrijke check () methodes uit de oudere versies.
46
foo.class bar.class waldo.class
Permissions protection domain B
bax.class access controller
applet class loader
protection domain A
secure class loader
Permissions
primordial class loader
system protection domain
system classes
System Permissions Figuur 4.4: Protection Domains
De eigenlijke implementatie van de toegangscontrole bevindt zich nu in de access controller. De enige bestaansreden van de security manager is het garanderen van compatibiliteit met oudere versies van de JDK. Secure Class Loader Java 2 introduceert met de java.security.SecureClassLoader een concrete implementatie van de abstracte java.lang.ClassLoader klasse. Het is de taak van de secure class loader om klassen in de juiste protection domains in te delen. Dit gebeurt door de actieve Policy instantie te consulteren. De secure class loader laadt alle klassen rechtstreeks of onrechtstreeks via een class loader die zelf rechtreeks of onrechtstreeks door de secure class loader geladen is. Enkel systeemklassen of klassen in het classpath worden niet door de secure class loader maar door de primordial class loader geladen. Java 2 predikt ook een nieuwe aanpak bij het laden van klassen. Bij de creatie van een class loader instantie zal altijd een ‘delegatie ouder’ gespecifieerd worden. De primordial class loader zal als basis van deze delegatie hi¨erarchie dienen. Indien een class loader een aanvraag tot het laden van een klasse krijgt, zal hij deze vraag in eerste instantie doorspelen aan zijn dele-
47
Internet 9 findClass(foo) 2 findLoadedClass(foo)
geladen klassen
applet class loader
1 loadClass(foo)
3 loadClass(foo) 4 findLoadedClass(foo)
geladen klassen
URL class loader 5 loadClass(foo)
6 findLoadedClass(foo)
geladen klassen
secure class loader 7 findSystemClass(foo)
8 findLoadedClass(foo)
systeem klassen
primordial class loader
Figuur 4.5: Laden van klassen via delegatie
48
gatie ouder. Op deze manier komt een aanvraag altijd eerst bij de primordial class loader terecht. Access Controller De access controller klasse is h´et centrale onderdeel van het Java 2 beveiligingsmodel. Het is een niet-instantieerbare klasse, enkel voorzien van statische methodes. Het is zijn taak om ervoor te zorgen dat kritische operaties enkel toegankelijk zijn voor code met de juiste rechten. We gaan hier na hoe de access manager dit concreet implementeert. Een beslissing om al dan niet toegang te verlenen tot een kritische operatie, kan enkel genomen worden indien men de juiste context kent. Hiermee bedoelen we dat het noodzakelijk is om te weten wie toegang tot deze operatie wenst, en in wiens naam dit is. Deze context is volledig gedefinieerd door de opeenvolging van methode-aanroepen in de thread, elk geassocieerd met een protection domain van de klasse waartoe deze methode behoort. Een eenvoudig toegangscontrole algoritme zou kunnen eisen dat al deze protection domains, verbonden met de opeenvolgende methodes, de nodige rechten bevatten alvorens toegang te verlenen. Dit eenvoudig algoritme is veilig, omdat het de zekerheid verschaft dat de vraag rechtstreeks of onrechtstreeks afkomstig is van een methode die zelf beschikt over de nodige rechten. Voor re¨ele toepassingen is dit algoritme echter te restrictief. Ter illustratie, nemen we het voorbeeld van een wachtwoord-wijzigingsprogramma. Dit programma roept hiervoor de controleerWachtwoord() methode uit een van de systeemklassen op, dat op zijn beurt het wachtwoord bestand laadt via de leesBestand() methode. Dit laatste is een kritische operatie, waardoor bovenstaand algoritme wordt toegepast om de toegangsrechten te controleren. Vermits het programma geen leesrechten heeft voor het wachtwoord bestand, zal het algoritme de toegang weigeren. Daarom is er een mechanisme voorzien dat systeemklassen de mogelijkheid geeft om een operatie uit te voeren, zonder naar de rechten van de oproepende methodes te kijken. Dit mechanisme is de doPrivileged() methode uit de access controller klasse, die enkel door methodes uit de systeemklasse kan opgeroepen worden. Het aangepaste algoritme gaat de context van recentste tot minst recente methode aanroep onderzoeken op aanwezigheid van de nodige toegangsrechten. Als bij een bepaalde methode aanroep niet genoeg rechten aanwezig zijn, dan zal toegang geweigerd worden. Indien daarentegen de privileged vlag gezet is, zal toegang verleend worden, onafhankelijk van de rechten van de andere methode-aanroepen. Zo kan de controleerWachtwoord() methode dit mechanisme gebruiken om, ondanks het ontbreken van de nodige rechten in 49
systeem klassen
overige klassen policy
verifier
signature check Certificate
primordial class loader class loader
code source
security manager
protection domain
access controller standaard zandbak
protection domain
system protection domain Figuur 4.6: Het beveiligingsmodel in JDK 1.2
de oproepende methode, toegang te verkrijgen tot het wachtwoordbestand. Het is de verantwoordelijkheid van de controleerWachtwoord() methode om ervoor te zorgen dat de aanroepende methode geen rechtstreekse toegang tot dit bestand krijgt.
4.5.3
Samengevat
Het Java 2 beveiligingsmodel bestaat uit verschillende interagerende onderdelen. Figuur 4.6 tracht deze complexe situatie samen te vatten. We overlopen kort de verschillende stappen bij de behandeling van een niet-systeemklasse: 1. De eerste stap is het laden van de klasse, dit is de verantwoordelijkheid van de class loader. 2. Tijdens dit laden wordt de bytecode verifier geconsulteerd. 50
3. Vervolgens wordt de code source bepaald, dit houdt onder andere een controle van de digitale handtekening en bijgevoegde certificaten in. 4. Aan de hand van deze code source wordt de actieve policy door de class loader ondervraagd over de rechten geassocieerd met deze code. 5. Als er nog geen protection domain bestaat die deze rechten bevat, dan wordt dit nu aangemaakt. De klasse wordt vervolgens aan dit protection domain toegewezen. 6. Indien het programma tijdens zijn uitvoering een kritische operatie wilt uitvoeren, wordt de security manager geraadpleegd. 7. Deze zal de vraag doorgeven aan de access controller, die door inspectie van de thread-context bepaalt of de code over genoeg rechten beschikt om deze operatie toe te staan.
51
Hoofdstuk 5
De verifier In hoofdstuk 4 hebben we de taak van de verifier samengevat als ‘controleren of onvertrouwde code de regels volgt’. Hier gaan we dieper in op de aard van de controles, en hun relevantie voor de beveiligingsarchitectuur.
5.1
Het laadproces[11]
Verificatie is nauw verbonden met het laadproces van de JVM. De verifier bestaat uit verschillende stappen, die doorheen het laden en de uitvoering van een klasse opeenvolgend actief zijn. We bespreken daarom eerst bondig de 3 delen van het laadproces in de Java virtuele machine: laden, linken, en initialisatie.
5.1.1
Laden
Herinner u de beschrijving van de class loader uit het vorige hoofdstuk. Wanneer de ‘loadClass’ methode aangeroepen wordt, zal die eerst trachten de klasse te laden via zijn delegatie ouders. Slechts indien dit mislukt, zal hij zelf tot laden overgaan. Wanneer de klasse getransfereerd is, moet deze ‘vertaald’ worden naar het interne klasse formaat. Het is immers mogelijk dat deze klasse zich niet in het standaard klasse formaat bevindt. Zo kan er bijvoorbeeld extra fout-detectie of -correctie informatie toegevoegd worden, wanneer de overdracht plaatsvindt over een onbetrouwbaar kanaal. Deze controle informatie moet dan door de class loader verwijderd worden. Na vertaling wordt de ‘defineClass’ methode van de java.lang.ClassLoader aangeroepen. Deze methode zal vervolgens de eerste verificatie stap uitvoeren. Deze stap bestaat in principe voornamelijk uit het controleren of de structuur van de aangeboden klasse voldoet aan het class file formaat. Enkel de controles die noodzakelijk zijn om probleemloos laden te garanderen, worden in deze
52
stap uitgevoerd. Na deze oppervlakige controle wordt opnieuw de ‘loadClass’ methode aangeroepen om de superklasse en de superinterfaces te laden. Op deze manier worden alle superklassen uit de hi¨erarchie van de eerste klasse geladen, tot uiteindelijk de java.lang.Object klasse bereikt wordt. Indien bij het laden van een klasse een fout gedetecteerd wordt, dan wordt een van volgende uitzonderingen gegooid: • ClassFormatError : de klasse voldoet niet aan de structuur regels. • UnsupportedClassVersionError : de JVM ondersteunt dit klasse formaat niet. • ClassCircularityError : de klasse is zijn eigen superklasse. • NoClassDefFoundError : de klasse kan niet gevonden worden.
5.1.2
Linken
Linken is het toevoegen van de geladen klasse aan de uitvoeringsomgeving. Het is de ‘resolveClass’ methode die verantwoordelijk is voor het linken van een klasse. Dit linken bestaat zelf uit drie onderdelen: verificatie, voorbereiding, en resolution van de symbolische referenties. Het eerste onderdeel is dus de verificatie, meer bepaald de tweede en deerde stap van het verificatie proces. Stap 2 doet een grondigere controle van de structuur van de klasse, en gaat ook de relaties met de superklassen na. Stap 3 kijkt naar de eigenlijk bytecode, en is veruit de ingewikkeldste stap. Indien verificatie faalt wordt een VerifyError gegooid. De voorbereiding, of preparation, houdt ondermeer de aanmaak van geheugenstructuren voor statische velden in. De velden worden vervolgens met de standaard waarden gevuld. Pas bij initialisatie worden de juiste waarden toegekend. Optioneel kunnen er ook nog andere interne geheugenstructuren aangemaakt worden. Zoals bijvoorbeeld een lijst van de beschikbare methodes van deze klasse, dit voorkomt het doorzoeken van de klasse en de superklasse bij iedere methode aanroep. Het laatste onderdeel is de resolution. Om een symbolische referentie te gebruiken, moet deze gecontroleerd en vervolgens omgezet worden naar een concrete referentie. Deze stap is een optioneel deel van het laadproces. Afhankelijk van de implementatie zullen alle symbolische referenties reeds tijdens het linken worden omgezet, dan wel bij het eerste gebruik van de referentie. Om compatibiliteit tussen virtuele machines te garanderen,
53
mogen fouten die optreden tijdens de resolution slechts gegooid worden wanneer de symbolische referentie voor de eerste keer gebruikt wordt. Volgende uitzonderingen kunnen optreden: • IllegalAccessError : een symbolische referentie die verwijst naar een veld of methode waar de aanroepende methode geen toegang toe heeft. • InstantiationError : poging tot instanti¨eren van een abstracte klasse of interface. • NoSuchFieldError : referentie naar een onbestaand veld. • NoSuchMethodError : referentie naar een onbestaande methode.
5.1.3
Initialisatie
Het laatste onderdeel van het laadproces is het initialiseren van de klasse. Hier worden de juiste waarden toegekend aan de statische velden. Alvorens een klasse ge¨ınitialiseerd kan worden, moet ook haar superklasse ge¨ınitialiseerd zijn. De methode is verantwoordelijk voor deze initialisatie. Deze methode wordt automatisch aangeroepen door de JVM; dit mag niet vanuit een andere methode gebeuren. De methode wordt door de Java compiler zelf voorzien indien nodig. De vierde stap den van een klasse. uitgevoerd worden, controles tijdens de
5.2
van het verificatie proces is geen deel van het laDeze stap zou eveneens gelijktijdig met stap 3 kunnen maar omwille van de performantie is gekozen om deze uitvoering te doen.
De eerste verificatie stap[4][6][11][23]
De eerste verificatie stap is een integraal onderdeel van het laden van een klasse. In dit laadproces wordt een klasse bestand, dat niet meer is dan een opeenvolging van bytes, in een interne datastructuur geladen. Om in de datastructuur te ‘passen’, moeten deze bytes aan bepaalde voorwaarden voldoen. De eerste stap van de verifier heeft dan ook enkel als taak om te controleren of het klasse bestand aan deze structurele voorwaarden voldoet. Zo zal er bijvoorbeeld gecontroleerd worden of het klasse bestand niet te veel of te weinig bytes bevat. Een grondigere controle wordt uitgesteld tot het linken van de klasse. Onderstaand schema beschrijft het bovenste niveau van een klasse bestand in de vorm van C struct:
54
ClassFile{ u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; } cp info, field info, method info, en attribute info zijn zelf samengestelde types. u1 stelt een tekenloos 1-byte type voor. u2 en u4 bestaan uit respectievelijk 2 en 4 bytes, die in big-endian volgorde samengevoegd worden. De header Het eerste item dat gecontroleerd wordt, is het ‘magic number’ waarmee alle klasse bestanden beginnen. Dit magic number is van het type u4 en heeft als waarde 0xCAFEBABE. Dit getal dient enkel om klasse bestanden van andere bestanden te kunnen onderscheiden. Vervolgens zijn er de ‘minor version’ en ‘major version’ getallen, die informatie geven over het formaat van het klasse bestand. Indien deze versie niet ondersteund wordt, dan faalt de verificatie. De constant pool De constant pool is een tabel van String constanten, klasse en interface namen, namen van velden en andere constanten, waarnaar verwezen wordt vanuit het klasse bestand. Vermits het aantal elementen variabel is, begint de constant pool eerst met een ‘constant pool count’. De basisstructuur van een constant pool item is als volgt: cp_info{ u1 tag; u1 info[]; } 55
De ‘tag’ geeft aan welk type constant pool item er kan verwacht worden. Zo zijn er 11 verschillende types gedefinieerd: • constant Class • constant Fieldref • constant Methodref • constant InterfaceMethodref • constant String • constant Integer • constant Float • constant Long • constant Double • constant NameAndType • constant Utf8
Aan de hand van de ‘tag’ is de type-specifieke inhoud van ‘info[ ]’ uit de basisstructuur gekend. Een voorbeeld is de beschrijving van een methode, en heeft volgende structuur: CONSTANT_Methodref_info{ u1 tag; u2 class_index; u2 name_and_type_index; } De ‘class index’ en ‘name and type index’ verwijzen op hun beurt weer naar een ander item in de constant pool, respectievelijk de naam van de klasse waarin de methode gedefinieerd is en de eigenlijke beschrijving van de methode. Java ondersteunt de Unicode standaard, en beschrijft daarom Strings in (een variant van) het Utf8 formaat. ascii karakters worden door 1 byte voorgesteld; andere karakters nemen 2 of 3 bytes in. Bij het laden wordt initieel enkel gecontroleerd of de ‘tag’ naar een bestaand type verwijst, en of de type-afhankelijke structuur van de constant pool items gerespecteerd wordt. De overige controles van de constant pool worden uitgesteld tot de tweede verificatie stap. Deze controles zijn namelijk niet echt noodzakelijk om de klasse in de interne structuur te plaatsen.
56
De klasse informatie De ‘access flags’ geven informatie over de toegangspermissies en eigenschappen van de klasse. De verifier moet hier controleren of de verschillede vlaggen compatibel zijn. Zo is het bijvoorbeeld niet mogelijk om de acc final en acc abstract tegelijk aan te zetten. Niet-gedefinieerde vlaggen worden gemaskeerd. De ‘this class’ en ‘super class’ verwijzen naar een constant Class info constant pool item, die respectievelijk de klasse en de superklasse beschrijven. De ‘interfaces count’ geeft aan hoeveel interfaces deze klasse implementeert. De ‘interfaces[interfaces count]’ verwijzen ook weer naar constant Class info constant pool items, die de ge¨ımplementeerde interfaces beschrijven. Het is de taak van de verifier om te controleren of er correct verwezen wordt naar de constant pool. De velden Een ‘fields count’ geeft aan hoeveel velden deze klasse bevat. Deze velden worden beschreven door een field info structuur: field_info{ u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; } Het eerste onderdeel zijn opnieuw de ‘access flags’ die informatie geven over de toegangspermissies en de eigenschappen van de velden. De ‘name index’ en ‘descriptor index’ verwijzen beide naar een constant Utf8 info constant pool item, met respectievelijk de naam en de beschrijving van het veld. Het aantal attributen wordt door ‘attributes count’ aangegeven, dit kan nul of meer zijn. De attributen worden in het (verder besproken) standaard attribuut formaat beschreven. Volgens de specificatie zijn volgende veld attributen gedefinieerd, maar anderen zijn ook mogelijk: • ConstantValue • Synthetic • Deprecated Ook hier wordt enkel nagegaan of de afgesproken structuur gerespecteerd wordt; verdere controles gebeuren in stap 2. 57
De methodes De ‘methods count’ geeft aan hoeveel methodes er zullen volgen. Die methodes worden beschreven in een method info structuur, en bevatten, indien ze niet abstract of native zijn, ook de eigenlijke bytecode. De structuur is als volgt: method_info{ u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; De structuur is identiek aan de field info structuur. Enkel de vlaggen zijn specifiek aan dit type, alsook enkele van de gedefinieerde methode attributen: • Code • Exceptions • Synthetic • Deprecated Het code attribuut bevat de eigenlijke bytecode, en wordt in de derde stap van het verificatie algoritme besproken. De attributen Tenslotte geeft ‘attributes count’ het aantal klasse attributen aan. De structuur van deze attributen is dezelfde als voor de methode en veld attributen: attribute_info{ u2 attribute_name_index; u2 attribute_length; u1 info[attribute_length]; } Het eerste onderdeel, de ‘attribute name index’ verwijst naar een Utf8 String in de constant pool die de naam van het attribuut aanduidt. De structuur van de ‘info[attribute length]’ tabel is afhankelijk van het attribuut. Volgende klasse attributen zijn gedefinieerd: • SourceFile • Deprecated 58
Het is een compiler toegestaan om extra attributen te defini¨eren. Zo is het mogelijk om een nieuw attribuut te voorzien met extra debuginformatie, specifiek voor een bepaalde debugtool. Door de JVM onbekende attributen worden genegeerd. Vandaar dat het de enige structuur is die expliciet de lengte vermeldt, anders zou niet geweten zijn hoeveel bytes er voor een onbekend attribuut overgeslagen moeten worden.
5.3
De tweede verificatie stap[4][6][11][23]
De tweede verificatie stap is eenvoudig samen te vatten als ‘alle resterende controles die op een klasse bestand kunnen uitgevoerd worden, zonder naar de eigenlijke bytecode te kijken’. Zowel de eerste als de tweede verificatie stap staan bijzonder summier beschreven in de JVM specificatie. Het is dan ook niet eenvoudig om te bepalen welke controles nu effectief door stap 1, dan wel stap 2 moeten uitgevoerd worden. Op het eerste zicht lijkt dit onderscheid willekeurig; klassen die gelinkt worden, zijn namelijk toch al geladen en hebben dus ook die eerste verificatie stap reeds achter de rug. Dus als we alle testen uit verificatie stap 2 zouden verplaatsen naar de eerste verificatie stap, dan wordt zeker alles tijdig getest. Nu worden klassen vaak geladen, zonder dat ze ooit gelinkt zullen worden. Deze klassen doorgaan dus enkel de eerste verificatie stap. Hieruit blijkt het belang van deze controles in alle JVM implementaties op eenzelfde moment in het laadproces uit te voeren. Anders is het mogelijk dat een klasse, die geladen maar niet gelinkt wordt, bij de ene JVM faalt omwille van een test die bij de andere slechts tijdens het linken zou uitgevoerd worden. Dit is uiteraard niet wenselijk. Waarom worden klassen geladen zonder ooit gelinkt te worden? Vaak is dit een gevolg van de derde verificatie stap. Als men wilt nagaan of twee types compatibel zijn, kan het nodig zijn om de bijhorende klassen te laden om zo hun gemeenschappelijke superklasse te kunnen bepalen. Zolang deze klassen niet effectief gebruikt worden, is het volledige link proces overbodig. We moeten enkel zeker zijn dat de type informatie verzameld tijdens het laadproces consistent is, zodat ze kan gebruikt worden in de derde verificatie stap. Daartoe wordt dus in stap 1 enkel de header, de structuur van de verschillende delen, en de klasse informatie gecontroleerd. Naleving van onderstaande regels wordt in de tweede verificatie stap gecontroleerd: • de superklasse mag niet ‘final’ zijn • er mogen geen ‘final’ methodes overschreven worden 59
• referenties naar de constant pool moeten naar een item van het correcte type verwijzen • methodes en velden moeten correcte namen en beschrijvingen hebben • niet-compatibele vlaggen mogen niet samen aanstaan • alle abstracte methodes van de superklasse en interfaces worden ge¨ımplementeerd, tenzij de klasse zelf abstract is • sommige attributen zijn verplicht, of mogen maar ´e´en keer aanwezig zijn • het aantal methode parameters is beperkt tot 255 • het return type van een overschreven methode moet gerespecteerd worden . . . Sommige van de controles hier uitgevoerd, zoals bijvoordeeld de twee eerst vermelde regels, hebben een directe impact op de beveiliging. Belangrijke klassen uit de beveiligingsarchitectuur steunen namelijk op deze regels. Andere regels dienen dan weer een correcte werking van de JVM te garanderen.
5.4
De derde verificatie stap[4][11][23]
De derde verificatie stap is ongetwijfeld het meest complexe deel van het hele verificatie proces. Deze stap kijkt naar de eigenlijke bytecode van de verschillende methodes. Iedere niet-abstracte en niet-native methode bevat juist ´e´en Code attribute van volgende vorm: Code_attribute{ u2 attribute_name_index; u4 attribute_length; u2 max_stack; u2 max_locals; u4 code_length; u1 code[code_length]; u2 exception_table_length; { u2 start_pc; u2 end_pc; u2 handler_pc; u2 catch_type; } exception_table[exception_table_length] u2 attributes_count; attributes_info attributes[attributes_count]; } 60
De eerste twee elementen, ‘attribute name index’ en ‘attribute length’, zijn afkomstig van de standaard attribute structuur. Het eerste element zal naar een constant Utf8 info constant pool item met de vermelding “Code” verwijzen. Vervolgens zijn er ‘max stack’ en ‘max locals’, die respectievelijk het maximaal aantal elementen op de operanden stapel en in de lokale variabelen lijst aangeven. Deze waarden worden tijdens het compileren bepaald. De ‘code length’ beschrijft de lengte van de eigenlijke bytecode, die in de ‘code[code length]’ tabel vervat zit. Deze tabel is slechts een opeenvolging van bytes, bestaande uit opcodes en een variabel aantal argumenten. De eerste taak zal zijn om deze opeenvolgende bytes op te delen per instructie, en in een datastructuur te plaatsen. Dan hebben we een uitzonderingstabel, zoals besproken in hoofdstuk 3. Het aantal try-catch blokken wordt gegeven door ‘exception table length’. De ‘exception table[exception table length]’ beschrijft dan start en einde van de beschermde code, het begin van de handler, en het type uitzondering waarop de catch werkt. Tenslotte kan een Code attribute zelf ook nog attributen hebben. De standaard gedefinieerde attributen, gebruikt om debug informatie te bewaren, zijn: • LineNumberTable • LocalVariableTable In de derde verificatie stap wordt iedere methode afzonderlijk behandeld. Het toegepaste algoritme bestaat uit twee grote delen: een control-flow analyse en een data-flow analyse.
5.4.1
Control-flow analyse
Na de control-flow analyse moeten volgende vragen (positief) beantwoord zijn: • Begint iedere instructie met een bestaande opcode? • Verwijzen alle argumenten naar constant pool items van het juiste type? • Zijn alle verwijzingen naar lokale variabelen binnen de juiste grenzen? • Wordt er enkel naar de opcode van een instructie gesprongen? • Hebben alle instructies de correcte argumenten? • Is het onmogelijk om ‘van de code te vallen’ ?
61
• Zijn alle verwijzingen in de uitzonderingstabel correct? Daarvoor gaat de control-flow analyse tweemaal door de bytecode. Bij de eerste doorgang wordt de code opgebroken in instructies. Men begint bij de eerste byte. Indien dit een bestaande opcode is, wordt aan de hand van het aantal argumenten, het begin van de volgende instructie bepaald. Zo gaat men verder tot het einde van de code bereikt is. De index van iedere opcode wordt bijgehouden in een tabel. Sommige instructies, zoals daar zijn tableswitch, lookupswitch en wide hebben een variabel aantal argumenten. Indien een instructie met een niet-bestaande opcode begint of de code eindigt midden in een instructie, faalt de verificatie. Tijdens de tweede doorgang gaat de verifier naar de inhoud van de argumenten kijken. Er zijn verschillende soorten argumenten te onderscheiden, zoals: • een lokale variabele index: er moet voldaan zijn aan 0 ≤ index < max locals • een constant pool index: – er moet voldaan zijn aan 0 < index < constant pool count – constant pool[index] moet het voor de opcode correcte type bevatten • een verwijzing naar een andere instructie: – er moet voldaan zijn aan 0 ≤ index < code length – index verwijst naar het begin van een instructie – code[index] is een bestaande opcode • een parameter: sommige opcodes verwachten een parameter, zoals de dimensie van een tabel, deze moet aan de opcode-specifieke voorwaarden voldoen • padding, of opvulsel: tableswitch en lookupswitch vereisen een correcte padding om de overige argumenten op een index die een veelvoud van 4 is, te laten beginnen • een andere opcode: de wide instructie verwacht een van 11 andere opcodes als argument Tenslotte wordt de uitzonderingstabel nog gecontroleerd. Zo moet bijvoorbeeld start pc < end pc, handler pc naar het begin van een instructie verwijzen en catch type naar een constant pool item van het juiste type verwijzen. We komen hier verder nog op terug, wanneer we de werking van de verifier met betrekking tot uitzonderingen bespreken. 62
Al deze controles hebben tot doel de correcte werking van de JVM te garanderen, zonder dat er tijdens de uitvoering nog controles noodzakelijk zijn. Het tweede deel, de data-flow analyse, zal trachten de type-safety van het programma te garanderen.
5.4.2
Data-flow analyse
Tijdens de data-flow analyse wordt nagegaan of op ieder punt van het programma, onafhankelijk van het gevolgde pad, volgende regels gelden: • de operanden stapel heeft op een bepaald punt altijd dezelfde hoogte en bevat steeds waarden van dezelfde types • lokale variabelen worden enkel aangesproken indien met zekerheid kan bepaald worden dat ze het correcte type hebben • een methode wordt slechts aangeroepen indien de operanden stapel de juiste parameters bevat • velden worden enkel waarden van een geschikt type toegekend Om deze controles te kunnen uitvoeren, wordt voor iedere instructie een kopie van de operanden stapel en de lokale variabelen op dat punt bijgehouden. Zo moet voor de operanden stapel de hoogte van de stapel en de types van de operanden geweten zijn. Voor iedere lokale variabele moet ofwel het type gekend zijn, ofwel moet hij als niet ge¨ıntialiseerd of onbruikbaar gemarkeerd zijn. Het algoritme De eerste stap is het aanmaken van een datastructuur waarin deze lokale variabelen en operanden stapels bewaard worden. Vervolgens worden de onderste lokale variabelen van de eerste instructie ge¨ınitialiseerd met de types uit de methode descriptor ; de overige lokale variabelen worden als niet-ge¨ınitialiseerd gemarkeerd. De operanden stapel is initieel leeg. Tenslotte start de eigenlijke data-flow analyse. Voor iedere instructie is er een changed bit, die aangeeft of de instructie al dan niet moet gecontroleerd worden. Initieel is deze bit enkel voor de eerste instructie aangezet. De volgende lus wordt nu uitgevoerd: 1. Selecteer een instructie met de changed bit aan. Als er zo geen instructie gevonden wordt, dan is de verificatie geslaagd. De changed bit van de geselecteerde instructie wordt afgezet. 2. Boots het effect van de geselecteerde instructie op de operanden stapel en de lokale variabelen na. Daarbij moet gecontroleerd worden of: 63
• er voldoende operanden beschikbaar zijn op de stapel • de operanden van het correcte type zijn • de gebruikte lokale variabelen van het correcte type zijn • er voldoende plaats is op de operanden stapel voor het resultaat Het resultaat op de operanden stapel en de lokale variabelen lijst is uiteraard afhankelijk van de instructie in kwestie. De istore 1 instructie zal bijvoorbeeld altijd ´e´en integer van de stapel halen, en een integer in lokale variabele 1 plaatsen. Voor de invokevirtual instructie is het resultaat daarentegen afhankelijk van de methode descriptor. 3. Bepaal alle instructies die kunnen volgen. In aanmerking komen: • de volgende instructie in de bytecode tabel • de doel instructie(s) van een control-flow instructie • iedere uitzonderings handler voor deze instructie 4. Voeg de status van de operanden stapel en lokale variabelen, na het nabootsen van de instructie, samen met de operanden stapel en lokale variabelen van elk van de opvolgende instructies. • Indien dit de eerste keer is dat de opvolgende instructie behandeld wordt, dan moet bij deze instructie een onveranderde kopie van de operanden stapel en lokale variabelen bewaard worden. Zet vervolgens de changed bit aan. • Indien de instructie reeds behandeld was, dan moeten de verder vermelde samenvoegingsregels toegepast worden. De changed bit wordt enkel aangezet indien er aanpassingen gedaan zijn. 5. Ga verder met stap 1. Dit beschrijft het algoritme voor data-flow analyse. Er rest ons enkel nog samenvoegingsregels voor de operanden stapel en de lokale variabelen toe te lichten. Als een bepaalde instructie meer dan eens via een verschillend pad bereikt wordt, dan wordt eerst gecontroleerd of de status van de verschillende paden compatibel is: • de operanden stapels moeten even groot zijn • de overeenkomstige operanden moeten hetzelfde type hebben Voor de nieuwe operanden stapel wordt: • ieder primitief type integraal overgenomen
64
• de ‘kleinste gemeenschappelijke superklasse’ van iedere referentie op de stapel geplaatst. Deze superklasse kan altijd gevonden worden, vermits iedere klasse java.lang.Object als superklasse heeft. Dit zal verder met het tweede voorbeeld duidelijk gemaakt worden. De nieuwe lokale variabelen worden als volgt bepaald: • lokale variabelen met overeenkomstige primitieve types blijven onveranderd • lokale variabelen met niet-overeenkomstige primitieve types worden als ‘onbruikbaar’ gemarkeerd • voor referenties wordt de ‘kleinste gemeenschappelijke superklasse’ in de juiste lokale variabele bewaard Als een programma dit algoritme met succes heeft doorstaan, zijn we zeker dat het type-safe is. We weten namelijk op elk moment welke types de waarden op de operanden stapel en in de lokale variabelen zullen hebben, en kunnen aldus garanderen dat er nooit een –voor deze gegevens– ongeschikte operatie op uitgevoerd zal worden. We geven nu enkele voorbeelden die dit algoritme toelichten. Voorbeeld: if en goto Een eerste voorbeeld is het volgende Java programmatje dat de faculteitsfunctie implementeert: public static int fac(int teller) { int fac = 1; while (teller != 0) { fac *= teller; teller--; } return fac; }
ICONST_1 ISTORE_1 loop: ILOAD_0 IFNE end body: ILOAD_1 ILOAD_0 IMUL ISTORE_1 IINC 0 -1 GOTO loop end: ILOAD_1 IRETURN
Figuur 5.1 toont de initi¨ele situatie voor de begin instructie. De eerste lokale variabele wordt ge¨ınitialiseerd met het Int type van de enige methode parameter; de tweede lokale variabele wordt gemarkeerd als nietge¨ınitialiseerd. De operanden stapel is initieel leeg, en de changed bit van de eerste instructie staat aan. 65
X
ICONST_1
I
changed operanden stapel bit
lokale variabelen
label
instructie
Figuur 5.1: De initi¨ele operanden stapel en lokale variabelen.
Na de initialisatie start de eigenlijke data-flow analyse. Figuur 5.2 toont de situatie na behandeling van de ifne instructie. Merk op dat bij twee instructies de changed bit aanstaat. We mogen nu willekeurig kiezen met welke van beide we verder gaan.
I
I
ICONST_1
I
ISTORE_1 ILOAD_0
I
I
I
I
I
I
?
?
?
ILOAD_0
?
?
?
IMUL
?
?
?
ISTORE_1
?
?
?
IINC 0 -1
?
?
?
GOTO loop
I
I
?
?
I
X
X ?
loop:
IFNE end body:
end:
ILOAD_1
ILOAD_1 IRETURN
Figuur 5.2: De operanden stapel en lokale variabelen na uitvoering van de ifne instructie. Wanneer we de goto instructie bereiken, hebben we voor het eerst een situatie waar de volgende instructie reeds behandeld is. We moeten dus controleren of de operanden stapel en lokale variabelen na uitvoering van de goto instructie overeen komen met de operanden stapel en lokale variabelen tijdens de eerste passage langs deze instructie. In beide gevallen is de operanden stapel leeg, en bevatten alle lokale variabelen een waarde van het 66
type Int. Er is dus aan de voorwaarden voldaan. De changed bit van deze instructie wordt niet aangezet, daar zowel de operanden stapel als de lokale variabelen ongewijzigd blijven. Figuur 5.3 geeft de eindsituatie weer. Alle instructies zijn behandeld, en geen enkele instructie heeft zijn changed bit nog aanstaan. De methode is dus met succes geverifi¨eerd. De ireturn instructie haalt de Int van de operanden stapel, en geeft deze waarde als resultaat van de methode aanroep terug. De methode zal dus eindigen met een lege operanden stapel. Dit is op zich geen verplichting, maar de meeste compilers trachten steeds code te produceren die de stapel leeg achterlaat.
I
I
I I I
I
I
I
ICONST_1
I
ISTORE_1 ILOAD_0
I
I
I
I
I
I
I
I
ILOAD_0
I
I
IMUL
I
I
ISTORE_1
I
I
IINC 0 -1
I
I
GOTO loop
I
I
I
I
loop:
IFNE end body:
end:
ILOAD_1
ILOAD_1 IRETURN
Figuur 5.3: De operanden stapel en lokale variabelen aan het einde van de data-flow analyse.
Voorbeeld: referenties In het vorige voorbeeld bevatten zowel de operanden stapel als de lokale variabelen enkel primitieve types. Het volgende programmatje is een (nogal vergezocht) voorbeeld om de behandeling van referenties bij het eenmaken
67
van een operanden stapel toe te lichten. Het is gebaseerd op een voorbeeld uit Programming for the Java virtual machine - Joshua Engel. Volgende klassen zijn van belang: abstract public class Mens{ abstract public void groet(); } public class Man{ public void groet(){ ... } } public class Vrouw{ public void groet(){ ... } } Beschouw nu de onderstaande ‘groetMens’ methode in Java en daarnaast in bytecode. Deze bytecode doet exact hetzelfde als de Java variant, maar zal vrijwel nooit op deze manier door een Java compiler gegenereerd worden. public static void groetMens ILOAD_0 (Boolean keuze, Man man, Vrouw vrouw) IFEQ false { true: ALOAD_1 if (keuze) man.groet(); GOTO groet else vrouw.groet(); false: ALOAD_2 return; groet: INVOKEVIRTUAL Mens/groet } RETURN We beginnen weer met de initialisatie van de lokale variabelen van de eerste instructie. Figuur 5.4 stelt deze situatie voor. Merk op dat een Boolean type intern door de JVM als een Int wordt voorgesteld. X changed bit
I operanden stapel
Vrouw lokale variabelen
ILOAD_0
Man label
instructie
Figuur 5.4: De initi¨ele operanden stapel en lokale variabelen. Tot en met de goto instructie is er niets verschillend ten opzichte van het vorige voorbeeld. Twee instructies komen nu in aanmerking voor behandeling: aload 2 en invokevirtual. Figuur 5.5 toont de situatie na behandeling van de goto instructie.
68
I
Vrouw
X X
Vrouw
?
I
Vrouw
Man
ILOAD_0
I
Vrouw
Man
IFEQ false
I
Vrouw
Man
I
Vrouw
Man
I
Vrouw
Man
false:
ALOAD_2
I
Vrouw
Man
groet:
INVOKEVIRTUAL Mens/groet
?
?
?
true:
ALOAD_1 GOTO groet
RETURN
Figuur 5.5: De operanden stapel en lokale variabelen na uitvoering van de goto instructie.
Stel nu dat de aload 2 instructie wordt geselecteerd. Na deze instructie zal de stapel ´e´en operand bevatten, met name een referentie naar een Man object. Wanneer deze operanden stapel nu vergeleken wordt met de operanden stapel van de volgende instructie, zien we dat de referenties niet overeen komen. De eerste bevat een referentie naar een Man object, de tweede een referentie naar een Vrouw object. Om deze twee operanden stapels samen te voegen gaan we op zoek naar hun kleinste gemeenschappelijke superklasse. Daartoe vergelijken we de klasse hi¨erarchie van beide klassen. De kleinste gemeenschappelijke superklasse is in dit geval de Mens klasse. De eengemaakte operanden stapel bevat ´e´en operand, met name een referentie van het type Mens. Figuur 5.7 toont de eindsituatie. Object
Object
Mens
Mens
Vrouw
Man
Figuur 5.6: Klasse hi¨erarchie
69
I
Vrouw
X
Mens
I
Vrouw
Man
ILOAD_0
I
Vrouw
Man
IFEQ false
I
Vrouw
Man
I
Vrouw
Man
I
Vrouw
Man
false:
ALOAD_2
I
Vrouw
Man
groet:
INVOKEVIRTUAL Mens/groet
I
Vrouw
Man
true:
ALOAD_1 GOTO groet
RETURN
Figuur 5.7: De operanden stapel en lokale variabelen aan het einde van de data-flow analyse.
5.4.3
Uitbreidingen op het data-flow algoritme
Data types van categorie 2 Voor datatypes van categorie 2, met name de long en double types, moeten speciale voorzorgen genomen worden tijdens de data-flow analyse. De waarden van deze types nemen twee opeenvolgende plaatsen van de operanden stapel of de lokale variabelen lijst in. Wanneer een long of double in de lokale variabele met index n bewaard wordt, dan krijgt de lokale variabele met index n+1 een speciale ‘unusable’ waarde. Wanneer een waarde in de lokale variabele met index n bewaard wordt, dan moet worden nagegaan of de lokale variabele met index n-1 een long of een double bevat. Indien dat zo is, dan wordt de lokale variabele met index n-1 ook als ‘unusable’ gemarkeerd. Een long of double op de operanden stapel wordt tijdens de data-flow analyse als twee waarden beschouwd. Wanneer bijvoorbeeld een dadd instructie wordt uitgevoerd, wordt gecontroleerd of er zich vier van deze waarden op de operanden stapel bevinden. Twee zulke samenhorende waarden moeten altijd als een ondeelbaar geheel beschouwd worden. De dup instructie kan bijvoorbeeld niet worden toegepast op een type van categorie 2. Deze instructie zal immers enkel de bovenste plaats van de stapel kopi¨eren en zo deze twee samenhorende waarden foutief opsplitsen. In de plaats van de dup, dient dup2 gebruikt te worden. Deze instructie kopieert de twee bovenste waarden gelijktijdig.
70
Try-finally constructie De try-finally constructie kan niet met het hoger beschreven algoritme geverifieerd worden. Het probleem ligt erin dat de finally handler vanaf diverse plaatsen in het programma kan bereikt worden. Op een punt in het programma dat via verschillende paden kan bereikt worden, wordt normalerwijze een eengemaakte lokale variabelen lijst gemaakt. Incompatibele types worden hierbij als ‘unusable’ gemarkeerd. De finally handler is uniek omdat de situaties die een subroutine sprong naar de finally handler bewerkstelligen, zo verschillend zijn. Zo kan een bepaalde lokale variabele, afhankelijk van de situatie, volgende types bevatten: • een uitzondering: als de jsr instructie uit een exception handler afkomstig is • een return waarde: als de jsr instructie wordt opgeroepen vlak voor de return instructie, met andere woorden bij normale be¨eindiging van het programma • een andere waarde: indien de jsr instructie vanaf een willekeurig punt wordt aangeroepen Dit heeft tot gevolg dat deze lokale variabelen bij de eenmaking van de lokale variabelen lijst als ‘unusable’ gemarkeerd worden. Voor de finally handler zal dit normaal geen probleem geven, maar wanneer de subroutine terugkeert, zullen deze waarden niet meer gebruikt kunnen worden. Zo zal de exception handler de uitzondering niet meer kunnen verder gooien, en de return instructie het return type niet meer kunnen teruggeven. Daarom is een speciaal regime bedacht voor jsr en ret instructies: • Voor iedere instructie in de finally handler wordt een lijst met jsr instructies bijgehouden, die noodzakelijk zijn om die instructie te bereiken. In de meeste gevallen zal deze lijst slechts ´e´en instructie bevatten. Slechts in de bijzonder uitzonderlijke situatie van geneste try-finally constructies kan deze lijst langer zijn. • Voor al deze instructies wordt per jsr instructie uit die lijst een bit vector bijgehouden. Wanneer de n-de bit aanstaat, dan betekent dit dat de lokale variabele met index ‘n’ gebruikt is sinds de aanroep van de bijhorende jsr instructie. • Na het aanroepen van de ret instructie, wordt een alternatief algoritme gebruikt om de lokale variabelen samen te voegen met de lokale variabelen van de opvolgende instructies. Voor iedere lokale variabele ‘n’ zijn er twee mogelijke situaties:
71
– de bit vector geeft aan dat de n-de lokale variabele gebruikt is tijdens de uitvoering van de finally handler : als n-de lokale variabele van de samengevoegde lokale variabelen lijst, kiezen we het type zoals v´o´ or uitvoering van de ret instructie – de bit vector geeft aan dat de n-de lokale variabele ongebruikt is tijdens de uitvoering van de finally handler : als n-de lokale variabele van de samengevoegde lokale variabelen lijst, kiezen we het type zoals v´o´ or uitvoering van de jsr instructie
5.5
De vierde verificatie stap[4][11][23]
De vierde verificatie stap houdt alle run-time controles in, die ook statisch zouden kunnen uitgevoerd worden. Het is omwille van de performantie dat deze testen worden uitgesteld tot de uitvoering van het programma. Meestal vinden deze controles slechts eenmaal plaats; namelijk de eerste keer dat er naar een bepaald type wordt verwezen, een bepaalde methode wordt aangeroepen, of een veld wordt benaderd. Bij de eerste verwijzing naar een type, wordt gecontroleerd of: • de bijbehorende klasse reeds geladen is; indien dit niet het geval is, wordt de klasse nu geladen • de aanroepende methode de juiste rechten heeft om naar deze klasse te verwijzen Bij de eerste benadering van een methode of veld, wordt gecontroleerd of: • de methode of het veld kan terug gevonden worden in de opgegeven klasse • de descriptor overeenkomt met de opgegeven descriptor • de aanroepende methode voldoende toegangsrechten heeft voor het aanroepen van de methode of het veld
5.6
Invloed op de beveiliging[4][12]
De besproken controles vallen uiteen in een drietal categorie¨en, degene die: 1. de correcte werking aantonen 2. type-safety verzekeren 3. toezien op de naleving van de toegangsvoorwaarden 72
De eerste categorie van testen zijn voornamelijk het terrein van de eerste en tweede verificatie stap, en het control-flow gedeelte van de derde verificatie stap. De invloed op de beveiliging is hier moeilijker in te schatten dan voor de andere categorie¨en, maar is zeker aanwezig. De impact is zeer implementatie-afhankelijk. Zo kan een sprong buiten de code leiden tot het crashen van de ene JVM, of de uitvoering van willekeurige code op een andere JVM. Een voorbeeld waar het falen van een van de testen uit deze categorie heeft geleid tot een beveiligingsprobleem, is in maart 1996 ontdekt door David Hopwood. Het toenmalige netscape 2.01 controleerde niet alle naamgevingsregels voor Java klassen. Zo kon men door de naam van een klasse te laten beginnen met een ‘/’ toegang verkrijgen tot eender welk bestand op de harde schijf. Voor de tweede categorie van testen is de data-flow analyse verantwoordelijk. Type-safety is een van de hoekstenen van de beveiligingsarchitectuur. Het volgende voorbeeld, afkomstig uit Securing Java - E. Felten & G. McGraw, maakt duidelijk hoe men de verwarring van twee types zou kunnen uitbuiten. Stel dat een applet twee references heeft naar hetzelfde object, namelijk object1. De ene reference wordt aanzien als een verwijzing naar een instantie van type T, terwijl de andere als een verwijzing naar een instantie van klasse U wordt beschouwd. T en U zijn als volgt gedefinieerd: class T{ SecurityManager x; } class U{ MyObject x; } Het applet voert nu onderstaande code uit: T t = object1; U u = object1; t.x = System.getSecurity(); //Security Manager MyObject m = u.x; Op deze manier heeft het applet een referentie naar de Security Manager, door de JVM beschouwd als een instantie van de MyObject klasse. Het applet kan nu zonder enige controle alle velden van de Security Manager aanpassen. De derde categorie betreft alle toegangscontrole testen uitgevoerd tijdens de vierde stap van de verificatie. De invloed op de beveiliging is overduidelijk. Zonder deze controles is data-encapsulatie onmogelijk, en kan men zonder meer private velden in bijvoorbeeld de Security Manager benaderen. 73
5.7
Faalt enkel gevaarlijke code voor de verifier?[4]
Niet alles kan statisch gecontroleerd worden. Zo is het bijvoorbeeld niet in alle gevallen mogelijk om te bewijzen of een programma al dan niet zal eindigen. In dit verband kan men ook niet voor alle programma’s met zekerheid aantonen of ze de stapel al dan niet zullen doen overlopen. Hoe doet de JVM verifier dit dan? Voor bepaalde categorie¨en van programma’s, is het zeer eenvoudig om bovenstaande eigenschappen te bewijzen. Zo zal men van een programma zonder control-flow instructies met zekerheid kunnen zeggen dat het zal eindigen en een eindige stapel zal achterlaten. Wat de JVM verifier doet, is trachten te bewijzen dat de onderzochte bytecode tot zo’n categorie van veilige programma’s behoort. Dit wil dus zeggen dat de verifier niet alle ongevaarlijke programma’s zal goedkeuren, maar enkel diegene, die via een eenvoudig algoritme kunnen geverifieerd worden. Onderstaand programma is een voorbeeld van zo’n veilig programma dat zal falen voor de data-flow analyse. ICONST_5 ISTORE_0 loop: IINC 0 -1 ILOAD_0 DUP IFEQ break GOTO loop break: ... Dit programma plaatst de getallen 4 tot en met 0 op de operanden stapel, en gaat dan verder met de rest van het programma. Als de maximale stapelgrootte niet te laag is gekozen, dan zal dit programma zich niet onveilig gedragen. Toch faalt het voor de data-flow analyse, omdat de operanden stapel bij iedere aanroep van de instructie met label ‘loop’ ´e´en operand meer bevat dan bij de vorige aanroep. Bij het ontwerpen van de verifier heeft men dus condities vastgelegd, die indien voldaan een correcte werking van het programma garanderen. Bij het defini¨eren van deze condities, heeft men een afweging gemaakt tussen volledigheid en snelheid waarmee de condities kunnen gecontroleerd worden.
74
Hoofdstuk 6
Implementatie In het vorige hoofdstuk hebben we de verifier besproken zoals beschreven in de JVM specificatie. In dit hoofdstuk bespreken we een eigen implementatie, bedoeld voor embedded systemen, die we uitgaande van deze vrij beschikbare specificatie gemaakt hebben. We bespreken hier de kenmerken van de eigen implementatie, de aanpassingen van het algoritme, de integratie met het Acunia platform, en het testen van de verifier.
6.1
Inleiding
De ontwikkeling van een Java verifier is een complexe aangelegenheid. De eerste stap is het zorgvuldig bestuderen van de 475 pagina’s dikke JVM specificatie. Daaruit dienen vervolgens een 600-tal verschillende axioma’s gedistilleerd te worden. Tenslotte moet er voor ieder van deze axioma’s correcte controles ge¨ımplementeerd worden; daarvoor zijn ongeveer 20.000 lijnen C code nodig. Bewijs dat een correcte implementatie geen evidentie is, wordt geleverd door de grote hoeveelheid fouten gevonden in verscheidene commerci¨ele verifiers, waaronder de referentie implementatie van Sun. Het is dan ook niet onze doelstelling om een volledige verifier te implementeren, dit is immers onmogelijk in het tijdsbestek van de thesis. We hebben daarentegen getracht om een structuur te ontwikkelen die een aantal controles implementeert, en toelaat om de overige controles later toe te voegen. De ontwikkelde verifier heeft enkele interessante kenmerken, die we hier even overlopen: • geheugenvriendelijk • bruikbaar als alleenstaand programma of als deel van een JVM • clean room implementatie • implementatie in C 75
• verzamelde gegevens kunnen gebruikt worden door JIT compiler • eenvoudig uit te breiden met pre-verifier capaciteit geheugenvriendelijk Het verificatie algoritme zoals besproken in hoofdstuk 5, gebruikt een tamelijk grote hoeveelheid geheugen. Wonka, de JVM van Acunia, richt zich op de wereld van de embedded systemen, zoals het Acunia platform, waar geheugen meestal niet in overvloed aanwezig is. Daarom hebben we getracht dit geheugengebruik zoveel mogelijk te beperken, en dit op twee vlakken. Ten eerste gebruiken we geheugenvriendelijke datastructuren, zoals bijvoorbeeld gelinkte lijsten en bit vectors. Ten tweede hebben we enkele wijzigingen aan het besproken algoritme aangebracht, daar gaan we verder op in. bruikbaar als alleenstaand programma of als deel van een JVM De verifier is in de eerste plaats bedoeld om ge¨ıntegreerd te worden met Wonka. Maar in plaats van de ontwikkeling rechtstreeks op dit platform toe te spitsen, hebben we ervoor geopteerd om een alleenstaande applicatie te ontwikkelen, die nadien in het Acunia platform kan ge¨ıntegreerd worden. De voornaamste reden hiervoor is dat we de verifier op deze manier ook nog voor andere doeleinden kunnen gebruiken. Enkele mogelijke toepassingen: • een firewall die alle passerende applets controleert via een alleenstaande verifier. Zo worden gebruikers van oudere browsers beschermd tegen fouten in de ingebouwde verifier. • de meeste commercieel beschikbare verifiers verwachten tijdens hun normale werking zeer sporadisch met incorrecte code geconfronteerd te worden. Wanneer er toch een fout optreedt, dan is de foutmelding meestal ook zeer summier. De alleenstaande verifier zou in dit geval kunnen gebruikt worden om meer gegevens te verzamelen over de verificatie error. • de alleenstaande verifier kan ook gebruikt worden om meer informatie te verzamelen over een klasse. We zullen verder zien welke nuttige gegevens tijdens het verificatie proces verzameld worden. clean room implementatie Omwille van licentie overwegingen, moet het een volledige clean room implementatie zijn. Dit wil zeggen dat de implementatie enkel mag gebaseerd zijn op de vrij beschikbare specificatie. Sun maakt de programma code van hun JVM referentie implementatie vrij beschikbaar, maar die mag enkel
76
geraadpleegd worden voor onderzoeksdoeleinden en niet-commeri¨ele projecten. Willen we deze verifier ooit integreren met Wonka, dan mag deze code niet geraadpleegd worden. implementatie in C Wonka is –zoals de meeste JVM’s– volledig in de programmeertaal C geschreven. Daarom is gekozen om de verifier implementatie ook in C uit te voeren. De standaard bibliotheek van C functies beschikbaar voor het Acunia platform, is echter niet volledig compatibel met de ANSI standaard. Correcte programma’s schrijven in C, is meestal moeilijker dan een overeenkomstig programma schrijven in Java. Het is de bedoeling van de verifier om het platform veiliger te maken, we moeten dus zorgvuldig te werk gaan om geen nieuwe onveiligheden te introduceren. verzamelde gegevens kunnen gebruikt worden door JIT compiler Tijdens het verificatie proces gaat de verifier een aantal keren door de bytecode. Gedurende dit proces worden een hele hoop gegevens verzameld over de klasse. We hebben ervoor gezorgd dat de verzamelde gegevens direct bruikbaar zijn voor andere toepassingen. Zo kan de JIT compiler deze gegevens gebruiken om optimalisatietechnieken uit te voeren. Op deze manier worden dus enkele tijdrovende passages van de bytecode overbodig. eenvoudig uit te breiden met pre-verifier capaciteit Sun heeft een aangepaste virtuele machine, de KVM, ontwikkeld voor zeer geheugenbeperkte systemen uit de CLDC categorie. Deze virtuele machine, de KVM genaamd, past een alternatief verificatie algoritme toe. Zo wordt een klasse bestand eerst ‘ge-pre-verified’, waarbij informatie over de inhoud van de operanden stapel en lokale variabelen in de klasse bewaard wordt. Deze informatie wordt dan tijdens de eigenlijke verificatie van het klasse bestand gebruikt om dit proces geheugenvriendelijker en sneller te maken. De specificatie voor deze KVM was niet beschikbaar toen we aan de eigen implementatie begonnen, waardoor we ze ook niet compatibel konden maken. Nu de specificatie aanwezig is, blijkt dat we die compatibiliteit op eenvoudige wijze kunnen verwezenlijken. We komen daar later op terug.
6.2
Overzicht van het programma
We geven hier een overzicht van de werking van de verifier. Het programma dient opgestart te worden met als parameters de namen van de klassen die men wil verifi¨eren. Zo zal bijvoorbeeld ‘verifier Vrouw.class Man.class
77
Mens.class’ deze drie klassen tegelijk behandelen. Volgende punten worden sequentieel uitgevoerd: 1. allocatie van een tabel waarin de klassen kunnen geladen worden 2. allocatie van een hash table voor het klassen diagram 3. initialisatie van het klassen diagram 4. voor alle klasse bestanden: • openen van het klasse bestand • de eerste verificatie stap • sluiten van het klasse bestand 5. opbouw van het klassen diagram: • alle klassen toevoegen aan het klassen diagram • controleren of superklassen en interfaces geladen zijn • directe verwijzingen naar superklassen en interfaces aanmaken 6. voor alle klasse bestanden: • de tweede verificatie stap 7. voor alle klasse bestanden: • de derde verificatie stap, bestaande uit de control-flow en dataflow analyse Iets dat doorheen het hele programma terugkomt, is de wijze waarop een aangeroepen functie signaleert dat er iets is misgelopen tijdens de uitvoering. Voor de meeste functies zal de return value ‘true’ duiden op een correcte uitvoering, en ‘false’ op een fout. De uitvoer naar het scherm is afhankelijk van het ingestelde debug level, zo is er ‘debug’, ‘low’, ‘normal’, ‘high’, ‘error’, en ‘quiet’. Het eerste geeft alle details, het laatste daarentegen enkel of de verificatie al dan niet geslaagd is. Het eerste punt is de allocatie van een klassen tabel. In deze tabel zullen tijdens het verdere verloop alle klassen geladen worden. De structuur van deze interne klasse voorstelling wordt besproken wanneer we de eerste verificatie stap behandelen.
78
Het tweede punt is de allocatie van een hash table. Deze hash table wordt gebruikt als voorstelling van de klassen hi¨erarchie en wordt het klassen diagram genoemd. typedef struct hash_table_item{ enum type {INTERFACE_TYPE, CLASS_TYPE} type; CONSTANT_Utf8_info class_name; class_file *class; struct hash_table_item *super_class; u2 interfaces_count; struct hash_table_item **interfaces; struct hash_table_item *next; } hash_table_item; typedef hash_table_item *hash_table[MAX_NB_CLASSES]; Een ‘hash table’ bestaat dus uit een aantal elementen van het type ‘hash table item’, die belangrijke informatie over de klasse bevatten. Deze informatie bevindt zich ook in de interne klasse structuur, maar meestal onder een minder bruikbare vorm, zoals bijvoorbeeld vlaggen of symbolische referenties. In Wonka is zo’n hash table overbodig omdat deze functionaliteit reeds voorzien is. ‘class’ verwijst dan naar de interne representatie van de klasse, het is met andere woorden een verwijzing naar een element van de hoger beschreven klassen tabel. ‘super class’ en ‘interfaces’ bevatten dan weer rechtstreekse verwijzingen naar andere hash table items, zodat het herhaaldelijk opzoeken van de symbolische referenties vermeden wordt. De hash table gebruikt gelinkte lijsten in het geval van collisions. Het laatste onderdeel van een hash table item is ‘next’, een verwijzing naar het volgende hash table item in de gelinkte lijst. Het aantal hash table items wordt tijdens het compileren vastgelegd, hiervoor kan best een priemgetal van de grootteorde van het aantal te laden klassen gekozen worden. De hash waarde wordt via een eenvoudige functie berekend uit de volledige benaming van de klasse. In het derde punt wordt het klassen diagram ge¨ınitialiseerd. Dit houdt in dat er hash table items voor enkele essenti¨ele basisklassen en interfaces aangemaakt worden. In JVM voorstelling zijn dat volgende klassen: • java/lang/Object • java/lang/String • java/lang/Cloneable • java/io/Serializable 79
• java/lang/Throwable • java/lang/Error • java/lang/Exception Het vierde punt is het eigenlijke laden van de klassen in de klassen tabel. Voor iedere klasse worden volgende stappen herhaald. Eerst wordt het bestand geopend. De zo verkregen FILE pointer wordt dan doorgegeven aan de ‘pass1’ functie, die de eerste stap van het verificatie algoritme uitvoert. Indien de verificatie slaagt, dan wordt de interne voorstelling van deze klasse toegevoegd aan de klassen tabel. Deze stap wordt verder gedetailleerd uitgelegd. Tenslotte wordt het bestand gesloten. Het vijfde punt is het toevoegen van de geladen klassen aan het klassen diagram. Eerst wordt voor iedere klasse een hash table item aangemaakt. Vervolgens wordt nagegaan of voor iedere klasse haar superklasse en superinterfaces geladen zijn; indien dit niet het geval is, eindigt de verifier met de vraag om deze klassen toe te voegen aan de lijst van te laden klassen. Het is dus noodzakelijk om alle klassen in de hi¨erarchie tegelijk te laten controleren. Na deze controles worden de directe referenties naar de superklassen en superinterfaces aan de hash table items toegevoegd. Tijdens het zesde en zevende punt worden door het aanroepen van de ‘pass2’ en ‘pass3’ functies, respectievelijk de tweede en derde verificatie stap uitgevoerd. Deze worden verder in meer detail besproken.
6.3
De eerste verificatie stap
De eerste verificatie stap wordt ge¨ımplementeerd in de ‘pass1’ functie. Tijdens deze verificatie stap wordt het klasse bestand in onderstaande interne klasse structuur geladen. struct class_file{ u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info *constant_pool; u2 access_flags; cp_info *this_class; cp_info *super_class; u2 interfaces_count; cp_info **interfaces; 80
u2 fields_count; field_info *fields; u2 methods_count; method_info *methods; u2 attributes_count; attribute_info *attributes; } Het hoogste niveau van deze interne klasse structuur is bijna identiek aan het class file formaat gedefinieerd in de JVM specificatie. Meestal zijn de constant pool offsets wel vervangen door directe referenties naar de constant pool. De beslissing om de interne klasse structuur zo nauw te laten aansluiten bij het class file formaat, bleek achteraf niet de meest elegante oplossing. Voornamelijk voor constant pool items leidt dit vaak tot een lange opeenvolging van verwijzingen. De volledige klasse structuur kan u in bijlage vinden. Voor elk van de onderdelen van de interne klasse voorstelling, is een functie voorzien die vanuit ‘pass1’ wordt opgeroepen. Zo zal bijvoorbeeld ‘load constant pool(file*, class file*)’ de constant pool laden. Tijdens dit laden worden de controles reeds uitgevoerd.
6.4
De tweede verificatie stap
De tweede verificatie stap wordt door de ‘pass2’ functie aangeroepen. Voorlopig is voor de alleenstaande verifier geen strikt onderscheid gemaakt tussen controles die in de eerste en de tweede verificatie stap thuis horen. Momenteel wordt hier voornamelijk gecontroleerd op referenties naar de constant pool. Bij de uiteindelijk integratie met het Acunia platform, dient er wel onderscheid gemaakt te worden tussen controles uit de eerste en de tweede verificatie stap.
6.5
Control-flow analyse[1]
De control-flow analyse wordt vanuit de derde verificatie stap aangeroepen. Het algoritme zoals beschreven in de JVM specificatie gaat hiervoor tweemaal door de code. Bij de eerste doorgang wordt het begin van iedere instructie ge¨ıdentificeerd, en bewaard in een data structuur. Tijdens de tweede doorgang gebeuren de eigenlijke testen: controle van de argumenten en control-flow instructies. We hebben dit algoritme grondig aangepast, zodat: • slechts ´e´en doorgang door de code noodzakelijk is, 81
• meer informatie verzameld wordt over de bytecode, en • de data-flow analyse minder geheugen zal innemen. In wat volgt bespreken we het aangepaste algoritme.
6.5.1
Control-flow graphs
Een control-flow graph is een manier om de verschillende paden binnen een programma voor te stellen. De control-flow graph is een ge¨orienteerde, samenhangende grafe G = (N,E), waarbij N de verzameling van nodes en E de verzameling van edges is. De nodes stellen de programma instructies voor, terwijl de edges de ‘flow of control’ voorstellen.
int x = 0, y = 0; for (int i = 0; i < 1000; i++) { if ((i % 5) == 0) { x = x + 1; } else { y = y + 1; } } return Figuur 6.1: Control-flow graph
Figuur 6.1 toont een Java programma en de bijhorende control-flow graph. De nodes in deze figuur noemen we basic blocks (BBs). Dit is een opeenvolging van instructies met slechts ´e´en ingangspunt en ´e´en uitgangspunt. Deze gegevens kunnen we tijdens de data-flow analyse gebruiken, om zo het benodigde geheugen te beperken. We zullen aantonen hoe we deze structuur effici¨ent en in lineaire tijd kunnen opstellen, tijdens het uitvoeren van bovenvermelde controles.
6.5.2
Algoritme
Om dit algoritme mogelijk te maken hebben we twee nieuwe datatypes gedefinieerd: typedef struct basic_block_ll{ u4 start_byte; u4 nb_in; 82
struct basic_block_ref_ll *in; u4 nb_out; struct basic_block_ref_ll *out; struct basic_block_ll *next; } basic_block_ll; typedef struct basic_block_ref_ll{ u4 reference; struct basic_block_ref_ll *next; } basic_block_ref_ll; Een ‘basic block ll’ is een gelinkte lijst die de BB structuur van de bytecode beschrijft. Wanneer er control flow is van BB 1 naar BB 2, dan zeggen we dat BB 1 bij BB 2 geregistreerd is. We zullen dan het ‘out’ veld van BB 1 voorzien van een ‘basic block ref ll’, dat naar BB 2 verwijst; en op dezelfde wijze zullen we het ‘in’ veld van BB 2 voorzien van een ‘basic block ref ll’, dat naar BB 1 verwijst. De ‘basic block ll’ elementen beschrijven dus met andere woorden de nodes van de control-flow graph, terwijl de edges door de ‘basic block ref ll’ elementen beschreven worden. Initieel beschouwen we de bytecode als bestaande uit ´e´en BB. Wanneer we tijdens het doorlopen van de code control-flow instructies tegenkomen, kan deze initi¨ele BB opgedeeld worden in kleinere BBs. Als deze BBs dan op correcte wijze bij elkaar geregistreerd worden, dan zal de resulterende structuur de volledige control-flow graph beschrijven. Het volledige algoritme is als volgt: 1. alloceer een bit vector ‘instructie’ bestaande uit ‘code length’ bits 2. alloceer een bit vector ‘blok’ bestaande uit ‘code length’ bits 3. alloceer de initi¨ele ‘basic block ll’ 4. stel i gelijk aan nul 5. zet de i-de bit van ‘instructie’ aan 6. controleer of de i-de byte uit de bytecode een geldige opcode bevat 7. controleer of de opvolgende bytes correcte argumenten bevatten 8. in het geval van een • control-flow instructie: – maak voor alle doelinstructies alsook de volgende instructie een nieuw BB aan, indien het nog niet bestaat 83
– indien op byte Y een nieuwe BB aangemaakt is, zet dan de Y-de bit van ‘blok’ aan – registreer de BBs waartussen control flow is • gewone instructie: – indien er een nieuw BB begint bij de volgende instructie: registreer het huidige BB bij het BB van de volgende instructie 9. tel het aantal argumenten bij i op 10. indien i < code length, ga verder met stap 5 De twee bit vectors kunnen nu gebruikt worden om te controleren of er geen illegale sprongen gemaakt zijn. De ‘instructie’ vector geeft het begin van iedere instructie aan; de ‘blok’ vector geeft ons het begin van iedere BB. We kunnen nu steunen op het feit dat iedere BB bij het begin van een instructie begint, en dat iedere doelinstructie van een control-flow instructie de eerste instructie van een BB is. Om te controleren op illegale sprongen hoeven we dus enkel deze twee bit vectoren te vergelijken: indien het begin van een bepaald BB niet overeenkomt met het begin van een instructie, dan weten we dat er een illegale sprong heeft plaatsgevonden. Dit kan heel effici¨ent gecontroleerd worden door ‘blok == blok & instructie’ te evalueren. Indien dit waar/onwaar geeft, dan heeft er zich geen/een illegale sprong voorgedaan. De gegevens uit de gelinkte lijst van BBs worden nu gebruikt om de control-flow graph in een compactere tabelstructuur te beschrijven. Deze tabel wordt aan het code attribuut toegevoegd om tijdens de data-flow analyse gebruikt te kunnen worden. Het voor de gelinkte lijst gealloceerde geheugen wordt terug vrijgegeven. De in stap 7 uitgevoerde opcode-afhankelijke controles op de argumenten zijn opgedeeld in 23 verschillende categori¨en. Er is een tabel met functie pointers voorzien, die voor iedere opcode de juiste controlefunctie bevat. Op deze manier wordt een lange switch constructie vermeden. De opdeling in de vermelde 23 categori¨en gebeurt aan de hand van het type instructie (control-flow of gewone intructie) en de eisen gesteld aan de argumenten. Waarom hebben we voor een gelinkte lijst structuur gekozen? Als alternatief zou men aan het begin van de control-flow analyse een grote tabel kunnen alloceren, waarin de relevante gegevens bewaard worden. Vermits we niet op voorhand weten hoeveel instructies de bytecode bevat, en dus zeker niet uit hoeveel BBs het programma opgebouwd is, moet de tabel evenveel elementen bevatten als het aantal bytes van de code. Door met een gelinkte lijst te werken kunnen we het geheugengebruik milderen, maar 84
betalen daarvoor wel de prijs van een complexer algoritme en een kleine overhead.
6.5.3
Voorbeeld
Om het hoger beschreven algoritme toe te lichten, passen we het toe op een concreet voorbeeld. We nemen daarvoor hetzelfde programma als in figuur 6.1, en gaan na hoe we met behulp van ons algoritme uit de bijbehorende bytecode voldoende gegevens kunnen verzamelen om de control-flow graph op te stellen. De bytecode voor het programma:
(2)
(3)
0 1 2 3 4 5 6 9 10 11 12 15 16
ICONST_0 ISTORE_1 ICONST_0 ISTORE_2 ICONST_0 ISTORE_3 GOTO 29 ILOAD_3 ICONST_5 IREM IFNE 22 ILOAD_1 ICONST_1
(4)
17 18 19 22 23 24 25 26 29 30 33 36
IADD ISTORE_1 GOTO 26 ILOAD_2 ICONST_1 IADD ISTORE_2 IINC 3 1 ILOAD_3 SIPUSH 1000 IF_ICMPLT 9 RETURN
Figuur 6.2 toont verschillende fases in de opbouw van de BB structuur. (1) is de initi¨ele situatie, de bytecode wordt als ´e´en groot BB aanzien. (2), (3) en (4) geven de situatie na behandeling van respectievelijk de eerste, tweede en derde control-flow instructie. (5) geeft dan uiteindelijk de volledige structuur weer. Als we dit nu vergelijken met de control-flow graph uit figuur 6.1, dan zien we dat ze dezelfde situatie voorstellen.
6.6
Data-flow analyse
Statische verificatie is wat betreft geheugengebruik geen goedkoop proces. Het grootste deel van dit geheugengebruik is voor rekening van de data-flow analyse. Specifiek voor embedded systemen met een beperkte hoeveelheid beschikbaar geheugen, loont het dan ook de moeite om eens na te gaan hoe we dit geheugengebruik naar beneden kunnen halen.
6.6.1
Aangepast algoritme
Herinner u uit de beschrijving van het data-flow algoritme uit hoofdstuk 5, dat wanneer een instructie tijdens dit iteratieve proces voor de eerste maal behandeld wordt, een kopie van de lokale variabelen en operanden stapel 85
in
0
0
in
8
out
29
in
9
0
in
8
out
9
in
14
out
15
15
in
12
29
21 out in
22 28 out 29
in
12
28 out 6
29
in
6
22
0
in
8
out
29
9
in
33
14 15
out
15
in
12
12
21
out
26
out
26
22
in
12
22
in
12
25 out
26
25
out
0
in
8
out
9 14
in out
15
15
in
21
26
in
28
out
29
in
36 out
36 out
36 out
36 out
(1)
(2)
(3)
(4)
29
19
6
22
26
in
19
28
out
29
29
in
6
26
35 out
9
36
in
33
36
out (5)
Figuur 6.2: Opeenvolgende stappen bij het aanmaken van de control-flow graph
met deze instructie geassocieerd wordt. Wanneer deze instructie later via een alternatief pad bereikt wordt, dan worden deze gegevens gebruikt om te controleren of de operanden stapel compatibel is, en vervolgens om een samengevoegde lokale variabelen lijst en operanden stapel te construeren. We kunnen ons de vraag stellen of het noodzakelijk is om alle instructies van een kopie van de lokale variabelen en operanden stapel te voorzien. Een groot deel van deze instructies zullen immers maar via ´e´en pad rechtstreeks bereikt kunnen worden. Hier kan de control-flow graph ons helpen. In plaats van ´e´en kopie per instructie, bewaren we nu nog maar ´e´en kopie per BB. Dit volstaat omdat we weten dat control flow enkel naar de eerste instructie van een BB vloeit. Het algoritme blijft grotendeels ongewijzigd. Waar we vroeger instructie per instructie bekeken, behandelen we nu een BB altijd in zijn geheel. Eerst bepalen we aan de hand van de methode descriptor het type van de parameters, die zich bij het aanroepen van de methode in lokale variabelen zullen bevinden. Het eerste BB wordt ge¨ınitiamiseerd met een lege operanden stapel en de zojuist bepaalde parameters in de lokale variabelen lijst. 86
22
25
De changed bit van het eerste BB wordt aangezet. Vervolgens start het algoritme: 1. Selecteer een BB met de changed bit aan. Indien zo geen BB kan gevonden worden, dan is de data-flow analyse geslaagd. 2. Boots het effect van de opeenvolgende instructies in het geselecteerde BB op de operanden stapel en lokale variabelen na. Daarbij wordt gecontroleerd of: • er voldoende operanden beschikbaar zijn op de stapel • de operanden van het correcte type zijn • de gebruikte lokale variabelen van het correcte type zijn • er voldoende plaats is op de operanden stapel voor het resultaat 3. Bepaal de volgende BBs aan de hand van het ‘out’ veld in het huidige BB. 4. Voeg de status van de operanden stapel en lokale variabelen, na het nabootsen van de opeenvolgende instructies, samen met de operanden stapel en lokale variabelen van elk van de opvolgende BBs. • Indien dit de eerste keer is dat het opvolgende BB behandeld wordt, dan moet bij dit BB een onveranderde kopie van de operanden stapel en lokale variabelen bewaard worden. Zet vervolgens de changed bit aan. • Indien het BB reeds behandeld was, dan moeten de eerder vermelde samenvoegingsregels toegepast worden. De changed bit wordt enkel aangezet indien er aanpassingen gedaan zijn. 5. Ga verder met stap 1.
6.6.2
Voordelen
Om de invloed van het aangepaste algoritme op het geheugengebruik te kunnen inschatten, is het belangrijk om een idee te hebben over het gemiddelde aantal instructies per BB. Daartoe hebben we de control-flow analyse op een groot aantal klassen toegepast. Figuur 6.3 toont het aantal BBs ten opzichte van de lengte (het aantal bytes) van ca. 3.000 verschillende methodes. Gemiddeld bevat een BB 11,5 bytes bytecode, dit komt overeen met ca. 7 instructies. Dit betekent dat er gemiddeld gezien ook 7 keer minder geheugen ingenomen wordt door de lokale variabelen en operanden stapels. Wat een belangrijke geheugenbesparing is. De BB structuur neemt uiteraard zelf ook geheugen in, maar als een goede voorstelling gekozen wordt, dan blijft die overhead relatief beperkt. 87
160
140
120
basic blocks
100
80
60
40
20
0 0
200
400
600
800
1000
1200
1400
1600
bytes
Figuur 6.3: Vergelijking van het aantal bytes en basic blocks voor ca. 3000 methodes
Een bijkomend voordeel is dat de opvolgende instructies effici¨enter kunnen bepaald worden. In plaats van deze bij iedere iteratie opnieuw te bepalen, hoeft dit slechts eenmaal, tijdens de control-flow analyse, te gebeuren. Deze informatie zit immers in de control-flow graph vervat. Vooral in het geval van de tableswitch, of lookupswitch instructies en voor exception handlers is dit belangrijk, daar het bepalen van de opvolgende instructies in deze gevallen moeilijker is.
6.6.3
Implementatie
We lichten hier bondig de voornaamste voorzieningen die we voor het dataflow algoritme ge¨ımplementeerd hebben toe. Deze opsomming is geenszins
88
volledig, zo zijn er verder nog functies voorzien om: assignment compatibility te testen, lokale variabelen samen te voegen, types te manipuleren, descriptors te parsen, BBs te selecteren, . . . data-structuur voor types De eerste opdracht was het defini¨eren van een data-structuur om de types die zich op de operanden stapel of in de lokale variabelen lijst bevinden voor te stellen. We hebben gekozen voor volgende structuur. typedef struct type_def{ enum type {REFERENCE_TYPE, RETURN_ADDRESS_TYPE, NEW_REFERENCE_TYPE, INIT_REFERENCE_TYPE, BOOLEAN_TYPE, CHAR_TYPE, FLOAT_TYPE, DOUBLE_TYPE, BYTE_TYPE, SHORT_TYPE, NULL_TYPE, INTEGER_TYPE, LONG_TYPE, VOID_TYPE} type; u1 array_dim; u1 new_reference_nb; struct hash_table_item *class; } type_def; ‘type’ beschrijft de categorie van het beschouwde type: reference type: verwijzing naar klasse of interface instantie new reference type: verwijzing naar een niet-ge¨ınitialiseerd object init reference type: verwijzing naar een object tijdens initialisatie return address type: het terugkeer adres bij een subroutine sprong boolean type, byte type, char type, short type: deze primitieve Java types hebben slechts een beperkte JVM ondersteunig, en worden enkel in arrays gebruikt integer type, float type: primitieve types van categorie 1 float type, double type: primitieve types van categorie 2 void type: dit is geen echt type, en wordt enkel gebruikt om aan te geven dat er geen return type is null type: speciale referentie zonder bijhorende klasse Indien ‘array dim’ verschillend van nul is, dan is het type in kwestie een array, met als inhoud het beschreven type. Voor sommige types zal de ‘array dim’ altijd gelijk zijn aan nul, vermits er geen arrays van dat type bestaan. 89
‘new reference nb’ wordt enkel gebruikt om niet-ge¨ınitialiseerde objecten van elkaar te kunnen onderscheiden. Op het moment dat zo’n object ge¨ınitialiseerd wordt, moet het type van alle referenties naar dit object op de operanden stapel of in de lokale variabelen lijst aangepast worden. Tenslotte is er ‘class’, dat informatie over de klasse of interface van een object bevat. Het is een pointer naar het klassen diagram, en vermits iedere klasse daar maximaal eenmaal voorkomt, zal elk object afkomstig van dezelfde klasse dezelfde pointer bevatten. instructie-emulatie functies Om het effect van een instructie op de operanden stapel en de lokale variabelen na te bootsen, zijn 86 verschillende functies gedefinieerd. De oproep van deze functies wordt opnieuw verwezenlijkt via een tabel met functie pointers, die afhankelijk van de opcode naar een bepaalde functie verwijzen. De instructie-emulatie functies worden aangeroepen met een operanden stapel en lokale variabelen lijst als parameters. Daarop zullen de nodige controles uitgevoerd worden. Indien die succesvol zijn, dan wordt een aangepaste operanden stapel en lokale variabelen lijst teruggegeven. manipulatie functies voor operanden stapel en lokale variabelen De operanden stapel wordt intern door een gelinkte lijst voorgesteld, de lokale variabelen door een tabel. Voor beiden zijn een aantal elementaire functies voorzien. De meeste controles gerelateerd met de operanden stapel en de lokale variabelen worden in deze functies uitgevoerd. Zo worden lokale variabelen indien nodig bijvoorbeeld automatisch als ‘unusable’ gemarkeerd, dit is dus niet de verantwoordelijkheid van iedere instructie-emulatie functie.
6.7
Acunia
Voor Acunia is voornamelijk de implementatie van de derde verificatie stap belangrijk. Een aantal van de controles uit verificatie stap 1 en 2 waren reeds ge¨ımplementeerd. Om na te gaan hoe vlot de uiteindelijke integratie met Wonka zal verlopen, hebben we reeds ´e´en deel ge¨ıntegreerd, met name de control-flow analyse. Deze integratie verliep over het algemeen probleemloos. Veel tijd werd gespendeerd aan het leren kennen van de relevante onderdelen van Wonka en het installeren van de virtuele machine op het eigen systeem. Enkel kleine aanpassingen waren noodzakelijk: • interne klasse formaat: het interne klasse formaat in Wonka is erg verschillend aan het interne klasse formaat besproken in dit hoofd90
stuk. Alle verwijzigingen naar de interne klasse structuur dienden dus gewijzigd te worden. Meestal betroffen het vereenvoudigingen. • data types: de meeste data types gedefinieerd in Wonka hebben een tegenhanger met een andere benaming in de alleenstaande versie van de verifier, de oplossing betrof dus meestal een eenvoudige zoek en vervang opdracht. • geheugenallocatie: de geheugenallocatie functies uit de standaard bibliotheek werden vervangen door Wonka-specifieke functies. • uitzonderingen: waar de alleenstaande verifier enkel een ‘return false’ doet bij het falen van de verificatie, dient in Wonka de bijhorende uitzondering gegooid te worden. • uitvoer van debug info: ook hier betreft het de vervanging van functies uit de standaard bibliotheek door Wonka-specifieke functies.
6.8
Pre-verificatie[18]
KVM is een JVM van Sun, die zich op embedded systemen met zeer beperkte systeembronnen richt. Voor deze systemen zijn zowel de statische als de dynamische geheugenvereisten van de verifier te groot. Daarom heeft men ‘pre-verificatie’ stap ingevoerd, waarbij extra informatie aan het klasse bestand toevoegd wordt, die het verificatie proces veel sneller en minder geheugenvereisend maakt. We zullen nu kort uitleggen hoe dat in z’n werk gaat. Tijdens de pre-verificatie wordt het code attribuut voorzien van een nieuw ‘Stackmap attribute’, dat informatie over de inhoud van de operanden en lokale variabelen op welgekozen punten van het programma bevat. De toegevoegde informatie maakt een klasse bestand bij benadering 5% groter. De eerste stap van de verificatie van een ge-pre-verifiede klasse, is het alloceren van voldoende geheugen om ´e´en kopie van de operanden stapel en ´e´en kopie van de lokale variabelen lijst te bewaren. Dit is het enige geheugen dat dient gealloceerd te worden. De geheugenvereisten zijn dus enkel afhankelijk van de maximale grootte van de operanden stapel en lokale variabelen lijst, en niet van het aantal instructies in de bytecode. Vervolgens worden de lokale variabelen ge¨ınitialiseerd met de methode parameters. Nu start het eigenlijke verificatie algoritme, daarbij worden de instructies lineair doorlopen. Voor elke instructie wordt het volgende uitgevoerd: • Indien er geen control-flow is vanaf de vorige instructie, met andere woorden als de vorige instructie een onvoorwaardelijke sprong is, dan 91
worden de operanden stapel en de lokale variabelen gelijkgesteld aan de informatie gevonden in de stack map behorende bij deze instructie. Als de huidige instructie geen stack map heeft, dan faalt de verificatie. • Indien er wel control-flow is vanaf de vorige instructie, dan zal weer gekeken worden of er een stack map is behorende bij deze instructie. Als dat zo is, dan wordt gecontroleerd of de operanden stapel en de lokale variabelen compatibel zijn met wat beschreven staat in de bijhorende stack map. Indien ze compatibel zijn, dan worden operanden stapel en lokale variabelen gelijkgesteld aan de informatie beschikbaar in de stack map. In het andere geval faalt de verificatie. • Als er een exception handler is voor deze instructie, dan worden de operanden stapel en lokale variabelen getest op compatibiliteit met de informatie in de stack map. Als ze niet compatibel zijn, of als er geen stack map gevonden wordt, dan faalt de verificatie. • De verifier zal nu het effect van de huidige instructie op de operanden stapel en lokale variabelen nabootsen. • De operanden stapel en lokale variabelen worden nu gecontroleerd op compatibiliteit met de informatie beschikbaar in de stack map van alle instructies die de huidige instructie niet direct opvolgen. Als ze niet compatibel zijn, of als er geen stack map gevonden wordt, dan faalt de verificatie. Het voordeel van dit algoritme is tweeledig. Ten eerste is het geheugengebruik veel lager dan bij het normale verificatie algoritme. Enkel ´e´en kopie van de operanden stapel en de lokale variabelen dient bijgehouden te worden. Ten tweede is het geen iteratief proces, zoals het normale verificatie proces, maar wordt de code slechts eenmaal van voor naar achter doorlopen. Niet iedere instructie dient voorzien te worden van een stack map. De instructies die wel een stack map krijgen, komen altijd overeen met de eerste instructie van een BB. Hieruit volgt dat onze eigen verificatie implementatie eenvoudig als pre-verifier kan gebruikt worden. Na het doorlopen van de verificatie is namelijk alle noodzakelijke informatie beschikbaar: de BB structuur en de operanden stapel en lokale variabelen aan het begin van iedere BB. Om als pre-verifier te kunnen dienen, dient deze informatie enkel nog bewaard te worden in het Stackmap attribute formaat.
6.9
Testen[18]
Een belangrijk onderdeel van een implementatie is de testfase. Maar hoe kan men een verifier implementatie op correctheid testen? Dit blijkt niet 92
eenvoudig te zijn. Van een correcte verifier wordt verwacht dat: 1. alle klassen die zich niet aan de regels houden, zullen worden afgekeurd. 2. alle klassen die zich wel aan de regels houden, niet zullen worden afgekeurd. Verschillende testtechnieken zijn voorhanden: • formele technieken • manuele code analyse • manueel gegenereerde testen • publieke testen • automatische testen Het toepassen van formele testtechnieken is veelbelovend omdat we hiermee kunnen bewijzen dat een bepaalde implementatie overeenkomstig is met zijn formele specificatie. Er is al heel wat onderzoek gedaan in deze richting, maar voorlopig is men er nog niet in geslaagd om een verifier volledig te testen op deze wijze. Manuele code analyse en manueel genereren van testen heeft zijn nut in het verleden meer dan eens bewezen. Zo hebben verscheidene beveiligingsexperten, waaronder in het bijzonder het ‘Secure Internet Programming team’ van Princeton University, op deze manier talloze fouten in implementaties van de Java beveiligingsarchitectuur blootgelegd. Nadelig is dat deze testen zeer veel tijd in beslag nemen, en bijgevolg erg duur zijn. Het ‘Kimera project’ aan de University of Washington, heeft in een poging om het testen van verifier implementaties automatisch, snel en vollediger te maken, een automatische testomgeving ontwikkeld. Deze testen zijn gebaseerd op het aanbrengen van willekeurige veranderingen in een verzameling van basisklassen. Deze ‘gemuteerde’ klassen worden dan aan verschillende verifiers gevoed. Indien zo’n klasse door alle verifiers goedgekeurd of afgewezen wordt, dan kan uit de test niets afgeleid worden. Als de verifiers echter een verschillende beslissing nemen, dan is er een fout gedetecteerd. Deze fout wordt dan met de hand onderzocht. Op deze manier heeft het Kimera project tientallen fouten ontdekt in commercieel beschikbare verifiers, met name die van Sun, Netscape en Microsoft. De mutaties op basisklassen bestonden uit 1-byte aanpassingen. Met slechts 5 basisklassen, heeft men op deze manier 81% van de axioma’s getest. Van de axioma’s die op deze manier niet getest werden, had het grootste deel te maken met fouten tijdens het linken of redundante testen.
93
Een testomgeving zoals hierboven besproken is relatief eenvoudig op te zetten. Het zou dus interessant zijn om dit in een later stadium van de implementatie effectief te doen. Voorlopig hebben we enkel getest op de tweede eis die aan een correcte verifier gesteld wordt, met name dat code die zich aan de regels houdt niet zal falen voor de verifier. Na implementatie van de control flow analyse, hebben we de verifier op een groot aantal klassen, met name alle klassen uit de standaard bibliotheek, toegepast. Op deze manier hebben we enkele fouten ontdekt, die vervolgens verbeterd werden.
94
Hoofdstuk 7
Besluit In dit hoofdstuk willen we nagaan in hoe verre we onze oorspronkelijke doelstelling hebben kunnen verwezenlijken. Als praktische doelstelling hadden we de implementatie van een geheugenvriendelijke verifier vooropgesteld. In plaats van de ontwikkeling rechtstreeks op het Acunia platform toe te spitsen, hebben we ervoor geopteerd om een alleenstaande applicatie te ontwikkelen, die nadien in het Acunia platform kan ge¨ıntegreerd worden. De implementatie van de verifier kan in twee grote delen worden opgesplitst: de structuur en de eigenlijke testen. Met de structuur bedoelen we alle hulpfuncties en datastructuren die de eigenlijke testen mogelijk maken. Deze structuur is volledig ge¨ımplementeerd, enkel de behandeling van uitzonderingen en finally constructies is nog niet voorzien. De testen uit de eerste en de tweede verificatie stap zijn grotendeels beschikbaar, er wordt echter nog niet gecontroleerd op vlaggen, verhoudingen tussen klassen en hun superklassen, en naamgevingsregels. De control-flow analyse is reeds volledig; de data-flow analyse mist nog testen in verband met instantiatie van objecten. De run-time controles van de vierde verificatie stap zijn voorlopig nog niet ge¨ımplementeerd, maar kunnen eenvoudig aan de derde verificatie stap toegevoegd worden. Vermits de volledige structuur beschikbaar is, zou het toevoegen van ontbrekende testen met geringe inspanning moeten mogelijk zijn. We verwachten dan ook een spoedige integratie met het Acunia platform. Op het gebied van geheugenvereisten heeft de aanpassing van het controlflow en data-flow algoritme een belangrijke besparing opgeleverd. Het is echter moeilijk om te vergelijken met de beschikbare commerci¨ele verifiers, omdat die allemaal een integraal deel zijn van een JVM. We vermoeden echter dat onze implementatie voor de gemiddelde klasse zeven maal minder geheugen zal nodig hebben.
95
Bibliografie [1] Dries Buytaert. J-spot. Acunia, 2000. [2] Steven Buytaert. Smartmove’s white paper on third generation telematics. SmartMove, 1998. [3] Peter Dibble. The reality of real-time java. Computer design, 1998. [4] Joshua Engel. Programming for the Java virtual machine. AddisonWesley, 1999. [5] D Flanagan. Java in a nutshell. O’Reilly, 1997. [6] Philip W. L. Fong. The mysterious pass one. http://www.cs.sfu.ca, 1997. [7] Dieter Gollmann. Computer security. Wiley, 1999. [8] Li Gong. Inside Java 2 Platform Security. Addison-Wesley, 1999. [9] Allen I. Holub. Inside the java vm. http://www.holub.com, 1999. [10] Brian Kernighan and Dennis M. Ritchie. C handbook. Prentice Hall, 1990. [11] Tim Lindholm and Frank Yellin. The Java Virtual Machine Specification Second Edition. Addison-Wesley, 1999. [12] Gary McGraw and Edward W. Felten. Securing Java. Wiley, 1999. [13] S Oak. Java Security. O’Reilly, 1998. [14] OSGi. Open services gateway initiative, specification overview. http://www.OSGi.org, 2000. [15] Wouter Piepers. News media backgrounder. SmartMove, 2000. [16] Vijay Saraswat. Java is http://www.research.att.com/ vj/bug.html, 1998.
96
not
type-safe.
[17] Emin G¨ un Sirer. Testing http://kimera.cs.washington.edu, 1998.
java
virtual
machines.
[18] Sun. Connected, limited device configuration: Adherence to java virtual machine specification. http://java.sun.com, 1996. [19] Sun. J2me technology for creating mobile devices. http://java.sun.com, 2000. [20] Sun. The java security homepage. http://java.sun.com/security, 2001. [21] Bill Venners. Security and the class verifier. JavaWorld, 1997. [22] Hiaping Xu. Java security model and bytecode verification. University of Illinois at Chicago, 1999. [23] Frank Yellin. Low level security http://java.sun.com/sfaq/verifier.html, 1996.
97
in
java.
Bijlage
98