Модули

  • @niko13teen

    Практика реверс-инжиниринга Android-приложений: Разбираем и нейтрализуем PairIP на реальном примере или как взломать MMO RPG. Часть 1.

    **Важное этическое и юридическое уточнение: Данное исследование было проведено в рамках легального и санкционированного пентеста. Вся работа выполнялась по договору с владельцем приложения или на специально выделенном для тестирования стенде. Использование подобных техник без явного письменного разрешения правообладателя является нарушением лицензионных соглашений и может повлечь юридическую ответственность.

    Так о чем я? Ах да, добро пожаловать в мир реального хакинга и реальных задач на интересных примерах. Кому нужно приложение пиццы, когда есть многопользовательская онлайн рпг? Но тут не все так просто, познакомьтесь с:

    PairIP — это коммерческая система защиты (DRM и анти-пиратства) для мобильных приложений, особенно популярная среди разработчиков на Flutter и React Native. Её главная задача — усложнить или сделать невозможным запуск приложения на неподдерживаемых или пиратских устройствах. А главное - усложнить жизнь хакерам. Как часто она встречается? Очень часто вы будете иметь с ней дело в низком и средних сегментах мобильных игр.

    Какую защиту предоставляет данная система:
    - Лицензионная проверка Google Play: Валидация легальности установки через сервисы Google.
    - Проверка целостности приложения (Integrity Check): Анализ сигнатуры APK, checksum критичных файлов, детект модификаций.
    - Детект нестандартного окружения: Выявление root: прав, запуска на эмуляторе (по таким признакам, как Build.FINGERPRINT, ro.boot.qemu), наличия инструментов отладки (Frida, Xposed).
    - Активное противодействие анализу: При обнаружении угрозы PairIP не просто сообщает об ошибке, а целенаправленно крашит приложение, вызывая System.exit() или выбрасывая необрабатываемые исключения. Это попытка "сломать" процесс динамического анализа.

    Первая кровь: Анализ и обнаружение угрозы.

    Наша цель — клиент MMO RPG (назовем её «game»), написанный на React Native. Задача проста: запустить его в контролируемой среде эмулятора (в моем случае Android SDK) для последующего анализа сетевого трафика и механик игры.

    Первый запуск — мгновенный провал. Ритуал знаком любому реверсеру: $adb multiple-install, $adb shell am start, и… ничего. Приложение запускается на долю секунды и бесшумно закрывается, не показав даже экрана-заставки. Никаких диалогов об ошибке, только черный экран. Это классическое поведение современной защиты — не спорить, не объяснять, а просто устранять угрозу.

    Logcat — наш лучший друг и свидетель.

    Когда приложение молчит, на помощь приходит системный журнал. Команда $adb logcat --pid=$(adb shell pidof com.game.app) мгновенно прояснила ситуацию:

    E LicenseClient: Error while checking license: com.pairip.licensecheck.LicenseCheckException: Licensing service could not process request.

    Вот он, «автограф» PairIP в дикой природе! Строка говорит нам многое:
    - Враг идентифицирован: Класс LicenseClient из пакета com.pairip.licensecheck.
    - Тактика противника: Не просто возврат false, а выбрасывание специального исключения LicenseCheckException.
    - Причина провала: Сервис лицензирования «не смог обработать запрос». На языке эмулятора это означает «я тебя вижу, самозванец».

    Защита сработала безупречно с её точки зрения: обнаружила нелегитимное окружение (эмулятор) и принудительно завершила процесс, не дав даже начать работу. Для разработчика — успех. Для меня — начало интересной задачи.

    От разведки к стратегии: План контратаки.

    Анализ исключения и понимание архитектуры PairIP позволяют сформулировать четкий план обхода. Нам нужно создать многоуровневую защиту от самой защиты:
    - Нейтрализовать механизм самоуничтожения. Заблокировать все вызовы System.exit() и Runtime.exit(), чтобы приложение не могло закрыться по команде PairIP.
    - Обезвредить сигнализацию. Перехватить конструктор LicenseCheckException и сделать его безвредным, подменяя фатальное сообщение.
    - Подкупить или обмануть «стражей». Найти все методы проверки внутри LicenseClient (особенно те, что возвращают boolean) и заставить их всегда докладывать об успехе.

    Для реализации этого плана нет инструмента лучше, чем Frida — фреймворк для динамической инструментации, позволяющий в реальном времени модифицировать работу Java-кода и нативных библиотек. Наша задача — написать скрипт, который станет «теневым телохранителем» приложения, перехватывающим все враждебные команды.

    Код: Пишем «антикраж» для системы «антикража».

    # bypass_license.js
    Java.perform(function() {
    console.log('[+] Стартуем обход защиты Pairip... Берем в работу.');

    // 1. Первым делом нейтрализуем их главное оружие - исключение при проверке лицензии
    // Это как подставить подушку, когда кто-то пытается кричать "Пираты!"
    try {
    var LicenseCheckException = Java.use('com.pairip.licensecheck.LicenseCheckException');
    LicenseCheckException.$init.overload('java.lang.String').implementation = function(msg) {
    console.log('[ХУК] Поймали на слове! Хотели кинуть: "' + msg + '" - не получится.');
    // Всегда возвращаем "успешную" ошибку вместо настоящей
    return this.$init('ВСЁ ЧИСТО, ПРОПУСКАЕМ');
    };
    console.log('[✓] Исключение проверки лицензии теперь беззубое');
    } catch(e) {
    console.log('[!] Не удалось обезвредить исключение: ' + e);
    }

    // 2. Приложение любит самоубиваться через System.exit() при обнаружении взлома
    // Мы просто вырываем предохранитель из этой гранаты
    try {
    var System = Java.use('java.lang.System');
    System.exit.overload('int').implementation = function(status) {
    console.log('[ХУК] Пытается нажать красную кнопку (код ' + status + ')! Отключаем...');
    // Вместо выхода кидаем ошибку, которая остановит вылет
    throw Java.use('java.lang.RuntimeException').$new('Кнопка не работает, сорян');
    };
    console.log('[✓] Аварийное отключение приложения заблокировано');
    } catch(e) {
    console.log('[!] Не удалось заблокировать System.exit: ' + e);
    }

    // 3. Теперь атакуем ядро защиты - класс LicenseClient
    // Ищем ВСЕ методы, которые хоть как-то проверяют лицензию
    try {
    var LicenseClient = Java.use('com.pairip.licensecheck.LicenseClient');

    // Вытаскиваем все методы из класса как есть
    var methods = LicenseClient.class.getDeclaredMethods();

    console.log('[~] Сканируем методы проверки... Найдено: ' + methods.length);

    // Бежим по всем методам и ставим крючки где пахнет проверкой
    methods.forEach(function(method) {
    var methodName = method.getName();
    var returnType = method.getReturnType().getName();

    // Методы, возвращающие boolean с "check" в названии - точно проверки
    // Их заставляем всегда говорить "ДА, лицензия есть!"
    if (returnType === 'boolean' && methodName.toLowerCase().includes('check')) {
    console.log('[+] Ловим за руку: ' + methodName + ' -> всегда true');
    try {
    LicenseClient[methodName].implementation = function() {
    console.log('[ХУК] ' + methodName + ' спрашивает "Есть лицензия?" - отвечаем "ДА!"');
    return true; // Всегда врём, что лицензия есть
    };
    } catch(e) {
    // Иногда метод нельзя перехватить - идём дальше
    }
    }

    // Остальные подозрительные методы просто мониторим
    if (methodName.toLowerCase().includes('check') ||
    methodName.toLowerCase().includes('validate') ||
    methodName.toLowerCase().includes('verify')) {
    console.log('[~] Заметили подозрительный метод: ' + methodName);
    }
    });

    // Дополнительная ловушка - ищем живые экземпляры LicenseClient в памяти
    try {
    Java.choose('com.pairip.licensecheck.LicenseClient', {
    onMatch: function(instance) {
    console.log('[+] В памяти нашли работающий LicenseClient: ' + instance);
    // Тут можно с ним что-то сделать, но пока просто наблюдаем
    },
    onComplete: function() {
    console.log('[✓] Поиск экземпляров завершен');
    }
    });
    } catch(e) {
    // Не критично, если не найдёт
    }

    console.log('[✓] Глубокие хуки на LicenseClient установлены');
    } catch(e) {
    console.log('[!] Проблема с анализом LicenseClient: ' + e);
    }

    // 4. Ещё один путь для выхода - Runtime.exit(), перекрываем и его
    // Это как второй пожарный выход, который тоже запираем
    try {
    var Runtime = Java.use('java.lang.Runtime');
    Runtime.exit.overload('int').implementation = function(status) {
    console.log('[ХУК] Пробуют выйти через Runtime! Затыкаем дыру.');
    return; // Просто игнорируем вызов
    };
    console.log('[✓] Запасной выход через Runtime тоже заблокирован');
    } catch(e) {
    // Не страшно, если не сработает
    }

    console.log('[✓] ВСЁ ГОТОВО. Защита Pairip обезврежена.');
    });

    БЕЗ ЛИШНИХ СЛОВ АТАКУЕМ!

    $frida -U -f com.game.app -l .\bypass_license.js

    [Android Emulator 5554::com.game.app ]-> [+] Стартуем обход защиты Pairip... Берем в работу.
    [✓] Исключение проверки лицензии теперь беззубое
    [✓] Аварийное отключение приложения заблокировано
    [~] Сканируем методы проверки... Найдено: 31
    [~] Заметили подозрительный метод: checkLicenseInternal
    [~] Заметили подозрительный метод: lambda$initializeLicenseCheck$0
    [~] Заметили подозрительный метод: lambda$initializeLicenseCheck$1
    [~] Заметили подозрительный метод: lambda$reportSuccessfulLicenseCheck$0
    [+] Ловим за руку: performLocalInstallerCheck -> всегда true
    [~] Заметили подозрительный метод: performLocalInstallerCheck
    [~] Заметили подозрительный метод: populateInputDataForLicenseCheckV2
    [~] Заметили подозрительный метод: initializeLicenseCheck
    [~] Заметили подозрительный метод: reportSuccessfulLicenseCheck
    [✓] Поиск экземпляров завершен
    [✓] Глубокие хуки на LicenseClient установлены
    [✓] Запасной выход через Runtime тоже заблокирован
    [✓] ВСЁ ГОТОВО. Защита Pairip обезврежена.
    [ХУК] performLocalInstallerCheck спрашивает "Есть лицензия?" - отвечаем "ДА!"

    Это победа. Приложение не просто запустилось — оно запустилось, обманув собственную защиту. Система лицензирования сообщила об успешной проверке (performLocalInstallerCheck -> TRUE), и механизм самоуничтожения был заблокирован.

    Теперь, когда клиент жив и работает в эмуляторе, открывается поле для реальной исследовательской работы. О которой я напишу во второй части. За лайки, я так пойму интересно ли Вам вообще :)))

    Ответов: 0 Репостов: 0 Лайков: 3
    0/360