Цель этой статьи предостеречь разработчиков от использования обфускаторов с функцией шифрования 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 -jar /tmp/ClassesDumpExractor.jar 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
Автор: receiver