Привет, Хабр.
С 1 апреля 2024 RuStore пред устанавливается на все телефоны, продаваемые в РФ.
Ну вот я Макс реверсил, решил заодно и RuStor поревесить.
Кратко что я нашел
Собственный код RuStore (VK):
-
Шоукейс: Список приложений с иконками в лаунчере подгружается через
POST api.rustore.ru/v1/showcase/startup-destinationпри запуске и периодически в фоне. -
Идентификация: Устройство привязывается к аккаунту через
ANDROID_ID. -
Установка по флагу: Установка MAX Messenger и аналогичного ПО происходит по серверному флагу
SAK_MAX_MESSENGER_INSTALLчерез «тихий» установщик. -
Тихая установка: Push-сообщение с сервера запускает скрытую фоновую установку приложения по имени пакета (логика
preorder/autoinstall,PreorderAutoInstallPushDto). -
SSO-провайдер: RuStore работает как поставщик VK-токенов для сторонних приложений через AIDL, без запроса согласия пользователя.
-
Подсистема Radar: Локальная SQLite-база
snapshot_recordsхранит userId, три геофикса (GPS, сеть, последний известный), BSSID Wi-Fi-роутера, список сотовых вышек, оператора SIM и признаки VPN/роуминга. -
Авто-обновление: Приложение обновляет любой софт в фоне, вне зависимости от того, откуда он был установлен.
-
Статистика: Сбор данных о том, какими приложениями и когда вы пользуетесь (
PACKAGE_USAGE_STATS); суточный воркер выгружает отчеты наapi.rustore.ru/user-event-handler(AltCraft) с привязкой к userId + vkId + deviceId. -
Тогглы: Реестр из 73 удалённо управляемых параметров VK ID SDK, включая флаги для MAX и
sak_messenger_skip_sms_android.
Подключённые SDK:
-
Kaspersky kavsdk: Полноценный антивирусный движок с KSN-телеметрией (19 потоков, включая P2P-узлы), классификатором приложений и
inotifyпо медиапапкам. -
Mail.ru libverify: Верификация по SMS/звонку, читает код подтверждения и номер flash-call.
-
VK SuperApp JS Bridge: 150+ методов, проброшенных во внутренний VK-браузер.
-
OK Tracer: Телеметрия производительности на
sdk-api.apptracer.ru. -
libsecrets.so: Обфусцированные секреты VK Push SDK.
Сервер может инициировать установку MAX
g20/f.java это MaxMessengerSeamlessInstallFlow. Установка ru.oneme.app (MAX) в рамках VK ID-флоу гейтирована серверным флагом SAK_MAX_MESSENGER_INSTALL (система удалённых тогглов VK, yn0/b.java, ключ sak_max_messenger_install). Флаг переключается с сервера, устройство не решает само.
Установку выполняет общий установщик стора wi2/e.java:
public final int g(String packageName) { PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(1); sessionParams.setInstallReason(4); // INSTALL_REASON_USER if (Build.VERSION.SDK_INT >= 31) sessionParams.setRequireUserAction(2); // USER_ACTION_NOT_REQUIRED if (Build.VERSION.SDK_INT >= 34) sessionParams.setRequestUpdateOwnership(true); sessionParams.setAppPackageName(packageName); return this.f116925e.createSession(sessionParams);}
setRequireUserAction(USER_ACTION_NOT_REQUIRED) это запрос на установку без подтверждения пользователя. Android исполняет его без диалога только если у установщика есть привилегия (UPDATE_PACKAGES_WITHOUT_USER_ACTION и/или статус update-owner, отсюда setRequestUpdateOwnership(true)). Для предустановленного системного магазина это условие выполняется, поэтому установка проходит тихо. Это штатный установщик RuStore, и MAX просто проходит через него, когда сервер включает флаг. Метод g принимает любое имя пакета, MAX
Справка: без нашего ведома и согласия в фоне скачивается и устанавливается MAX — правительственный мессенджер, в котором полностью отсутствует E2E-шифрование. Подобное поведение абсолютно недопустимо ни по правилам Android для доверенных каталогов, ни по стандартам любого адекватного магазина приложений.
Push с сервера запускает тихую установку приложения по имени пакета
Установка MAX это не единственный путь, которым сервер инициирует установку. В отдельной фиче ru.vk.store.feature.preorder.autoinstall установка запускается обычным push-сообщением.
Полезная нагрузка push-а разбирается в PreorderAutoInstallPushDto (.../impl/data/PreorderAutoInstallPushDto.java):
@Serializableclass PreorderAutoInstallPushDto { String packageName; // что установить String name; String icon;}
Маппер qq1/a.java (PreorderAutoInstallPushMapper) десериализует тело push-а в этот DTO:
PreorderAutoInstallPushDto dto = json.decode(PreorderAutoInstallPushDto.serializer(), str);return new PreorderAutoInstallPush(dto.getPackageName(), dto.getName(), Url(dto.getIcon()));
Дальше PreorderAutoInstallPushObserver (tq1/h.java) передаёт это в GetPreorderAutoInstallActionInteractor (sq1/a.java), который выбирает действие из перечисления PreorderAutoInstallAction:
INSTALL, NOTIFICATION, SCREEN
На ветке INSTALL контроллер PreorderAutoInstallController (tq1/c.java) зовёт InstallAppUseCaseImpl (storeapp/install/impl/domain/j.java), и тот ставит задачу установки в очередь:
// InstallAppUseCaseImpl.a(...)installRequestRepository.a(new InstallRequest.Enqueue( packageName, appId, name, iconUrl, versionCode, signatures, ...));
Очередь разбирает install-воркер, а сам пакет ставит тот же привилегированный установщик wi2/e.java с setRequireUserAction(USER_ACTION_NOT_REQUIRED). На привилегированном предустановленном магазине это проходит в фоне без тапа пользователя. Источник установки PUSH прямо зафиксирован в MainActivity.java (PreorderAutoInstallReadyAnalyticsSource.PUSH).
Выбор между INSTALL и NOTIFICATION/SCREEN гейтится GetSilentInstallAvailabilityUseCase (ld1/a.java) и серверным тогглом (FlipperRepository). Когда тихая установка доступна и тоггл включён, берётся INSTALL, иначе пользователю показывается уведомление или экран.
Штатный, согласованный сценарий выглядит законно: вы жмёте «Предзаказать» (POST v1/preorder, vq1/a.java), в диалоге предзаказа можете включить авто-установку (AUTO_INSTALL, setAutoInstall), и на релизе приложение ставится само. Это вы запросили сами.
Решение принимает связка GetPreorderAutoInstallActionInteractor (sq1/b.java) и PreorderAutoInstallController (tq1/c.java). Интерактор выбирает только тип действия (INSTALL / NOTIFICATION / SCREEN) исходя из разрешений, серверного тоггла (FlipperRepository) и доступности тихой установки. Контроллер берёт имя пакета прямо из тела пуша (PreorderAutoInstallPushDto.packageName), обращается с ним к каталогу RuStore (storeAppRepository.e(...)) и отдаёт в установку (InstallAppUseCaseImpl -> привилегированный wi2/e.java).
Прикол в том, что проверки, что вы вообще оформляли предзаказ на этот пакет попросту нету. Среди зависимостей интерактора и контроллера нет ни одного источника данных о ваших предзаказах (настоящий PreorderApi / PreorderNewRepository из vq1 сюда не инжектится), поэтому сопоставить присланный пакет с вашими реальными предзаказами эта цепочка в принципе не может. Что сервер назвал, то и ставится.
Как сделал бы вменяемый разработчик: разрешать ставить только тот пакет, на который у пользователя реально есть предзаказ, и проверять это на клиенте, вместо того чтобы верить серверу на слово. Это важно и понятно, инфраструктура такого масштаба ( 67 лямов установок ) регулярно становится целью атак, и при возможности отправлять пуши текущая схема вырождается в «назови любой пакет из каталога -> он встанет в фоне» на устройствах с доступной тихой установкой, без всякого предзаказа со стороны жертвы.
И давайте не будем отметать ещё один вариант: в совокупности факторов это вполне могли оставить так намеренно. Привилегия тихой установки, серверные тогглы и отсутствие проверки происхождения пакета складываются слишком удобно, чтобы по умолчанию списывать всё на недосмотр.
Справка: в систему зашит классический бэкдор. Отсутствие на клиенте элементарной проверки того, действительно ли вы запрашивали это приложение, превращает механизм в инструмент принудительной дистрибуции. Серверу достаточно отправить Push-сигнал, чтобы скрытно раскатать любой софт на миллионы устройств. По стандартам безопасности любого адекватного магазина приложений по типу Fdroid или Auroraoss — тихая установка нового ПО без явного согласия пользователя — это абсолютно недопустимый произвол.
Подсистема Radar: локальная база истории перемещений, привязанная к пользователю
RuStore несёт собственную подсистему телеметрии Radar (пакет ru.vk.store.lib.analytics.system.radar). Это собственный код магазина (не сторонний SDK). Radar периодически снимает «снапшот» устройства, складывает его в локальную базу SQLite и раз в 12 часов выгружает накопленное на сервер.
Самое наглядное доказательство того, что собирается, это схема таблицы. Room-база RadarDatabase создаёт таблицу snapshot_records (dh2/c.java, класс RadarDatabase_Impl). Ниже её реальный CREATE TABLE, имена колонок дословные, форматирование и сокращения мои:
CREATE TABLE `snapshot_records` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT, `sequenceNumber` INTEGER NOT NULL, `userId` TEXT, -- идентификатор пользователя `createdTime` INTEGER NOT NULL, `reason` TEXT NOT NULL, -- причина снимка -- три независимых геофикса, у каждого полный набор координат: `lastLocationlatitude` REAL, `lastLocationlongitude` REAL, `lastLocationaltitude` REAL, `lastLocationaccuracy` REAL, `lastLocationspeed` REAL, `gpsLocationlatitude` REAL, `gpsLocationlongitude` REAL, `gpsLocationaltitude` REAL, `networkLocationlatitude` REAL, `networkLocationlongitude` REAL, -- сотовая сеть: `networkStatecellularExtendedInfosimOperator` TEXT, -- оператор SIM `networkStatecellularExtendedInfonetworkOperator` TEXT, `networkStatecellularExtendedInfoisRoaming` INTEGER, -- роуминг `networkStatecellularExtendedInfocellInfoList` BLOB, -- список вышек `networkStatecellularExtendedInfosimInfoList` BLOB, -- сеть и трафик: `networkStatenetworkInfovpnEnabled` INTEGER, -- включён ли VPN `networkStatenetworkInfosignalStrength` INTEGER, `networkStatesystemTrafficStatstotalBytes` INTEGER NOT NULL, `networkStateprocessTrafficStatstotalBytes` INTEGER NOT NULL, -- Wi-Fi, к которому подключён телефон: `connectedWifiInfobssid` TEXT, -- MAC роутера `connectedWifiInfofreq` INTEGER, -- батарея: `batteryInfochargePercent` REAL, `batteryInfopowerSaveMode` INTEGER)
Каждая запись содержит сразу несколько слоёв данных: идентификатор пользователя (userId), три геофикса (GPS, по сети, последний известный), MAC роутера, список сотовых вышек, оператор SIM, признак роуминга, признак использования VPN, заряд батареи, объём трафика и причину снимка. Привязка к userId означает, что записи не обезличены на уровне схемы.
DTO для отправки (SnapshotDto) дополнительно несёт отпечаток устройства (DeviceStateDto: manufacturer, model, osVersion, поле tac это первые цифры IMEI) и clientState (ClientStateDto) с полями identifier и userIdentifier.
Под-структуры геолокации и сетей:
// LocationDto.javalon, lat, accuracy, speed, altitude, hasAltitude, source, elapsedTime// WifiNetworkInfoDto.javaisConnected, signalLevel, level, bbsid, freq // bbsid = MAC точки доступа// CellInfoDto.javatype, mcc, mnc, area, cellId, rfcn, pscPci, bandwidth, signalList, isRegistered
В одной записи лежат сразу четыре независимых способа определить местоположение: GPS-координаты, координаты по сети, Cell ID соты и MAC-адрес роутера. Этого набора достаточно для точного геопозиционирования даже при выключенном GPS.
Когда снимается снапшот (перечисление SnapshotReason, трекер RadarSnapshotTracker это kh2/n.java, интервал берётся с сервера через фич-флаг ru.vk.store.lib.featuretoggle.a.D0):
APP_WAKEUP, HEARTBEAT_APP, APP_BACKGROUND, NETWORK_CHANGED
при пробуждении приложения, по серверному таймеру-heartbeat, при уходе в фон и при смене сети. Подсистему запускает RadarInitializer (presentation/a.java), запись строит RadarAnalytics.snapshot (kh2/e.java) и кладёт в БД через dh2.r.
Частота и сроки берутся с сервера и видны в реестре тогглов (ru/vk/store/lib/featuretoggle/a.java), значения по умолчанию:
techRadarHeartbeatIntervalSeconds = 120 // снимок раз в 2 минуты (это и есть флаг D0)techRadarRecordDaysLimit = 15 // записи копятся в БД до 15 днейtechRadarFlushSnapshotsHourVariants = 2, 3, 4, 14, 15, 16 // часы выгрузкиtechRadarBatchSize = 15techRadarAnalyticsEnabled = false // мастер-выключатель, включается с сервера
То есть при включённой подсистеме это снимок местоположения раз в 2 минуты по heartbeat, плюс снимки по событиям, с накоплением в локальной базе до 15 дней и пакетной выгрузкой в заданные часы.
Куда уходит. Накопленные записи раз в 12 часов выгружает RadarFlushSnapshotWorker -> jh2.e SendSnapshotsUseCase -> gh2.g SnapshotRemoteRepository -> gh2.e ReportDataSource -> gh2.d, где собирается HTTP-запрос на OkHttp:
// gh2/d.java (ReportDataSource$post)a0.a aVar2 = new a0.a();aVar2.h("https://reef.vk-cdn.net/ru-vk-store/stat/v1/ev"); // POSTaVar2.f(b13); // тело = батч снапшотовreturn eVar.f43452b.a(new a0(aVar2)).e();
Endpoint: POST https://reef.vk-cdn.net/ru-vk-store/stat/v1/ev, Content-Type application/x-www-form-urlencoded (gh2/e.java). Отпечаток устройства привязан к тому же deviceId, что и список приложений: в gh2/b.java (DeviceStateMapper) this.f43440a = gVar.a(), та же di2.g с ANDROID_ID плюс хэш железа.
Сбор гейтится удалёнными тогглами (фич-флаги «Flipper», FlipperRepository; в dh2/r.java и presentation/a.java это Feature.Remote), а геофиксы наполняются только при выданном разрешении на локацию (ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATION в манифесте присутствуют). Колонка userId объявлена nullable, то есть заполняется при наличии пользовательской сессии.
Для работы каталога приложений три геофикса, MAC роутера, список сотовых вышек, оператор SIM, признак роуминга и флаг VPN не требуются. По схеме snapshot_records это полноценная история перемещений с локальным буфером в SQLite, привязкой к пользователю и серверным управлением частотой снятия.
Я даже не придумал как их оправдать.
Справка: С точки зрения здравого смысла и базовой приватности, каталогу приложений для работы абсолютно не нужны ваши точные координаты, MAC-адреса роутеров и данные сотовых вышек каждые 2 минуты. Никакой технической необходимости для скачивания программ в этом нет. По факту, под видом системного магазина в ваш телефон просто вшит полноценный GPS-маячок. Он непрерывно пишет подробную историю ваших перемещений и намертво привязывает её к аппаратному ID устройства.
Список приложений уходит на сервер VK при запуске
При старте и периодически в фоне RuStore собирает список установленных пакетов и отправляет его на сервер.
Цепочка вызовов:
PackageManager.getInstalledPackages(flags) | sd1/j.java - InstalledAppDataSource | sd1/p.java - InstalledAppRepositoryImpl | aj1/a.java - GetStartDestinationUseCaseImpl | yi1/d.java - StartDestinationRepository vPOST https://api.rustore.ru/v1/showcase/startup-destination
DTO запроса (ru/vk/store/feature/navigation/startDestination/impl/data/UserAppsDto.java):
@kotlinx.serialization.Serializablepublic final class UserAppsDto { private final List<String> packageNames;}
deviceId уходит в заголовке, packageNames в теле.
Список фильтруется: в sd1/h.java/sd1/j.java остаются приложения, у которых есть иконка в лаунчере (android.intent.category.LAUNCHER / LEANBACK_LAUNCHER) и которые установлены с кодом (не PERSISTENT, не DATA_ONLY). Флаг getInstalledPackages по умолчанию 0, при включённом тоггле 512. Но и этого хватает: сервер видит ваши банки, мессенджеры, ВПНЫ, конкурирующие магазины и то, как этот набор меняется со временем.
Header deviceId формируется в di2/g.java:
@kotlinx.serialization.Serializablepublic final class UserAppsDto { private final List<String> packageNames;}
f32037c это хэш из Build.MANUFACTURER/MODEL/HARDWARE/DEVICE. ANDROID_ID сбрасывается только при factory reset, идентификатор устойчивый и привязан к устройству на годы.
Фоновое обновление зарегистрировано как PeriodicWorkRequest с именем PeriodicUpdateStartDestination (bj1/e.java, enqueueUniquePeriodicWork). Список обновляется регулярно.
Нужно это чтоб сверить локальные версии (versionCode) ваших приложений с теми, что лежат на сервере, чтобы предложить обновление… хотел бы я сказать но запрос UserAppsDto несёт только packageNames, без версий. А этот update-флоу не «предлагает» обновление. В том же контроллере. По факту на сервер уходит список всех лаунчабл-приложений.
Справка: Казалось бы, любому магазину нужен список установленных пакетов, чтобы проверять наличие обновлений. Но разработчики даже не удосужились прикрыться этой отговоркой. В данных, которые улетают на сервер, передаются только названия пакетов — без их версий. А не зная текущую версию приложения, предложить обновление технически невозможно. Это прямо доказывает, что рутинная системная функция используется исключительно для тотальной инвентаризации. RuStore регулярно сливает на серверы VK полный слепок вашего устройства: какие VPN вы используете, какие банки, защищенные мессенджеры или сторонние магазины у вас стоят. Весь этот список намертво привязывается к аппаратному ID смартфона (ANDROID_ID).
Статистика использования приложений уходит на сервер RuStore
Помимо списка установленных пакетов, RuStore снимает поведение использования и выгружает его на свой сервер. Сбор делает ru.vk.store.lib.usagestats.UsageStatsProviderImpl (yl2/*).
Активность по каждому пакету (yl2/d.java):
((UsageStatsManager) getSystemService("usagestats")).queryUsageStats(0, from, to);// на каждый UsageStats -> map[packageName] = { время на переднем плане, частота, последнее использование }
Плюс событийная статистика (yl2/b.java, queryEventStats), конфигурации (yl2/a.java, queryConfigurations) и суммарный трафик устройства по типам сети (yl2/c.java):
// yl2/c.javanetworkStatsManager.querySummaryForDevice(networkType, null, from, to);
Собранное превращается в аналитические события. ff2/m.java (UsageStatsCollectorDelegate) на каждую запись зовёт сендер аналитики:
// ff2/m.javaanalyticsSender.a("globalActivity.eventStats", map);// поля map дословно:// eventType, first_time_stamp, last_time_stamp, count, total_time, last_event_time
Сендер ng2.b (центральный og2/f.java) раздаёт события в подключённые системы аналитики, одна из них AltCraft (tg2/b.java). Конечная точка (pg2/e.java):
// pg2/e.java"https://api.rustore.ru/user-event-handler/"// API rg2/a.java: @o("v1/uploadEvents"), @o("v2/uploadEvents")
Выгрузку делает AltCraftFlushEventsWorker батчем раз в 12 часов, по умолчанию подсистема включена (тоггл altcraftAnalytics = true, ru/vk/store/lib/featuretoggle/a.java). Каждое событие штампуется идентификаторами из состояния аналитики ng2/c.java (AltCraftEventDto): sessionId, userId, vkId, deviceId (fingerprint). Вариант v2/uploadEvents это UploadEventsDtoSigned, подписанный нативным ключом из libbridge.so (модуль user-events-secret-storage), то есть поток подписан и устойчив к подделке.
Запускает сбор суточный UsageStatsCollectorWorker (ru.vk.store.feature.usagestats.impl.presentation, CoroutineWorker). Доступность гейтится серверными тогглами (FlipperRepository, ef2/b.java) и выданным доступом к данным об использовании. Есть и экран запроса доступа UsageStatsDialogDestination; один из потребителей это Game Center (...gamecenter.stats.impl).
PACKAGE_USAGE_STATS это special-access, queryUsageStats вернёт данные только при выданном доступе «Доступ к данным об использовании»;
Вход в аккаунт для выгрузки не нужен. В отправителе AltCraft (tg2/b.java) метод a(String userId) пустой, отправка гейтится серверным тогглом и доступом к данным об использовании, сессия не требуется. Событие всегда несёт deviceId; userId/vkId остаются пустыми, пока вы не вошли. То есть при включённом тоггле и выданном доступе агрегаты уходят на api.rustore.ru с привязкой к устойчивому deviceId даже у неавторизованного пользователя.
Справка: Данные о том, как часто и как долго вы пользуетесь приложениями (Usage Stats) — это сверхчувствительная информация. Тайминги запуска ваших мессенджеров, VPN и банков сливаются в систему аналитики и намертво привязываются к несбрасываемому аппаратному ID вашего смартфона. Разработчики могли бы сказать: «Ну это же просто для безобидной фичи меню ‘Время в играх’!». Только вот RuStore мониторит экранное время вообще всех ваших приложений, а не только игр, и выгружает всё это на свои серверы, вместо того чтобы считать локально на устройстве. Тогда вы могли бы возразить: «Наверное, это нужно для облачной синхронизации между моими устройствами?». И снова мимо: этот агрессивный сбор и отправка статистики работают на полную катушку, даже если вы вообще не вошли в аккаунт. Никакой синхронизацией тут и не пахнет. Мы имеем наглую, ничем не оправданную слежку за вашей активностью.
RuStore раздаёт VK-токены другим приложениям через AIDL
ud0/b.java реализует AIDL-интерфейс com.vk.silentauth.ISilentAuthInfoProvider:
List<PackageInfo> installed = pm.getInstalledPackages(64); // GET_SIGNATURES// анти-спуфинг: UID вызывающего должен владеть заявленным пакетом,// а переданный хэш подписи совпасть с реальной подписью пакета:// "Invalid apk hash. Don't try to trick me ;)"// "We can't recognize you"e("auth.getCredentialsForServiceMulti", ...);long id = Binder.clearCallingIdentity();try { return F(...); } finally { Binder.restoreCallingIdentity(id); }
Стороннее приложение, прошедшее проверку подписи, получает VK Silent Auth токен без диалога пользователю (Binder.clearCallingIdentity). На клиенте нет фиксированного списка разрешённых VK-приложений: проверки лишь доказывают, что вызывающий действительно тот пакет, за который себя выдаёт. Кому выдать токен, решает сервер VK (getCredentialsForServiceMulti). Для работы механизма RuStore при запросе перечисляет подписи установленных пакетов.
Справка: Механизм Silent Auth в RuStore устроен так, что ваше согласие на передачу данных просто выброшено за ненадобностью. Диалоговое окно «Разрешить приложению доступ к аккаунту» ? Неа, достаточно один раз залогиниться в RuStore, и любое партнерское приложение на вашем смартфоне может запросить ваш VK ID. Кто решает, кому можно отдать ваши данные? Не вы. Это решает сервер VK, просто занося нужный софт в свой «белый список». В результате сторонние приложения мгновенно и абсолютно незаметно связывают ваши действия с вашим реальным профилем. По сути, системный магазин использует свои привилегии, чтобы втихаря раздавать ключи от вашего аккаунта нужным партнерам. Абсурдно, что вообще приходится объяснять, почему скрытая торговля чужими сессиями без ведома пользователя — это дикость и полное уничтожение приватности.
Удалённые тогглы: 73 серверных переключателя
MAX-установка и VKC_BACKUP_SENDING входят в общий реестр серверных флагов VK ID SDK. Полный список в yn0/b.java (enum EnumC2199b, 73 значения). У каждого тоггла строковый ключ и метод getKey(), поддерживаемые ключи отдаёт getSupportedFeatures(), значения тянутся с сервера.
Самые прикольные ключи:
sak_max_messenger_install // скрытая установка MAXvkc_backup_sending // выгрузка всех сохраненных токенов авторизации с устройстваsak_messenger_skip_sms_android // принудительный обход SMS-верификацииsak_pass_autocompete_android // разрешение на запись логинов и паролей в системуvkc_hitman_captcha_android // перенаправление обработки капчи на альт. резолверsak_vk_ru_v2_android // скрытое переключение базового домена API бэкенда
Вокруг одного только MAX три отдельных серверных флага, Полный список всех флагов есть в этом гист
Справка: Технически, даже по суровым правилам Google Play наличие удаленных флагов — это не нарушение. На этом работает половина мобильной индустрии, тот же Firebase. Это абсолютно стандартный инструмент для плавных релизов и A/B тестов. Но дьявол, как всегда, в наглости реализации. Одно дело — удаленно включать новую кнопку в интерфейсе, и совсем другое — закладывать рубильники, которые позволяют в любой момент выкачать с устройства чужие сессии авторизации (vkc_backup_sending), молча обойти SMS-верификацию (sak_messenger_skip_sms_android) или прокинуть логины с паролями прямо в систему (sak_pass_autocompete_android).
Kaspersky kavsdk
Внутри RuStore живёт полноценный Kaspersky SDK: нативный движок, KSN-телеметрия, P2P-сеть репутации, классификатор приложений. Сам kavsdk как и сам Kaspersky заслуживает отдельной статьи.
19 потоков телеметрии KSN, включая P2P
com/kavsdk/internal/cloudrequests/CloudStatisticType.java содержит ровно 19 значений:
APCLOUD, FIRMWARE, OAS, P2P, RAW, WAV, WIFI, WLIP, WLIPS, OVERLAP,WHOCALLS, KSNQ_2, FEATURE_USAGE, LIN, CALL_REPORT, CALL_FILTER,ODS, ERROR_STATISTICS, HTTP_TRANSPORT_QUALITY
P2P означает, что устройство может не только получать данные репутации из KSN, но и отдавать их в сеть. Логика в нативном sendAll():
Это затрудняет блокировку трафика: данные могут идти мимо центрального сервера.
В KsnStatisticType есть три потока детской активности:
CHILD_ACTIVITY_SEARCH_REQUEST,CHILD_ACTIVITY_WEB_BROWSER_REQUEST,CHILD_ACTIVITY_APPLICATION_USAGE
MD5 + SHA256 каждого APK с installerPackageName
Wlip.java это нативная отправка профиля одного APK:
private static native boolean send( String apkFileName, String parentPath, String packageName, String installerPackageName, // откуда установлено String packageStringForStatistics, int versionCode, byte[] hash1, byte[] hash2, long nativePtr, int wlipVerdict, int wlipTrustScenario, boolean isSafetyNetEnabled, int googleSafetyNetCategory, byte[] apkSha256);
installerPackageName означает, что KSN знает, через какой магазин или источник установлено каждое приложение.
Wlips.java (пакетная отправка всех APK) гарантирует доставку на уровне GC:
public final void finalize() { if (this.f47 != 0) { release(this.f47); throw new IllegalStateException("Statistics has not been sent"); } super.finalize();}
По каждому APK WlipsAppInfo собирает:
public class WlipsAppInfo implements Externalizable { private final byte[] mApkMd5; private final byte[] mDexMd5; private long mApkPermissions; // permissions битовой маской private boolean mAppDefaultSmsManager; private boolean mAppDeviceAdmin; private boolean mAppUsesAccessibilityServices; private String mBundleName; private int mFlags;}
Build.FINGERPRINT + факт рутования
kavsdk/o/io.java заполняет FirmwareStatistic: mIsRooted и mFingerprint = Build.FINGERPRINT, отправка под флагом CloudStatisticType.FIRMWARE.
inotify по пользовательским директориям
MultiObserverThread (нативный inotify: init/observe/startWatching) наблюдает за DIRECTORY_PICTURES/MOVIES/DOWNLOADS/DCIM. Фактическая отправка гейтится серверными флагами.
Нативный обход ограничений рефлексии Android 9+
SdkUtils:
public static native Method getDeclaredMethod(Class cls, String str, Class<?>... clsArr);public static native boolean setenv(String name, String value);public static native int killParasiteProcesses();
С Android 9 доступ к скрытым @hide-API через обычную рефлексию ограничен (greylist/blacklist). Нативная реализация getDeclaredMethod обходит это напрямую через JNI, минуя Java-слой. setenv правит переменные окружения процесса на нативном уровне, killParasiteProcesses завершает посторонние процессы. С точки зрения Google Play Developer Program Policy обход hidden-API запрещён.
Классификация приложений по 31 категории
public enum KlAppCategory { BusinessSoftware, EducationalSoftware, Entertainment_SocialNetworks, Entertainment_OnlineShopping, Information_Medical, Information_MappingApplications, RemoteAccessTool, SecuritySoftware, Browsers, /* ... 31 всего */}public class KlApplicationInfo { public final String mAgeCategory; public final KlAppCategory mKLCategory;}
Каждое установленное приложение классифицируется по категории и возрастному рейтингу, информация агрегируется в KSN. Information_Medical или RemoteAccessTool в профиле дают дополнительный аналитический сигнал.
Справка: Будем технически объективны, перед нами стандартные паттерны работы антивирусного комбайна, написанного не Android-, а Windows-разработчиками. Главная претензия здесь не к самому Касперскому, а к уровню инженерии RuStore. Имея гигантские бюджеты на разработку национального магазина, создатели не стали писать или адаптировать легковесный сканер (на манер Google Play Protect), а тупо вшили сторонний антивирусный SDK «как есть», со всем его легаси-багажом. В результате получился архитектурный Франкенштейн, нарушающий все мыслимые правила:
-
Обход системных ограничений (Malicious Behavior): Вместо цивилизованной адаптации под современные реалии Android, интегрированный движок ломает систему через колено. Нативная реализация
getDeclaredMethodчерез JNI для доступа к запрещенным скрытым API (greylist) и жесткое убийство чужих процессов (killParasiteProcesses) — это архаичные и грязные хаки. -
Слежка за галереей (Scoped Storage Policy): Самый абсурдный пункт. Из-за того, что SDK вставили не глядя, системный
inotifyведет постоянный фоновый мониторинг директорий с вашими личными фотографиями (DCIM,Pictures). Зачем каталогу приложений следить за появлением новых фото в галерее пользователя? -
Избыточное профилирование (User Data / Sensitive Information): Сбор хэшей (MD5/SHA256) и источника установки (
installerPackageName) можно оправдать нуждами сети репутации KSN. Но неадаптированный движок тянет за собой и функционал тотальной слежки, классифицируя абсолютно все ваши приложения по 31 категории (включая чувствительныеInformation_MedicalилиRemoteAccessTool). Секьюрити-тулза по инерции собирает исчерпывающий конфиденциальный профиль пользователя. -
P2P-сеть и теневой трафик: Использование P2P-модулей — древняя практика антивирусов для экономии на собственных серверах или обхода анти-антивирусов. Только вот встраивать транзитный P2P-узел в системный магазин приложений в 2026 году — это верх непрофессионализма.
У меня есть только один вопрос: как этот антивирус не блокирует сам себя? Хотя это же калspersky, что с них взять, у них же лапки.
Аппаратные идентификаторы устройства
Помимо ANDROID_ID, в составе RuStore есть код, который читает несбрасываемые аппаратные идентификаторы.
Kaspersky dualsim (com/kaspersky/components/dualsim/SimAccessorImpl.java), класс помечен @SuppressLint({"HardwareIds"}):
m302.getSubscriberId(); // IMSIm29.getIccId(); // серийный номер SIMSimUtils.getImei(i14) / getDeviceId(); // IMEI
SimUtils.java при этом лезет рефлексией в скрытые dual-SIM методы (getDeviceIdGemini, getSubscriberIdGemini и т.д.) и во внутренний класс прошивки com.android.internal.telephony.RILConstants$SimCardID.
Mail.ru libverify (ru/mail/libverify/a0/c.java):
a(tm, "getSimSerialNumber", b()); // серийный номер SIMa(tm, "getDeviceId", b()); // IMEIa(tm, "getSubscriberId", b()); // IMSI
Он же читает GSF-идентификатор Google (ru/mail/libverify/platform/firebase/b/a.java):
getContentResolver().query( Uri.parse("content://com.google.android.gsf.gservices"), null, null, new String[]{"android_id"}, null);
Вызовы обернуты в catch (SecurityException), и на Android 10+ для обычного приложения вернут null. Но код то присутствует.
Справка: Начиная с Android 10, сама операционная система жестко блокирует доступ к неизменяемым аппаратным идентификаторам (IMEI, IMSI, ICCID). Логика защиты банальна: никто не должен иметь возможность следить за вами по серийным номерам «железа», даже Google этичнее VK. Но RuStore плевать хотел на эти ограничения ОС. Используя свои системные привилегии и грязную рефлексию скрытых методов, они банально обходят блокировки Android и вытаскивают эти номера из модема. А чтобы мало не показалось, стор заодно тащит из системы ваш Google GSF ID. В итоге формируется пожизненный аппаратный слепок пользователя. Вы можете выходить из аккаунтов, менять симки и даже делать полный сброс устройства (Factory Reset) — это уже не поможет. Ваша железка помечена навсегда, и отвязать свой реальный профиль от этого телефона вы больше не сможете.
libsecrets.so: Секреты на рабочем столе Дениса
Разработчики RuStore (а точнее, модуля VK Push SDK, а еще точнее Денис.
/Users/d.pismennyy/Desktop/rustore-push-sdk-secrets/app/src/main/cpp/prod/secretsprod.cpp # приет некий Денис
решили защитить токены авторизации пуш-уведомлений, спрятав их в нативную C++ библиотеку libsecrets.so. JNI-экспорты выглядят многообещающе:
Java_com_vk_push_authsdk_Secrets_getvkv2Java_com_vk_push_authsdk_Secrets_getrustorev2Java_com_vk_push_authsdk_Secrets_getdefaulv2Java_com_vk_push_authsdk_Secrets_getokv2Java_com_vk_push_authsdk_Secrets_getzenv2Java_com_vk_push_authsdk_Secrets_getmailv2
Секреты обфусцированы и зашиты в бинарник. Однако «нативное хранение» лишь слегка замедляет процесс, но никак не спасает от реверса.
Анализ Secrets.java показывает, что во все эти методы всегда передается одна и та же статичная строка: com.vk.push.authsdk. Алгоритм деобфускации, восстановленный из ассемблера, до смешного прост: secret = customDecode( obf[i] XOR sha256_hex("com.vk.push.authsdk")[i] ).
То есть ключ для расшифровки — это просто SHA256-хэш от хардкодной константы. Вместо того чтобы мучиться с декомпиляцией их «уникальной» функции customDecode, достаточно написать 50 строк на C: подгрузить libsecrets.so через dlopen, подсунуть фейковую JNI-таблицу и попросить библиотеку расшифровать самой себя. Библиотека послушно выдает всё «на блюдечке».
Вот что она отдает в рантайме (действующие токены частично замазал звездочками, понятно почему):
-
vk :
qUuUUvx2vUsSu43nUAV******** -
rustore :
sxxuX4A4Xwtq34XwwpZ******** -
mail :
yzU4YZUTvWSYxqvxrzr******** -
ok :
u2k4n4xV1pvzzzWVyU4******** -
zen :
zqy44W02tnu23vwUU3Y******** -
default :
WVynx04tWyqZUzsxnVT********
(Я так и не выяснил, валидны ли эти ключи на сервере и что именно они позволяют получить. Но код подтверждает, что они относятся к Push Auth SDK и используются как HMAC-ключи для подписи bootstrap-JWT. Сервер при этом проверяет package name и сертификат приложения. Прятать такие ключи в C++ с подобным усердием всё равно забавно: всё, что поставляется каждому клиенту, рано или поздно можно извлечь. )
Итого
RuStore это магазин с глубокой телеметрией экосистемы VK и набором тяжёлых сторонних SDK. Подтверждается:
-
сбор списка лаунчабл-приложений с привязкой к
ANDROID_ID; -
собственная подсистема
Radar: локальная базаsnapshot_recordsсuserId, тремя геофиксами, MAC роутера, списком сотовых вышек, оператором SIM, роумингом и флагом VPN; при включении снимок раз в 2 минуты, хранение 15 дней, выгрузка наPOSTreef.vk-cdn.net/ru-vk-store/stat/v1/ev; -
фоновое авто-обновление любого приложения на устройстве (не только из RuStore) через привилегированный установщик, по умолчанию включено;
-
сбор статистики использования приложений (какими приложениями и когда вы пользуетесь) через
PACKAGE_USAGE_STATSи выгрузка наapi.rustore.ru(AltCraft) с привязкой кuserId + vkId + deviceId; -
AIDL-раздача VK-токенов без диалога;
-
серверно-управляемая установка MAX через привилегированный установщик;
-
серверный канал «push -> тихая фоновая установка пакета по имени» в фиче
preorder/autoinstall; -
реестр из 73 удалённых тогглов, которыми поведение клиента переключается с сервера;
-
чтение аппаратных идентификаторов (IMEI, IMSI, ICCID) силами kavsdk и libverify;
-
полноценный Kaspersky-движок с KSN-телеметрией, P2P-каналом и классификацией приложений.
Спасибо всем кто читал, попытался быть обьективным, ну и ии слоп к коду не подпускал.
Подписывайтесь на t.me/openlibrecommunity — там обновления и новые статьи, а еще я ищу работу.
Поддержать нас донатом
Поддержать нас донатом
РУБЛИ. СБП, КАРТА: pay.cloudtips.ru/p/28c476e5
КРИПТА. TON, USDT: UQD_Qc2cxLGe1P4wANi46cKdEvvzyJRrJTYPvGX2KAZDnsDh
КРИПТА, TRC 20, USDT: TYQqdACH5PrScvsMowSyS8JjaaF5wvFf5Q
КРИПТА, BTC: bc1qvw0ts0jk5e5dfj9fdez76j9ck95lqz04fpf02a
UPD: Ха как вам идея реверсить по одному русскому приложению в месяц пока они не позовут меня к себе работать…
UPD: Ну, теперь пока мне не кинет офер какой нить бигтех я не перестану писать статьи
ссылка на оригинал статьи https://habr.com/ru/articles/1046710/