Skip to content

Нейрослоп - исследование детекта VPN нашло кое-что интересное в smali. Возможно, стоит рассмотреть и при необходимости вырезать до конца #38

Description

@SelfHarmony

Клонирую репозиторий и начинаю анализ.

Репозиторий склонирован и разобран. Сразу важная поправка по постановке задачи, потому что она меняет выводы.

Что это на самом деле

Это не «червь, маскирующийся под мессенджер». Это три 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.smaligetUserId() возвращает 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.smaliLogController: методы 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 на месте.

Итог

Опасность здесь не в «черве». Опасны две вещи:

  1. В самом MAX сидит обфусцированный модуль DPS, который по удалённой команде (флаг dps) определяет реальный IP пользователя в обход VPN через сторонние IP-echo сервисы, детектит VPN по интерфейсам tun/tap/ppp/ipsec, и отправляет связку «ID аккаунта + IP + флаг VPN + оператор + deviceId» на trace-flow.ru. Это не аналитика продуктового качества — это инфраструктура деанонимизации, и она спроектирована так, чтобы её не нашли грепом.

  2. Мод даёт ложное чувство безопасности. Он глушит сторонние SDK и локальный VPN-детект, но оставляет нетронутыми и DPS, и собственный телеметрийный канал MAX. При этом это APK от анонимного автора с правами на контакты, камеру, микрофон, геолокацию и REQUEST_INSTALL_PACKAGES (самообновление с whitemaxmod.com), пересобранный и переподписанный чужим ключом — то есть к рискам оригинала добавляется полное доверие к автору мода.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions