Memecahkan enkripsi kode byte Java

9 Mei 2003

T: Jika saya mengenkripsi file .class saya dan menggunakan classloader khusus untuk memuat dan mendekripsinya dengan cepat, apakah ini akan mencegah dekompilasi?

J: Masalah dalam mencegah dekompilasi kode byte Java hampir sama tuanya dengan bahasanya sendiri. Meskipun berbagai alat kebingungan tersedia di pasaran, programmer Java pemula terus memikirkan cara baru dan cerdas untuk melindungi kekayaan intelektual mereka. Dalam angsuran Tanya Jawab Java ini , saya menghilangkan beberapa mitos seputar ide yang sering diulang di forum diskusi.

Kemudahan ekstrim dimana .classfile Java dapat direkonstruksi menjadi sumber Java yang sangat mirip dengan aslinya sangat berkaitan dengan tujuan dan trade-off desain kode byte Java. Antara lain, kode byte Java dirancang untuk kekompakan, kemandirian platform, mobilitas jaringan, dan kemudahan analisis oleh juru bahasa kode-byte dan kompiler dinamis JIT (just-in-time) / HotSpot. Bisa dibilang, .classfile yang dikompilasi mengekspresikan maksud pemrogram dengan sangat jelas sehingga lebih mudah dianalisis daripada kode sumber aslinya.

Beberapa hal bisa dilakukan, jika tidak untuk mencegah pembusukan sepenuhnya, setidaknya membuatnya lebih sulit. Misalnya, sebagai langkah pasca-kompilasi Anda dapat memijat .classdata untuk membuat kode byte lebih sulit dibaca ketika didekompilasi atau lebih sulit untuk didekompilasi menjadi kode Java yang valid (atau keduanya). Teknik seperti melakukan overloading nama metode ekstrim bekerja dengan baik untuk yang pertama, dan memanipulasi aliran kontrol untuk membuat struktur kontrol yang tidak mungkin untuk direpresentasikan melalui sintaks Java bekerja dengan baik untuk yang terakhir. Obfuscator komersial yang lebih sukses menggunakan campuran teknik ini dan lainnya.

Sayangnya, kedua pendekatan tersebut harus benar-benar mengubah kode yang akan dijalankan JVM, dan banyak pengguna takut (memang seharusnya demikian) bahwa transformasi ini dapat menambahkan bug baru ke aplikasi mereka. Selanjutnya, metode dan penggantian nama bidang dapat menyebabkan panggilan refleksi berhenti bekerja. Mengubah nama kelas dan paket sebenarnya dapat merusak beberapa Java API lainnya (JNDI (Penamaan Java dan Antarmuka Direktori), penyedia URL, dll.). Selain nama yang diubah, jika asosiasi antara offset kode byte kelas dan nomor baris sumber diubah, memulihkan jejak tumpukan pengecualian asli bisa menjadi sulit.

Lalu ada opsi untuk mengaburkan kode sumber Java asli. Tetapi pada dasarnya hal ini menyebabkan serangkaian masalah serupa.

Enkripsi, bukan obfuscate?

Mungkin hal di atas telah membuat Anda berpikir, "Bagaimana jika alih-alih memanipulasi kode byte saya mengenkripsi semua kelas saya setelah kompilasi dan mendekripsi mereka dengan cepat di dalam JVM (yang dapat dilakukan dengan classloader kustom)? Kemudian JVM mengeksekusi saya kode byte asli dan tidak ada yang perlu didekompilasi atau direkayasa balik, bukan? "

Sayangnya, Anda salah, baik dalam berpikir bahwa Anda adalah orang pertama yang mengemukakan ide ini dan berpikir bahwa ide itu benar-benar berhasil. Dan alasannya tidak ada hubungannya dengan kekuatan skema enkripsi Anda.

Pembuat enkode kelas sederhana

Untuk mengilustrasikan ide ini, saya mengimplementasikan aplikasi contoh dan classloader kustom yang sangat sepele untuk menjalankannya. Aplikasi ini terdiri dari dua kelas pendek:

kelas publik Utama {public static void main (string akhir [] args) {System.out.println ("hasil rahasia =" + MySecretClass.mySecretAlgorithm ()); }} // Akhir dari paket kelas my.secret.code; impor java.util.Random; public class MySecretClass {/ ** * Coba tebak, algoritma rahasia hanya menggunakan generator nomor acak ... * / public static int mySecretAlgorithm () {return (int) s_random.nextInt (); } private static final Random s_random = new Random (System.currentTimeMillis ()); } // Akhir kelas

Aspirasi saya adalah menyembunyikan implementasi my.secret.code.MySecretClassdengan mengenkripsi .classfile yang relevan dan mendekripsinya dengan cepat saat runtime. Untuk itu, saya menggunakan alat berikut (beberapa detail dihilangkan; Anda dapat mengunduh sumber lengkap dari Sumber):

public class EncryptedClassLoader extends URLClassLoader {public static void main (Final String [] args) throws Exception {if ("-run" .equals (args [0]) && (args.length> = 3)) {// Buat kustom loader yang akan menggunakan loader saat ini sebagai // delegasi induk: final ClassLoader appLoader = new EncryptedClassLoader (EncryptedClassLoader.class.getClassLoader (), File baru (args [1])); // Pemuat konteks utas juga harus disesuaikan: Thread.currentThread () .setContextClassLoader (appLoader); aplikasi Kelas terakhir = appLoader.loadClass (args [2]); Metode terakhir appmain = app.getMethod ("main", Kelas baru [] {String [] .class}); String terakhir [] appargs = String baru [args.length - 3]; System.arraycopy (args, 3, appargs, 0, appargs.length); appmain.invoke (null, Object baru [] {appargs}); } lain jika ("-encrypt".sama dengan (args [0]) && (args.length> = 3)) {... mengenkripsi kelas yang ditentukan ...} else lempar IllegalArgumentException (USAGE) baru; } / ** * Mengganti java.lang.ClassLoader.loadClass () untuk mengubah aturan delegasi * induk-anak biasa cukup untuk dapat "merebut" kelas aplikasi * dari hidung classloader sistem. * / public Class loadClass (nama String akhir, penyelesaian boolean akhir) melempar ClassNotFoundException {if (TRACE) System.out.println ("loadClass (" + name + "," + resol + ")"); Kelas c = null; // Pertama, periksa apakah kelas ini telah ditentukan oleh classloader ini // instance: c = findLoadedClass (name); if (c == null) {Class parentVersion = null; coba {// Ini agak tidak ortodoks:melakukan percobaan beban melalui // parent loader dan catat apakah induknya didelegasikan atau tidak; // apa yang dicapai adalah pendelegasian yang tepat untuk semua inti // dan kelas ekstensi tanpa saya harus memfilter nama kelas: parentVersion = getParent () .loadClass (name); if (parentVersion.getClassLoader ()! = getParent ()) c = parentVersion; } catch (ClassNotFoundException ignore) {} catch (ClassFormatError ignore) {} if (c == null) {coba {// OK, baik 'c' dimuat oleh sistem (bukan bootstrap // atau ekstensi) loader (di dalam kasus mana saya ingin mengabaikan // definisi itu) atau induknya gagal sama sekali; dengan cara apa pun saya // mencoba mendefinisikan versi saya sendiri: c = findClass (name); } catch (ClassNotFoundException ignore) {// Jika gagal, kembali ke versi induk // [yang bisa jadi null pada saat ini]: c = parentVersion;}}} jika (c == null) memunculkan ClassNotFoundException baru (nama); jika (menyelesaikan) menyelesaikan Kelas (c); kembali c; } / ** * Mengganti java.new.URLClassLoader.defineClass () agar dapat memanggil * crypt () sebelum mendefinisikan kelas. * / dilindungi Kelas findClass (nama String akhir) melempar ClassNotFoundException {if (TRACE) System.out.println ("findClass (" + name + ")"); // file .class tidak dijamin dapat dimuat sebagai sumber daya; // tetapi jika kode Sun yang melakukannya, jadi mungkin bisa menambang ... Final String classResource = name.replace ('.', '/') + ".class"; URL final classURL = getResource (classResource); if (classURL == null) melempar ClassNotFoundException baru (name); lain {InputStream in = null; coba {in = classURL.openStream (); byte terakhir [] classBytes = readFully (in); // "mendekripsi": crypt (classBytes);if (TRACE) System.out.println ("didekripsi [" + nama + "]"); return defineClass (nama, classBytes, 0, classBytes.length); } catch (IOException ioe) {throw new ClassNotFoundException (name); } akhirnya {if (in! = null) coba {in.close (); } catch (Exception ignore) {}}}} / ** * classloader ini hanya mampu memuat kustom dari satu direktori. * / private EncryptedClassLoader (induk ClassLoader final, classpath File final) memunculkan MalformedURLException {super (new URL [] {classpath.toURL ()}, parent); if (induk == null) melempar IllegalArgumentException baru ("EncryptedClassLoader" + "memerlukan induk delegasi bukan null"); } / ** * De / mengenkripsi data biner dalam larik byte tertentu. Memanggil metode itu lagi * membalikkan enkripsi. * / private static void crypt (byte terakhir [] data) {for (int i = 8;i <data.length; ++ i) data [i] ^ = 0x5A; } ... metode pembantu lainnya ...} // Akhir kelas

EncryptedClassLoadermemiliki dua operasi dasar: mengenkripsi sekumpulan kelas tertentu dalam direktori classpath tertentu dan menjalankan aplikasi yang sebelumnya dienkripsi. Enkripsi sangat mudah: pada dasarnya terdiri dari membalik beberapa bit dari setiap byte dalam konten kelas biner. (Ya, XOR lama yang bagus (eksklusif OR) hampir tidak ada enkripsi sama sekali, tapi bersabarlah. Ini hanya ilustrasi.)

Classloading oleh EncryptedClassLoaderperlu lebih diperhatikan. Implementasi saya subclass java.net.URLClassLoaderdan menimpa keduanya loadClass()dan defineClass()untuk mencapai dua tujuan. Salah satunya adalah dengan membengkokkan aturan delegasi classloader Java 2 yang biasa dan mendapatkan kesempatan untuk memuat kelas terenkripsi sebelum classloader sistem melakukannya, dan yang lainnya adalah memanggil crypt()segera sebelum panggilan ke defineClass()yang jika tidak terjadi di dalam URLClassLoader.findClass().

Setelah menyusun semuanya ke dalam bindirektori:

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

Saya "mengenkripsi" kedua kelas Maindan MySecretClass:

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

Kedua kelas binini sekarang telah diganti dengan versi terenkripsi, dan untuk menjalankan aplikasi asli, saya harus menjalankan aplikasi melalui EncryptedClassLoader:

> java -cp bin Pengecualian Utama di utas "main" java.lang.ClassFormatError: Utama (Jenis kumpulan konstan ilegal) di java.lang.ClassLoader.defineClass0 (Metode Asli) di java.lang.ClassLoader.defineClass (ClassLoader.java: 502) di java.security.SecureClassLoader.defineClass (SecureClassLoader.java:123) di java.net.URLClassLoader.defineClass (URLClassLoader.java:250) di java.net.URLClassLoader.access00 (URLClassLoader.java:54) di java. net.URLClassLoader.run (URLClassLoader.java:193) di java.security.AccessController.doPrivileged (Metode Asli) di java.net.URLClassLoader.findClass (URLClassLoader.java:186) di java.lang.ClassLoader.loadClass (ClassLoader. java: 299) di sun.misc.Launcher $ AppClassLoader.loadClass (Launcher.java:265) di java.lang.ClassLoader.loadClass (ClassLoader.java:255) di java.lang.ClassLoader.loadClassInternal (ClassLoader.java:315 )>java -cp bin EncryptedClassLoader -run bin Utama didekripsi [Utama] didekripsi [my.secret.code.MySecretClass] hasil rahasia = 1362768201

Benar saja, menjalankan decompiler apa pun (seperti Jad) pada kelas terenkripsi tidak berfungsi.

Saatnya menambahkan skema perlindungan kata sandi yang canggih, menggabungkannya menjadi executable asli, dan menagih ratusan dolar untuk "solusi perlindungan perangkat lunak", bukan? Tentu saja tidak.

ClassLoader.defineClass (): Titik intersep yang tak terhindarkan

Semua ClassLoaderharus mengirimkan definisi kelas mereka ke JVM melalui satu titik API yang didefinisikan dengan baik: java.lang.ClassLoader.defineClass()metode. The ClassLoaderAPI memiliki beberapa overloads dari metode ini, tetapi mereka semua panggilan ke defineClass(String, byte[], int, int, ProtectionDomain)metode. Ini adalah finalmetode yang memanggil kode native JVM setelah melakukan beberapa pemeriksaan. Penting untuk dipahami bahwa tidak ada classloader yang dapat menghindari pemanggilan metode ini jika ingin membuat yang baru Class.

The defineClass()metode adalah satu-satunya tempat di mana keajaiban menciptakan Classobjek dari sebuah array byte datar dapat berlangsung. Dan coba tebak, array byte harus berisi definisi kelas yang tidak terenkripsi dalam format yang terdokumentasi dengan baik (lihat spesifikasi format file kelas). Memutus skema enkripsi sekarang menjadi masalah sederhana dengan mencegat semua panggilan ke metode ini dan mendekompilasi semua kelas yang menarik sesuai keinginan Anda (saya sebutkan opsi lain, JVM Profiler Interface (JVMPI), nanti).