Цель этой статьи предостеречь разработчиков от использования обфускаторов с функцией шифрования class-файлов для защиты своих приложений и от бессмысленной траты денег на них.Вопросы защиты байт-кода от реверс-инжиниринга и обхода этой защиты подробно рассмотрены в фундаментальной работе Дмитрия Лескова — Protect Your Java Code — Through Obfuscators And Beyond.
Механизм шифрования class-файлов предполагает, что содержимое классов хранится в зашифрованном виде, а при старте приложения через специализированный СlassLoader или JVMTI-интерфейс, расшифрованный байт-код грузится в виртуальную машину Java.
Методы обхода такой защиты подробно упомянуты в вышеуказанной статье, к счастью или к сожалению, существует ряд продуктов, в состав которых входит нативная компонента взаимодействующая с JVM и отслеживающая режим отладки или наличие посторонних агентов. Но и это, несмотря на все заверения разработчиков подобных продуктов, нисколько не защищает ваш байт-код.
Для того чтобы продемонстрировать уязвимость ВСЕХ обфускаторов, шифрующих класс-файлы, достаточно запустить защищаемое ими приложение с опцией -XX:+TraceClassLoading и убедиться в том, что все зашифрованные class-файлы благополучно видятся на этом уровне трассировки JVM. Мы пойдем дальше, возьмем исходники OpenJDK и вставим выгрузку байт-кода загружаемых class-файлов.
Для эксперимента мы будем использовать Debian Linux 6.0.5 (Stable) и бандл с исходниками OpenJDK7. Инструкция по установке JDK из исходных кодов на других платформах доступна здесь: OpenJDK Build README.
Для того, чтобы минимизировать количество вносимых изменений в исходный код OpenJDK, мы будем при включенной опции -XX:+TraceClassLoading сохранять байт-код всех загруженных классов в файл classes.dump относительно рабочего каталога. Структура файла следующая:
{ int lengthClassName, byte[] className, int lengthByteCode, byte[] bytecode }, { next record … }, …
Подготовим окружение для сборки:
# apt-get install openjdk-6-jdk # apt-get build-dep openjdk-6
Далее необходимо скачать удобным для вас способом исходники OpenJDK и наш патч, который добавит следующий код в функцию ClassFileParser::parseClassFile, в файл hotspot/src/share/vm/classfile/classFileParser.cpp:
// dumping class bytecode // dump file format: // length of the class name - 4 bytes // class name // length of the class bytecode - 4 bytes // byte code // ... next class ... ClassFileStream* cfs = stream(); FILE * pFile; int length = cfs->length(); int nameLength = strlen(this_klass->external_name()); pFile = fopen("classes.dump","ab"); // size of the class name fputc((int)((nameLength >> 24) & 0XFF), pFile ); fputc((int)((nameLength >> 16) & 0XFF), pFile ); fputc((int)((nameLength >> 8) & 0XFF), pFile ); fputc((int)(nameLength & 0XFF), pFile ); // class name fwrite (this_klass->external_name() , 1, nameLength, pFile ); // size of the class bytecode fputc((int)((length >> 24) & 0XFF), pFile ); fputc((int)((length >> 16) & 0XFF), pFile ); fputc((int)((length >> 8) & 0XFF), pFile ); fputc((int)(length & 0XFF), pFile ); // class bytecode fwrite (cfs->buffer() , 1 , length, pFile ); fclose(pFile);
Убедимся, что JDK собирается нормально:
# export LANG=C ALT_BOOTDIR=/usr/lib/jvm/java-6-openjdk ALLOW_DOWNLOADS=true # make sanity && make
Применим патч и запустим сборку
# cd $OPENJDK_SRC # patch -p1 < $PATH_TO_PATCH_FILE # make
Далее, перейдем в в bin каталог собранной JRE: $OPENJDK_SRC/build/linux-i586/j2re-image/bin/
Для тестирования работоспособности запустим java с единственным параметром -XX:+TraceClassLoading:
# ./java -XX:+TraceClassLoading
И посмотрим на classes.dump, в нем будут все class-файлы, которые загружает JRE при старте.
И теперь самое интересное, возьмем Java-приложение с зашифрованным байт-кодом, для примера можно использовать триалку какого-нибудь обфускатора с этой функцией. Я не буду по понятным соображениям упоминать конкретных названий, достаточно поискать в Google по ключу «byte-code encryption». Внутри SomeClassGuard.jar в иерархии com/****/someclassguard/engine содержатся зашифрованные class-файлы, можете убедиться в этом сами натравив любой декомпилятор или посмотреть в HEX-просмотрщике заголовок файла.
Теперь запустим SomeClassGuard.jar:
# ./java -XX:+TraceClassLoading -jar SomeClassGuard.jar
Далее, нам необходимо распаковать получившийся после запуска SomeClassGuard.jar файл classes.dump, для этого напишем небольшую Java-программу:
package openjdkmod; import java.io.DataInputStream; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; /** * Classes dump format extractor class. * Author Ivan Kinash kinash@licel.ru */ public class ClassesDumpExractor { /** * Extract contents classes.dump to specified dir */ public static void main(String[] args) throws FileNotFoundException, IOException { if (args.length != 2) { System.err.println("Usage openjdkmod.ClassesDumpExtractor <classes.dump file> <out dir>"); System.exit(-1); } File classesDumpFile = new File(args[0]); if (!classesDumpFile.exists()) { System.err.println("Source file: " + args[0] + " not found!"); System.exit(-1); } File outDir = new File(args[1]); if (!outDir.exists()) { outDir.mkdirs(); } DataInputStream din = new DataInputStream(new FileInputStream(classesDumpFile)); while (true) { try { int classNameLength = din.readInt(); byte[] classNameBytes = new byte[classNameLength]; din.readFully(classNameBytes); String className = new String(classNameBytes); System.out.println("className:" + className); int classLength = din.readInt(); byte[] classBytes = new byte[classLength]; din.readFully(classBytes); File parentDir = className.indexOf(".")>0?new File(outDir, className.substring(0,className.lastIndexOf(".")).replace(".", File.separator)):outDir; if(!parentDir.exists()) parentDir.mkdirs(); File outFile = new File(parentDir, (className.indexOf(".")>0?className.substring(className.lastIndexOf(".")+1):className)+".class"); FileOutputStream outFos = new FileOutputStream(outFile); outFos.write(classBytes); outFos.close(); } catch (EOFException e) { din.close(); return; } } } }
И запустим её с параметрами:
# java openjdkmod.ClassesDumpExractor classes.dump dump_directory
На выходе мы получим каталог с расшифрованными class-файлами.
Выводы.
Защита class-файлов с помощью шифрования абсолютно бессмысленная, опасная, дорогая (как минимум, не бесплатная) затея.
Если вам нужно защищать ваш байт-код:
1) Используйте компиляторы байт-кода в нативный код.
2) Сочетание классического обфускатора с обфускатором с функцией шифрования строк.
Для супер-защиты: используйте внешние устройства, поддерживающие защищенное хранение и исполнение байт-кода внутри себя.
Примененную выше методику можно использовать для отладки различных приложений, когда нужно посмотреть, какой байт-код загружается в процессе работы.
Note1:
Тех же самых результатов можно достичь, без модификации исходного кода JDK — используя класс sun.misc.Unsafe, правда при этом нужно немного покопаться в формате хранения class-ов внутри JVM.
Note2:
Ну и разумеется, автор не несет ответственности за использование вами данных, содержащихся в этой статье.
Note3: Исходная картинка взята отсюда: it.wikipedia.org/wiki/File:Netbeans-Duke.png
