SAMPLE REPORT — REDACTED ПРИМЕР ОТЧЁТА — РЕДАКТИРОВАНО
← Back to home ← На главную

[REDACTED] 4.5.6 — Security Audit Report

Package: [REDACTED]
Version: 4.5.6 (build 405601)
Build: production / release
Target: [REDACTED]_4.5.6.xapk
Date: 2026.02.09

[REDACTED] 4.5.6 — Отчёт по аудиту безопасности

Пакет: [REDACTED]
Версия: 4.5.6 (билд 405601)
Сборка: production / release
Цель: [REDACTED]_4.5.6.xapk
Дата: 2026.02.09

1. Application Overview

XAPK bundle contains:

Dynamic Feature Modules deliver translations for 8 languages: en, uk, ru, ja, ko, vi, id, th. Each has a _tr variant for traditional Chinese characters.

Embedded assets:

1. Обзор приложения

XAPK-бандл содержит:

Dynamic Feature Modules доставляют переводы для 8 языков: en, uk, ru, ja, ko, vi, id, th. У каждого есть вариант _tr для традиционных китайских иероглифов.

Встроенные ассеты:

2. SDK & Service Stack

2. Стек SDK и сервисов

3. Trial Implementation

3.1. Client-Side 7-Day Trial

File: SubscriptionUseCaseImpl.java, method checkAppInstalledDate()

The trial is NOT a Google Play free trial. It is entirely client-side:

  1. On first launch, app saves appInstalledDate as Long timestamp in Android DataStore
  2. checkAppInstalledDate() reads the saved date
  3. If installDate + 7 days > current date → returns true, full premium access

Logic: DateExtensionsKt.addDays(new Date(appInstalledDate), 7).after(new Date())

Vulnerability: Trivially bypassed by clearing app data — resets install date and grants a new 7-day trial infinitely. Timestamp stored in DataStore (SharedPreferences-based), modifiable with root access or via backup/restore.

Mitigation: Bind trial to user account. No account = no trial. Server-side tracking of trial activation date by user_id. Client only displays state; backend makes the decision.

3.2. Server-Side 2-Day Access

File: SubscriptionUseCaseImpl.java, method checkServerProductFlag()

Mitigation: Same as trial — server-side control. Client queries backend for current access state rather than storing date locally.

3.3. App Review Bypass

File: SubscriptionUseCaseImpl.java, method isAppInReview()

FirebaseRemoteConfig.getBoolean("app_in_review"), default false, refresh interval 3600 seconds.

Risk: When set to true via Firebase Remote Config, ALL users receive premium access unconditionally. Intended for App Store review periods. If Firebase project is compromised → universal premium bypass.

Mitigation: Scope the flag — combine with specific build number or version code. Or use a separate flavor/build type for review instead of Runtime Config that applies to everyone.

3.4. Daily Feature Limits (Free/Trial Users)

File: FeatureDayLimits.java

3. Реализация триала

3.1. Клиентский 7-дневный триал

Файл: SubscriptionUseCaseImpl.java, метод checkAppInstalledDate()

Триал НЕ является бесплатным пробным периодом Google Play. Он полностью клиентский:

  1. При первом запуске приложение сохраняет appInstalledDate как Long timestamp в Android DataStore
  2. checkAppInstalledDate() читает сохранённую дату
  3. Если installDate + 7 дней > текущая дата, возвращает true, полный премиум-доступ

Логика: DateExtensionsKt.addDays(new Date(appInstalledDate), 7).after(new Date())

Уязвимость: тривиально обходится очисткой данных приложения, сбрасывает дату установки и даёт новый 7-дневный триал бесконечно. Timestamp хранится в DataStore (на базе SharedPreferences), модифицируется с root-доступом или через backup/restore.

Митигация: привязать триал к аккаунту пользователя. Без аккаунта — нет триала. Серверсайд трекинг даты активации триала по user_id. Клиент только отображает состояние, решение принимает бэкенд.

3.2. Серверный 2-дневный доступ

Файл: SubscriptionUseCaseImpl.java, метод checkServerProductFlag()

Митигация: аналогично триалу, серверсайд контроль. Клиент запрашивает у бэкенда текущий статус доступа, а не хранит дату локально.

3.3. Обход при ревью приложения

Файл: SubscriptionUseCaseImpl.java, метод isAppInReview()

FirebaseRemoteConfig.getBoolean("app_in_review"), по умолчанию false, интервал обновления 3600 секунд.

Риск: при значении true через Firebase Remote Config ВСЕ пользователи получают премиум-доступ безусловно. Предназначено для периодов ревью в App Store. При компрометации Firebase-проекта — универсальный обход премиума.

Митигация: ограничить область действия флага. Проверять комбинацию флага + конкретный build number или version code. Или использовать отдельный flavor/build type для ревью.

3.4. Дневные лимиты функций (бесплатные/триальные пользователи)

Файл: FeatureDayLimits.java

4. Subscription Implementation

4.1. Architecture

Three-tier stack:

  1. Google Play Billing v8.3.0 — base payment processor
  2. RevenueCat — subscription state, entitlements, offerings
  3. Superwall — paywall display via WebView

Single entitlement: "[REDACTED]_plus" — all plans grant identical premium access.

4.2. Products

File: SubscriptionManager.java, method fetchProducts()

4.3. Premium Decision Chain

File: SubscriptionUseCaseImpl.java, method updateProducts()

MutableStateFlow<Boolean>, initialized false — single source of truth. Decision chain:

  1. isLaoshiPlusEnabled() already true? → return, skip everything
  2. checkAppInstalledDate() true? → state = true (7-day trial)
  3. checkServerProductFlag() true? → state = true (2-day server grant)
  4. Recheck enabled? → return
  5. isAppInReview() true? → state = true, return (Firebase flag)
  6. isAppInstallerValid(context) false? → return (abort, don't check RevenueCat)
  7. Query RevenueCat entitlements → if active: state = true

Mitigation: Move subscription check to backend. Client requests server with auth token; server checks subscription via RevenueCat S2S API and returns current state.

4.4. RevenueCat Configuration

File: [REDACTED]Application.java, method initializeRevenueCat()

4.5. Superwall Paywall

Files: SuperwallContainer.java, RevenueCatPurchaseController.java

4.6. Installer Validation

File: ContextExtensionsKt.java, method isAppInstallerValid()

public static final boolean isAppInstallerValid(Context context) {
    return true;  // HARDCODED — no validation
}

Mitigation: Enable real installer check. More robust: server-side verification of APK signing certificate hash per request.

4. Реализация подписок

4.1. Архитектура

Трёхуровневый стек:

  1. Google Play Billing v8.3.0, базовый платёжный процессор
  2. RevenueCat, состояние подписки, entitlements, offerings
  3. Superwall, отображение paywall через WebView

Единственный entitlement: "[REDACTED]_plus", все тарифы подписки дают идентичный премиум-доступ.

4.2. Продукты

Файл: SubscriptionManager.java, метод fetchProducts()

4.3. Цепочка принятия решения о премиуме

Файл: SubscriptionUseCaseImpl.java, метод updateProducts()

MutableStateFlow<Boolean>, инициализирован false — единственный источник истины. Цепочка решений:

  1. isLaoshiPlusEnabled() уже true? → return, пропустить всё
  2. checkAppInstalledDate() true? → state = true (7-дневный триал)
  3. checkServerProductFlag() true? → state = true (2-дневный серверный грант)
  4. Повторная проверка? → return
  5. isAppInReview() true? → state = true, return (флаг Firebase)
  6. isAppInstallerValid(context) false? → return (прервать, не проверять RevenueCat)
  7. Запросить entitlements RevenueCat → если активен: state = true

Митигация: вынести проверку подписки на бэкенд. Клиент запрашивает сервер с токеном авторизации, сервер проверяет статус через RevenueCat S2S API.

4.4. Конфигурация RevenueCat

Файл: [REDACTED]Application.java, метод initializeRevenueCat()

4.5. Paywall Superwall

Файлы: SuperwallContainer.java, RevenueCatPurchaseController.java

4.6. Валидация установщика

Файл: ContextExtensionsKt.java, метод isAppInstallerValid()

public static final boolean isAppInstallerValid(Context context) {
    return true;  // ЗАХАРДКОЖЕНО, никакой валидации
}

Митигация: включить реальную проверку установщика. Более надёжный вариант: серверсайд верификация хэша подписи APK при каждом запросе.

5. Dynamic Content Loading

5.1. Backend Endpoints

5.2. REST API Endpoints (Retrofit)

Main Service (base: [REDACTED]):

Auth Service (base: [REDACTED]): GET mail/register, POST otp

Cloud Service (base: [REDACTED]): POST simple-sharing-upload, GET simple-sharing-download, GET remove-user-data

Slack Service (base: [REDACTED]): POST [REDACTED] — teacher info to Slack webhook

5.3. gRPC Services

File: GrpcServiceFactory.java, connection: [REDACTED]:1443

All services authenticate via Supabase JWT token in gRPC metadata headers.

5.4. Supabase

File: Supabase.java

5. Динамическая загрузка контента

5.1. Эндпоинты бэкенда

5.2. REST API эндпоинты (Retrofit)

Основной сервис (база: [REDACTED]):

Auth-сервис (база: [REDACTED]): GET mail/register, POST otp

Cloud-сервис (база: [REDACTED]): POST simple-sharing-upload, GET simple-sharing-download, GET remove-user-data

Slack-сервис (база: [REDACTED]): POST [REDACTED] — информация об учителе в Slack webhook

5.3. gRPC-сервисы

Файл: GrpcServiceFactory.java, подключение: [REDACTED]:1443

Все сервисы аутентифицируются через Supabase JWT токен в metadata-заголовках gRPC.

5.4. Supabase

Файл: Supabase.java

6. License Check (com.pairip.licensecheck)

6.1. Mechanism

Files: LicenseActivity.java, LicenseClient.java, LicenseClient$1.java

Google Play license check runs on app start:

  1. LicenseClient starts license verification on initialization
  2. On failure, launches LicenseActivity with ActivityType:
    • PAYWALL (ordinal 0) — shows Google Play paywall + kills app
    • ERROR (ordinal 1) — shows error dialog + kills app
  3. closeApp() calls finishAndRemoveTask() then System.exit(0)

6.2. Missing Anti-Tamper

Mitigation: Implement Play Integrity API for device and app integrity verification. Send verdict to backend for server-side verification.

6. Проверка лицензии (com.pairip.licensecheck)

6.1. Механизм

Файлы: LicenseActivity.java, LicenseClient.java, LicenseClient$1.java

Проверка лицензии Google Play запускается при старте приложения:

  1. LicenseClient запускает проверку лицензии при инициализации
  2. При неудаче запускает LicenseActivity с ActivityType:
    • PAYWALL (ordinal 0), показывает paywall Google Play + убивает приложение
    • ERROR (ordinal 1), показывает диалог ошибки + убивает приложение
  3. closeApp() вызывает finishAndRemoveTask() затем System.exit(0)

6.2. Отсутствие защиты от модификации

Митигация: внедрить Play Integrity API для проверки целостности устройства и приложения. Verdict отправлять на бэкенд для серверсайд верификации.

7. Hardcoded Secrets & API Keys

7.1. BuildConfig.java

7.2. Other Locations

Mitigation: Basic Auth credentials must be removed from code. Slack webhook should be revoked and moved server-side. Remaining keys are standard for mobile but combined with weak server validation simplify reverse engineering.

7. Захардкоженные секреты и API-ключи

7.1. BuildConfig.java

7.2. Другие места

Митигация: Basic Auth креденшелы убрать из кода. Slack webhook отозвать и вынести URL на серверсайд. Остальные ключи стандартны для мобилок, но в связке со слабой серверной валидацией упрощают реверс.

8. Applied & Verified Patches

8.1. Patch Summary

Four smali patches applied to the decoded APK:

  1. SubscriptionUseCaseImpl.smaliisLaoshiPlusEnabled() always returns true
  2. SubscriptionUseCaseImpl.smalicheckAppInstalledDate() always returns true (infinite trial)
  3. LicenseActivity.smalionStart() calls finish() immediately (skip license check)
  4. LicenseClient$1.smalirun() is no-op instead of System.exit(0)

8.2. Runtime Verification (Emulator, Medium_Phone_API_35)

02-09 23:35:16.161 D [REDACTED]_PATCH: LicenseActivity.onStart() PATCHED -> finish() immediately
02-09 23:35:16.943 D [REDACTED]_PATCH: isLaoshiPlusEnabled() PATCHED -> always true
02-09 23:35:50.096 D [REDACTED]_PATCH: isLaoshiPlusEnabled() PATCHED -> always true
02-09 23:36:32.994 D [REDACTED]_PATCH: isLaoshiPlusEnabled() PATCHED -> always true

8. Применённые и проверенные патчи

8.1. Сводка патчей

Четыре smali-патча применены к декодированному APK:

  1. SubscriptionUseCaseImpl.smaliisLaoshiPlusEnabled() всегда возвращает true
  2. SubscriptionUseCaseImpl.smalicheckAppInstalledDate() всегда возвращает true (бесконечный триал)
  3. LicenseActivity.smalionStart() вызывает finish() немедленно (пропуск проверки лицензии)
  4. LicenseClient$1.smalirun() no-op вместо System.exit(0)

8.2. Верификация (эмулятор, Medium_Phone_API_35)

02-09 23:35:16.161 D [REDACTED]_PATCH: LicenseActivity.onStart() PATCHED -> finish() immediately
02-09 23:35:16.943 D [REDACTED]_PATCH: isLaoshiPlusEnabled() PATCHED -> always true
02-09 23:35:50.096 D [REDACTED]_PATCH: isLaoshiPlusEnabled() PATCHED -> always true
02-09 23:36:32.994 D [REDACTED]_PATCH: isLaoshiPlusEnabled() PATCHED -> always true

9. Security Findings Summary

Critical

  1. Client-side 7-day trial: Pure timestamp check in DataStore, no server validation. Unlimited trials via app data clearing.
  2. isAppInstallerValid() hardcoded to true: Sideload protection completely absent.
  3. app_in_review Firebase flag grants universal premium: One Remote Config key = full premium for all users.

High

  1. Single MutableStateFlow<Boolean>: Trivial Frida hook target. One boolean controls all premium.
  2. No modification/root/debug detection: APK can be freely patched, debugged, and hooked.
  3. Supabase anon JWT hardcoded, expires 2033: Data security entirely depends on RLS policies.

Medium

  1. Basic Auth credentials in code: CloudService endpoints accessible.
  2. Slack webhook token exposed: Can post to team Slack channel.
  3. RevenueCat proxied through non-standard endpoint: Unclear security posture.
  4. Server product flag also client-side: Same DataStore timestamp vulnerability.

Low / Informational

  1. All API keys in BuildConfig — standard for mobile but enables enumeration.
  2. Subscription notification delay timings exposed (2d / 6d).
  3. Spaced repetition intervals exposed (2h, 23h, 5d, 19d, 120d, 180d).
  4. DB_UPDATE_DATE = 2023-06-09 — embedded database age revealed.

9. Сводка находок безопасности

Критические

  1. Клиентский 7-дневный триал: чистая проверка timestamp в DataStore, без серверной валидации. Неограниченные триалы через очистку данных.
  2. isAppInstallerValid() захардкожен в true: защита от сайдлоада полностью отсутствует.
  3. Флаг app_in_review в Firebase даёт универсальный премиум: один ключ Remote Config = полный премиум для всех.

Высокие

  1. Один MutableStateFlow<Boolean>: тривиальная цель для хука Frida, один boolean контролирует весь премиум.
  2. Отсутствие детекции модификации/root/отладки: APK можно свободно патчить, отлаживать и хукать.
  3. Supabase anon JWT захардкожен, истекает в 2033: безопасность данных полностью зависит от политик RLS.

Средние

  1. Basic Auth креденшелы в коде: эндпоинты CloudService доступны.
  2. Токен Slack webhook раскрыт: можно постить в Slack-канал команды.
  3. RevenueCat проксируется через нестандартный эндпоинт: неясная безопасность.
  4. Серверный флаг продукта тоже клиентский: та же уязвимость с timestamp в DataStore.

Низкие / Информационные

  1. Все API-ключи в BuildConfig — стандартно для мобилок, но позволяет перечисление.
  2. Тайминги напоминаний о подписке раскрыты (2д / 6д).
  3. Интервалы интервального повторения раскрыты (2ч, 23ч, 5д, 19д, 120д, 180д).
  4. DB_UPDATE_DATE = 2023-06-09 — возраст встроенной базы раскрыт.

10. Prioritized Recommendations

Priority 1 — Quick Wins

Priority 2 — Architecture Changes

Priority 3 — Hardening

10. Рекомендации по приоритету

Приоритет 1 — Быстрые победы

Приоритет 2 — Изменения архитектуры

Приоритет 3 — Hardening

11. Tools Used

11. Использованные инструменты