Προγραμματισμός

Σπάσιμο κρυπτογράφησης κώδικα byte Java

9 Μαΐου 2003

Ε: Εάν κρυπτογραφήσω τα αρχεία μου .class και χρησιμοποιήσω έναν προσαρμοσμένο classloader για να τα φορτώσω και να τα αποκρυπτογραφήσω εν κινήσει, αυτό θα αποτρέψει την αποσύνθεση;

ΕΝΑ: Το πρόβλημα της αποτροπής της αποκωδικοποίησης κώδικα byte Java είναι σχεδόν τόσο παλιά όσο η ίδια η γλώσσα. Παρά την ποικιλία εργαλείων συσκότισης που διατίθενται στην αγορά, οι αρχάριοι προγραμματιστές Java συνεχίζουν να σκέφτονται νέους και έξυπνους τρόπους για την προστασία της πνευματικής τους ιδιοκτησίας. Σε αυτό Ε & Α Java δόση, διαλύω μερικούς μύθους γύρω από μια ιδέα που επαναλαμβάνεται συχνά σε φόρουμ συζητήσεων.

Η εξαιρετική ευκολία με την οποία η Java .τάξη Τα αρχεία μπορούν να ανακατασκευαστούν σε πηγές Java που μοιάζουν πολύ με τα πρωτότυπα που έχουν να κάνουν με τους σχεδιαστικούς στόχους και τους συμβιβασμούς byte-code Java. Μεταξύ άλλων, ο κώδικας byte Java σχεδιάστηκε για συμπαγή, ανεξαρτησία πλατφόρμας, κινητικότητα δικτύου και ευκολία ανάλυσης από διερμηνείς κωδικών byte και δυναμικούς μεταγλωττιστές JIT (just-in-time) / HotSpot. Αναμφισβήτητα, το μεταγλωττισμένο .τάξη Τα αρχεία εκφράζουν την πρόθεση του προγραμματιστή τόσο ξεκάθαρα ώστε να είναι πιο εύκολο να αναλυθούν από τον αρχικό πηγαίο κώδικα.

Μπορούν να γίνουν πολλά πράγματα, αν όχι για να αποφευχθεί εντελώς η αποσύνθεση, τουλάχιστον για να γίνει πιο δύσκολη. Για παράδειγμα, ως βήμα μετά τη συλλογή μπορείτε να κάνετε μασάζ .τάξη δεδομένα για να κάνει τον κώδικα byte είτε πιο δύσκολο να διαβαστεί όταν αποσυντεθεί ή πιο δύσκολο να αποσυντεθεί σε έγκυρο κώδικα Java (ή και τα δύο). Τεχνικές όπως η εκτέλεση υπερβολικής φόρτωσης ονομασίας μεθόδου λειτουργούν καλά για την πρώτη, και ο χειρισμός της ροής ελέγχου για τη δημιουργία δομών ελέγχου που δεν είναι δυνατόν να αναπαρασταθούν μέσω της σύνταξης Java λειτουργούν καλά για την τελευταία. Οι πιο επιτυχημένοι εμπορικοί σκοπευτές χρησιμοποιούν ένα συνδυασμό αυτών και άλλων τεχνικών.

Δυστυχώς, και οι δύο προσεγγίσεις πρέπει στην πραγματικότητα να αλλάξουν τον κώδικα που θα εκτελεστεί το JVM και πολλοί χρήστες φοβούνται (δικαίως) ότι αυτός ο μετασχηματισμός μπορεί να προσθέσει νέα σφάλματα στις εφαρμογές τους. Επιπλέον, η μετονομασία μεθόδου και πεδίου μπορεί να προκαλέσει τη διακοπή λειτουργίας των κλήσεων προβληματισμού. Η αλλαγή των πραγματικών ονομάτων κλάσης και πακέτων μπορεί να καταστρέψει πολλά άλλα Java API (JNDI (Java Naming and Directory Interface), παρόχους URL κ.λπ.). Εκτός από τα αλλαγμένα ονόματα, εάν αλλάξει η συσχέτιση μεταξύ των αντισταθμιστικών κωδικών byte κλάσης και των αριθμών γραμμής προέλευσης, η ανάκτηση των αρχικών ίχνων στοίβας εξαίρεσης θα μπορούσε να γίνει δύσκολη.

Στη συνέχεια, υπάρχει η επιλογή να συγκαλυφθεί ο αρχικός πηγαίος κώδικας Java. Αλλά ουσιαστικά αυτό προκαλεί ένα παρόμοιο σύνολο προβλημάτων.

Κρυπτογράφηση, όχι σκοτεινή;

Ίσως τα παραπάνω σας έκαναν να σκεφτείτε, "Λοιπόν, τι γίνεται αν αντί να χειριστώ τον κώδικα byte, κρυπτογραφώ όλες τις τάξεις μου μετά τη μεταγλώττιση και τις αποκρυπτογραφώ εν κινήσει μέσα στο JVM (το οποίο μπορεί να γίνει με προσαρμοσμένο φορτωτή τάξης); πρωτότυπος κωδικός byte και όμως δεν υπάρχει τίποτα για αποσύνθεση ή αναστροφή μηχανικού, σωστά; "

Δυστυχώς, θα κάνατε λάθος, τόσο στο να σκεφτόσασταν ότι ήσασταν ο πρώτος που καταλήξατε σε αυτήν την ιδέα όσο και στο να σκεφτείτε ότι λειτουργεί πραγματικά. Και ο λόγος δεν έχει καμία σχέση με τη δύναμη του σχήματος κρυπτογράφησής σας.

Ένας απλός κωδικοποιητής κλάσης

Για να επεξηγήσω αυτήν την ιδέα, εφάρμοσα ένα δείγμα εφαρμογής και έναν πολύ ασήμαντο προσαρμοσμένο classloader για να το τρέξω. Η εφαρμογή αποτελείται από δύο σύντομες τάξεις:

δημόσια τάξη Main {public static void main (final String [] args) {System.out.println ("μυστικό αποτέλεσμα =" + MySecretClass.mySecretAlgorithm ()); }} // Πακέτο πακέτου τέλους κλάσης my.secret.code; εισαγωγή java.util.Random; δημόσια τάξη MySecretClass {/ ** * Μαντέψτε τι, ο μυστικός αλγόριθμος χρησιμοποιεί απλώς μια γεννήτρια τυχαίων αριθμών ... * / public static int mySecretAlgorithm () {return (int) s_random.nextInt (); } ιδιωτικό στατικό τελικό Random s_random = new Random (System.currentTimeMillis ()); } // Τέλος μαθήματος 

Η φιλοδοξία μου είναι να κρύψω την εφαρμογή του my.secret.code.MySecretClass κρυπτογραφώντας το σχετικό .τάξη αρχεία και τα αποκρυπτογραφούν εν κινήσει κατά το χρόνο εκτέλεσης. Για το σκοπό αυτό, χρησιμοποιώ το ακόλουθο εργαλείο (παραλείπονται ορισμένες λεπτομέρειες. Μπορείτε να κατεβάσετε την πλήρη πηγή από τους πόρους):

δημόσια κλάση EncryptedClassLoader επεκτείνει το URLClassLoader {public static void main (final String [] args) ρίχνει την Εξαίρεση {if ("-run" .equals (args [0]) && (args.length> = 3)) {// Δημιουργία προσαρμοσμένου loader που θα χρησιμοποιήσει τον τρέχοντα φορτωτή ως // γονέας αντιπροσωπείας: final ClassLoader appLoader = new EncryptedClassLoader (EncryptedClassLoader.class.getClassLoader (), new File (args [1])); // Ο φορτωτής περιβάλλοντος νήματος πρέπει επίσης να προσαρμοστεί: Thread.currentThread () .setContextClassLoader (appLoader); final Class app = appLoader.loadClass (args [2]); τελική μέθοδος appmain = app.getMethod ("main", new Class [] {String [] .class}); final String [] appargs = new String [args.length - 3]; System.arraycopy (args, 3, appargs, 0, appargs.length); appmain.invoke (μηδέν, νέο αντικείμενο [] {appargs}); } αλλιώς εάν ("-encrypt" .equals (args [0]) && (args.length> = 3)) {... κρυπτογράφηση καθορισμένων κλάσεων ...} αλλιώς ρίξτε νέα IllegalArgumentException (USAGE); } / ** * Αντικαθιστά το java.lang.ClassLoader.loadClass () για να αλλάξετε τους συνηθισμένους κανόνες εξουσιοδότησης γονέα-παιδιού * αρκετά ώστε να μπορείτε να "αρπάξετε" τάξεις εφαρμογών * από τη μύτη του φορτωτή συστήματος. * / public Class loadClass (τελικό όνομα συμβολοσειράς, τελική δυαδική επίλυση) ρίχνει ClassNotFoundException {if (TRACE) System.out.println ("loadClass (" + name + "," + resolus + ")"); Κατηγορία c = null; // Πρώτα, ελέγξτε αν αυτή η κλάση έχει ήδη καθοριστεί από αυτόν τον classloader // instance: c = findLoadedClass (name); if (c == null) {Class parentsVersion = null; δοκιμάστε {// Αυτό είναι ελαφρώς ανορθόδοξο: κάντε ένα δοκιμαστικό φορτίο μέσω του // γονικού φορτωτή και σημειώστε αν ο γονέας εκχωρήθηκε ή όχι. // αυτό που επιτυγχάνει είναι η σωστή ανάθεση για όλες τις βασικές // και τάξεις επέκτασης χωρίς να χρειάζεται να φιλτράρω το όνομα της τάξης: parentsVersion = getParent () .loadClass (name); if (parentsVersion.getClassLoader ()! = getParent ()) γ = γονείςVersion; } catch (ClassNotFoundException ignore) {} catch (ClassFormatError ignore) {} εάν (c == null) {δοκιμάστε {// ΟΚ, είτε το "c" φορτώθηκε από το σύστημα (όχι το bootstrap // ή την επέκταση) loader (σε σε ποια περίπτωση θέλω να αγνοήσω αυτόν τον ορισμό // ή ο γονέας απέτυχε εντελώς. Οποιοσδήποτε τρόπος I // προσπαθώ να καθορίσω τη δική μου έκδοση: c = findClass (όνομα); } catch (ClassNotFoundException Αγνοήστε) {// Εάν αυτό απέτυχε, επιστρέψτε στην έκδοση του γονέα // [η οποία θα μπορούσε να είναι μηδενική σε αυτό το σημείο]: c = parentsVersion; }}} εάν (c == null) ρίξτε νέο ClassNotFoundException (όνομα); εάν (επίλυση) resolClass (c); επιστροφή γ; } / ** * Αντικαθιστά το java.new.URLClassLoader.defineClass () για να μπορείτε να καλέσετε * crypt () προτού ορίσετε μια τάξη. * / προστατευμένη κλάση findClass (τελικό όνομα συμβολοσειράς) ρίχνει ClassNotFoundException {if (TRACE) System.out.println ("findClass (" + name + ")"); // Τα αρχεία .class δεν είναι εγγυημένα ότι μπορούν να φορτωθούν ως πόροι. // αλλά αν ο κώδικας της Sun το κάνει, τότε ίσως μπορώ να κάνω ... final String classResource = name.replace ('.', '/') + ".class"; τελικό URL classURL = getResource (classResource); εάν (classURL == null) ρίξτε νέο ClassNotFoundException (όνομα); αλλιώς {InputStream in = null; δοκιμάστε το {in = classURL.openStream (); final byte [] classBytes = readFully (σε); // "αποκρυπτογράφηση": crypt (classBytes); εάν (TRACE) System.out.println ("αποκρυπτογραφημένο [" + όνομα + "]"); return defineClass (όνομα, classBytes, 0, classBytes.length); } catch (IOException) {ρίξτε νέο ClassNotFoundException (όνομα); } τέλος {if (in! = null) δοκιμάστε το {in.close (); } catch (Εξαίρεση αγνοήστε) {}}}} / ** * Αυτός ο classloader έχει τη δυνατότητα προσαρμογής μόνο φόρτωσης από έναν μόνο κατάλογο. * / ιδιωτικό EncryptedClassLoader (τελικό ClassLoader γονικό, final File classpath) ρίχνει MalformedURLException {super (νέο URL [] {classpath.toURL ()}, γονικό); εάν (γονέας == null) ρίξτε νέο IllegalArgumentException ("EncryptedClassLoader" + "απαιτεί μη μηδενικό γονέα εξουσιοδότησης"); } / ** * Απενεργοποίηση / κρυπτογράφηση δυαδικών δεδομένων σε έναν δεδομένο πίνακα byte. Κλήση ξανά της μεθόδου * αντιστρέφει την κρυπτογράφηση. * / ιδιωτική στατική κενή κρυπτογράφηση (τελικό byte [] δεδομένα) {για (int i = 8; i <data.length; ++ i) data [i] ^ = 0x5A; } ... περισσότερες μέθοδοι βοηθού ...} // Τέλος τάξης 

EncryptedClassLoader έχει δύο βασικές λειτουργίες: κρυπτογράφηση ενός δεδομένου συνόλου κλάσεων σε έναν συγκεκριμένο κατάλογο classpath και εκτέλεση μιας προηγουμένως κρυπτογραφημένης εφαρμογής. Η κρυπτογράφηση είναι πολύ απλή: αποτελείται βασικά από την αναστροφή ορισμένων bit κάθε byte στα περιεχόμενα της δυαδικής κλάσης. (Ναι, το καλό παλιό XOR (αποκλειστικό OR) δεν είναι σχεδόν καθόλου κρυπτογράφηση, αλλά αντέχω μαζί μου. Αυτό είναι απλώς μια εικόνα.)

Φόρτωση τάξης από EncryptedClassLoader αξίζει λίγο περισσότερη προσοχή. Οι υποκατηγορίες εφαρμογής μου java.net.URLClassLoader και υπερισχύει και των δύο loadClass () και defineClass () να επιτύχει δύο στόχους. Το ένα είναι να κάμψετε τους συνηθισμένους κανόνες εκχώρησης του Java 2 classloader και να έχετε την ευκαιρία να φορτώσετε μια κρυπτογραφημένη κλάση πριν το κάνει ο classloader του συστήματος και ένα άλλο να επικαλεστεί κρύπτη() αμέσως πριν από την κλήση προς defineClass () αλλιώς συμβαίνει μέσα URLClassLoader.findClass ().

Αφού συγκεντρώσετε τα πάντα στο αποθήκη Ευρετήριο:

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

"Κρυπτογραφώ" και τα δύο Κύριος και MySecretClass τάξεις:

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

Αυτές οι δύο τάξεις στο αποθήκη τώρα έχουν αντικατασταθεί με κρυπτογραφημένες εκδόσεις και για να εκτελέσω την αρχική εφαρμογή, πρέπει να εκτελέσω την εφαρμογή EncryptedClassLoader:

> java -cp bin Κύρια εξαίρεση στο νήμα "main" java.lang.ClassFormatError: Main (Παράνομος τύπος σταθερού pool) στο java.lang.ClassLoader.defineClass0 (Native Method) στο java.lang.ClassLoader.defineClass (ClassLoader.java: 502) στο java.security.SecureClassLoader.defineClass (SecureClassLoader.javaonym23) στο java.net.URLClassLoader.defineClass (URLClassLoader.java:2350) στο java.net.URLClassLoader.access00 (URLClassLoader.java:54) στο java.net. net.URLClassLoader.run (URLClassLoader.java:193) στο java.security.AccessController.doPrivileged (εγγενής μέθοδος) στο java.net.URLClassLoader.findClass (URLClassLoader.java :86) στο java.lang.ClassLoader.loadClass (ClassLoader.loadClass java: 299) στο sun.misc.Launcher $ AppClassLoader.loadClass (Launcher.java: 265) στο java.lang.ClassLoader.loadClass (ClassLoader.java: 5555) στο java.lang.ClassLoader.loadClassInternal (ClassLoader.java:315 )> java -cp bin EncryptedClassLoader -run bin Main αποκρυπτογραφημένο [Main] αποκρυπτογραφημένο [my.secret.code.MySecretClass] μυστικό αποτέλεσμα = 1362768201 

Σίγουρα, η εκτέλεση οποιουδήποτε αποκωδικοποιητή (όπως Jad) σε κρυπτογραφημένες τάξεις δεν λειτουργεί.

Ώρα να προσθέσετε ένα εξελιγμένο σύστημα προστασίας κωδικού πρόσβασης, να το τυλίξετε σε ένα εγγενές εκτελέσιμο και να χρεώσετε εκατοντάδες δολάρια για μια «λύση προστασίας λογισμικού», σωστά; Φυσικά και όχι.

ClassLoader.defineClass (): Το αναπόφευκτο σημείο αναχαίτισης

Ολα ClassLoaderΠρέπει να παραδώσουν τους ορισμούς της κατηγορίας τους στο JVM μέσω ενός καλά καθορισμένου σημείου API: το java.lang.ClassLoader.defineClass () μέθοδος. ο ClassLoader Το API έχει αρκετές υπερφορτώσεις αυτής της μεθόδου, αλλά όλοι τους καλούν στο defineClass (String, byte [], int, int, ProtectionDomain) μέθοδος. Είναι ένα τελικός μέθοδος που καλεί στον εγγενή κώδικα JVM μετά από μερικούς ελέγχους. Είναι σημαντικό να το κατανοήσουμε αυτό κανένας classloader δεν μπορεί να αποφύγει την κλήση αυτής της μεθόδου εάν θέλει να δημιουργήσει μια νέα Τάξη.

ο defineClass () Η μέθοδος είναι το μόνο μέρος όπου η μαγεία της δημιουργίας ενός Τάξη μπορεί να πραγματοποιηθεί αντικείμενο από μια επίπεδη συστοιχία byte. Και μαντέψτε τι, ο πίνακας byte πρέπει να περιέχει τον μη κρυπτογραφημένο ορισμό κλάσης σε καλά τεκμηριωμένη μορφή (δείτε την προδιαγραφή μορφής αρχείου κλάσης). Η διακοπή του σχήματος κρυπτογράφησης είναι τώρα ένα απλό ζήτημα της αναχαίτισης όλων των κλήσεων σε αυτήν τη μέθοδο και της αποσύνθεσης όλων των ενδιαφέρων τάξεων στην επιθυμία της καρδιάς σας (αναφέρω μια άλλη επιλογή, JVM Profiler Interface (JVMPI), αργότερα).

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