Клонирую репозиторий и начинаю анализ.
Репозиторий склонирован и разобран. Сразу важная поправка по постановке задачи, потому что она меняет выводы.
Что это на самом деле
Это не «червь, маскирующийся под мессенджер». Это три apktool-дампа (smali + res + lib) реального российского мессенджера MAX (ru.oneme.app, версии 26.24.0 и 26.28.0), в которые сторонний автор внёс патчи и назвал это «WhiteMAX Mod». Кодовая база — наследник TamTam/OK (ru.ok.tamtam, ru.ok.android.onelog).
Патчей мода в 26.28.0 всего 19 мест в 19 файлах; всё остальное (≈26 000 smali-файлов) — оригинальный код MAX. Так что интересное лежит не в моде, а в самом MAX. Ниже — то, что нашлось.
1. Детект VPN — четыре независимых механизма
(а) Локальный, через ConnectivityManager. smali/de4.smali — это one.me.net.connection.impl.internal.ConnectionInfoNougatImpl (имя видно по вложенному классу на строке 548). В строке ~1364 он делает NetworkCapabilities.hasTransport(4) (TRANSPORT_VPN) и кладёт результат пятым полем в Lae4; (строки 1364-1366), а в лог пишет "(VPN detected)".
(б) Серверный флаг isVpn. smali/smf.smali:117 — парсер ответа сервера с полями isVpn, location, reg-country-code, recovery-url, callsSeed. Т.е. бэкенд по IP сам определяет VPN/прокси и присылает вердикт клиенту. Далее smali/qmf.smali:275,299 кладёт его в gbj.c.
(в) Гейт блокировки. smali_classes2/gbj.smali объединяет оба сигнала, режим задаётся серверным конфигом (PmsProperty):
show-vpn-chat-bottomsheet (индекс 0x144) — для чатов/контактов;
show-vpn-call-bottomsheet (0x145) — для звонков;
- значение
1 = верить только локальному детекту, 2 = только серверному isVpn, 3 = требовать оба.
Если гейт сработал — вместо действия показывается one/me/vpnconnectedwarning/VpnConnectedWarningBottomSheet с текстом «Отключите VPN» / «Чтобы пользоваться MAX» (res/values/strings.xml:1943-1944), а звонок просто не стартует (smali_classes4/vb3.smali:59,102, wb3.smali, smali_classes2/ol1.smali:76). Есть ещё панель в звонке one/me/calls/ui/ui/call/panels/VpnPanelWidget и снекбар «Лучше без VPN» под флагом show-vpn-snackbar. В мини-аппах — WebAppHttpClient$WebAppHasVpnException → код ошибки has_vpn.
(г) Скрытый детект по сетевым интерфейсам — см. следующий пункт. Это самое серьёзное.
2. Скрытый модуль «DPS» (trace-flow.ru) — главная находка
В APK есть пакет ru.trace_flow.dps — отдельный SDK v1.2.2 со строковой обфускацией: все строки лежат XOR-зашифрованными hex-блобами и расшифровываются в рантайме через Lwk8;->b(String) (первые 8 hex-символов — 32-битный ключ, дальше побайтовый XOR). Все Android-API он дёргает через рефлексию, чтобы не светиться в статическом анализе.
Расшифровав строки, получаем:
smali/zek.smali — детектор VPN по именам сетевых интерфейсов: перебирает NetworkInterface.getNetworkInterfaces() и ищет подстроки tun (стр. 176), ppp, tap, ipsec (стр. 212). Плюс рефлексивный hasTransport(4).
smali/t4i.smali — определение реального публичного IP пользователя через внешние echo-сервисы:
https://api.ipify.org https://ip.mail.ru/
https://checkip.amazonaws.com https://ifconfig.me/ip
https://ipv4-internet.yandex.net/api/v0/ip
https://ipv6-internet.yandex.net/api/v0/ip
плюс host для проверки доступности api.oneme.ru и базовый URL бэкенда https://trace-flow.ru (стр. 31).
smali/t3k.smali — формат отчёта. POST https://<host>/api/v1/report?ver=, Content-Encoding: gzip, заголовок Authorization, тело:
{"snapshots":[{"id":…,"clientTs":…,"appVersion":…,"ip":…,
"connectionType":…,"operator":…,"vpn":…,"deviceId":…,"uid":…,
"hosts":[…],"status":…}]}
Серверный конфиг модуля: reportHosts, selfIpDetectionDomains, reachabilityHosts, sampleRate, maxSnapshots, snapshotLifetimeMs, dontReportUntil, timeoutMs. Кэшируется в dps_config.bin, dps_dont_report.bin, dps_log_entries.bin.
Кто запускает. ru.trace_flow.dps.internal.DpsInitProvider в манифесте отключён (android:enabled="false") — но это отвлекающий манёвр. Реально DPS поднимается напрямую из smali/one/me/android/initialization/AccountInitializer.smali:89-236:
- гейт — серверный boolean-флаг с ключом
dps (e5d.h3, индекс 0xd9; соседний ключ — host-reachability);
- API-ключ зашит:
"ply5hDvhupghrHVA5rqQD1ypiXAxbmE4A68ZzBa8ioc=" (стр. 136);
- в модуль пробрасываются коллбэки:
smali/g6.smali → getUserId() возвращает ID аккаунта MAX, smali/h6.smali → deviceId, smali/i6.smali → версия;
- отдельный пул потоков с именем
"dps";
- триггер —
Application.registerActivityLifecycleCallbacks (jt5.smali:288), т.е. отчёты уходят по факту пользования приложением, с сэмплированием по sampleRate.
Что это значит по сути: приложение умеет по команде с сервера связать в одной записи ID аккаунта MAX ↔ реальный публичный IP (полученный в обход VPN-туннеля, через сторонние echo-сервисы) ↔ факт наличия VPN ↔ оператора ↔ идентификатор устройства и отправить это на сторонний хост trace-flow.ru. Ни в UI, ни в строках ресурсов упоминаний об этом нет.
3. Отправка метрик
Собственный канал MAX. smali/ae9.smali — LogController: методы g() → события типа ACTION, h() → CLICK, j(type,name,params,critical). Каждое событие автообогащается operator (оператор SIM) и connection_type (стр. 893, 924). Батчами, с ретраями, уходит по WebSocket-опкоду 5 = LOG (smali/kfc.smali:416).
Отдельно smali_classes2/t20.smali:2700-2802 шлёт событие HOST_REACHABILITY / GET_HOST_REACHABILITY с полями hosts, operator, connection_type и vpn: 1, если локальный детект сработал.
smali/one/me/android/DailyAnalyticsWorker.smali — ежедневный фоновый воркер, шлёт событие PERMISSION / permission_status со статусом разрешений: push, contacts, fsi, gallery, camera, microphone, geo.
Серверные флаги логирования в e5d.smali: log-full, log-sensitive, log-messages-meta, log-chat-meta, log-violations, presence-stat, opcode-stat-config, net-stat-config, error-stat-limit. То есть глубину логирования (включая метаданные сообщений и чатов) сервер регулирует удалённо.
Сторонние трекеры в APK: MyTracker (tracker-api.vk-analytics.ru), ru.ok.tracer (sdk-api.apptracer.ru) — крэши, heap-дампы, minidump, native-bridge, sampling-профайлер; OneLog; VK Push Omicron (e.mail.ru) с DeviceFingerprint (device_id, разрешение экрана, язык, таймзона, модель, sim_mcc, sim_mnc); RuStore Push (vkpns.rustore.ru); Firebase.
4. Сбор данных
- Телефонная книга целиком.
smali_classes3/duc.smali:600-810 — «open phonebook flow», читает ContactsContract.Data: display_name, data1, data2/3/5, photo_uri, is_primary. smali/n30.smali вешает ContentObserver на контакты и пересинхронизирует при изменении. Для мессенджера это ожидаемо, но объём полный.
- Идентификатор устройства —
smali/ek5.smali:175 берёт Settings.Secure.ANDROID_ID («Generating new device id» / «Can't get hardware device id»), сохраняет и шлёт как deviceId. У DPS свой — SHA-256 от ANDROID_ID (smali_classes2/t95.smali).
- Хендшейк сессии (
smali_classes3/ap6.smali:314-380): locale, deviceLocale, appVersion, arch, screen, deviceName, deviceType, osVersion, timezone, pushDeviceType.
- Экспортированный без permission провайдер
com.vk.push.core.deviceid.contentprovider.VkpnsDeviceIdContentProvider (android:exported="true", <action com.vk.push.GET_DEVICE_ID/>) — сквозной идентификатор устройства, доступный любому приложению на телефоне.
- Root-детект — есть, но внутри MyTracker (
smali/com/my/tracker/core/o/m$a.smali): пути su, /sbin/.magisk/, magisk.db, test-keys, Runtime.exec("which su"). В моде точки входа MyTracker заглушены.
- Буфер обмена читается только на вставке в поле ввода, геолокация — только фичи «поделиться локацией» и AppCompat TwilightManager.
SEND_SMS нет, READ_SMS нет, динамической загрузки dex-кода нет.
- Опкоды протокола (полный список в
smali/kfc.smali) включают TRANSCRIBE_MEDIA (голосовые уходят на серверную расшифровку), CHAT_CHECK_ESIA (ЕСИА/Госуслуги), CONTACT_INFO_BY_PHONE, SESSIONS_INFO, PHONE_WEBAPP_SHARE.
5. NDK
Все 16 .so — стандартные библиотеки, ничего подозрительного:
| Библиотека |
Что это |
libmax.so |
rlottie (8 JNI-символов Java_one_me_rlottie_RLottieDrawable_*) |
libjingle_peerconnection_so.so |
WebRTC |
libEnhancementLibShared.so |
TFLite-обработка изображения/видео |
libffmpg, libjlottie, libqrcode, libzstd, libgleff |
медиа/кодеки/QR |
| Facebook Fresco + AndroidX Camera |
изображения |
Единственный «системный» вызов в нативном коде — __system_property_get (штатный android_get_device_api_level). Ни ptrace, ни anti-debug, ни сетевого кода в .so нет — вся чувствительная логика на Java/Kotlin.
6. Что мод реально сделал (и чего не сделал)
Полный список патчей в 26.28.0-1.5.2 (заглушки видно по нехарактерному форматированию smali — без пустых строк между инструкциями):
Вырезано: MyTracker (8 методов → return-void), OneLogImpl (append/flush/upload×2), весь ru.ok.tracer (TracerInitializer, CrashReportInitializer, HeapDumpInitializer, DiskUsageInitializer, Minidump, NativeBridgeInitializer/Installer, PerformanceMetricsInitializer, LoggerInitializer, TracerLite, TracerCrashReportLite.report, SamplingProfilerInitializer).
Подменено: de4.c() (smali/de4.smali:449) → return false — глушит локальный VPN-детект; smali/fu.smali:383 → URL сайта заменён на https://whitemaxmod.com.
Добавлено: one/me/mods/Mods.smali + два листенера — локальные тумблеры «Нечиталка» (noread) и «Офлайн» (offline) в SharedPreferences, хуки в hih.smali:44 и k13.smali:253. Это единственные три файла с не-SourceFile в .source, т.е. единственный дописанный руками код. Сетевой активности в нём нет.
Чего мод НЕ сделал:
- DPS/trace-flow.ru остался полностью активным —
AccountInitializer не тронут;
- свой лог-канал MAX (опкод 5
LOG) не блокируется. Причём в старой версии 26.24.0-1.5.1 блокировка была — ручная вставка в smali/e1b.smali:1613 с меткой :op5_pass, дропавшая исходящие пакеты с opcode 5. В 1.5.1.1 её убрали, в 26.28.0-1.5.2 её нет;
- обход детекта VPN неполный: при значении конфига
2 (доверять только серверному isVpn) блокировка звонков и чатов сработает и в моде, потому что заглушен только локальный путь;
- заявление README «Многие запросы переадресованы на 0.0.0.0» в текущей сборке не подтверждается — ни одной подмены хоста на
0.0.0.0 в 26.28.0 нет, эндпоинты api2.oneme.ru, tracker-api.vk-analytics.ru, sdk-api.apptracer.ru, trace-flow.ru на месте.
Итог
Опасность здесь не в «черве». Опасны две вещи:
-
В самом MAX сидит обфусцированный модуль DPS, который по удалённой команде (флаг dps) определяет реальный IP пользователя в обход VPN через сторонние IP-echo сервисы, детектит VPN по интерфейсам tun/tap/ppp/ipsec, и отправляет связку «ID аккаунта + IP + флаг VPN + оператор + deviceId» на trace-flow.ru. Это не аналитика продуктового качества — это инфраструктура деанонимизации, и она спроектирована так, чтобы её не нашли грепом.
-
Мод даёт ложное чувство безопасности. Он глушит сторонние SDK и локальный VPN-детект, но оставляет нетронутыми и DPS, и собственный телеметрийный канал MAX. При этом это APK от анонимного автора с правами на контакты, камеру, микрофон, геолокацию и REQUEST_INSTALL_PACKAGES (самообновление с whitemaxmod.com), пересобранный и переподписанный чужим ключом — то есть к рискам оригинала добавляется полное доверие к автору мода.
Клонирую репозиторий и начинаю анализ.
Репозиторий склонирован и разобран. Сразу важная поправка по постановке задачи, потому что она меняет выводы.
Что это на самом деле
Это не «червь, маскирующийся под мессенджер». Это три apktool-дампа (smali + res + lib) реального российского мессенджера MAX (
ru.oneme.app, версии 26.24.0 и 26.28.0), в которые сторонний автор внёс патчи и назвал это «WhiteMAX Mod». Кодовая база — наследник TamTam/OK (ru.ok.tamtam,ru.ok.android.onelog).Патчей мода в 26.28.0 всего 19 мест в 19 файлах; всё остальное (≈26 000 smali-файлов) — оригинальный код MAX. Так что интересное лежит не в моде, а в самом MAX. Ниже — то, что нашлось.
1. Детект VPN — четыре независимых механизма
(а) Локальный, через ConnectivityManager.
smali/de4.smali— этоone.me.net.connection.impl.internal.ConnectionInfoNougatImpl(имя видно по вложенному классу на строке 548). В строке ~1364 он делаетNetworkCapabilities.hasTransport(4)(TRANSPORT_VPN) и кладёт результат пятым полем вLae4;(строки 1364-1366), а в лог пишет"(VPN detected)".(б) Серверный флаг
isVpn.smali/smf.smali:117— парсер ответа сервера с полямиisVpn,location,reg-country-code,recovery-url,callsSeed. Т.е. бэкенд по IP сам определяет VPN/прокси и присылает вердикт клиенту. Далееsmali/qmf.smali:275,299кладёт его вgbj.c.(в) Гейт блокировки.
smali_classes2/gbj.smaliобъединяет оба сигнала, режим задаётся серверным конфигом (PmsProperty):show-vpn-chat-bottomsheet(индекс0x144) — для чатов/контактов;show-vpn-call-bottomsheet(0x145) — для звонков;1= верить только локальному детекту,2= только серверномуisVpn,3= требовать оба.Если гейт сработал — вместо действия показывается
one/me/vpnconnectedwarning/VpnConnectedWarningBottomSheetс текстом «Отключите VPN» / «Чтобы пользоваться MAX» (res/values/strings.xml:1943-1944), а звонок просто не стартует (smali_classes4/vb3.smali:59,102,wb3.smali,smali_classes2/ol1.smali:76). Есть ещё панель в звонкеone/me/calls/ui/ui/call/panels/VpnPanelWidgetи снекбар «Лучше без VPN» под флагомshow-vpn-snackbar. В мини-аппах —WebAppHttpClient$WebAppHasVpnException→ код ошибкиhas_vpn.(г) Скрытый детект по сетевым интерфейсам — см. следующий пункт. Это самое серьёзное.
2. Скрытый модуль «DPS» (trace-flow.ru) — главная находка
В APK есть пакет
ru.trace_flow.dps— отдельный SDK v1.2.2 со строковой обфускацией: все строки лежат XOR-зашифрованными hex-блобами и расшифровываются в рантайме черезLwk8;->b(String)(первые 8 hex-символов — 32-битный ключ, дальше побайтовый XOR). Все Android-API он дёргает через рефлексию, чтобы не светиться в статическом анализе.Расшифровав строки, получаем:
smali/zek.smali— детектор VPN по именам сетевых интерфейсов: перебираетNetworkInterface.getNetworkInterfaces()и ищет подстрокиtun(стр. 176),ppp,tap,ipsec(стр. 212). Плюс рефлексивныйhasTransport(4).smali/t4i.smali— определение реального публичного IP пользователя через внешние echo-сервисы:плюс host для проверки доступности
api.oneme.ruи базовый URL бэкендаhttps://trace-flow.ru(стр. 31).smali/t3k.smali— формат отчёта.POST https://<host>/api/v1/report?ver=,Content-Encoding: gzip, заголовокAuthorization, тело:{"snapshots":[{"id":…,"clientTs":…,"appVersion":…,"ip":…, "connectionType":…,"operator":…,"vpn":…,"deviceId":…,"uid":…, "hosts":[…],"status":…}]}Серверный конфиг модуля:
reportHosts,selfIpDetectionDomains,reachabilityHosts,sampleRate,maxSnapshots,snapshotLifetimeMs,dontReportUntil,timeoutMs. Кэшируется вdps_config.bin,dps_dont_report.bin,dps_log_entries.bin.Кто запускает.
ru.trace_flow.dps.internal.DpsInitProviderв манифесте отключён (android:enabled="false") — но это отвлекающий манёвр. Реально DPS поднимается напрямую изsmali/one/me/android/initialization/AccountInitializer.smali:89-236:dps(e5d.h3, индекс0xd9; соседний ключ —host-reachability);"ply5hDvhupghrHVA5rqQD1ypiXAxbmE4A68ZzBa8ioc="(стр. 136);smali/g6.smali→getUserId()возвращает ID аккаунта MAX,smali/h6.smali→ deviceId,smali/i6.smali→ версия;"dps";Application.registerActivityLifecycleCallbacks(jt5.smali:288), т.е. отчёты уходят по факту пользования приложением, с сэмплированием поsampleRate.Что это значит по сути: приложение умеет по команде с сервера связать в одной записи ID аккаунта MAX ↔ реальный публичный IP (полученный в обход VPN-туннеля, через сторонние echo-сервисы) ↔ факт наличия VPN ↔ оператора ↔ идентификатор устройства и отправить это на сторонний хост
trace-flow.ru. Ни в UI, ни в строках ресурсов упоминаний об этом нет.3. Отправка метрик
Собственный канал MAX.
smali/ae9.smali—LogController: методыg()→ события типаACTION,h()→CLICK,j(type,name,params,critical). Каждое событие автообогащаетсяoperator(оператор SIM) иconnection_type(стр. 893, 924). Батчами, с ретраями, уходит по WebSocket-опкоду 5 =LOG(smali/kfc.smali:416).Отдельно
smali_classes2/t20.smali:2700-2802шлёт событиеHOST_REACHABILITY / GET_HOST_REACHABILITYс полямиhosts,operator,connection_typeиvpn: 1, если локальный детект сработал.smali/one/me/android/DailyAnalyticsWorker.smali— ежедневный фоновый воркер, шлёт событиеPERMISSION / permission_statusсо статусом разрешений: push, contacts, fsi, gallery, camera, microphone, geo.Серверные флаги логирования в
e5d.smali:log-full,log-sensitive,log-messages-meta,log-chat-meta,log-violations,presence-stat,opcode-stat-config,net-stat-config,error-stat-limit. То есть глубину логирования (включая метаданные сообщений и чатов) сервер регулирует удалённо.Сторонние трекеры в APK: MyTracker (
tracker-api.vk-analytics.ru), ru.ok.tracer (sdk-api.apptracer.ru) — крэши, heap-дампы, minidump, native-bridge, sampling-профайлер; OneLog; VK Push Omicron (e.mail.ru) сDeviceFingerprint(device_id, разрешение экрана, язык, таймзона, модель,sim_mcc,sim_mnc); RuStore Push (vkpns.rustore.ru); Firebase.4. Сбор данных
smali_classes3/duc.smali:600-810— «open phonebook flow», читаетContactsContract.Data:display_name,data1,data2/3/5,photo_uri,is_primary.smali/n30.smaliвешаетContentObserverна контакты и пересинхронизирует при изменении. Для мессенджера это ожидаемо, но объём полный.smali/ek5.smali:175берётSettings.Secure.ANDROID_ID(«Generating new device id» / «Can't get hardware device id»), сохраняет и шлёт какdeviceId. У DPS свой — SHA-256 от ANDROID_ID (smali_classes2/t95.smali).smali_classes3/ap6.smali:314-380):locale,deviceLocale,appVersion,arch,screen,deviceName,deviceType,osVersion,timezone,pushDeviceType.com.vk.push.core.deviceid.contentprovider.VkpnsDeviceIdContentProvider(android:exported="true",<action com.vk.push.GET_DEVICE_ID/>) — сквозной идентификатор устройства, доступный любому приложению на телефоне.smali/com/my/tracker/core/o/m$a.smali): путиsu,/sbin/.magisk/,magisk.db,test-keys,Runtime.exec("which su"). В моде точки входа MyTracker заглушены.SEND_SMSнет,READ_SMSнет, динамической загрузки dex-кода нет.smali/kfc.smali) включаютTRANSCRIBE_MEDIA(голосовые уходят на серверную расшифровку),CHAT_CHECK_ESIA(ЕСИА/Госуслуги),CONTACT_INFO_BY_PHONE,SESSIONS_INFO,PHONE_WEBAPP_SHARE.5. NDK
Все 16
.so— стандартные библиотеки, ничего подозрительного:libmax.soJava_one_me_rlottie_RLottieDrawable_*)libjingle_peerconnection_so.solibEnhancementLibShared.solibffmpg,libjlottie,libqrcode,libzstd,libgleffЕдинственный «системный» вызов в нативном коде —
__system_property_get(штатныйandroid_get_device_api_level). Ниptrace, ни anti-debug, ни сетевого кода в.soнет — вся чувствительная логика на Java/Kotlin.6. Что мод реально сделал (и чего не сделал)
Полный список патчей в 26.28.0-1.5.2 (заглушки видно по нехарактерному форматированию smali — без пустых строк между инструкциями):
Вырезано:
MyTracker(8 методов →return-void),OneLogImpl(append/flush/upload×2), весьru.ok.tracer(TracerInitializer,CrashReportInitializer,HeapDumpInitializer,DiskUsageInitializer,Minidump,NativeBridgeInitializer/Installer,PerformanceMetricsInitializer,LoggerInitializer,TracerLite,TracerCrashReportLite.report,SamplingProfilerInitializer).Подменено:
de4.c()(smali/de4.smali:449) →return false— глушит локальный VPN-детект;smali/fu.smali:383→ URL сайта заменён наhttps://whitemaxmod.com.Добавлено:
one/me/mods/Mods.smali+ два листенера — локальные тумблеры «Нечиталка» (noread) и «Офлайн» (offline) в SharedPreferences, хуки вhih.smali:44иk13.smali:253. Это единственные три файла с не-SourceFileв.source, т.е. единственный дописанный руками код. Сетевой активности в нём нет.Чего мод НЕ сделал:
AccountInitializerне тронут;LOG) не блокируется. Причём в старой версии 26.24.0-1.5.1 блокировка была — ручная вставка вsmali/e1b.smali:1613с меткой:op5_pass, дропавшая исходящие пакеты с opcode 5. В 1.5.1.1 её убрали, в 26.28.0-1.5.2 её нет;2(доверять только серверномуisVpn) блокировка звонков и чатов сработает и в моде, потому что заглушен только локальный путь;0.0.0.0в 26.28.0 нет, эндпоинтыapi2.oneme.ru,tracker-api.vk-analytics.ru,sdk-api.apptracer.ru,trace-flow.ruна месте.Итог
Опасность здесь не в «черве». Опасны две вещи:
В самом MAX сидит обфусцированный модуль DPS, который по удалённой команде (флаг
dps) определяет реальный IP пользователя в обход VPN через сторонние IP-echo сервисы, детектит VPN по интерфейсамtun/tap/ppp/ipsec, и отправляет связку «ID аккаунта + IP + флаг VPN + оператор + deviceId» наtrace-flow.ru. Это не аналитика продуктового качества — это инфраструктура деанонимизации, и она спроектирована так, чтобы её не нашли грепом.Мод даёт ложное чувство безопасности. Он глушит сторонние SDK и локальный VPN-детект, но оставляет нетронутыми и DPS, и собственный телеметрийный канал MAX. При этом это APK от анонимного автора с правами на контакты, камеру, микрофон, геолокацию и
REQUEST_INSTALL_PACKAGES(самообновление сwhitemaxmod.com), пересобранный и переподписанный чужим ключом — то есть к рискам оригинала добавляется полное доверие к автору мода.