Java-Bytecode-Verschlüsselung knacken

9. Mai 2003

F: Wenn ich meine .class-Dateien verschlüssele und sie mithilfe eines benutzerdefinierten Klassenladeprogramms im laufenden Betrieb lade und entschlüssele, verhindert dies die Dekompilierung?

A: Das Problem, die Dekompilierung von Java-Bytecode zu verhindern, ist fast so alt wie die Sprache selbst. Trotz einer Reihe von auf dem Markt erhältlichen Verschleierungstools denken unerfahrene Java-Programmierer weiterhin über neue und clevere Möglichkeiten zum Schutz ihres geistigen Eigentums nach. In diesem Java Q & A- Teil zerstreue ich einige Mythen über eine Idee, die häufig in Diskussionsforen aufgearbeitet wird.

Die extreme Leichtigkeit, mit der Java- .classDateien in Java-Quellen rekonstruiert werden können, die den Originalen sehr ähnlich sind, hat viel mit den Zielen und Kompromissen beim Entwurf von Java-Bytecode zu tun. Java-Bytecode wurde unter anderem für Kompaktheit, Plattformunabhängigkeit, Netzwerkmobilität und einfache Analyse durch Bytecode-Interpreter und dynamische JIT- (Just-in-Time) / HotSpot-Compiler entwickelt. Die kompilierten .classDateien drücken wohl die Absicht des Programmierers so deutlich aus, dass sie leichter zu analysieren sind als der ursprüngliche Quellcode.

Es können verschiedene Dinge getan werden, um die Dekompilierung nicht vollständig zu verhindern, zumindest um sie schwieriger zu machen. Als Schritt nach der Kompilierung können Sie beispielsweise die .classDaten massieren , um den Bytecode beim Dekompilieren entweder schwerer zu lesen oder in gültigen Java-Code (oder beides) zu dekompilieren. Techniken wie das Überladen extremer Methodennamen funktionieren bei ersteren gut, und das Manipulieren des Kontrollflusses zum Erstellen von Kontrollstrukturen, die nicht durch Java-Syntax dargestellt werden können, funktionieren bei letzteren gut. Die erfolgreicheren kommerziellen Verschleierer verwenden eine Mischung dieser und anderer Techniken.

Leider müssen beide Ansätze tatsächlich den Code ändern, den die JVM ausführen wird, und viele Benutzer befürchten (zu Recht), dass diese Umwandlung ihren Anwendungen neue Fehler hinzufügen könnte. Darüber hinaus kann das Umbenennen von Methoden und Feldern dazu führen, dass Reflection-Aufrufe nicht mehr funktionieren. Das Ändern der tatsächlichen Klassen- und Paketnamen kann mehrere andere Java-APIs (JNDI (Java Naming and Directory Interface), URL-Anbieter usw.) beschädigen. Zusätzlich zu geänderten Namen kann es schwierig werden, die ursprünglichen Ausnahmestapelspuren wiederherzustellen, wenn die Zuordnung zwischen Klassenbytecode-Offsets und Quellzeilennummern geändert wird.

Dann besteht die Möglichkeit, den ursprünglichen Java-Quellcode zu verschleiern. Grundsätzlich verursacht dies jedoch ähnliche Probleme.

Verschlüsseln, nicht verschleiern?

Vielleicht hat Sie das oben Gesagte zum Nachdenken gebracht: "Nun, was ist, wenn ich anstelle der Manipulation des Bytecodes alle meine Klassen nach der Kompilierung verschlüssele und sie im laufenden Betrieb in der JVM entschlüssele (was mit einem benutzerdefinierten Klassenladeprogramm möglich ist)? Dann führt die JVM meinen aus Original-Byte-Code und doch gibt es nichts zu dekompilieren oder zurückzuentwickeln, oder? "

Leider würden Sie sich irren, sowohl wenn Sie denken, dass Sie als erster auf diese Idee gekommen sind, als auch wenn Sie denken, dass sie tatsächlich funktioniert. Und der Grund hat nichts mit der Stärke Ihres Verschlüsselungsschemas zu tun.

Ein einfacher Klassencodierer

Um diese Idee zu veranschaulichen, habe ich eine Beispielanwendung und einen sehr einfachen benutzerdefinierten Klassenladeprogramm implementiert, um sie auszuführen. Die Anwendung besteht aus zwei kurzen Klassen:

public class Main {public statisch void main (final String [] args) {System.out.println ("secret result =" + MySecretClass.mySecretAlgorithm ()); }} // Ende des Klassenpakets my.secret.code; import java.util.Random; öffentliche Klasse MySecretClass {/ ** * Ratet mal, der geheime Algorithmus verwendet nur einen Zufallszahlengenerator ... * / public static int mySecretAlgorithm () {return (int) s_random.nextInt (); } private static final Random s_random = new Random (System.currentTimeMillis ()); } // Ende des Unterrichts

Mein Ziel ist es, die Implementierung zu verbergen, my.secret.code.MySecretClassindem ich die relevanten .classDateien verschlüssele und sie zur Laufzeit im laufenden Betrieb entschlüssele. Zu diesem Zweck verwende ich das folgende Tool (einige Details wurden weggelassen; Sie können die vollständige Quelle von Resources herunterladen):

public class EncryptedClassLoader erweitert URLClassLoader {public static void main (final String [] args) löst eine Ausnahme aus {if ("-run" .equals (args [0]) && (args.length> = 3)) {// Erstelle eine benutzerdefinierte Loader, der den aktuellen Loader als // übergeordnetes Delegierungselement verwendet: final ClassLoader appLoader = new EncryptedClassLoader (EncryptedClassLoader.class.getClassLoader (), neue Datei (args [1])); // Thread-Kontextlader muss ebenfalls angepasst werden: Thread.currentThread () .setContextClassLoader (appLoader); letzte Klasse app = appLoader.loadClass (args [2]); endgültige Methode appmain = app.getMethod ("main", neue Klasse [] {String [] .class}); final String [] appargs = neuer String [args.length - 3]; Systemarraycopy (args, 3, appargs, 0, appargs.length); appmain.invoke (null, neues Objekt [] {appargs}); } else if ("-encrypt".equals (args [0]) && (args.length> = 3)) {... bestimmte Klassen verschlüsseln ...} else throw new IllegalArgumentException (USAGE); } / ** * Überschreibt java.lang.ClassLoader.loadClass (), um die üblichen Eltern-Kind-Delegierungsregeln * so weit zu ändern, dass Anwendungsklassen * unter der Nase des Systemklassenladers "entführt" werden können. * / public Class loadClass (endgültiger Stringname, endgültige boolesche Auflösung) löst ClassNotFoundException aus {if (TRACE) System.out.println ("loadClass (" + Name + "," + Auflösung + ")"); Klasse c = null; // Überprüfen Sie zunächst, ob diese Klasse bereits von dieser // Klasseninstallationsinstanz definiert wurde: c = findLoadedClass (name); if (c == null) {Klasse parentVersion = null; try {// Das ist etwas unorthodox:Führen Sie einen Probeladen über den // übergeordneten Loader durch und notieren Sie, ob der übergeordnete delegiert hat oder nicht. // was dies bewirkt, ist eine ordnungsgemäße Delegierung für alle // Kern- und Erweiterungsklassen, ohne dass ich nach dem Klassennamen filtern muss: parentVersion = getParent () .loadClass (name); if (parentVersion.getClassLoader ()! = getParent ()) c = parentVersion; } catch (ClassNotFoundException ignore) {} catch (ClassFormatError ignore) {} if (c == null) {try {// OK, entweder 'c' wurde vom Systemloader (nicht vom Bootstrap // oder der Erweiterung) geladen (in In diesem Fall möchte ich diese // Definition ignorieren.) Oder das übergeordnete Element ist insgesamt fehlgeschlagen. So oder so // versuche ich meine eigene Version zu definieren: c = findClass (name); } catch (ClassNotFoundException ignore) {// Wenn dies fehlschlägt, greifen Sie auf die Version des Elternteils zurück // [die zu diesem Zeitpunkt null sein könnte]: c = parentVersion;}}} if (c == null) löst eine neue ClassNotFoundException (name) aus; if (resolve) resolveClass (c); return c; } / ** * Überschreibt java.new.URLClassLoader.defineClass (), um * crypt () aufrufen zu können, bevor eine Klasse definiert wird. * / protected Class findClass (endgültiger Stringname) löst ClassNotFoundException aus {if (TRACE) System.out.println ("findClass (" + name + ")"); // .class-Dateien können nicht garantiert als Ressourcen geladen werden. // aber wenn Suns Code es tut, kann es vielleicht meins sein ... final String classResource = name.replace ('.', '/') + ".class"; endgültige URL classURL = getResource (classResource); if (classURL == null) löst eine neue ClassNotFoundException (name) aus; sonst {InputStream in = null; try {in = classURL.openStream (); letztes Byte [] classBytes = readFully (in); // "entschlüsseln": crypt (classBytes);if (TRACE) System.out.println ("entschlüsselt [" + Name + "]"); return defineClass (name, classBytes, 0, classBytes.length); } catch (IOException ioe) {neue ClassNotFoundException (Name) auslösen; } endlich {if (in! = null) try {in.close (); } catch (Ausnahme ignorieren) {}}}} / ** * Dieser Klassenladeprogramm kann nur benutzerdefiniert aus einem einzelnen Verzeichnis geladen werden. * / private EncryptedClassLoader (endgültiger ClassLoader-Elternteil, letzter File-Klassenpfad) löst MalformedURLException aus {super (neue URL [] {classpath.toURL ()}, Eltern); if (parent == null) löst eine neue IllegalArgumentException aus ("EncryptedClassLoader" + "erfordert eine übergeordnete Nicht-Null-Delegierung"); } / ** * Entschlüsselt Binärdaten in einem bestimmten Byte-Array. Durch erneutes Aufrufen der Methode * wird die Verschlüsselung umgekehrt. * / private statische leere Krypta (letzte Byte [] Daten) {for (int i = 8;i <data.length; ++ i) Daten [i] ^ = 0x5A; } ... mehr Hilfsmethoden ...} // Ende der Klasse

EncryptedClassLoaderEs gibt zwei grundlegende Vorgänge: Verschlüsseln einer bestimmten Gruppe von Klassen in einem bestimmten Klassenpfadverzeichnis und Ausführen einer zuvor verschlüsselten Anwendung. Die Verschlüsselung ist sehr einfach: Sie besteht im Wesentlichen darin, einige Bits jedes Bytes im Inhalt der Binärklasse umzudrehen. (Ja, das gute alte XOR (exklusives ODER) ist fast überhaupt keine Verschlüsselung, aber ertrage es mit mir. Dies ist nur eine Illustration.)

Das Laden von Klassen EncryptedClassLoaderverdient etwas mehr Aufmerksamkeit. Meine Implementierung unterteilt java.net.URLClassLoaderund überschreibt beide loadClass()und defineClass()um zwei Ziele zu erreichen. Eine besteht darin, die üblichen Java 2-Classloader-Delegierungsregeln zu biegen und die Möglichkeit zu erhalten, eine verschlüsselte Klasse zu laden, bevor der Systemklassenloader dies tut, und eine andere darin, sie crypt()unmittelbar vor dem Aufruf aufzurufen defineClass(), der ansonsten im Inneren erfolgt URLClassLoader.findClass().

Nachdem Sie alles in das binVerzeichnis kompiliert haben :

> javac -d bin src / *. java src / my / secret / code / *. java 

Ich "verschlüssele" beide Mainund MySecretClassKlassen:

> java -cp bin EncryptedClassLoader -encrypt bin Main my.secret.code.MySecretClass verschlüsselt [Main.class] verschlüsselt [my \ secret \ code \ MySecretClass.class] 

Diese beiden Klassen binwurden jetzt durch verschlüsselte Versionen ersetzt. Um die ursprüngliche Anwendung auszuführen, muss ich die Anwendung ausführen über EncryptedClassLoader:

> java -cp bin Main Ausnahme im Thread "main" java.lang.ClassFormatError: Main (illegaler Konstantentyp) bei java.lang.ClassLoader.defineClass0 (native Methode) bei java.lang.ClassLoader.defineClass (ClassLoader.java: 502) unter java.security.SecureClassLoader.defineClass (SecureClassLoader.java:123) unter java.net.URLClassLoader.defineClass (URLClassLoader.java:250) unter java.net.URLClassLoader.access00 (URLClassLoader.java:54). net.URLClassLoader.run (URLClassLoader.java:193) unter java.security.AccessController.doPrivileged (native Methode) unter java.net.URLClassLoader.findClass (URLClassLoader.java:186) unter java.lang.ClassLoader.loadClass (ClassL. java: 299) unter sun.misc.Launcher $ AppClassLoader.loadClass (Launcher.java:265) unter java.lang.ClassLoader.loadClass (ClassLoader.java:255) unter java.lang.ClassLoader.loadClassInternal (ClassLoader.java:315) )>java -cp bin EncryptedClassLoader -run bin Main entschlüsselt [Main] entschlüsselt [my.secret.code.MySecretClass] geheimes Ergebnis = 1362768201

Sicher genug, das Ausführen eines Dekompilers (wie Jad) für verschlüsselte Klassen funktioniert nicht.

Es ist Zeit, ein ausgeklügeltes Kennwortschutzschema hinzuzufügen, dieses in eine native ausführbare Datei zu packen und Hunderte von Dollar für eine "Softwareschutzlösung" zu verlangen, oder? Natürlich nicht.

ClassLoader.defineClass (): Der unvermeidliche Schnittpunkt

Alle ClassLoadermüssen ihre Klassendefinitionen über einen genau definierten API-Punkt an die JVM senden: die java.lang.ClassLoader.defineClass()Methode. Die ClassLoaderAPI hat mehrere Überladungen dieser Methode, aber alle rufen die defineClass(String, byte[], int, int, ProtectionDomain)Methode auf. Diese finalMethode ruft nach einigen Überprüfungen den nativen JVM-Code auf. Es ist wichtig zu verstehen, dass kein Klassenlader das Aufrufen dieser Methode vermeiden kann, wenn er eine neue erstellen möchte Class.

Die defineClass()Methode ist der einzige Ort, an dem die Magie des Erstellens eines ClassObjekts aus einem flachen Byte-Array stattfinden kann. Und raten Sie mal, das Byte-Array muss die unverschlüsselte Klassendefinition in einem gut dokumentierten Format enthalten (siehe Spezifikation des Klassendateiformats). Das Aufheben des Verschlüsselungsschemas ist jetzt eine einfache Angelegenheit, bei der alle Aufrufe dieser Methode abgefangen und alle interessanten Klassen nach Herzenslust dekompiliert werden (ich erwähne später eine andere Option, JVM Profiler Interface (JVMPI)).