Programmering

Krakning af Java-byte-kodekryptering

9. maj 2003

Spørgsmål: Hvis jeg krypterer mine .class-filer og bruger en brugerdefineret classloader til at indlæse og dekryptere dem i farten, vil dette forhindre dekompilering?

EN: Problemet med at forhindre Java-byte-kodedekompilering er næsten lige så gammelt som selve sproget. På trods af en række tiltrækningsværktøjer, der er tilgængelige på markedet, tænker uerfarne Java-programmerere på nye og smarte måder at beskytte deres intellektuelle ejendom på. Heri Java Q&A rate, jeg fjerner nogle myter omkring en idé, der ofte genopvaskes i diskussionsfora.

Den ekstreme lethed, som Java .klasse filer kan rekonstrueres i Java-kilder, der ligner originalerne, har meget at gøre med Java-byte-kodedesignmål og kompromiser. Blandt andet blev Java-byte-kode designet til kompakthed, platformuafhængighed, netværksmobilitet og let analyse af byte-kodetolke og JIT (just-in-time) / HotSpot dynamiske compilere. Formentlig den kompilerede .klasse filer udtrykker programmørens hensigt så klart, at de kunne være lettere at analysere end den oprindelige kildekode.

Flere ting kan gøres, hvis ikke for at forhindre dekompilering fuldstændigt, i det mindste for at gøre det vanskeligere. For eksempel, som et post-kompileringstrin, kan du massere .klasse data for at gøre bytekoden enten sværere at læse ved dekompilering eller sværere at dekompilere til gyldig Java-kode (eller begge dele). Teknikker som at udføre ekstrem overbelastning af metodenavne fungerer godt for førstnævnte og manipulation af kontrolflow for at skabe kontrolstrukturer, der ikke er mulig at repræsentere gennem Java-syntaks, fungerer godt for sidstnævnte. De mere succesrige kommercielle obfuskatorer bruger en blanding af disse og andre teknikker.

Desværre skal begge tilgange faktisk ændre koden, som JVM vil køre, og mange brugere er bange (med rette) for, at denne transformation kan tilføje nye bugs til deres applikationer. Desuden kan omdøbning af metode og felt medføre, at reflektionsopkald stopper med at arbejde. Ændring af aktuelle klasse- og pakkenavne kan bryde adskillige andre Java API'er (JNDI (Java Naming and Directory Interface), URL-udbydere osv.). Ud over ændrede navne, hvis sammenhængen mellem klasse-byte-kodeforskydninger og kildelinjenumre ændres, kan det være vanskeligt at gendanne de originale undtagelsesspor.

Derefter er der mulighed for at tilsløre den originale Java-kildekode. Men grundlæggende forårsager dette et lignende sæt problemer.

Krypter, ikke tilsløres?

Måske har ovenstående fået dig til at tænke, "Nå, hvad hvis jeg i stedet for at manipulere bytekode krypterer alle mine klasser efter kompilering og dekrypterer dem i farten inde i JVM (hvilket kan gøres med en brugerdefineret classloader)? Så udfører JVM min original byte kode, og alligevel er der intet at dekompilere eller reverse engineering, ikke? "

Desværre ville du tage fejl, både når du troede, at du var den første til at komme med denne idé, og når du troede, at den faktisk fungerer. Og årsagen har intet at gøre med styrken i din krypteringsordning.

En simpel klassekoder

For at illustrere denne idé implementerede jeg en prøveapplikation og en meget triviel brugerdefineret classloader til at køre den. Ansøgningen består af to korte klasser:

public class Main {public static void main (final String [] args) {System.out.println ("secret result =" + MySecretClass.mySecretAlgorithm ()); }} // Klassens pakke slutter my.secret.code; importere java.util.Random; public class MySecretClass {/ ** * Gæt hvad, den hemmelige algoritme bruger bare en tilfældig talgenerator ... * / public static int mySecretAlgorithm () {return (int) s_random.nextInt (); } privat statisk endelig Tilfældig s_random = ny tilfældig (System.currentTimeMillis ()); } // Afslutning af undervisningen 

Mit mål er at skjule implementeringen af min.secret.code.MySecretClass ved at kryptere det relevante .klasse filer og dekryptere dem i farten ved kørsel. Til dette formål bruger jeg følgende værktøj (nogle detaljer udeladt; du kan downloade den fulde kilde fra ressourcer):

public class EncryptedClassLoader udvider URLClassLoader {public static void main (final String [] args) throw Exception {if ("-run" .equals (args [0]) && (args.length> = 3)) {// Opret en brugerdefineret loader, der bruger den aktuelle loader som // delegationsforælder: final ClassLoader appLoader = ny EncryptedClassLoader (EncryptedClassLoader.class.getClassLoader (), ny fil (args [1])); // Trådkontekst loader skal også justeres: Thread.currentThread () .setContextClassLoader (appLoader); endelig klasse-app = appLoader.loadClass (args [2]); endelig metode appmain = app.getMethod ("main", ny klasse [] {String [] .class}); endelig streng [] appargs = ny streng [args.længde - 3]; System.arraycopy (args, 3, appargs, 0, appargs.length); appmain.invoke (null, nyt objekt [] {appargs}); } ellers hvis ("-encrypt" .equals (args [0]) && (args.length> = 3)) {... krypter angivne klasser ...} Ellers smider nyt IllegalArgumentException (USGE); } / ** * Tilsidesætter java.lang.ClassLoader.loadClass () for at ændre de sædvanlige forældre-barn * delegeringsregler lige nok til at være i stand til at "snappe" applikationsklasser * under systemklasselæsers næse. * / public Class loadClass (final String name, final boolean resolution) kaster ClassNotFoundException {if (TRACE) System.out.println ("loadClass (" + name + "," + resolution + ")"); Klasse c = null; // Kontroller først, om denne klasse allerede er defineret af denne classloader // instans: c = findLoadedClass (navn); hvis (c == null) {Class parentVersion = null; prøv {// Dette er lidt uortodoks: lav en prøveindlæsning via // forældrelæsseren og bemærk, om forælderen er delegeret eller ej; // hvad dette opnår er korrekt delegering til alle kerne // og udvidelsesklasser uden at jeg behøver at filtrere på klassens navn: parentsVersion = getParent () .loadClass (name); hvis (parentVersion.getClassLoader ()! = getParent ()) c = parentVersion; } fange (ClassNotFoundException ignorere) {} fange (ClassFormatError ignorere) {} hvis (c == null) {prøv {// OK, enten 'c' blev indlæst af systemet (ikke bootstrap // eller udvidelsen) loader (i hvilket tilfælde jeg vil ignorere den // definition) eller forælderen mislykkedes helt; på begge måder forsøger jeg // at definere min egen version: c = findClass (name); } fange (ClassNotFoundException ignorere) {// Hvis det mislykkedes, skal du falde tilbage på forældrenes version // [som kunne være nul på dette tidspunkt]: c = parentsVersion; }}} hvis (c == null) smider nyt ClassNotFoundException (navn); hvis (løse) løse Klasse (c); returnere c; } / ** * Tilsidesætter java.new.URLClassLoader.defineClass () for at kunne kalde * crypt () inden du definerer en klasse. * / beskyttet Class findClass (endelig strengnavn) kaster ClassNotFoundException {if (TRACE) System.out.println ("findClass (" + name + ")"); // .class-filer kan ikke garanteres at indlæses som ressourcer; // men hvis Suns kode gør det, kan det måske mine ... final String classResource = name.replace ('.', '/') + ".class"; endelig URL classURL = getResource (classResource); hvis (classURL == null) smider ny ClassNotFoundException (navn); ellers {InputStream in = null; prøv {in = classURL.openStream (); endelig byte [] classBytes = readFully (in); // "dekrypter": krypt (classBytes); hvis (TRACE) System.out.println ("dekrypteret [" + navn + "]"); returnere definitionClass (navn, classBytes, 0, classBytes.length); } fange (IOException ioe) {smid ny ClassNotFoundException (navn); } endelig {if (in! = null) prøv {in.close (); } fange (Undtagelse ignorere) {}}}} / ** * Denne classloader er kun i stand til tilpasset indlæsning fra en enkelt mappe. * / private EncryptedClassLoader (endelig ClassLoader forælder, endelig File classpath) kaster MalformedURLException {super (ny URL [] {classpath.toURL ()}, overordnet); hvis (forælder == null) smider nyt IllegalArgumentException ("EncryptedClassLoader" + "kræver en ikke-nul delegationsforælder"); } / ** * De / krypterer binære data i et givet byte-array. Hvis du kalder metoden igen * vender krypteringen om. * / privat statisk ugyldig krypt (slutbyte [] data) {for (int i = 8; i <data.length; ++ i) data [i] ^ = 0x5A; } ... flere hjælpemetoder ...} // Klassens afslutning 

EncryptedClassLoader har to grundlæggende operationer: kryptering af et givet sæt klasser i et givet klassesti-bibliotek og kørsel af et tidligere krypteret program. Krypteringen er meget ligetil: den består i bund og grund at vende nogle bit af hver byte i det binære klasses indhold. (Ja, den gode gamle XOR (eksklusiv OR) er næsten ingen kryptering overhovedet, men hold mig med. Dette er kun en illustration.)

Classloading af EncryptedClassLoader fortjener lidt mere opmærksomhed. Min implementering underklasser java.net.URLClassLoader og tilsidesætter begge dele loadClass () og defineClass () at nå to mål. Den ene er at bøje de sædvanlige regler for delegering af Java 2-klasselæsser og få en chance for at indlæse en krypteret klasse, før systemklasseladeren gør det, og en anden er at påberåbe sig krypt () straks før opkaldet til defineClass () der ellers sker indeni URLClassLoader.findClass ().

Efter at have samlet alt i beholder vejviser:

> javac -d bin src / *. java src / min / hemmelighed / kode / *. java 

Jeg "krypterer" begge dele Vigtigste og MySecretClass klasser:

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

Disse to klasser i beholder er nu blevet erstattet med krypterede versioner, og for at køre den oprindelige applikation skal jeg køre applikationen igennem EncryptedClassLoader:

> java -cp bin Hovedundtagelse i tråd "main" java.lang.ClassFormatError: Main (Ulovlig konstant pooltype) ved java.lang.ClassLoader.defineClass0 (Native Method) ved 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:250) på java.net.URLClassLoader.access00 (URLClassLoader.java:54) på ​​java. net.URLClassLoader.run (URLClassLoader.java:193) på java.security.AccessController.doPrivileged (Native Method) på java.net.URLClassLoader.findClass (URLClassLoader.java:186) på java.lang.ClassLoader.loadClass (ClassLoader. java: 299) på sun.misc.Launcher $ AppClassLoader.loadClass (Launcher.java:265) ved java.lang.ClassLoader.loadClass (ClassLoader.java:255) hos java.lang.ClassLoader.loadClassInternal (ClassLoader.java:315 )> java -cp bin EncryptedClassLoader-run bin Main decrypted [Main] decrypted [my.secret.code.MySecretClass] secret result = 1362768201 

Sikker nok fungerer det ikke at køre en decompiler (såsom Jad) på krypterede klasser.

Tid til at tilføje et sofistikeret ordning med adgangskodebeskyttelse, indpakke dette i en indfødt eksekverbar og opkræve hundreder af dollars for en "softwarebeskyttelsesløsning", ikke? Selvfølgelig ikke.

ClassLoader.defineClass (): Det uundgåelige skæringspunkt

Alle ClassLoaders er nødt til at levere deres klassedefinitioner til JVM via et veldefineret API-punkt: java.lang.ClassLoader.defineClass () metode. Det ClassLoader API har flere overbelastninger af denne metode, men alle kalder dem på defineClass (String, byte [], int, int, ProtectionDomain) metode. Det er en endelig metode, der kalder på JVM-indbygget kode efter at have foretaget et par kontroller. Det er vigtigt at forstå det ingen classloader kan undgå at kalde denne metode, hvis den vil oprette en ny Klasse.

Det defineClass () metoden er det eneste sted, hvor magien ved at skabe en Klasse objekt ud af et fladt byte-array kan finde sted. Og gæt hvad, byte-arrayet skal indeholde den ukrypterede klassedefinition i et veldokumenteret format (se klassefilformatspecifikationen). At bryde krypteringsplanen er nu et simpelt spørgsmål om at opfange alle opkald til denne metode og dekompilere alle interessante klasser efter dit hjertes ønske (jeg nævner en anden mulighed, JVM Profiler Interface (JVMPI), senere).