Programmering

Sprekker Java-byte-kodekryptering

9. mai 2003

Spørsmål: Hvis jeg krypterer .class-filene mine og bruker en tilpasset klasselaster for å laste og dekryptere dem på farten, vil dette forhindre dekompilering?

EN: Problemet med å forhindre Java byte-kodedekompilering er nesten like gammelt språket. Til tross for en rekke forvirringsverktøy som er tilgjengelige på markedet, fortsetter nybegynnere Java-programmerere å tenke på nye og smarte måter å beskytte deres intellektuelle eiendom på. I dette Java Q&A avdrag, fjerner jeg noen myter rundt en idé som ofte blir gjennomvasket i diskusjonsfora.

Den ekstreme lettheten Java .klasse filer kan rekonstrueres til Java-kilder som ligner originalene, har mye å gjøre med Java-byte-kodedesignmål og kompromisser. Blant annet ble Java-bytekode designet for kompaktitet, plattformuavhengighet, nettverksmobilitet og enkel analyse av byte-kodetolker og JIT (just-in-time) / HotSpot dynamiske kompilatorer. Uten tvil den kompilert .klasse filer uttrykker programmørens hensikt så tydelig at de kan være lettere å analysere enn den opprinnelige kildekoden.

Flere ting kan gjøres, om ikke for å forhindre dekompilering helt, i det minste for å gjøre det vanskeligere. For eksempel, som et trinn etter samling, kan du massere .klasse data for å gjøre bytekoden enten vanskeligere å lese når de dekompileres eller vanskeligere å dekompilere til gyldig Java-kode (eller begge deler). Teknikker som å utføre overbelastning av ekstreme metodenavn fungerer bra for førstnevnte, og manipulering av kontrollflyt for å skape kontrollstrukturer som ikke er mulig å representere gjennom Java-syntaks, fungerer bra for sistnevnte. De mer vellykkede kommersielle obfuskatorene bruker en blanding av disse og andre teknikker.

Dessverre må begge tilnærmingene endre koden JVM vil kjøre, og mange brukere er redde (med rette) for at denne transformasjonen kan legge til nye feil i applikasjonene deres. Videre kan navn og metode endre navn på at refleksjonssamtaler slutter å virke. Endring av faktiske klasse- og pakkenavn kan bryte flere andre Java APIer (JNDI (Java Naming and Directory Interface), URL-leverandører, etc.). I tillegg til endrede navn, hvis sammenhengen mellom klassebytekodeforskyvninger og kildelinjenumre endres, kan det bli vanskelig å gjenopprette de originale unntakssporene.

Deretter er det muligheten til å tilsløre den originale Java-kildekoden. Men grunnleggende forårsaker dette et lignende sett med problemer.

Kryptere, ikke tilsløre?

Kanskje det ovennevnte har fått deg til å tenke, "Vel, hva hvis jeg i stedet for å manipulere bytekode, krypterer jeg alle klassene mine etter kompilering og dekrypterer dem i farten inne i JVM (som kan gjøres med en tilpasset klasselaster)? Så utfører JVM min original byte-kode, og likevel er det ingenting å dekompilere eller reversere, ikke sant? "

Dessverre vil du ta feil, både når du tenker at du var den første som kom på denne ideen, og i å tenke at den faktisk fungerer. Og årsaken har ingenting å gjøre med styrken i krypteringsordningen.

En enkel klassekoder

For å illustrere denne ideen implementerte jeg et eksempel på applikasjon og en veldig triviell tilpasset klasselaster for å kjøre den. Søknaden består av to korte klasser:

public class Main {public static void main (final String [] args) {System.out.println ("secret result =" + MySecretClass.mySecretAlgorithm ()); }} // Slutt på kurspakken my.secret.code; importere java.util.Random; public class MySecretClass {/ ** * Gjett hva, den hemmelige algoritmen bruker bare en tilfeldig tallgenerator ... * / public static int mySecretAlgorithm () {return (int) s_random.nextInt (); } privat statisk slutt Tilfeldig s_random = ny Tilfeldig (System.currentTimeMillis ()); } // Slutten på timen 

Mitt mål er å skjule implementeringen av my.secret.code.MySecretClass ved å kryptere det aktuelle .klasse filer og dekryptere dem på farten ved kjøretid. For å gjøre dette bruker jeg følgende verktøy (noen detaljer utelatt; du kan laste ned hele kilden fra Resources):

public class EncryptedClassLoader utvider URLClassLoader {public static void main (final String [] args) throw Exception {if ("-run" .equals (args [0]) && (args.length> = 3)) {// Create a custom laster som vil bruke den nåværende lasteren som // delegasjonsforelder: endelig ClassLoader appLoader = ny EncryptedClassLoader (EncryptedClassLoader.class.getClassLoader (), ny fil (args [1])); // Trådkontekstlaster må også justeres: Thread.currentThread () .setContextClassLoader (appLoader); final Class app = appLoader.loadClass (args [2]); final Method appmain = app.getMethod ("main", ny klasse [] {String [] .class}); final String [] appargs = new String [args.length - 3]; System.arraycopy (args, 3, appargs, 0, appargs.length); appmain.invoke (null, nytt objekt [] {appargs}); } annet hvis ("-encrypt" .equals (args [0]) && (args.length> = 3)) {... krypter spesifiserte klasser ...} ellers kaster nye IllegalArgumentException (USGE); } / ** * Overstyrer java.lang.ClassLoader.loadClass () for å endre de vanlige foreldre-barn * delegeringsreglene akkurat nok til å kunne "snappe" applikasjonsklasser * under systemklasserens nese. * / public Class loadClass (final String name, final boolean resolution) kaster ClassNotFoundException {if (TRACE) System.out.println ("loadClass (" + name + "," + resolution + ")"); Klasse c = null; // Sjekk først om denne klassen allerede er definert av denne klasselasteren // forekomst: c = findLoadedClass (navn); hvis (c == null) {Class parentesVersion = null; prøv {// Dette er litt uortodoks: gjør en prøveinnlasting via // foreldrelaster og merk om foreldrene er delegert eller ikke; // hva dette oppnår er riktig delegering for alle kjerne // og utvidelsesklasser uten at jeg trenger å filtrere på klassenavn: foreldreVersjon = getParent () .loadClass (navn); hvis (parentVersion.getClassLoader ()! = getParent ()) c = foreldreversjon; } catch (ClassNotFoundException ignore) {} catch (ClassFormatError ignore) {} if (c == null) {try {// OK, enten 'c' ble lastet av systemet (ikke bootstrap // eller utvidelsen) i hvilket tilfelle jeg vil ignorere den // definisjonen) eller foreldrene mislyktes helt; uansett hva jeg // prøver å definere min egen versjon: c = findClass (name); } fange (ClassNotFoundException ignorere) {// Hvis det mislyktes, fall tilbake på foreldrenes versjon // [som kan være null på dette punktet]: c = foreldreversjon; }}} hvis (c == null) kaster nytt ClassNotFoundException (navn); hvis (løse) løse Klasse (c); retur c; } / ** * Overstyrer java.new.URLClassLoader.defineClass () for å kunne ringe * crypt () før du definerer en klasse. * / beskyttet Class findClass (endelig strengnavn) kaster ClassNotFoundException {if (TRACE) System.out.println ("findClass (" + name + ")"); // .class-filer kan ikke garanteres at de kan lastes inn som ressurser; // men hvis Suns kode gjør det, kan det kanskje min ... final String classResource = name.replace ('.', '/') + ".class"; endelig URL classURL = getResource (classResource); hvis (classURL == null) kaster nytt ClassNotFoundException (navn); annet {InputStream in = null; prøv {in = classURL.openStream (); endelig byte [] classBytes = readFully (in); // "dekryptere": krypt (classBytes); hvis (TRACE) System.out.println ("dekryptert [" + navn + "]"); return defineClass (navn, classBytes, 0, classBytes.length); } fange (IOException ioe) {kast ny ClassNotFoundException (navn); } til slutt {if (in! = null) prøv {in.close (); } catch (Unntak ignorere) {}}}} / ** * Denne klasselasteren kan bare tilpasses fra en enkelt katalog. * / private EncryptedClassLoader (final ClassLoader parent, final File classpath) kaster MalformedURLException {super (ny URL [] {classpath.toURL ()}, overordnet); hvis (foreldre == null) kaster ny IllegalArgumentException ("EncryptedClassLoader" + "krever en ikke-null delegeringsforelder"); } / ** * De / krypterer binære data i en gitt byte-array. Å ringe metoden igjen * reverserer krypteringen. * / privat statisk ugyldig krypt (sluttbyte [] data) {for (int i = 8; i <data.length; ++ i) data [i] ^ = 0x5A; } ... flere hjelpemetoder ...} // Klassen er avsluttet 

EncryptedClassLoader har to grunnleggende operasjoner: kryptering av et gitt sett med klasser i en gitt klassesti-katalog og kjøring av et tidligere kryptert program. Krypteringen er veldig grei: den består i utgangspunktet av å bla noen biter av hver byte i innholdet i den binære klassen. (Ja, den gode gamle XOR (eksklusiv OR) er nesten ingen kryptering i det hele tatt, men vær med meg. Dette er bare en illustrasjon.)

Klasselasting av EncryptedClassLoader fortjener litt mer oppmerksomhet. Min underklasser for implementering java.net.URLClassLoader og overstyrer begge deler loadClass () og defineClass () å oppnå to mål. Den ene er å bøye de vanlige Java 2 classloader-delegeringsreglene og få sjansen til å laste en kryptert klasse før systemklasselasteren gjør det, og en annen er å påkalle krypt () umiddelbart før samtalen til defineClass () som ellers skjer inni URLClassLoader.findClass ().

Etter å ha samlet alt i søppel katalog:

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

Jeg "krypterer" begge deler Hoved og MySecretClass klasser:

> java -cp bin EncryptedClassLoader -crypt bin Hoved my.secret.code.MySecretClass kryptert [Main.class] kryptert [my \ secret \ code \ MySecretClass.class] 

Disse to klassene i søppel har nå blitt erstattet med krypterte versjoner, og for å kjøre den opprinnelige applikasjonen, må jeg kjøre applikasjonen gjennom EncryptedClassLoader:

> java -cp bin Hoved unntak i tråden "main" java.lang.ClassFormatError: Main (Ulovlig konstant bassengtype) ved java.lang.ClassLoader.defineClass0 (Native Method) på java.lang.ClassLoader.defineClass (ClassLoader.java: 502) på java.security.SecureClassLoader.defineClass (SecureClassLoader.java:123) på java.net.URLClassLoader.defineClass (URLClassLoader.java:250) på java.net.URLClassLoader.access00 (URLClassLoader.java:54) på ​​java. net.URLClassLoader.run (URLClassLoader.java:193) at java.security.AccessController.doPrivileged (Native Method) at java.net.URLClassLoader.findClass (URLClassLoader.java:186) at java.lang.ClassLoader.loadClass (ClassLoader. java: 299) på sun.misc.Launcher $ AppClassLoader.loadClass (Launcher.java:265) på java.lang.ClassLoader.loadClass (ClassLoader.java:255) på java.lang.ClassLoader.loadClassInternal (ClassLoader.java:315 )> java -cp bin EncryptedClassLoader -run bin Main decrypted [Main] decrypted [my.secret.code.MySecretClass] secret result = 1362768201 

Visst nok, kjører en dekompilator (som Jad) på krypterte klasser fungerer ikke.

På tide å legge til et sofistikert ordning for passordbeskyttelse, pakke dette inn i en innfødt kjørbar datamaskin og belaste hundrevis av dollar for en "programvarebeskyttelsesløsning", ikke sant? Selvfølgelig ikke.

ClassLoader.defineClass (): Det uunngåelige skjæringspunktet

Alle ClassLoaders må levere sine klassedefinisjoner til JVM via ett veldefinert API-punkt: java.lang.ClassLoader.defineClass () metode. De ClassLoader API har flere overbelastninger av denne metoden, men alle ringer til defineClass (String, byte [], int, int, ProtectionDomain) metode. Det er en endelig metoden som kaller inn JVM-innfødt kode etter å ha gjort noen få sjekker. Det er viktig å forstå det ingen klasselaster kan unngå å kalle denne metoden hvis den ønsker å lage en ny Klasse.

De defineClass () metoden er det eneste stedet der magien med å skape en Klasse objekt ut av en flat byte-array kan finne sted. Og gjett hva, byte-matrisen må inneholde den ukrypterte klassedefinisjonen i et veldokumentert format (se klassefilformatspesifikasjonen). Å bryte krypteringsskjemaet er nå et enkelt spørsmål om å avlytte alle samtaler til denne metoden og dekompilere alle interessante klasser etter ditt hjertes ønske (jeg nevner et annet alternativ, JVM Profiler Interface (JVMPI), senere).

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