Programovanie

Praskajúce šifrovanie bajtového kódu Java

9. mája 2003

Otázka: Ak zašifrujem svoje súbory .class a pomocou bežného načítavača tried ich načítam a dešifrujem za behu, zabráni to dekompilácii?

A: Problém zabránenia dekompilácii bajtového kódu Java je takmer taký starý ako samotný jazyk. Napriek radu nástrojov na zahmlievanie, ktoré sú na trhu k dispozícii, začínajúci programátori v oblasti Java stále vymýšľajú nové a chytré spôsoby ochrany svojho duševného vlastníctva. V tomto Java Q&A pokračovanie, vyvraciam niektoré mýty týkajúce sa myšlienky, ktorá je často pretváraná v diskusných fórach.

Extrémna ľahkosť, s akou Java .trieda súbory je možné rekonštruovať na zdroje Java, ktoré sa veľmi podobajú originálom, má veľa spoločného s cieľmi a kompromismi v oblasti Java byte-code design. Bajtový kód Java bol okrem iného navrhnutý pre kompaktnosť, nezávislosť na platforme, mobilitu v sieti a ľahkú analýzu interpretmi bajtových kódov a dynamickými kompilátormi JIT (just-in-time) / HotSpot. Pravdepodobne zostavené .trieda súbory vyjadrujú zámer programátora tak zreteľne, že je možné ich ľahšie analyzovať ako v pôvodnom zdrojovom kóde.

Môže sa urobiť niekoľko vecí, ak nie úplne zabrániť dekompilácii, aspoň ju sťažiť. Napríklad ako krok po kompilácii môžete masírovať .trieda dáta, aby sa bajtový kód pri dekompilovaní ťažšie čítal, alebo ťažšie dekompiloval do platného kódu Java (alebo oboch). Techniky ako vykonávanie preťaženia extrémnych názvov metód fungujú dobre pre prvé a manipulácia s tokom riadenia na vytvorenie riadiacich štruktúr, ktoré nie je možné reprezentovať pomocou syntaxe Java, fungujú dobre aj pre druhé. Úspešnejší komerční obfuskátory používajú kombináciu týchto a ďalších techník.

Bohužiaľ, oba prístupy musia skutočne zmeniť kód, ktorý bude JVM spúšťať, a mnoho používateľov sa bojí (oprávnene), že táto transformácia môže pridať nové chyby do ich aplikácií. Premenovanie metódy a poľa môže ďalej spôsobiť, že volania reflexie prestanú fungovať. Zmena skutočných názvov tried a balíkov môže zlomiť niekoľko ďalších rozhraní Java API (JNDI (Java Naming and Directory Interface), poskytovatelia URL atď.). Okrem zmenených mien, ak sa zmení asociácia medzi posunmi bajtového kódu triedy a číslami zdrojových riadkov, obnovenie pôvodných stôp zásobníka výnimiek by mohlo byť ťažké.

Potom existuje možnosť zahmlievať pôvodný zdrojový kód Java. Ale v zásade to spôsobuje podobný súbor problémov.

Šifrovať, nie zahmlievať?

Možno vás vyššie uvedené prinútilo zamyslieť sa: „No, čo ak namiesto manipulácie s bajtovým kódom zašifrujem všetky svoje triedy po kompilácii a dešifrujem ich za behu vo vnútri JVM (čo je možné pomocou vlastného triediča súborov)? Potom JVM vykoná moje pôvodný bajtový kód, a napriek tomu nie je čo dekompilovať alebo spätne analyzovať, však? “

Bohužiaľ by ste sa mýlili, a to tak v domnení, že ste s touto myšlienkou prišli ako prví, aj v domnení, že to skutočne funguje. Dôvod nemá nič spoločné so silou vašej šifrovacej schémy.

Jednoduchý kódovač triedy

Na ilustráciu tohto nápadu som implementoval ukážkovú aplikáciu a veľmi triviálny vlastný classloader, ktorý ju spustil. Aplikácia pozostáva z dvoch krátkych tried:

public class Main {public static void main (final String [] args) {System.out.println ("tajný výsledok =" + MySecretClass.mySecretAlgorithm ()); }} // Koniec balíka triedy my.secret.code; import java.util.Random; public class MySecretClass {/ ** * Hádajte čo, tajný algoritmus iba používa 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 ()); } // Koniec triedy 

Mojou snahou je skryť implementáciu my.secret.code.MySecretClass šifrovaním príslušných .trieda súbory a ich dešifrovanie za behu programu. Na tento účel používam nasledujúci nástroj (niektoré podrobnosti vynechané; celý zdroj si môžete stiahnuť zo zdrojov):

public class EncryptedClassLoader extends URLClassLoader {public static void main (final String [] args) throws Exception {if ("-run" .equals (args [0]) && (args.length> = 3)) {// Vytvoriť vlastný nakladač, ktorý použije aktuálny nakladač ako // delegácia rodič: final ClassLoader appLoader = nový EncryptedClassLoader (EncryptedClassLoader.class.getClassLoader (), nový súbor (args [1])); // Musí byť upravený aj kontextový zavádzač vlákien: Thread.currentThread () .setContextClassLoader (appLoader); final Class app = appLoader.loadClass (args [2]); final Method appmain = app.getMethod ("main", new Class [] {String [] .class}); finálne String [] zariadenia = nový String [args.length - 3]; System.arraycopy (args, 3, prístroje, 0, prístroje. Dĺžka); appmain.invoke (null, new Object [] {applgs}); } else if ("-encrypt" .equals (args [0]) && (args.length> = 3)) {... zašifrovať určené triedy ...} else throw new IllegalArgumentException (USAGE); } / ** * Prepíše java.lang.ClassLoader.loadClass (), aby sa zmenili obvyklé pravidlá delegovania rodiča a dieťaťa * natoľko, aby bolo možné „chytiť“ aplikačné triedy * spod nosa systémového triediča. * / public Class loadClass (konečný názov reťazca, konečné logické riešenie) hodí ClassNotFoundException {if (TRACE) System.out.println ("loadClass (" + meno + "," + vyriešiť + ")"); Trieda c = null; // Najskôr skontrolujte, či už táto trieda bola definovaná týmto načítavačom triedy // inštancia: c = findLoadedClass (name); if (c == null) {Class parentVersion = null; skúste {// Toto je trochu neortodoxné: vykonajte skúšobné načítanie pomocou // nadradeného zavádzača a všimnite si, či je nadradený delegovaný alebo nie; // čím sa to dosiahne, je správne delegovanie pre všetky základné // a rozširujúce triedy bez toho, aby som musel filtrovať názov triedy: 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 takom prípade chcem túto // definíciu ignorovať) alebo rodič úplne zlyhal; tak či tak sa // pokúsim definovať svoju vlastnú verziu: c = findClass (name); } catch (ClassNotFoundException ignore) {// Ak sa to nepodarilo, vráťte sa k verzii rodiča // [ktorá môže byť v tomto okamihu nulová]: c = parentVersion; }}} if (c == null) throw new ClassNotFoundException (name); if (vyriešiť) vyriešiťClass (c); návrat c; } / ** * Prepíše java.new.URLClassLoader.defineClass (), aby bolo možné zavolať * crypt () pred definovaním triedy. * / chránená trieda findClass (konečný názov reťazca) hodí ClassNotFoundException {if (TRACE) System.out.println ("findClass (" + name + ")"); // súbory triedy nie sú zaručene načítateľné ako zdroje; // ale ak to robí Sunov kód, tak snáď dokáže ťažiť ... final String classResource = name.replace ('.', '/') + ".class"; cieľová URL classURL = getResource (classResource); if (classURL == null) throw new ClassNotFoundException (name); else {InputStream in = null; try {in = classURL.openStream (); konečný bajt [] classBytes = readFully (in); // "dešifrovať": crypt (classBytes); if (TRACE) System.out.println ("dešifrované [" + meno + "]"); návrat defineClass (name, classBytes, 0, classBytes.length); } catch (IOException ioe) {throw new ClassNotFoundException (name); } nakoniec {if (in! = null) skúsiť {in.close (); } catch (Výnimka ignorovaná) {}}}} / ** * Tento zavádzač triedy je schopný vlastného načítania iba z jedného adresára. * / private EncryptedClassLoader (konečný rodič ClassLoader, konečná trieda súboru) hodí MalformedURLException {super (nová URL [] {classpath.toURL ()}, rodič); if (parent == null) throw new IllegalArgumentException ("EncryptedClassLoader" + "vyžaduje nenulovú delegáciu rodiča"); } / ** * De / šifruje binárne údaje v danom bajtovom poli. Ďalším volaním metódy * sa šifrovanie obráti. * / private static void crypt (final byte [] data) {for (int i = 8; i <data.length; ++ i) data [i] ^ = 0x5A; } ... ďalšie pomocné metódy ...} // Koniec triedy 

EncryptedClassLoader má dve základné operácie: šifrovanie danej sady tried v danom adresári classpath a spustenie predtým šifrovanej aplikácie. Šifrovanie je veľmi priame: spočíva v podstate v preklopení niektorých bitov každého bajtu v obsahu binárnej triedy. (Áno, starý dobrý XOR (exkluzívny OR) nie je takmer žiadne šifrovanie, ale majte so sebou. Toto je len ilustrácia.)

Nakladanie podľa triedy EncryptedClassLoader si zaslúži trochu viac pozornosti. Moje implementačné podtriedy java.net.URLClassLoader a prepíše obe loadClass () a defineClass () dosiahnuť dva ciele. Jedným z nich je ohýbanie obvyklých pravidiel delegovania Java Classloaderu a získanie šance načítať šifrovanú triedu skôr, ako to urobí systémový Classloader, a ďalšie je vyvolanie krypta () bezprostredne pred hovorom na číslo defineClass () že inak sa deje vo vnútri URLClassLoader.findClass ().

Po zostavení všetkého do kôš adresár:

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

„Zašifrujem“ obe Hlavný a MySecretClass triedy:

> java -cp bin EncryptedClassLoader -encrypt bin hlavný my.secret.code.MySecretClass šifrovaný [Main.class] šifrovaný [môj \ tajný \ kód \ MySecretClass.class] 

Tieto dve triedy v kôš boli teraz nahradené šifrovanými verziami a aby som mohol spustiť pôvodnú aplikáciu, musím aplikáciu spustiť až do konca EncryptedClassLoader:

> java -cp bin Hlavná výnimka vo vlákne "main" java.lang.ClassFormatError: Main (typ nelegálneho konštantného fondu) na java.lang.ClassLoader.defineClass0 (natívna metóda) 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 jave. net.URLClassLoader.run (URLClassLoader.java:193) na java.security.AccessController.doPrivileged (natívna metóda) na java.net.URLClassLoader.findClass (URLClassLoader.java:186) na java.lang.ClassLoader.loadClass (ClassLoader. java: 299) na sun.misc.Launcher $ AppClassLoader.loadClass (Launcher.java:265) na java.lang.ClassLoader.loadClass (ClassLoader.java:255) na 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ýsledok = 1362768201 

Spustenie ľubovoľného dekompilátora (napríklad Jad) na šifrovaných triedach určite nefunguje.

Je čas pridať sofistikovanú schému ochrany heslom, zabaliť ju do natívneho spustiteľného súboru a účtovať stovky dolárov za „riešenie ochrany softvéru“, však? Samozrejme, že nie.

ClassLoader.defineClass (): nevyhnutný priesečník

Všetky ClassLoadermusia dodať svoje definície tried na JVM prostredníctvom jedného presne definovaného bodu API: java.lang.ClassLoader.defineClass () metóda. The ClassLoader API má niekoľko preťažení tejto metódy, ale všetky volajú do defineClass (String, byte [], int, int, ProtectionDomain) metóda. Je to konečné metóda, ktorá po vykonaní niekoľkých kontrol zavolá do natívneho kódu JVM. Je dôležité tomu rozumieť žiadny classloader sa nemôže vyhnúť volaniu tejto metódy, ak chce vytvoriť novú Trieda.

The defineClass () metóda je jediným miestom, kde sa kúzlo vytvárania a Trieda objekt z plochého bajtového poľa môže prebiehať. A hádajte čo, bajtové pole musí obsahovať nezašifrovanú definíciu triedy v dobre zdokumentovanom formáte (pozri špecifikáciu formátu súboru triedy). Prelomenie šifrovacej schémy je teraz jednoduchou záležitosťou zachytenia všetkých hovorov na túto metódu a dekompilovania všetkých zaujímavých tried podľa vášho srdcového želania (neskôr spomeniem inú možnosť, JVM Profiler Interface (JVMPI)).

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