Programování

Praskání šifrování bajtového kódu Java

9. května 2003

Otázka: Pokud zašifruji své soubory .class a za běhu je použiju k vlastnímu načítání tříd a dešifrování, zabrání to dekompilaci?

A: Problém zabránění dekompilaci bajtového kódu Java je téměř stejně starý jako samotný jazyk. Navzdory řadě nástrojů na zmatení dostupných na trhu začínající programátoři Java stále vymýšlejí nové a chytré způsoby ochrany svého duševního vlastnictví. V tomhle Java Q&A pokračování, vyvracím některé mýty kolem myšlenky, která se často opakuje v diskusních fórech.

Extrémní snadnost, s jakou Java .třída soubory lze rekonstruovat na zdroje Java, které se velmi podobají originálu, má hodně co do činění s cíli a kompromisy návrhu Java byte-code design. Bajtový kód Java byl mimo jiné navržen pro kompaktnost, nezávislost na platformě, mobilitu v síti a snadnou analýzu pomocí interpretů bajtových kódů a dynamických kompilátorů JIT (just-in-time) / HotSpot. Pravděpodobně sestaveno .třída Soubory vyjadřují záměr programátora tak jasně, že by mohly být snadněji analyzovatelné než původní zdrojový kód.

Lze udělat několik věcí, ne-li úplně zabránit dekompilaci, alespoň ji ztěžovat. Například jako krok po kompilaci můžete masírovat .třída data, aby byl bajtový kód při dekompilaci těžší čitelný, nebo těžší dekompilace na platný kód Java (nebo obojí). Techniky, jako je provádění extrémního přetížení názvu metody, fungují dobře pro první a manipulace s tokem řízení k vytvoření řídicích struktur, které není možné reprezentovat prostřednictvím syntaxe Java, fungují dobře pro druhé. Úspěšnější komerční obfuskátory používají kombinaci těchto a dalších technik.

Bohužel oba přístupy musí skutečně změnit kód, který JVM spustí, a mnoho uživatelů se bojí (oprávněně), že tato transformace může do jejich aplikací přidat nové chyby. Kromě toho může přejmenování metody a pole způsobit, že volání reflexe přestanou fungovat. Změna skutečných názvů tříd a balíků může narušit několik dalších rozhraní Java API (JNDI (Java Naming and Directory Interface), poskytovatelé URL atd.). Kromě změněných názvů může být obtížné obnovit původní trasování zásobníku výjimek, pokud dojde ke změně asociace mezi posuny bytového kódu třídy a čísly zdrojových řádků.

Pak existuje možnost zamlžit původní zdrojový kód Java. To však v zásadě způsobuje podobný soubor problémů.

Zašifrovat, ne zkazit?

Možná vás výše uvedené přimělo přemýšlet: „No, co když místo toho, abych manipuloval s bytovým kódem, zašifruji všechny své třídy po kompilaci a dešifruji je za běhu uvnitř JVM (což lze provést pomocí vlastního Classloaderu)? Pak JVM provede můj původní bajtový kód, a přesto není co dekompilovat nebo zpětně analyzovat, že? “

Bohužel byste se mýlili, a to jak v domnění, že jste s touto myšlenkou přišli jako první, tak v domnění, že ve skutečnosti funguje. A důvod nemá nic společného se silou vašeho šifrovacího schématu.

Jednoduchý kodér třídy

Pro ilustraci této myšlenky jsem implementoval ukázkovou aplikaci a velmi triviální vlastní classloader pro její spuštění. Aplikace se skládá ze dvou krátkých tříd:

public class Main {public static void main (final String [] args) {System.out.println ("secret result =" + MySecretClass.mySecretAlgorithm ()); }} // Konec balíčku třídy my.secret.code; import java.util.Random; veřejná třída MySecretClass {/ ** * Hádejte, tajný algoritmus používá pouze generátor náhodných čísel ... * / public static int mySecretAlgorithm () {return (int) s_random.nextInt (); } private static final Random s_random = new Random (System.currentTimeMillis ()); } // Konec třídy 

Mým cílem je skrýt implementaci my.secret.code.MySecretClass šifrováním příslušných .třída soubory a dešifrování za běhu za běhu. Za tímto účelem používám následující nástroj (některé podrobnosti jsou vynechány; celý zdroj si můžete stáhnout ze zdrojů):

veřejná třída EncryptedClassLoader rozšiřuje URLClassLoader {public static void main (final String [] args) vyvolá Exception {if ("-run" .equals (args [0]) && (args.length> = 3)) {// Vytvořit vlastní zavaděč, který použije aktuální zavaděč jako // nadřazený delegát: final ClassLoader appLoader = nový EncryptedClassLoader (EncryptedClassLoader.class.getClassLoader (), nový soubor (args [1])); // Musí být upraven také kontextový zavaděč vláken: Thread.currentThread () .setContextClassLoader (appLoader); final Class app = appLoader.loadClass (args [2]); final Method appmain = app.getMethod ("main", nová třída [] {řetězec [] .class}); final String [] zařízení = nový String [args.length - 3]; System.arraycopy (arg, 3, aparáty, 0, aparatura.délka); appmain.invoke (null, nový objekt [] {přístroje}); } else if ("-encrypt" .equals (args [0]) && (args.length> = 3)) {... zašifrovat zadané třídy ...} else vrhnout novou IllegalArgumentException (USAGE); } / ** * Přepíše java.lang.ClassLoader.loadClass (), aby změnila obvyklá pravidla delegování rodič-dítě * jen natolik, aby byla schopna „chytit“ aplikační třídy * zpod nosu systémového třídiče. * / public Class loadClass (konečné jméno řetězce, konečné booleovské řešení) vyvolá ClassNotFoundException {if (TRACE) System.out.println ("loadClass (" + name + "," + resolve + ")"); Třída c = null; // Nejprve zkontrolujte, zda tato třída již byla definována tímto classloaderem // instance: c = findLoadedClass (name); if (c == null) {Class parentVersion = null; zkuste {// To je trochu neortodoxní: proveďte zkušební načtení pomocí // nadřazeného zavaděče a všimněte si, zda je nadřazený delegován nebo ne; // čeho se tím dosáhne, je správné delegování pro všechny základní // a rozšiřující třídy, aniž bych musel filtrovat podle názvu třídy: parentVersion = getParent () .loadClass (name); if (parentVersion.getClassLoader ()! = getParent ()) c = parentVersion; } catch (ClassNotFoundException ignore) {} catch (ClassFormatError ignore) {} if (c == null) {try {// OK, either 'c' was loaded by the system (not the bootstrap // or extension) loader (in v takovém případě chci tuto // definici ignorovat) nebo rodič selhal úplně; v obou případech se // pokusím definovat svou vlastní verzi: c = findClass (name); } catch (ClassNotFoundException ignore) {// Pokud se to nepodařilo, přepněte zpět na verzi rodiče // [která by v tomto okamžiku mohla mít hodnotu null]: c = parentVersion; }}} if (c == null) throw new ClassNotFoundException (name); if (vyřešit) vyřešitClass (c); návrat c; } / ** * Přepíše java.new.URLClassLoader.defineClass (), aby bylo možné zavolat * crypt () před definováním třídy. * / protected Class findClass (final String name) throws ClassNotFoundException {if (TRACE) System.out.println ("findClass (" + name + ")"); // není zaručeno, že soubory .class budou načteny jako zdroje; // ale pokud to dělá Sunův kód, tak snad může těžit ... final String classResource = name.replace ('.', '/') + ".class"; final URL classURL = getResource (classResource); if (classURL == null) throw new ClassNotFoundException (name); else {InputStream in = null; zkuste {in = classURL.openStream (); finální bajt [] classBytes = readFully (in); // "dešifrovat": crypt (classBytes); if (TRACE) System.out.println ("dešifrováno [" + jméno + "]"); návrat defineClass (name, classBytes, 0, classBytes.length); } catch (IOException ioe) {hodit novou ClassNotFoundException (name); } konečně {if (in! = null) try {in.close (); } catch (Výjimka ignorována) {}}}} / ** * Tento Classloader je schopen vlastního načítání pouze z jednoho adresáře. * / private EncryptedClassLoader (finální nadřazený ClassLoader, finální classpath souboru) vyvolá MalformedURLException {super (nová URL [] {classpath.toURL ()}, nadřazený); if (parent == null) throw new IllegalArgumentException ("EncryptedClassLoader" + "vyžaduje nenulovou delegaci rodiče"); } / ** * De / šifruje binární data v daném bajtovém poli. Opětovné volání metody * obrátí šifrování. * / private static void crypt (final byte [] data) {for (int i = 8; i <data.length; ++ i) data [i] ^ = 0x5A; } ... další pomocné metody ...} // Konec třídy 

EncryptedClassLoader má dvě základní operace: šifrování dané sady tříd v daném adresáři classpath a spuštění dříve šifrované aplikace. Šifrování je velmi přímé: spočívá v zásadním převrácení některých bitů každého bajtu v obsahu binární třídy. (Ano, starý dobrý XOR (exkluzivní OR) není téměř žádné šifrování, ale mějte se mnou. Toto je jen ilustrace.)

Načítání podle EncryptedClassLoader zaslouží si trochu více pozornosti. Moje implementační podtřídy java.net.URLClassLoader a přepíše oba loadClass () a defineClass () dosáhnout dvou cílů. Jedním z nich je ohýbání obvyklých pravidel delegování třídy Java 2 Classloader a získání šance načíst šifrovanou třídu dříve, než to provede systémový Classloader, a další je vyvolání krypta() bezprostředně před zavoláním na defineClass () to se jinak děje uvnitř URLClassLoader.findClass ().

Po sestavení všeho do zásobník adresář:

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

„Šifruji“ obojí Hlavní a MySecretClass třídy:

> java -cp bin EncryptedClassLoader -encrypt bin Hlavní my.secret.code.MySecretClass šifrovaný [Main.class] šifrovaný [můj \ tajemství \ kód \ MySecretClass.class] 

Tyto dvě třídy v zásobník nyní byly nahrazeny šifrovanými verzemi a pro spuštění původní aplikace musím aplikaci spustit EncryptedClassLoader:

> java -cp bin Hlavní výjimka ve vlákně "main" java.lang.ClassFormatError: Main (nelegální typ konstantního fondu) na java.lang.ClassLoader.defineClass0 (nativní metoda) na java.lang.ClassLoader.defineClass (ClassLoader.java: 502) na java.security.SecureClassLoader.defineClass (SecureClassLoader.java:123) na java.net.URLClassLoader.defineClass (URLClassLoader.java:250) na java.net.URLClassLoader.access00 (URLClassLoader.java:54) na java. net.URLClassLoader.run (URLClassLoader.java:193) na java.security.AccessController.doPrivileged (nativní metoda) na java.net.URLClassLoader.findClass (URLClassLoader.java:186) na java.lang.ClassLoader.loadClass (ClassLoader. java: 299) at sun.misc.Launcher $ AppClassLoader.loadClass (Launcher.java:265) at java.lang.ClassLoader.loadClass (ClassLoader.java:255) at java.lang.ClassLoader.loadClassInternal (ClassLoader.java:315 )> java -cp bin EncryptedClassLoader -run bin Hlavní dešifrovaný [Hlavní] dešifrovaný [my.secret.code.MySecretClass] tajný výsledek = 1362768201 

Jistě, spuštění libovolného dekompilátoru (například Jad) na šifrovaných třídách nefunguje.

Je čas přidat propracované schéma ochrany heslem, zabalit jej do nativního spustitelného souboru a účtovat stovky dolarů za „softwarové řešení ochrany“, že? Samozřejmě že ne.

ClassLoader.defineClass (): nevyhnutelný průsečík

Všechno ClassLoadermusí dodat své definice tříd na JVM prostřednictvím jednoho dobře definovaného bodu API: java.lang.ClassLoader.defineClass () metoda. The ClassLoader API má několik přetížení této metody, ale všechny volají do defineClass (String, byte [], int, int, ProtectionDomain) metoda. Je to finále metoda, která volá do nativního kódu JVM po provedení několika kontrol. Je důležité tomu rozumět žádný classloader se nemůže vyhnout volání této metody, pokud chce vytvořit novou Třída.

The defineClass () metoda je jediným místem, kde kouzlo vytváření a Třída objekt z plochého bajtového pole může probíhat. A hádejte co, bajtové pole musí obsahovat nezašifrovanou definici třídy v dobře zdokumentovaném formátu (viz specifikace formátu souboru třídy). Prolomení šifrovacího schématu je nyní jednoduchá záležitost zachycení všech volání této metody a dekompilace všech zajímavých tříd podle vašeho přání (později zmíním další možnost, JVM Profiler Interface (JVMPI)).

$config[zx-auto] not found$config[zx-overlay] not found