Статьи

Оружие — отличный эквалайзер: OpenJDK Hack vs. Class Encryption

Цель этой статьи — предупредить разработчиков о бесполезности использования обфускаторов шифрования файлов классов для защиты приложений и о бесполезности расходования средств на них.

Все методы защиты описаны в фундаментальной работе Дмитрия Лескова —  Защити свой Java-код — через обфускаторы и не только .

Когда мы используем метод шифрования файлов класса, мы предполагаем, что байт-код зашифрован, и когда приложение запускает дешифрованный байт-код через пользовательский ClassLoader или JVMTI-интерфейс загружается в JVM.

Как обойти этот вид защиты вы можете найти в статье Дмитрия Лескова, но сегодня есть некоторые продукты, которые имеют собственный компонент, который взаимодействует с JVM и отслеживает режим отладки и / или неавторизованные агенты. Но, несмотря на все заверения разработчиков, эти продукты совсем не защищают ваш байт-код.

Чтобы показать вам уязвимость всех обфускаторов, которые шифруют классы, мы предпримем некоторые шаги.

Прежде всего, мы должны запустить зашифрованное приложение с параметром «-XX: + TraceClassLoading» и быть уверенным, что все зашифрованные файлы классов видны на этом уровне трассировки.

Загрузка исходных кодов OpenJDK и исправление его для сброса зашифрованных классов в файл.

Для эксперимента мы используем Debian Linux 6.0.5 (стабильная версия) и исходный пакет OpenJDK7. Если вы используете другую платформу, пожалуйста, смотрите OpenJDK Build README для инструкций о том, как собрать OpenJDK.

Чтобы свести к минимуму количество модификаций, если установлена ​​опция -XX: + TraceClassLoading, JDK будет выгружать все загруженные классы в файл $ WORKDIR / 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

Затем измените dir на bin встроенной JVM: $ OPENJDK_SRC / build / linux-i586 / j2re-image / bin /

Чтобы проверить работу исправленного JRE, запустите java с -XX: + TraceClassLoading:

# ./java -XX:+TraceClassLoading

Если все правильно, вы увидите файл classes.dump со всеми классами, которые JRE загружает в начальной фазе.

А теперь самый интересный момент. Загрузите зашифрованное приложение, например, пробную версию обфускатора с классом шифрования. Я не буду упоминать по понятным причинам конкретные имена, достаточно найти в Google ключ «шифрование байт-кода». 

Допустим, у нас есть SomeClassGuard.jar. Внутри SomeClassGuard.jar в иерархии com / **** / someclassguard / engine находятся файлы, зашифрованные классом, вы можете увидеть это сами, используя любой декомпилятор, или посмотреть заголовок файла в HEX-viewer.

Запустите SomeClassGuard.jar:

# ./java -XX:+TraceClassLoading -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 [email protected]
*/
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
 ");
           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

На выходе мы получим каталог с расшифрованными файлами классов.

Вывод

Классовое шифрование бессмысленно, опасно и дорогое занятие.

Если вы хотите защитить свой код, есть несколько способов сделать это:

  • Использовать байт-код Java для компиляторов собственного кода
  • Объедините классический обфускатор с обфускаторами шифрования строк

Для обеспечения максимальной безопасности используйте внешние устройства, которые поддерживают безопасное хранение и выполнение внутреннего байт-кода.

Вы можете использовать метод, описанный выше, для отладки различных приложений, когда вы хотите выяснить, какой байт-код загружается в JVM во время выполнения.

Примечание 1 : Вы можете достичь того же результата без какой-либо модификации JDK, используя класс sun.misc.Unsafe, если знаете о методах хранения классов внутри JVM.

Примечание 2 : Автор не несет ответственности за неправильное использование информации из статьи.

Примечание 3 : Оригинальный источник изображения: http://it.wikipedia.org/wiki/File:Netbeans-Duke.png