Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Код на Java не так прост, как кажется. На первый взгляд взлом Java-приложения выглядит несложной задачей, благо подходящих декомпиляторов великое множество. Но если тебе доведется столкнуться с защитой Bytecode encryption, задача многократно усложняется. В этой статье я подробно расскажу, как бороться с этой напастью.
У начинающего программиста, немного освоившего Java, создается ложное впечатление, что писать программы на ней не просто, а очень просто. У начинающего хакера подобное впечатление может сложиться о взломе написанных на Java программ. И вправду, делов‑то: берешь обычный ZIP-архиватор, распаковываешь JAR-файлы, затем выбираешь декомпилятор на свой вкус и декомпилируешь полученные CLASS-файлы хочешь по одному, а хочешь — весь проект разом. На выходе получаешь исходники проекта на блюдечке.
warning
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
Правда, иногда приходится повозиться с обфускацией или поковыряться грязными руками в JVM-байт‑коде (процесс я описывал в статье «Грязный Джо. Взламываем Java-приложения с помощью dirtyJOE»). Все равно это выглядит намного проще маеты с нативным кодом под злобными протекторами типа «Фемиды» или даже дотнет‑приложениями.
На самом деле Java ничем не лучше упомянутых технологий, и столь простые случаи бывают далеко не всегда. Попробую подготовить тебя к неожиданностям на этом тернистом пути, для чего расскажу об одном из способов защиты Java-кода от декомпиляции и о методах борьбы с ним.
Как обычно, сразу переходим к примеру. Есть некая графическая программа, при загрузке которой происходит валидация лицензии на удаленном сервере. Если валидной лицензии нет или сервер недоступен, программа вежливо предлагает повторить попытку или закрывается, выбор невелик. Попробуем, просто чтобы поучиться и повысить свою эрудицию, заставить ее работать даже после отказа удаленного сервера.
При ближайшем рассмотрении нам бросается в глаза, что исполняемый модуль программы — простенький загрузчик Java Runtime Environment, вызывающий javaw c длиннющей командной строкой, которая содержит перечень JAR-модулей и библиотек. В конце этого списка мы видим имя главного класса.
Запустив поиск по JAR-архивам, мы находим сам файл класса. И вот здесь нас поджидает неприятный сюрприз: все имеющиеся в нашем распоряжении декомпиляторы напрочь отказываются работать с этим файлом, и даже dirtyJOE ругается на неподдерживаемый формат. Открыв файл в hex-редакторе, мы обнаруживаем, что ругается он вполне справедливо: от нормального скомпилированного CLASS-файла здорового человека здесь осталась только сигнатура CAFEBABE
, все остальное содержимое заполнено высокоэнтропийным белым шумом упакованных или зашифрованных данных.
Вид зашифрованного файла Main.class в HEX-редакторе
Глядя на эту картину, я испытывал сильное чувство дежавю. С чем‑то подобным я уже сталкивался, когда разбирал bytcode-обфускатор PHP SourceGuardian или хотя бы тот же .NET Reactor: кто‑то явно вклинивается в процесс загрузки JVM-байт‑кода и расшифровывает его на лету. Но как такое можно реализовать на Java?
Для ответа на этот вопрос попробуем слегка углубиться в теорию функционирования Java-машины. Поскольку она столь же кросс‑платформенна, как и .NET (на самом деле в определенном смысле даже более кросс‑платформенна), то байт‑код JVM не интерпретируется, а однократно компилируется в платформенно зависимый нативный код при загрузке класса. Мы даже знаем, как называется данный процесс, — JIT-компиляция (just in time, «временная компиляция на лету»). А значит, точно так же, как и в .NET, мы можем приаттачиться во время работы программы отладчиком x64dbg к процессу javaw.exe
, что позволит нам отлаживать скомпилированный нативный код как родной.
Правда, удовольствия в этом мало, поскольку скомпилированный код недружественно выглядит (в отличие, скажем, от результата JIT-компиляции того же .NET). Код сильно оптимизирован, многопоточен и шустр, но крайне неудобен для реверсинга.
Пример откомпилированного JIT нативного кода
Конечно, определенные вкусности имеются и там. К примеру, если покопаться в модулях jvm.dll
, java.dll
, jli.dll
и других, то можно обнаружить много стандартных базовых функций, упрощающих процесс отладки. Возможно, я когда‑нибудь расскажу о них в другой статье. Много материала по этой теме можно найти в интернете: например, переводные статьи, публиковавшиеся на «Хабрахабре»: «Java HotSpot JIT компилятор — устройство, мониторинг и настройка» и «Как работает Graal — JIT-компилятор JVM на Java». Но сейчас цель у нас другая: разобраться, как именно происходит процесс шифрования и дешифрования байт‑кода, а также восстановить исходный код Java из шифрованного. В .NET, к примеру, нам удалось найти вход JIT-компилятора, попробуем это сделать и для Javа.
Снова коснусь теории, не углубляясь в подробности. По сути, для реализации подмены байт‑кода в JVM обычно используется два основных интерфейса, каждый из которых реализует собственный подход. Один из них называется JVMCI — JVM compiler interface — и служит непосредственно для подключения собственного JIT-компилятора Java, написанного опять же на Java. По понятным причинам это явно не наш случай (у нас все классы зашифрованы, начиная с главного).
А вот второй, JVM Tool Interface (JVMTI), похоже, именно то, что нам надо, поэтому рассмотрим его поподробнее. JVM Tool Interface — это чертовски полезный интерфейс взаимодействия с виртуальной машиной JVM. Он позволяет расширить ее функциональность, не затрагивая код. Для полного описания возможностей этого инструмента одной статьи не хватит, поэтому я снова отсылаю любопытных к матчасти.
Все полезняшки этого интерфейса реализуются через так называемые агенты — внешние плагины. У них множество функций, но главное — они дают полный доступ к загружаемому байт‑коду и контроль над ним. Именно это нам сейчас и нужно. Агенты загружаются из javaw, для этого нужно указать специальные параметры в манифесте или же в командной строке. К примеру, самый распространенный тип агентов — javaagent. Они написаны на Java и подключаются через соответствующую командную строку javaagent:agent.jar
. Этот тип агентов тоже имеет полный доступ к байт‑коду, его подмена часто используется в обфускации и модификации кода, но не в нашем случае.
В нашем приложении задействован нативный JVMTIAgent, вызов которого можно обнаружить, внимательно проанализировав командную строку и найдя в ней следующий параметр: -agentlib:JavaLoader
. JVMTIAgent представляет собой динамическую библиотеку (в случае Windows это DLL, а в случае, например, «Линукса» — SO), из которой экспортируются какие‑то из следующих функций:
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved);
— эта функция вызывается при запуске агента, если он указан в параметре командной строки -agentpath:
или -agentlib:
, как в нашем случае;
JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char* options, void* reserved);
— данная функция вызывается, если агент не загружается при запуске, тогда мы сначала подключаемся к целевому процессу, а затем отправляем команду соответствующему целевому процессу для загрузки агента;
JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm);
— функция вызывается при удалении агента, опционально.
Мы уже обнаружили в рабочем каталоге динамическую библиотеку с оригинальным названием JavaLoader.dll
, при загрузке которой в дизассемблер IDA и находим искомые функции — перед нами действительно JVMTIAgent, декриптующий байт‑код при загрузке нужного класса. Но как он это делает?
Снова покурим спецификацию JVM Tool Interface, ссылку на которую я привел выше. Общий принцип работы агента — установка собственных пользовательских callback-обработчиков на определенные события. В данном случае нас интересует событие JVMTI_EVENT_CLASS_FILE_LOAD_HOOK
, вызываемое сразу после загрузки массива байт‑кода нужного класса из файла, но перед JIT-компиляцией данного класса. Примерная реализация установки такого обработчика выглядит так:
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) { jvmtiEventCallbacks callbacks; jvmtiEnv * jvmtienv = jvmti(agent); jvmtiError jvmtierror; memset(&callbacks, 0, sizeof(callbacks)); callbacks.ClassFileLoadHook = &eventHandlerClassFileLoadHook; // Новый обработчик JVMTI_EVENT_CLASS_FILE_LOAD_HOOK jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv, &callbacks, sizeof(callbacks)); // Установка обработчиков jvmtierror = (*jvmtienv)->SetEventNotificationMode(jvmtienv, JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, (jthread)NULL); // Разрешаем обработку события JVMTI_EVENT_CLASS_FILE_LOAD_HOOK
Покопавшись при помощи IDA в коде процедуры Agent_OnLoad
, находим соответствующее место.
Установка обработчика события JVMTI_EVENT_CLASS_FILE_LOAD_HOOK в агенте JavaLoader
Источник: xakep.ru