Разбиване на криптиране на байт-код на Java

9 май 2003 г.

Въпрос: Ако шифровам своите .class файлове и използвам персонализиран loadload за да ги зареждам и декриптирам в движение, това ще предотврати ли декомпилация?

О: Проблемът с предотвратяването на декомпилация на байтов код на Java е почти толкова стар, колкото и самият език. Въпреки редица инструменти за замъгляване, предлагани на пазара, начинаещите програмисти на Java продължават да измислят нови и интелигентни начини за защита на интелектуалната си собственост. В тази част от въпроси и отговори на Java разсейвам някои митове около идея, често преправяна в дискусионните форуми.

Изключителната лекота, с която Java .classфайловете могат да бъдат реконструирани в Java източници, които много приличат на оригиналите, има много общо с целите и компромисите на Java за байтов код. Наред с други неща, байтовият код на Java е проектиран за компактност, независимост от платформата, мобилност на мрежата и лекота на анализ от интерпретатори на байт-код и динамични компилатори JIT (точно навреме) / HotSpot. Може да се каже, че компилираните .classфайлове изразяват намерението на програмиста толкова ясно, че могат да бъдат по-лесни за анализиране от оригиналния изходен код.

Няколко неща могат да бъдат направени, ако не и да се предотврати напълно декомпилацията, поне да се направи по-трудно. Например като стъпка след компилация можете да масажирате .classданните, за да направите байтовия код или по-труден за четене при декомпилиране, или по-труден за декомпилиране във валиден Java код (или и двете). Техники като извършването на екстремно претоварване на имена на методи работят добре за първите и манипулирането на контролния поток за създаване на контролни структури, които не е възможно да се представят чрез синтаксиса на Java, работят добре за вторите. По-успешните търговски обфускатори използват комбинация от тези и други техники.

За съжаление и двата подхода трябва действително да променят кода, който JVM ще изпълнява, и много потребители се страхуват (с право), че тази трансформация може да добави нови грешки към техните приложения. Освен това, преименуването на метод и поле може да доведе до спиране на работата на обажданията за отражение. Промяната на действителните имена на класове и пакети може да повреди няколко други Java API (JNDI (Java Naming and Directory Interface), URL доставчици и др.). В допълнение към променените имена, ако асоциацията между изместванията на байт-кода на класа и номерата на изходните редове се промени, възстановяването на първоначалните следи от стека на изключения може да стане трудно.

Тогава има опция за замъгляване на оригиналния код на Java. Но по същество това причинява подобен набор от проблеми.

Шифроване, не замъгляване?

Може би горното ви е накарало да се замислите: „Е, какво, ако вместо да манипулирам байтов код, шифровам всичките си класове след компилация и ги декриптирам в движение вътре в JVM (което може да се направи с персонален loadloader)? Тогава JVM изпълнява моя оригинален байтов код и въпреки това няма какво да се декомпилира или да се направи обратен инженер, нали?

За съжаление бихте сгрешили, както в мисълта, че сте първи измислили тази идея, така и в мисълта, че тя действително работи. И причината няма нищо общо със силата на вашата схема за криптиране.

Прост енкодер от клас

За да илюстрирам тази идея, внедрих примерно приложение и много тривиален потребителски loadload за неговото стартиране. Приложението се състои от два кратки класа:

публичен клас Main {public static void main (final String [] args) {System.out.println ("secret result =" + MySecretClass.mySecretAlgorithm ()); }} // Край на класния пакет my.secret.code; импортиране на java.util.Random; публичен клас MySecretClass {/ ** * Познайте какво, тайният алгоритъм просто използва генератор на случайни числа ... * / public static int mySecretAlgorithm () {return (int) s_random.nextInt (); } частен статичен финал Random s_random = нов Random (System.currentTimeMillis ()); } // Край на класа

Моят стремеж е да скрия изпълнението, my.secret.code.MySecretClassкато шифровам съответните .classфайлове и ги дешифрирам в движение по време на изпълнение. За тази цел използвам следния инструмент (някои подробности са пропуснати; можете да изтеглите пълния източник от ресурси):

публичен клас EncryptedClassLoader разширява URLClassLoader {public static void main (final String [] args) хвърля изключение {if ("-run" .equals (args [0]) && (args.length> = 3)) {// Създайте персонализиран товарач, който ще използва текущия товарач като // родител на делегиране: окончателен ClassLoader appLoader = нов EncryptedClassLoader (EncryptedClassLoader.class.getClassLoader (), нов файл (аргументи [1])); // Контекстният зареждач на нишки също трябва да бъде коригиран: Thread.currentThread () .setContextClassLoader (appLoader); окончателен клас app = appLoader.loadClass (аргументи [2]); окончателен метод appmain = app.getMethod ("main", нов клас [] {String [] .class}); окончателен низ [] appargs = нов низ [args.length - 3]; System.arraycopy (args, 3, appargs, 0, appargs.length); appmain.invoke (null, нов обект [] {appargs}); } else if ("-encrypt".equals (args [0]) && (args.length> = 3)) {... шифроване на определени класове ...} else хвърля нов IllegalArgumentException (USAGE); } / ** * Заменя java.lang.ClassLoader.loadClass (), за да промени обичайните правила за делегиране родител-дете *, достатъчно, за да може да „изтръгне“ класовете на приложения * от носа на системния клас. * / public Class loadClass (окончателно име на низа, окончателно логическо разрешаване) хвърля ClassNotFoundException {if (TRACE) System.out.println ("loadClass (" + name + "," + разрешение + ")"); Клас c = нула; // Първо проверете дали този клас вече е дефиниран от този loadloader // екземпляр: c = findLoadedClass (име); if (c == null) {Клас parentVersion = null; опитайте {// Това е малко неортодоксално:направете пробно зареждане чрез // родителския товарач и отбележете дали родителят е делегирал или не; // това, което постига, е правилно делегиране за всички основни // и разширени класове, без да се налага да филтрирам върху името на класа: parentVersion = getParent () .loadClass (name); if (parentVersion.getClassLoader ()! = getParent ()) c = parentVersion; } catch (ClassNotFoundException ignore) {} catch (ClassFormatError ignore) {} if (c == null) {try {// OK, или 'c' е зареден от системата (не bootstrap // или разширението) loader (в в който случай искам да игнорирам тази // дефиниция) или родителят изобщо се провали; така или иначе // се опитвам да дефинирам собствената си версия: c = findClass (име); } catch (ClassNotFoundException ignore) {// Ако това не успее, върнете се към версията на родителя // [която в този момент може да е нула]: c = parentVersion;}}} if (c == null) хвърля нов ClassNotFoundException (име); ако (разреши) resolClass (c); връщане 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 (име); else {InputStream in = null; опитайте {in = classURL.openStream (); окончателен байт [] classBytes = readFully (in); // "дешифриране": крипта (classBytes);if (TRACE) System.out.println ("декриптиран [" + име + "]"); връщане defineClass (име, classBytes, 0, classBytes.length); } catch (IOException ioe) {хвърли нов ClassNotFoundException (име); } накрая {if (in! = null) опитайте {in.close (); } catch (изключение се игнорира) {}}}} / ** * Този loadloader може да зарежда само от една директория. * / private EncryptedClassLoader (окончателен родител на ClassLoader, окончателен File classpath) изхвърля MalformedURLException {super (нов URL [] {classpath.toURL ()}, родител); ако (родител == нула) хвърли нов IllegalArgumentException ("EncryptedClassLoader" + "изисква родител без делегиране с нула"); } / ** * Де / криптира двоични данни в даден байтов масив. Повторното извикване на метода * обръща криптирането. * / private static void crypt (окончателен байт [] данни) {for (int i = 8;i <data.length; ++ i) данни [i] ^ = 0x5A; } ... още помощни методи ...} // Край на класа

EncryptedClassLoaderима две основни операции: криптиране на даден набор от класове в дадена директория на classpath и стартиране на предварително криптирано приложение. Криптирането е много лесно: състои се от основно обръщане на някои битове от всеки байт в съдържанието на двоичния клас. (Да, добрият стар XOR (изключителен ИЛИ) почти изобщо не криптира, но имайте предвид мен. Това е само илюстрация.)

Зареждането на клас от EncryptedClassLoaderзаслужава малко повече внимание. Подкласовете ми за изпълнение java.net.URLClassLoaderи отменят и двете, loadClass()и defineClass()за постигане на две цели. Едното е да се огънат обичайните правила за делегиране на Java 2 classloader и да се получи възможност за зареждане на криптиран клас, преди системният loadloader да го направи, а другото е да се извика crypt()непосредствено преди извикването на defineClass()това, което иначе се случва вътре URLClassLoader.findClass().

След компилиране на всичко в binдиректорията:

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

„Шифровам“ Mainи MySecretClassкласовете:

> java -cp bin EncryptedClassLoader -encrypt bin Основен my.secret.code.MySecretClass шифрован [Main.class] шифрован [my \ secret \ code \ MySecretClass.class] 

Тези два класа в binсега са заменени с криптирани версии и за да стартирам оригиналното приложение, трябва да стартирам приложението чрез EncryptedClassLoader:

> java -cp bin Основно изключение в нишка "main" java.lang.ClassFormatError: Main (Недопустим постоянен тип пул) при java.lang.ClassLoader.defineClass0 (Native Method) в java.lang.ClassLoader.defineClass (ClassLoader.java 502) на java.security.SecureClassLoader.defineClass (SecureClassLoader.java:123) на java.net.URLClassLoader.defineClass (URLClassLoader.java:250) на java.net.URLClassLoader.access00 (URLClass. net. java: 299) при sun.misc.Launcher $ AppClassLoader.loadClass (Launcher.java:265) при java.lang.ClassLoader.loadClass (ClassLoader.java:255) при java.lang.ClassLoader.loadClassInternal (ClassLoader.java )>java -cp bin EncryptedClassLoader -run bin Основна дешифрирана [Основна] декриптирана [my.secret.code.MySecretClass] секретен резултат = 1362768201

Разбира се, стартирането на който и да е декомпилатор (като Jad) на криптирани класове не работи.

Време е да добавите сложна схема за защита с парола, да я опаковате в роден изпълним файл и да начислите стотици долари за „решение за софтуерна защита“, нали? Разбира се, че не.

ClassLoader.defineClass (): Неизбежната точка на прихващане

Всички ClassLoaderтрябва да доставят своите дефиниции на класове на JVM чрез една добре дефинирана точка на API: java.lang.ClassLoader.defineClass()методът. В ClassLoaderAPI има няколко претоварвания на този метод, но всички от тях се обади в defineClass(String, byte[], int, int, ProtectionDomain)метода. Това е finalметод, който извиква JVM роден код след извършване на няколко проверки. Важно е да се разбере, че никой loadloader не може да избегне извикването на този метод, ако иска да създаде нов Class.

В defineClass()метод е единственото място, където магията на създаване на Classобект от плоска масив от байтове може да се осъществи. И познайте какво, байтовият масив трябва да съдържа дешифрирана дефиниция на клас в добре документиран формат (вижте спецификацията на формата на файла на класа). Нарушаването на схемата за криптиране сега е просто въпрос за прихващане на всички повиквания към този метод и декомпилиране на всички интересни класове според желанието на сърцето ви (споменавам друга опция, JVM Profiler Interface (JVMPI), по-късно).