Programiranje

Razbijanje šifriranja bajt-kode Java

9. maj 2003

V: Če svoje datoteke .class šifriram in jih uporabim za nalaganje razredov po meri, da jih sproti nalagam in dešifriram, bo to preprečilo dekompilacijo?

A: Problem preprečevanja razgradnje bajt-kode Java je skoraj tako star kot sam jezik. Kljub vrsti orodij za zakrivanje, ki so na voljo na trgu, programerji začetniki Java še naprej razmišljajo o novih in pametnih načinih zaščite svoje intelektualne lastnine. V tem Vprašanja in odgovori o Javi naenkrat razblinim nekaj mitov okoli ideje, ki se pogosto ponavlja na forumih za razprave.

Izjemna enostavnost Java .razred datoteke je mogoče rekonstruirati v javanske vire, ki so zelo podobni izvirnikom, ima veliko opraviti s cilji in kompromisi pri oblikovanju bajt-kode Java. Med drugim je bila bajt-koda Java zasnovana za kompaktnost, neodvisnost od platforme, mobilnost omrežja in enostavnost analize s pomočjo tolmačev bajtnih kod in dinamičnih prevajalnikov JIT (just-in-time) / HotSpot. Verjetno, sestavljeno .razred datoteke tako jasno izražajo namen programerja, da bi jih bilo lažje analizirati kot izvirno izvorno kodo.

Narediti je mogoče več stvari, če ne celo za popolno preprečitev dekompilacije, vsaj da bi to otežili. Na primer, kot korak po sestavljanju lahko masirate .razred podatke, da je bajtno kodo težje brati, če je dekompilirana, ali težje dekompilirati v veljavno kodo Java (ali oboje). Tehnike, kot je izvajanje ekstremnih preobremenitev imen metod, delujejo dobro za prve in manipuliranje toka nadzora za ustvarjanje nadzornih struktur, ki jih ni mogoče predstaviti s sintakso Java, delujejo dobro za druge. Uspešnejši komercialni obfuskatorji uporabljajo kombinacijo teh in drugih tehnik.

Na žalost morata oba pristopa dejansko spremeniti kodo, ki jo bo JVM zagnal, in mnogi uporabniki se bojijo (upravičeno), da bo ta preobrazba njihovim aplikacijam dodala nove napake. Poleg tega lahko preimenovanje metode in polja preneha delovati. Spreminjanje dejanskih imen razredov in paketov lahko zlomi več drugih Java API-jev (JNDI (Java Naming and Directory Interface), ponudnike URL-jev itd.). Če se spremeni spremenjena imena, če se spremeni povezava med odmiki bajt-kode razreda in številkami izvorne vrstice, bi lahko postalo težko obnoviti prvotne sledi skladov izjem.

Potem obstaja možnost zameglitve izvirne izvorne kode Java. Toda v osnovi to povzroča podoben niz težav.

Šifriranje, ne zamegljeno?

Morda ste zaradi zgoraj navedenega pomislili: "No, kaj pa, če namesto da bi manipuliral z bajtno kodo, po prevajanju šifriram vse svoje razrede in jih sproti dešifriram v JVM (kar je mogoče storiti z nalagalnikom razredov po meri)? Nato JVM izvrši moj izvirna bajtna koda in vendar ni ničesar za razstaviti ali obratno izdelati, kajne? "

Na žalost bi se zmotili tako v razmišljanju, da ste prvi prišli na to idejo kot v razmišljanju, da to dejansko deluje. In razlog nima nič skupnega z močjo vaše sheme šifriranja.

Preprost kodirnik razreda

Za ponazoritev te ideje sem za zagon uporabil vzorčno aplikacijo in zelo nepomemben naložitelj razredov po meri. Aplikacija je sestavljena iz dveh kratkih tečajev:

javni razred Main {public static void main (final String [] args) {System.out.println ("secret result =" + MySecretClass.mySecretAlgorithm ()); }} // Konec paketa predavanj my.secret.code; uvoz java.util.Random; javni razred MySecretClass {/ ** * Ugani, skrivni algoritem samo uporablja generator naključnih števil ... * / public static int mySecretAlgorithm () {return (int) s_random.nextInt (); } zasebni statični končni naključni s_random = nov naključni (System.currentTimeMillis ()); } // Konec predavanja 

Moja težnja je skriti izvajanje my.secret.code.MySecretClass s šifriranjem ustreznega .razred datotek in jih med izvajanjem sproti dešifrira. V ta namen uporabljam naslednje orodje (nekatere podrobnosti so izpuščene; celoten vir lahko prenesete iz virov):

javni razred EncryptedClassLoader razširja URLClassLoader {public static void main (final String [] args) vrže izjemo {if ("-run" .equals (args [0]) && (args.length> = 3)) {// Ustvari po meri nalagalnik, ki bo trenutni nalagalnik uporabil kot // nadrejeni delegat: končni ClassLoader appLoader = new EncryptedClassLoader (EncryptedClassLoader.class.getClassLoader (), nova datoteka (args [1])); // Treba je prilagoditi tudi nalagalnik konteksta niti: Thread.currentThread () .setContextClassLoader (appLoader); končni razred app = appLoader.loadClass (args [2]); končna metoda appmain = app.getMethod ("main", nov razred [] {String [] .class}); končni niz [] appargs = nov niz [args.length - 3]; System.arraycopy (args, 3, appargs, 0, appargs.length); appmain.invoke (null, nov objekt [] {appargs}); } else if ("-encrypt" .equals (args [0]) && (args.length> = 3)) {... šifriraj določene razrede ...} else vrzi novo IllegalArgumentException (USAGE); } / ** * Preglasi java.lang.ClassLoader.loadClass (), da spremeni običajna pravila delegiranja starš-otrok * ravno toliko, da lahko "ugrabi" razrede aplikacij * pod nosom sistemskega učitelja. * / javni razred loadClass (končno ime niza, končna logična razrešitev) vrže ClassNotFoundException {if (TRACE) System.out.println ("loadClass (" + ime + "," + razreši + ")"); Razred c = nič; // Najprej preverimo, ali je ta razred že določil ta učitelj nalaganja // primer: c = findLoadedClass (ime); if (c == null) {Class roditeljaVersion = null; poskusite {// To je nekoliko neobičajno: izvedite poskusno nalaganje prek // nadrejenega nalagalnika in upoštevajte, ali je nadrejeni delegiran ali ne; // s tem se doseže pravilno pooblaščanje vseh razredov jedra // in razširitev, ne da bi mi bilo treba filtrirati ime razreda: parentVersion = getParent () .loadClass (ime); if (parentVersion.getClassLoader ()! = getParent ()) c = parentVersion; } catch (ClassNotFoundException ignore) {} catch (ClassFormatError ignore) {} if (c == null) {try {// OK, v sistem je naložen bodisi 'c' (ne bootstrap // ali extension) (v v tem primeru želim prezreti to // definicijo) ali nadrejeni v celoti ni uspel; tako ali tako // poskušam določiti svojo različico: c = findClass (ime); } catch (ClassNotFoundException ignore) {// Če to ni uspelo, se vrnite na nadrejeno različico // [ki je v tem trenutku lahko nična]: c = parentVersion; }}} if (c == null) vrže nov ClassNotFoundException (ime); if (razreši) resolClass (c); vrnitev c; } / ** * Preglasi java.new.URLClassLoader.defineClass (), da lahko pokliče * crypt () pred določitvijo razreda. * / zaščiten razred findClass (končno ime niza) vrže ClassNotFoundException {if (TRACE) System.out.println ("findClass (" + ime + ")"); // .class datotek ni mogoče naložiti kot vire; // če pa Sun-ova koda to stori, lahko morda mine ... final String classResource = name.replace ('.', '/') + ".class"; končni URL classURL = getResource (classResource); če (classURL == null) vrže nov ClassNotFoundException (ime); else {InputStream in = null; poskusite {in = classURL.openStream (); končni bajt [] classBytes = readFully (in); // "dešifriraj": kripta (classBytes); if (TRACE) System.out.println ("dešifrirano [" + ime + "]"); vrni defineClass (ime, classBytes, 0, classBytes.length); } catch (IOException ioe) {throw new ClassNotFoundException (ime); } končno {if (in! = null) poskusite {in.close (); } catch (Exception ignore) {}}}} / ** * Ta nalagalnik razredov lahko nalaga samo iz enega imenika. * / private EncryptedClassLoader (končni nadrejeni razred ClassLoader, končna pot do datoteke File) vrže MalformedURLException {super (nov URL [] {classpath.toURL ()}, nadrejeni); če (nadrejena == null) vrže novo IllegalArgumentException ("EncryptedClassLoader" + "zahteva nadrejeni nadrejeni delež, ki ni nulen"); } / ** * De / šifrira binarne podatke v dani bajtni matriki. Ponovni klic metode * razveljavi šifriranje. * / zasebna statična praznina kripta (končni bajt [] podatki) {za (int i = 8; i <data.length; ++ i) podatke [i] ^ = 0x5A; } ... več pomožnih metod ...} // Konec predavanja 

EncryptedClassLoader ima dve osnovni operaciji: šifriranje danega nabora razredov v določenem imeniku poti razredov in zagon predhodno šifrirane aplikacije. Šifriranje je zelo preprosto: v bistvu se prevrne nekaj bitov vsakega bajta v vsebini binarnega razreda. (Da, stari dobri XOR (izključno ALI) skorajda sploh ne šifrira, vendar bodite z mano. To je le ilustracija.)

Nalaganje predavanj z EncryptedClassLoader si zasluži malo več pozornosti. Moji podrazredi izvajanja java.net.URLClassLoader in preglasi oba loadClass () in defineClass () za dosego dveh ciljev. Eno je upogniti običajna pravila za prenos pooblaščenca Java 2 in dobiti priložnost za nalaganje šifriranega razreda, preden sistemski nalagalnik razredov to stori, drugo pa poklicati kripta () neposredno pred klicem na defineClass () ki se sicer dogaja znotraj URLClassLoader.findClass ().

Po sestavi vse v zabojnik imenik:

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

Oba "šifriram" Glavna in MySecretClass razredi:

> java -cp bin EncryptedClassLoader -encrypt bin Main my.secret.code.MySecretClass šifriran [Main.class] šifriran [my \ secret \ code \ MySecretClass.class] 

Ta dva razreda v zabojnik so zdaj nadomeščene s šifriranimi različicami, in če želite zagnati prvotno aplikacijo, jo moram zagnati skozi EncryptedClassLoader:

> java -cp bin Glavna izjema v niti "main" java.lang.ClassFormatError: Main (nedovoljena vrsta konstantnega področja) pri java.lang.ClassLoader.defineClass0 (Native Method) 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 (URLClass. net.URLClassLoader.run (URLClassLoader.java:193) na java.security.AccessController.doPrivileged (Native Method) na java.net.URLClassLoader.findClass (URLClassLoader.java:186) na java.lang.ClassLoerLL. 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 )> java -cp bin EncryptedClassLoader -run bin Glavni dešifriran [Main] dešifriran [my.secret.code.MySecretClass] skrivni rezultat = 1362768201 

Seveda, izvajanje katerega koli dekompilatorja (kot je Jad) na šifriranih razredih ne deluje.

Čas je, da dodate prefinjeno shemo zaščite z geslom, jo ​​zavijete v izvorno izvedljivo datoteko in za "rešitev za zaščito programske opreme" zaračunate stotine dolarjev, kajne? Seveda ne.

ClassLoader.defineClass (): neizogibna prestrezna točka

Vse ClassLoadermorajo svoje definicije razredov dostaviti JVM prek ene natančno določene točke API: java.lang.ClassLoader.defineClass () metoda. The ClassLoader API ima več preobremenitev te metode, vendar vse zahtevajo defineClass (String, byte [], int, int, ProtectionDomain) metoda. Je dokončno metoda, ki po nekaj preverjanjih pokliče v izvorno kodo JVM. Pomembno je, da to razumemo noben učitelj nalaganja se ne more izogniti klicu te metode, če želi ustvariti novo Razred.

The defineClass () metoda je edino mesto, kjer je čarobnost ustvarjanja a Razred predmet iz ploščatega bajtnega polja. In uganite kaj, bajtna matrika mora vsebovati nešifrirano definicijo razreda v dobro dokumentirani obliki (glejte specifikacijo oblike datoteke razreda). Razbijanje sheme šifriranja je zdaj preprosto prestrezanje vseh klicev te metode in razstavljanje vseh zanimivih razredov po vaši želji (kasneje omenim še eno možnost, vmesnik JVM Profiler Interface (JVMPI)

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