Модули
-
Практика реверс-инжиниринга 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), и механизм самоуничтожения был заблокирован.
Теперь, когда клиент жив и работает в эмуляторе, открывается поле для реальной исследовательской работы. О которой я напишу во второй части. За лайки, я так пойму интересно ли Вам вообще :)))