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

Цель этой статьи предостеречь разработчиков от использования обфускаторов с функцией шифрования 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
Поделиться публикацией

Комментарии 16

    0
    Вы получили некоторые class-файлы приложения. Ну и что? Всего приложения у вас нет. Для этого нужно каким-то образом заставить программу загрузить все классы в ней. Иначе восстановленный таким образом набор .class-файлов потом начнет не ожиданно падать когда пользователь сделает что-то необычное.
    • НЛО прилетело и опубликовало эту надпись здесь
        0
        Не очень понятно, что конкретно значит «used by a class». Плюс некоторые классы в современных приложениях загружаются через рефлекшен.
          0
          «used by a class» значит все классы, которые в данном класс-файле лежат в constant pool. То есть, все те, которые он явно, не через reflection, использует.
          • НЛО прилетело и опубликовало эту надпись здесь
              0
              Вывод — не хранить обфусцированные классы в classpath, и не обращаться к ним иначе как через reflection?

              (Хотя можно ведь еще интерпретатор брейнфака встроить и часть кода не брейнфак переписать! ((-: )
            0
            Но у этого уже на много больше пользы на практике.
          –10
          Серьезные дяди, которым важно шифрование, не пользуються OpenJDK — раз, даже если и пользуются, — любое несимметричное шифрование без приватного ключа не позволит вам расшифровать сп*зженные jar'ки — вы банально не получите байткод для своего дампа.
          Если предположить, что у вас лицензионная jar'ка и вы, как любопытный инженер все же решили поиграться — конечно, это возможно, так же как возможно взломать любую программу и любой шифр, если у вас есть приватный ключ. Тут как бы подразумевается, что вам доверили код.

          Если речь идет о class encryption вообще, то:
          Бессмысленно — нет, с помощью шифрованияи и подписи, можно гарантировать что класс загружен в jvm неизменным, например, по сети
          Опасно — не совсем понятно почему
          Дорого — да, ну и что?

          Если речь идет только об обфускации, чтобы нерадивые клиенты не взламывали святой код — согласен, это все равно, что Майкрософт скажет, что вот вам Виндовс продадим, только не смотрите ассемблерный код нашего ядра, — все равно все будут думать про слона :)

          Компиляция в нативный код? А смысл тогда писать код на java? Любви ради и хохмы для?

          А вообще статья интересная. Спасибо
            +3
            >Серьезные дяди, которым важно шифрование, не пользуються OpenJDK

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

            >любое несимметричное шифрование без приватного ключа не позволит вам расшифровать сп*зженные jar'ки — вы банально не получите байткод для своего дампа.

            Если программа запускается на вашем компьютере, значит в итоге она где-то расшифровывается, и где-то в ней хранится ключ и алгоритм расшифровки (в бутстраппере обычно). Вы путаете с другим применением несимметричного шифрования — лицензионные ключи и их взлом. Вот в этом случае действительно невозможно написать кейген не зная приватного ключа, если конечно алгоритм хороший и реализован без косяков
              –2
              Я не говорю что OpenJDK плох, я имею ввиду, что само использование ее в проекте дает злоумышленнику преимущество в виде доступного кода, который он может модифицировать и провернуть такой хак. Если программа заточена под IBM JDK, то и патч некуда применить, кода то нет, и собрать некак.

              А про дешифрование, я это и написал и я с вами согласен, если у вас ЕСТЬ ключ для загрузки класса, то вы его МОЖЕТЕ расшифровать, но само НАЛИЧИЕ ключа подразумевает, что вам ДОВЕРИЛИ код, например вы купили лицензию или подписали драконовские условия. И в таком случае да, ЕСЛИ вы сможете вот так похачить JDK, то вы наконец ПОЛУЧИТЕ код.

              Ключ не обязательно должен быть зашит, он может быть в файле, может быть USB-токеном, может быть другой jar'кой, все зависит от реалзации класслоадера.

              Просто поразительно, что вы единственный из 9ти решились откомментировать.
                +1
                > само использование ее в проекте
                Автор программы не использовал OpenJDK в своём проекте. Её только злоумышленник и использовал чтоб программу вскрыть. Неужели непонятно?

                > Если программа заточена под IBM JDK
                Уважаемый, это Java — какая заточка под какие «JDK»? О чём вы вообще?
                Затачивать Java код под конкретные JRE вряд-ли получится, но даже если бы это было возможно — это глупо. Даже буде такая «заточка» появится, имея OpenJDK можно будет внести необходимые модификации, чтоб имитировать нужное JRE.
                Но таких заточек нет, и никто этого не делает — какой смысл в кроссплатформенном Java приложении если оно «работает только на JRE версия такая-то под win AMD 64» например? Никакого (-:
                И примеров таких «заточек» среди серь
                — 0 штук.

                > если у вас ЕСТЬ ключ для загрузки класса
                  0
                  Пардон — комментарий оборвался на полуслове.

                  > И примеров таких «заточек» среди серь
                  среди серьёзных приложений — 0 штук.
                  Да и среди несерьёзных пожалуй тоже.

                  > если у вас ЕСТЬ ключ для загрузки класса
                  Если программа работает, значит классы загружаются — самой программой. Злоумышленнику ничего не нужно иметь, никаких ключей.

                  > вам ДОВЕРИЛИ код, например вы купили лицензию или подписали драконовские условия
                  «Доверили» код? Какой такой код? Байткод чтоли? Логично, что для того, чтоб декомпилировать байткод, его нужно иметь (-:
                  Но ведь уважаемый — статья то _о целесообразности применения обфускаторов байткода_. Не о USB ключах (автор их какраз скорее рекомендует кстати) и т.п., а именно об обфускаторах. А зачем обфускаторы байткода? Именно затем, чтоб не боятся этот байткод потом раздавать. Ни о каком «доверии» речь не идёт.

                  > Просто поразительно, что вы единственный из 9ти решились откомментировать
                  Да как-то сложно комментировать такую бессмыслицу. И, боюсь, бесполезно. Уж извините.
                    –2
                    Мне вообще все понятно, и не нужно лишней кислоты в комментарии, мой аргумент вполне резонный — что категоричное заявляние, что класс енкрипшен «бесполезен, опасен и дорог», не выдерживает критики.

                    То есть для вас не существет пакетов com.sun и проч, вы не знаете что такое IBM WebSphere, Azul и не понимаете, что привязать приложение вендор сможет, если захочет. Вы также не понимаете как работает шифрование, например, по алгоритму RSA?

                    А имея OpenJDK, можно так просто взять и сделать из нее IBM JDK? А насколько это «бесполезно, опасно и дорого» в сравнении с использованием class encryption?

                    Статья о применении класс енкрипшен как средства обфускации, это вы верно заметили. Но видно ваше понимание распространения jar'ок ограничивается скачиванием из интернета, а загрузка классов возможна только с диска. Что ж, тогда спорить с вами действительно не о чем.

                    В частности я утверждаю, что при кастомном класслоадере, привязке продукта к проприетарной JDK, например IBM, и шифровании приватным ключом, такой фокус не пройдет, либо будет непередаваемо бесполезен, опасен и дорог по затратам.

                    Конечно, если вы хотите спрятать суперсекретную библиотеку рисования календаря для Swing — это перебор, и там обфускация — действительно слабенькая защита, но если задача того потребует, вполне можно сделать обфускацию, которую взломать будет достаточно дорого, чтобы отбить желание этим заниматся.

                      0
                      «для вас не существет пакетов com.sun и проч, вы не знаете что такое IBM WebSphere, Azul и не понимаете, что привязать приложение вендор сможет, если захочет»
                      Уважаемый, у вас есть хотя-бы один пример такой привязки? Хотя-бы один?
                      И вообще говоря да, мне интересно как именно вы привязали бы своё приложение к IBM JVM например (и да — WebSphere тут причём? Неужели не стартует на других JVM кроме IBM-овской? Ой не верю (-: ).

                      «не понимаете как работает шифрование, например, по алгоритму RSA»
                      Просветите меня, как связано «шифрование, например, по алгоритму RSA» и (де)обфусцирование байткода?

                      «А имея OpenJDK, можно так просто взять и сделать из нее IBM JDK?»
                      Это зависит от того, как именно вы «привязывались к IBM JVM». Не думаю что «найти и обезвредить» такую привязку — даже если она вдруг существует, о чём см. выше — будет так уж сложно.

                      «ваше понимание распространения jar'ок ограничивается скачиванием из интернета, а загрузка классов возможна только с диска»
                      Да нет, пусть классы грузятся, например, из базы, в которой они хранятся в зашифрованном виде. Что с того? Автор статьи о том и пишет, что при загрузке они будут услужливо расшифрованы класслоадером — ведь JVM «зашифрованный» байткод не запустит.

                      «я утверждаю, что при кастомном класслоадере, привязке продукта к проприетарной JDK, например IBM, и шифровании приватным ключом, такой фокус не пройдет»
                      Про привязку см. первый пункт. А без привязки см. пункт выше.

                      «если задача того потребует, вполне можно сделать обфускацию, которую взломать будет достаточно дорого»
                      Если уже привязываться к конкретной JVM, можно и саму JVM модифицировать — хрен редьки не слаще. Но к обфускации байткода это уже никоим образом не относится.
                      Как, вообще говоря и собственно мифическая привязка к JVM — обфускаторы её не делают.
                      (Хотя её вообще никто не делает (-: )
            0
            > Я не говорю что OpenJDK плох, я имею ввиду, что само использование ее в проекте дает злоумышленнику преимущество в виде доступного кода

            В статье имеется ввиду другой кейс: кто-то написал программу с защитой от декомпиляции под Oracle JDK (хотя с большой вероятностью она запустится и на Open JDK). А мы взяли и запустили ее под модифицированной Open JDK и получили дешифрованные классы.

            > у вас ЕСТЬ ключ для загрузки класса, то вы его МОЖЕТЕ расшифровать, но само НАЛИЧИЕ ключа подразумевает, что вам ДОВЕРИЛИ код

            Тут опять путаница. Вы хотели, наверное, написать «Доверили запуск программы», а не код? В этом случае как раз и работает предложенный вариант
              0
              Большое спасибо за комментарии.
              И в заключение, хотелось бы еще раз сделать несколько акцентов, по определенным причинам.
              Речь идет не о привязке приложения к какой-то платформе (совтферной или хардварной), а о том что любое приложение написанное в соответсвии со стандартом Java не может быть защищено с использованием шифрования классов, вернее классы могут быть зашифрованы, но только толку 0.
              И хоть OpenJDK (в котором тоже есть com.sun.* пакеты, как ни странно), хоть Oracle JDK, хоть IBM JDK — это реализации Javа, сертифицированные Sun/Oracle и поэтому обязаны в рамках сертификационных тестов обеспечивать совместимость со стандартом Java.
              Так, по поводу custom ClassLoader — без нативной части он легко вскрывается, вот отличная статья на эту тему: blogs.oracle.com/sundararajan/entry/retrieving_class_files_from_a
              А для IBM JDK и Websphere — дамп всех класс-файлов, насколько я помню, вообще стандартная опция — все загруженные классы в core*.jar выгружаются, подробности здесь: www.ibm.com/developerworks/java/jdk/diagnosis/

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.