Как взломать программу java
Способы обхода триала в различных программах — одна из самых интересных тем прикладного реверс‑инжиниринга, и я уже не раз посвящал ей свои статьи. Настало время вернуться к этой тематике снова. Наш сегодняшний пациент — приложение, выполненное в виде JAR-модуля, которое мы исследуем без полного реверса и пересборки проекта.
warning
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
В заметке «В обход стражи. Отлаживаем код на PHP, упакованный SourceGuardian» мы рассматривали программу, реализованную в виде локального веб‑интерфейса. Работает она так: под Windows запускается локальный сервер Apache c набором PHP-модулей, а пользователь взаимодействует с приложением через браузер, в котором набирает адрес localhost . Программа, взломом которой мы займемся сегодня, действует похожим образом, только написана она на Java и поставляется в виде файла . JAR . Наша задача — отучить приложение от деморежима.
По счастью, нам известно, где лежат стартующие в виде сервиса исполняемые модули программы в формате . EXE и соответствующий JAR-файл. По своей сути JAR — это обычный ZIP-архив, в который упакованы части проекта. Поскольку мы собираемся править код, нас интересуют модули *. CLASS , содержащие откомпилированный JVM-байт‑код. Декомпиляторов и способов их применения множество, существуют даже инструменты вроде JD-GUI, способные полностью восстановить проект из исполняемого файла. Чаще всего взломщики используют общеизвестный JAD, который из‑за его распространенности ловкие обфускаторы давно научились обманывать, что, в свою очередь, стало причиной появления более продвинутых декомпиляторов вроде CFR. Эта война щитов и мечей, пуль и бронежилетов обещает быть долгой, нам остается только запастись попкорном. Но не будем тут останавливаться, а вместо этого предположим, что мы декомпилировали проект одним из описанных способов до Java-исходников и даже проанализировали полученный код.
Применительно к нашему подопытному приложению это выглядело примерно так. Декомпилировав все‑все‑все CLASS-файлы, мы так и не обнаружили ничего похожего на обращение к лицензии, однако в подкаталоге BOOT-INF/ lib нашего JAR-архива нашлось множество упакованных JAR-библиотек, среди которых сразу бросилась в глаза библиотека license-1. 2. 12. jar . Распаковав и декомпилировав ее, мы наткнулись на два CLASS-модуля, содержащих две любопытные функции. Одна возвращает демонстрационный режим, вторая активирует опцию 1 по умолчанию:
Наша задача — сделать так, чтобы функция isDemo всегда возвращала false , а в функции setDefault нужно заменить опцию 1 опцией 256 . Вот здесь и начинается самое интересное, то, ради чего и написана эта статья.
Ты спросишь: раз у нас имеются в наличии все исходники и код, то почему бы просто не перекомпилировать весь проект, поменяв эти две процедуры на нужные? К сожалению, прямой метод не всегда самый простой. В нашем случае в интересующих нас модулях много зависимостей, а проект очень большой, многие модули сильно обфусцированы. Кроме того, код восстановился частично с кучей ошибок, из‑за чего проект полностью не соберется. Можно, конечно, покопать обфускацию и попробовать руками вытащить исходный текст программы, но решать эту (возможно, даже, гораздо более сложную) задачу ради двух простых патчей в коде как‑то лень. Вдобавок пересборке проекта может помешать отсутствие установленного JDK на компьютере. Устанавливать его и разбираться в особенностях компиляции Java-проектов мне тоже неохота. Поэтому мы, как обычно, ищем самый простой путь — патч откомпилированного JVM-кода.
В этом нам поможет интересная, но малоизвестная утилита dirtyJOE. Открываем в ней наш CLASS-модуль, на вкладке Methods видим полный список методов класса. Находим в нем искомую isDemo и тыкаем в нее, открывая окно редактирования.
Окно редактирования dirtyJOE
Это, конечно, не исходник на Java, но здесь хотя бы можно редактировать байт‑код, сверяясь с логикой исходника. Возможности программы минималистичны: редактировать можно только в виде hex-значений кодов инструкций. По счастью, мнемоника и описание текущей исправленной инструкции отображается в окошке над окном кода, а сам список инструкций с описанием каждой имеется в хелпе (причем только список, без опкодов: явно, чтобы хакерам жизнь медом не казалась и пришлось искать шестнадцатеричные опкоды инструкций самостоятельно). По сути, нам надо закоротить данную функцию, сделав возвращаемым значением 0 ( false) . Находим в таблице инструкцию помещения 0 на стек ( iconst_0 ), ее опкод ( 3 ) и ставим ее в самое начало метода, а после нее — сразу возврат ( ireturn ).
Исправляем инструкцию
Закрываем окно редактирования, сохраняем CLASS-модуль, затем меняем исправленный модуль в архиве license-1. 2. 12. jar , который, в свою очередь, копируем на место старого в основном JAR-модуле. С предвкушением перезапускаем программу и обнаруживаем, что она не работает. Мы что‑то сделали не так.
Продолжение доступно только участникам
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Код на 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-редакторе
Пример откомпилированного JIT нативного кода
Снова коснусь теории, не углубляясь в подробности. По сути, для реализации подмены байт‑кода в 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-компиляцией данного класса. Примерная реализация установки такого обработчика выглядит так:
Время от времени пентестерам приходится сталкиваться с Java-приложениями. Это могут быть различные серверы, клиенты или просто десктопные программы. И иногда возникает необходимость «пропатчить» такое приложение. Зачем это нужно? Каждый случай возникновения такой необходимости уникален. К примеру:
- Сложный протокол общения между сервером и клиентом. Чтобы отправлять произвольные запросы – патчим;
- Захардкожены настройки. Чтобы поменять – патчим;
- Для демонстрации последствий проблем типа «race condition» – патчим.
Application
Для рассказа нам понадобится подопытное приложение. Конечно, мы напишем его сами. Его структура на рисунке 1.
рисунок 1
Как видим, оно достаточно простое: два класса, которые запускают приложение и рисуют GUI. Вот их исходники. Main.java:
Классы очень простые. Main – создаёт окно Window. Window – печатает текст в 3 поля. Текст он берёт из наших целевых классов, на примере которых мы и будем рассматривать способы модификации. Как выглядит запущенное приложение, смотрите на рисунке 2.
рисунок 2
Из инструментов нам понадобится только JDK и JavaDecompiler. Приступаем к патчу.
Level 1
Наше приложение – это jar-архив. Для начала просто вставляем его в JavaDecompiler и получаем исходник интересующего нас класса. Level1.java:
Замечательно, теперь мы копируем этот код в текстовый файл с расширением «java». Это самый простой класс, который только можно придумать. Теперь меняем этот исходник, как пожелаем. Мы заменили возвращаемое значение на «My msg». Далее выполняем команду:
В результате мы получим готовый Level1.class. Затем нам нужно просто добавить его в наш jar-архив. Сделаем это следующей командой:
Обратим внимание, что мы создали путь, идентичный названию пакета, в котором расположен наш класс, и переложили туда бинарник.
Результат – на рисунке 3.
рисунок 3
Рассмотрим случай посложнее.
Level 2
Проделываем всё то же самое и получаем исходник. Level2.java:
В нём я поменяю «Super» на «New». И попытаюсь скомпилировать:
Не получилось, потому что в классе есть ссылка на другой объект из этого пакета. Но java очень дружелюбна и позволяет указать при компиляции любой путь, где лежат классы. Т.е. можно просто выполнить команду:
В результате, у нас опять готовый бинарник. Точно так же обновляем jar-архив и смотрим результат на рисунке 4:
рисунок 4
Ну и рассмотрим последний случай, в котором добавится ещё немного сложностей.
Level 3
Декомпилируем наш класс. Level3.java:
Здесь добавим строчку "-update-". Как мы можем сразу заметить, добавился сторонний пакет. И, если мы попытаемся собрать, получим ошибку:
И опять же, java выручает своей дружелюбностью. Ей можно просто указать путь, где лежат jar-архивы зависимостей. Делается это так:
(да, тут указание пути класса лишнее, т.к. он запакован в архив и лежит в той же директории, но т.к. это не всегда так, я его всё же указал)
И далее всё так же – результат на рисунке 5.
Рисунок 5
В итоге мы рассмотрели примеры, как просто и быстро можно модифицировать Java-приложения. Конечно, рассмотрели мы на примере jar-архива, но вы же понимаете, что если классы не запакованы, то всё гораздо проще?
Компьютерные игры открывают перед нами новые миры. И мир читов — один из них. Сегодня мы вместе пройдем путь от теории к практике и напишем собственный чит. Если ты хочешь научиться взламывать исполняемые файлы, то это может стать неплохим упражнением.
Виды читов и применяемые тактики
Существуют разные виды читов. Можно разделить их на несколько групп.
External — внешние читы, которые работают в отдельном процессе. Если же мы скроем наш external-чит, загрузив его в память другого процесса, он превратится в hidden external.
Internal — внутренние читы, которые встраиваются в процесс самой игры при помощи инжектора. После загрузки в память игры в отдельном потоке вызывается точка входа чита.
Pixelscan — вид читов, который использует картинку с экрана и паттерны расположения пикселей, чтобы получить необходимую информацию от игры.
Network proxy — читы, которые используют сетевые прокси, те, в свою очередь, перехватывают трафик клиента и сервера, получая или изменяя необходимую информацию.
Есть три основные тактики модификации поведения игры.
- Изменение памяти игры. API операционной системы используется для поиска и изменения участков памяти, содержащих нужную нам информацию (например, жизни, патроны).
- Симуляция действий игрока: приложение повторяет действия игрока, нажимая мышкой в заранее указанных местах.
- Перехват трафика игры. Между игрой и сервером встает чит. Он перехватывает данные, собирая или изменяя информацию, чтобы обмануть клиент или сервер.
Большинство современных игр написаны для Windows, поэтому и примеры мы будем делать для нее же.
Пишем игру на C
Принцип игры прост: нажимаешь Enter и проигрываешь. Не особо честные правила, да? Попробуем их изменить.
Приступим к реверс-инжинирингу
Исполняемый файл игры
У нас есть файл игры. Но вместо исходного кода мы будем изучать память и поведение приложения.
Начнем с поведения игры
При каждом нажатии Enter жизни игрока уменьшаются на 15. Начальное количество жизней — 100.
Изучать память мы будем при помощи Cheat Engine. Это приложение для поиска переменных внутри памяти приложения, а еще хороший дебаггер. Перезапустим игру и подключим к ней Cheat Engine.
Подключение CE к игре
Первым делом мы получаем список всех значений 85 в памяти.
Все значения, которые нашел CE
Нажмем Enter, и показатель жизней будет равен 70 . Отсеем все значения.
Значение найдено
Вот и нужное значение! Изменим его и нажмем Enter для проверки результата.
Значение изменено
Скрин игры, после того как мы нажали Enter
Проблема в том, что после перезапуска игры значение будет уже по другому адресу. Каждый раз отсеивать его нет никакого смысла. Необходимо прибегнуть к сканированию AOB (Array Of Bytes — массив байтов).
При каждом новом открытии приложения из-за рандомизации адресного пространства (ASLR) структура, описывающая игрока, будет находиться на новом месте. Чтобы найти ее, необходимо сначала обнаружить сигнатуру. Сигнатура — это набор не меняющихся в структуре байтов, по которым можно искать в памяти приложения.
После нескольких нажатий на Enter количество жизней изменилось на 55 . Снова найдем нужное значение в памяти и откроем регион, в котором оно находится.
Регион памяти
Выделенный байт и есть начало нашего int32 -числа. 37 00 00 00 — число 55 в десятичной форме.
Я скопирую небольшой регион памяти и вставлю в блокнот для дальнейшего изучения. Теперь перезапустим приложение и снова найдем значение в памяти. Снова скопируем такой же регион памяти и вставим в блокнот. Начнем сравнение. Цель — найти байты рядом с этой сигнатурой, которые не будут меняться.
Начинаем сравнивать байты
Проверим байты перед структурой.
Как видишь, выделенные байты не изменились, значит, можно попробовать использовать их как сигнатуру. Чем меньше сигнатура, тем быстрее пройдет сканирование. Сигнатура 01 00 00 00 явно будет слишком часто встречаться в памяти. Лучше взять 03 00 00 01 00 00 00 . Для начала найдем ее в памяти.
Сигнатура не уникальна
Сигнатура найдена, но она повторяется. Необходима более уникальная последовательность. Попробуем ED 03 00 00 01 00 00 00 .
В подтверждение уникальности получим такой результат:
Сигнатура уникальна
Нам необходимо найти отступ от сигнатуры, чтобы получить ее стартовый адрес, а не адрес жизней. Пока сохраним найденную сигнатуру и отложим на некоторое время. Не беспокойся, мы к ней еще вернемся.
Жизненный цикл external
Используя функцию OpenProcess , внешние читы получают дескриптор для нужного процесса и вносят необходимые изменения в код (патчинг) или считывают и изменяют переменные внутри памяти игры. Для модификации памяти используются функции ReadProcessMemory и WriteProcessMemory .
Так как динамическое размещение данных в памяти мешает записать нужные адреса и постоянно к ним обращаться, можно использовать технику поиска AOB. Жизненный цикл external-чита выглядит так:
- Найти ID процесса.
- Получить дескриптор к этому процессу с нужными правами.
- Найти адреса в памяти.
- Пропатчить что-то, если нужно.
- Отрисовать GUI, если он имеется.
- Считывать или изменять память по мере надобности.
Продолжение доступно только участникам
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Недавно я усиленно разрабатывал свое приложение под Android, и в процессе защиты платной версии понял, что довольно сложно обезопасить приложение от взлома. Ради спортивного интереса решил попробовать убрать рекламу из одного бесплатного приложения, в котором баннер предлагается скрыть, если заплатить денежку через In-App Purchase.
В этой статье я опишу, как мне удалось убрать рекламу бесплатно и в конце — несколько слов о том, как усложнить задачу взломщикам.
Шаг 1. Получаем «читаемый» код приложения. Чтобы добыть APK приложения из телефона, нужны root права. Вытягиваем приложение из телефона с помощью adb (пусть, для конспирации, у нас будет приложение greatapp.apk):
adb pull /data/app/greatapp.apkХабраюзер overmove подсказал мне, что root необязателен, можно с помощью Astro сделать бэкап любого приложения, и оно будет скопировано в /mnt/sdcard.
Хабраюзер MegaDiablo подсказал мне, что и Astro необязателен. Список установленных приложений и их файлы apk можно узнать через утилиту pm в шелле, а когда уже известно имя файла, его можно стянуть через adb pull /data/app/app.filename.apk .
APK — это ZIP архив, достаем оттуда интересующий нас файл classes.dex со скомпилированным кодом.
Будем использовать ассемблер/дизассемблер smali/baksmali для наших грязных дел.
java -jar baksmali-1.3.2.jar classes.dex
На выходе получаем директорию out с кучей файлов *.smali . Каждый из них соответствует файлу .class . Естественно, все обфусцированно по самое не хочу, выглядит эта директория вот так:
Попытаемся понять, где в этой обфусцированной куче «говорится» о рекламе. Сначала я просто сделал поиск с текстом " AdView " (View, отображающий рекламу из AdMob SDK) по всем файлам. Нашелся сам AdView.smali , R$id.smali и некий d.smali . AdView.smali смотреть не очень интересно, R.$id я как-то сначала проигнорировал, и пошел сразу в таинственный d.smali .
Шаг 2. Пойти по неверному пути.
Вот и метод a() в файле d.smali с первым упоминанием AdView (я решил, скриншотом лучше, а то без форматирования это очень уныло читать):
Метод ничего не возвращает, поэтому я, недолго думая, решил просто вставить поближе к началу return-void . Когда я все собрал и запустил, приложение радостно крэшнулось. Лог из adb logcat :
Понятно, что наш AdView в результате манипуляций должным образом не создался. Забудем пока про d.smali .
Похоже, это идентификатор View с рекламой. Поищем, где он используется, сделав поиск по значению 0x7f080006 . Получаем всего два результата: тот же R$id и GreatApp.smali . В GreatApp.smali текст уже гораздо интереснее (комментарии мои):
Видно, что этот идентификатор используется для поиска View (строка 588) и буквально сразу же AdView удаляется с экрана (строка 595). Видимо, удаляется, если пользователь заплатил за отсутствие рекламы? Если посмотреть немного выше, то взгляд цепляется за строчку 558 с «ключевыми словами»:
invoke-static , Lnet/robotmedia/billing/BillingController;->isPurchased(Landroid/content/Context;Ljava/lang/String;)Z
robotmedia — сторонняя (open source) библиотека, призванная упростить работу с in-app billing-ом в андроиде. Почему же она не была полностью обфусцирована? Ну да ладно, повезло.
Видно, что метод isPurchased() возвращает строку, которая с помощью Boolean.valueOf() преобразуется в объект Boolean и, наконец, в обычный boolean через booleanValue() .
И тут самое интересное, в строке 572 мы переходим в некий :cond_32 , если значение результата == false . А иначе начинается уже просмотренный код поиска и удаления AdView .
Шаг 4. Минимальное изменение, собрать и запустить.
Что ж, дело за малым — удаляем эту ключевую строку, собираем приложение и сразу инсталлируем на телефон:
java -jar ..\smali\smali-1.3.2.jar ..\smali\out -o classes.dex
apkbuilder C:\devel\greatapp\greatapp_cracked.apk -u -z C:\devel\greatapp\greatapp_noclasses.apk -f C:\devel\greatapp\classes.dex
jarsigner -verbose -keystore my-release-key.keystore -storepass testtest -keypass testtest greatapp_cracked.apk alias_name
adb install greatapp_cracked.apk
( greatapp_noclasses.apk — это оригинальный APK приложения, из которого удален classes.dex, сертификаты создаются с помощью Android SDK).
И ура, запускаем приложение, никакой рекламы!
Читайте также: