Pull to refresh

Против лома нет приёма: OpenJDK hack vs. Class Encryption

Reading time5 min
Views8.6K
Цель этой статьи предостеречь разработчиков от использования обфускаторов с функцией шифрования 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
Tags:
Hubs:
+46
Comments16

Articles

Change theme settings