Sicherheit und der Klassenprüfer

Der Artikel dieses Monats setzt die Diskussion über Javas Sicherheitsmodell fort, das im August in "Under the Hood" begonnen wurde. In diesem Artikel gab ich einen allgemeinen Überblick über die in die Java Virtual Machine (JVM) integrierten Sicherheitsmechanismen. Ich habe mir auch einen Aspekt dieser Sicherheitsmechanismen genau angesehen: die integrierten Sicherheitsfunktionen der JVM. In "Under the Hood" im September habe ich die Klassenladearchitektur untersucht, ein weiterer Aspekt der in JVM integrierten Sicherheitsmechanismen. Diesen Monat werde ich mich auf den dritten Punkt der Sicherheitsstrategie der JVM konzentrieren: den Klassenprüfer.

Der Klassendateiverifizierer

Jede virtuelle Java-Maschine verfügt über einen Klassendateiverifizierer, der sicherstellt, dass geladene Klassendateien eine ordnungsgemäße interne Struktur haben. Wenn der Klassendateiverifizierer ein Problem mit einer Klassendatei feststellt, wird eine Ausnahme ausgelöst. Da eine Klassendatei nur eine Folge von Binärdaten ist, kann eine virtuelle Maschine nicht wissen, ob eine bestimmte Klassendatei von einem gut gemeinten Java-Compiler oder von zwielichtigen Crackern generiert wurde, die die Integrität der virtuellen Maschine gefährden wollen. Infolgedessen verfügen alle JVM-Implementierungen über einen Klassendateiverifizierer, der für nicht vertrauenswürdige Klassen aufgerufen werden kann, um sicherzustellen, dass die Klassen sicher verwendet werden können.

Eines der Sicherheitsziele, mit deren Hilfe der Klassendateiverifizierer erreicht werden kann, ist die Robustheit des Programms. Wenn ein fehlerhafter Compiler oder ein versierter Cracker eine Klassendatei generiert hat, die eine Methode enthält, deren Bytecodes eine Anweisung zum Überspringen des Methodenendes enthalten, kann diese Methode beim Aufrufen zum Absturz der virtuellen Maschine führen. Aus Gründen der Robustheit ist es daher wichtig, dass die virtuelle Maschine die Integrität der importierten Bytecodes überprüft.

Obwohl Entwickler von Java Virtual Machine entscheiden können, wann ihre Virtual Machines diese Überprüfungen durchführen, führen viele Implementierungen die meisten Überprüfungen unmittelbar nach dem Laden einer Klasse durch. Eine solche virtuelle Maschine analysiert Bytecodes einmal (und überprüft ihre Integrität), bevor sie jemals ausgeführt werden. Im Rahmen der Überprüfung von Bytecodes stellt die virtuelle Java-Maschine alle Sprunganweisungen sicher - zum Beispiel goto(immer springen),ifeq(Sprung, wenn die Spitze des Stapels Null ist) usw. - einen Sprung zu einer anderen gültigen Anweisung im Bytecode-Stream der Methode verursachen. Infolgedessen muss die virtuelle Maschine nicht jedes Mal, wenn sie beim Ausführen von Bytecodes auf einen Sprungbefehl stößt, nach einem gültigen Ziel suchen. In den meisten Fällen ist das einmalige Überprüfen aller Bytecodes vor ihrer Ausführung eine effizientere Methode, um die Robustheit zu gewährleisten, als das Überprüfen jedes Bytecode-Befehls bei jeder Ausführung.

Ein Klassendateiverifizierer, der seine Prüfung so früh wie möglich durchführt, arbeitet höchstwahrscheinlich in zwei unterschiedlichen Phasen. Während der ersten Phase, die unmittelbar nach dem Laden einer Klasse stattfindet, überprüft der Klassendateiverifizierer die interne Struktur der Klassendatei, einschließlich der Überprüfung der Integrität der darin enthaltenen Bytecodes. Während der zweiten Phase, in der Bytecodes ausgeführt werden, bestätigt der Klassendateiverifizierer die Existenz symbolisch referenzierter Klassen, Felder und Methoden.

Phase eins: Interne Kontrollen

Während der ersten Phase überprüft der Klassendateiverifizierer alles, was zum Einchecken einer Klassendatei möglich ist, indem nur die Klassendatei selbst betrachtet wird (ohne andere Klassen oder Schnittstellen zu untersuchen). In Phase 1 der Überprüfung von Klassendateien wird sichergestellt, dass die importierte Klassendatei ordnungsgemäß erstellt wurde, intern konsistent ist, den Einschränkungen der Java-Programmiersprache entspricht und Bytecodes enthält, die für die virtuelle Java-Maschine sicher ausgeführt werden können. Wenn der Klassendateiverifizierer feststellt, dass eine dieser Bedingungen nicht erfüllt ist, wird ein Fehler ausgegeben, und die Klassendatei wird vom Programm niemals verwendet.

Überprüfen des Formats und der internen Konsistenz

Neben der Überprüfung der Integrität der Bytecodes führt der Prüfer in Phase 1 zahlreiche Überprüfungen auf das richtige Klassendateiformat und die interne Konsistenz durch. Beispielsweise muss jede Klassendatei mit denselben vier Bytes beginnen, der magischen Zahl : 0xCAFEBABE. Der Zweck von magischen Zahlen besteht darin, Dateiparsern das Erkennen eines bestimmten Dateityps zu erleichtern. Das erste, was ein Klassendateiverifizierer wahrscheinlich überprüft, ist, dass die importierte Datei tatsächlich mit beginnt 0xCAFEBABE.

Der Klassendateiverifizierer überprüft außerdem, ob die Klassendatei weder abgeschnitten noch mit zusätzlichen nachgestellten Bytes erweitert wurde. Obwohl verschiedene Klassendateien unterschiedlich lang sein können, gibt jede einzelne Komponente in einer Klassendatei sowohl ihre Länge als auch ihren Typ an. Der Prüfer kann anhand der Komponententypen und -längen die richtige Gesamtlänge für jede einzelne Klassendatei ermitteln. Auf diese Weise kann überprüft werden, ob die importierte Datei eine Länge hat, die mit dem internen Inhalt übereinstimmt.

Der Prüfer überprüft auch einzelne Komponenten, um sicherzustellen, dass es sich um wohlgeformte Instanzen ihres Komponententyps handelt. Beispielsweise wird ein Methodendeskriptor (der Rückgabetyp der Methode sowie die Anzahl und Typen ihrer Parameter) in der Klassendatei als Zeichenfolge gespeichert, die einer bestimmten kontextfreien Grammatik entsprechen muss. Eine der Überprüfungen, die der Prüfer an einzelnen Komponenten durchführt, besteht darin, sicherzustellen, dass jeder Methodendeskriptor eine wohlgeformte Zeichenfolge der entsprechenden Grammatik ist.

Darüber hinaus prüft der Klassendateiverifizierer, ob die Klasse selbst bestimmte Einschränkungen einhält, die durch die Spezifikation der Java-Programmiersprache auferlegt werden. Beispielsweise erzwingt der Prüfer die Regel, dass alle Klassen außer der Klasse Objecteine Oberklasse haben müssen. Daher überprüft der Klassendateiverifizierer zur Laufzeit einige der Java-Sprachregeln, die zur Kompilierungszeit hätten erzwungen werden müssen. Da der Prüfer nicht wissen kann, ob die Klassendatei von einem wohlwollenden, fehlerfreien Compiler generiert wurde, überprüft er jede Klassendatei, um sicherzustellen, dass die Regeln eingehalten werden.