Реверс инжиниринг модуля проверки успешности блокировок внутри мессенджера MAX

В сети начали появляться сообщения о странных обращениях мессенджера MAX к Telegram и WhatsApp, из-за чего в сети начали выдвигаться предположения касательно природы и целей этих запросов. Но одно дело предполагать, другое дело знать. Мало ли это какая-то интеграция или случайный аналитический модуль. Поэтому чтобы понять самому и рассказать вам я решил посмотреть внутрь клиента и понять что и зачем он делает.

TL;DR - содержит шпионский модуль, который сделали разработчики MAX для слежки за теми кто использует VPN, они постарались сделать этот модуль не блокируемым и прикрутили удаленное управление.

Подготовка

Так как клиент MAX не содержит отладочной информации и его реверс инжиниринг затруднен, то на первом этапе я решил просто посмотреть какие сетевые запросы делает подопытный. Для этого нам понадобится:

  1. mitmproxy. Я использовал режим wireguard (--mode wireguard), так как он позволяет перехватывать вообще весь трафик.
  2. Эмулятор Android. Я использовал Android Emulator, который идет в комплекте с Android Studio.
  3. Загрузить корневой сертификат mitmproxy в системное хранилище эмулятора.
  4. WireGuard клиент на android. Я использовал официальный. При старте mitmproxy/mitmweb показывает qr код и конфигурацию для wg клиента.
  5. Собственно сам мессенджер MAX. В исследовании я использовал версию MAX_(RS)_v.26.4.3(6552)(8.0-15.0)(arm7a,arm64-8a,x86,x86-64), которую нашел на 4pda.
  6. JADX для анализа APK.

В этой статье я опущу инструкции по загрузке корневого сертификата, настройку эмулятора и подключение к wireguard так как интернет содержит тысячи инструкций для этого.

Перехват трафика

Что же, запускаем mitmweb, подключаем эмулятор и смотрим что за запросы ходят в интернет.

(Я удалил часть мусора, не имеющего отношения к делу)

Что же, мы видим в том числе интересующие нас запросы, но почему-то обмен данными с api.oneme.ru (api домен мессенджера) отображается как TCP поток, а не HTTP(S)/WebSocket.

Изначально мне казалось, что это gRPC, так как трафик был похож на бинарную мешанину с вкраплениями строк, но protoc --decode_raw ничего не показал.

Анализ протокола

Анализ протокола занял у меня несколько часов, но по итогу я выяснил что каждое сообщение состоит из заголовка (10 байт) и полезной нагрузки (опционально сжатой). Вот пример заголовка

0a|0100|01|0006|01|000087|data

Состоит из

Поле Значение Описание
0a 10 Версия протокола
0100 0x0100 Command (вероятно, маршрутизация на разные сервисы)
01 1 Порядковый номер запроса (SEQ)
0006 0x0006 Опкод (OPCODE)
01 1 Флаг сжатия
000087 135 Размер полезной нагрузки
data Данные в формате MessagePack

По поводу msgpack - привет ребятам из VK, это их любимая игрушка.

Так что для анализа трафика пришлось написать аддон для mitmproxy, который распаковывает трафик на лету.
maxproto_dump.py (2,8 КБ)

Данные дампятся в консоль и в maxproto_decoded.txt

И что же внутри?

Что же, запускаем mitmweb с нашим аддоном и смотрим что внутри

mitmweb --mode wireguard -s maxproto_dump.py

Я запустил прокси и одно из первых сообщений, которые я увидел было

C->S
VER: 10 | CMD: 0 | SEQ: 25 | OPCODE: 0x1
Payload Data:
{'interactive': False}
==================================================

C->S
VER: 10 | CMD: 0 | SEQ: 26 | OPCODE: 0x5
Payload Data:
{ 'events': [ { 'event': 'GET_HOST_REACHABILITY',
                'params': { 'connection_type': 2,
                            'hosts': { 'api.oneme.ru': 3,
                                       'calls.okcdn.ru': 3,
                                       'gosuslugi.ru': 3,
                                       'gstatic.com': 3,
                                       'main.telegram.org': 3,
                                       'mmg.whatsapp.net': 3,
                                       'mtalk.google.com': 3},
                            'ip': 'REDACTED',
                            'operator': '25001:MTS',
                            'vpn': 1},
                'sessionId': REDACTED,
                'time': 17726REDACTED,
                'type': 'HOST_REACHABILITY',
                'userId': REDACTED}]}

(очевидно я скрыл чувствительные данные)

Ничего себе. Вот что мы тут видим.

  • connection_type - Тип соединения
Код Значение
0 Тип соединения неизвестен
1 Нет соединения
2 Wi-Fi
3 Mobile slow
4 Mobile fast
  • hosts - список хостов для проверки и статус этой проверки. Значения могут быть такие
Код Ping (ICMP) TCP:443 Итог
0 FAIL FAIL хост недоступен полностью
1 OK FAIL пинг есть, HTTPS недоступен
2 FAIL OK пинга нет, HTTPS доступен
3 OK OK пинг есть, HTTPS доступен
  • ip — Очевидно, IP адрес клиента. Причём в разных событиях может приходить IP, полученный из разных источников.

  • operator — строка содержит PLMN код оператора, состоящий из из мобильного кода страны и кода оператора:

    • MCC: 250 (RUS)
    • MNC: 01 (в данном случае - MTS)
  • vpn — флаг, показывающий активно ли vpn подключение в системе. Этот флаг ограничен только статусом используется ли VPN ПО на самом телефоне (нативный Android API)

Так же было обнаружено, что этот модуль включается и отключается удаленно сервером. При логине/обновлении сессии возвращается конфигурация, которая содержит флаг host-reachability, что делает возможным включение этой функции таргетно для отдельных аккаунтов.

Как это работает

  1. При старте приложения берется список адресов источников ip и перемешивается:

  2. IP добывается асинхронно с таймаутом 3000ms, причем ответ 127.0.0.1 игнорируется

  3. Параллельно опрашиваются хосты назначения, используя:

    • ping (ICMP)
    • connect TCP:443 (проверка доступности по HTTPS). Таймаут такой же - 3000ms
  4. При сворачивании/разворачивании приложения данные отправляются на api.oneme.ru сообщением HOST_REACHABILITY

Отдельно хочется отметить, что это не заброшенный модуль, он развивается от версии к версии и есть признаки того, что планируется приделать к нему полноценный модуль выполнения команд с сервера чтобы превратить его в карманный Ревизор для Роскомнадзора.

Так же от версии к версии список проверяемых хостов меняется. К примеру проверка Telegram и WhatsApp то включается, то выключается (но не удаляется, apk всегда содержит эти хосты в своем коде). Я считаю что сейчас идет тестирование и обкатка, после чего ничего не стоит переключить этот модуль в полностью удаленно управляемый режим.

Эта часть не будет содержать скриншотов, так как это все равно на первый взгляд выглядит как непонятная мешанина кода с примесями smali.

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

Отправной точкой поиска будет поиск в коде HOST_REACHABILITY, который без труда находится в public final vb7. vb7 ссылается на значения из public abstract class zb7, который содержит C строки, который JADX неверно интерпретировал как массив int8. Вот так выглядит его контент на самом деле:

IP добывается асинхронно через qb7 → pb7 с таймаутом 3000 мс. sources/p000/qb7.java:45

В pb7 берётся список URL-ов (IPv4/IPv6 Yandex, ifconfig.me, ipify, checkip.amazonaws.com, ip.mail.ru), перемешивается и дальше перебирается до первого валидного IP по regex (и отбрасывается 127.0.0.1).

Когда вызывается GET_HOST_REACHABILITY:

Инициализация задачи делается при старте приложения: HostReachabilityTask вызывает new xb7().m22182c() (sources/one/p010me/android/OneMeApplication.java и sources/p000/C0136c6.java)

xb7.m22182c() регистрирует listener в p3i только если включён PMS-флаг host-reachability (sources/p000/xb7.java:164 и sources/p000/j06.java:457). Сами PMS ключи лежат в sources/ru/p026ok/tamtam/android/prefs/PmsKey.java

Дальше каждый раз при переходе приложения в foreground p3i вызывает mo462j(), и для xb7 стартует корутина vb7 (если предыдущая ещё не активна). (sources/p000/p3i.java:102 и sources/p000/dk6.java:92)

В vb7 есть задержка 3000 мс перед стартом проверки/репорта.

А как именно проверяет:

  • ping выполняется через стандартный InetAddress.isReachable (ub7 → jy2(case 3))

  • TCP connect на host:443 с таймаутом 3000 мс (tb7 → xb7.m22181a → sq2(case 25))

Маппинг кодов происходит в sources/p000/rb7.java, но используйте smali, jadx тут показывает криво.

connection_type это 1 если нет соединения и enum zw3.f79639a если есть.
vpn - это проверка NetworkCapabilities.TRANSPORT_VPN через ConnectivityManager (sources/p000/vb7.java:125 и sources/p000/hw3.java:186)
operator - берётся из TelephonyManager.getNetworkOperator() + ":" + getNetworkOperatorName(), иначе "undefined"

Таргетированное удаленное управление

PMS приходят с сервера как часть config в ответ на логин. Ответ парсится в sources/p000/qea.java:900. Поэтому сервер может вернуть разные значения для разных пользователей/сессий. При logout этот конфиг чистится (sources/p000/olc.java:34)

Что все это значит?

Ну, кажется все и так очевидно, но давайте поговорим о нюансах:

  1. Это точно получилось не случайно. Они любят рассказывать о opensource модулях аналитики, но это не тот случай. Очевидно этот модуль был разработан внутри VK и наличие заблокированных и ограниченных ресурсов говорит нам о том, что они и являются целью проверки.
  2. Эти данные отправляются не на отдельный аналитический домен, а смешиваются с основным трафиком мессенджера так, чтобы заблокировать эту аналитику не заблокировав мессенджер было невозможно. Отдельным бонусом идет то, что их протокол не декодируется автоматическими инструментами.
  3. Методика проверки (ping + tcp:443) это прямая проверка успешности блокировки ресурса на ТСПУ. ТСПУ не режет пинги, но ограничивает доступ к конкретным портам/протоколам.
  4. Очевидно, что выбор источников получения IP не случаен, это 50/50 российские и зарубежные сервисы. Зачем? Чтобы ловить умников, которые настроили маршрутизацию трафика и не заворачивают в туннель местный трафик.
  5. Текущие функции удаленного управления и кажущаяся неизбежность их совершенствования превращает национальный мессенджер в государственный шпионский инструмент (spyware)
  6. Этот подход очень хорош для отлавливания и блокировки личных (приватных) ВПН серверов, у которых обычно одинаковый входной и выходной ip.
  7. Этот подход очень хорош для привязывания пользователей конкретных впн сервисов к конкретным людям (я не буду развивать тему что из этого следует).
  8. Возможность включать эту функцию таргетно для отдельных людей или групп очень настораживает.
  9. Отправка PLMN кода оператора будет являться неплохим маркером того, что пользователь скорее всего в РФ. При этом, в отличии от геолокации, запретить собирать информацию о мобильном операторе не получится.

Вероятно, они хотят превратить миллионы устройств в сканеры успешности своих блокировок и поиска тех, кто их обходит.

Я видел мысли вроде “А почему сбербанк так не может или, может, уже делает”. Может и делает, но подумайте сколько времени средний человек проводит в приложении сбербанка, а сколько в современном мессенджере, который фактически является соцсетью?

А что делать то?

Ну кажется решение простейшее - удалите его.

Если не можете удалить по каким-либо обстоятельствам, то у вас буквально пара вариантов (кроме технически сложных, но тут кто на что горазд):

  1. Если у вас Android, можно установить приложение в отдельное, изолированное рабочее пространство. Обычно такое пространство не наследует VPN соединение основного профиля.
    • Samsung — защищённая папка Knox
    • Xiaomi / Redmi / POCO — Второе пространство
    • Huawei / Honor — PrivateSpace (Личное пространство)
    • Универсальный вариант, в том числе для Pixel / Motorola / Nothing: Shelter, Island, Insular
  2. Если у вас IOS или совсем не хотите риска, то, вероятно, стоит купить для этого отдельный самый дешевый телефон. Самый дешевый android на момент написания статьи в DNS стоит около 5 тысяч рублей.
  3. Заблокировать все перечисленные сервисы получения ip адресов. Но это ненадежно, в любой момент могут добавить новые.

Ну и конечно - рассказать друзьям. Помните, даже если вам нечего скрывать, то это может лишить самого обычного бытового комфорта и доступа в большую часть интернета.

Кому не жалко - проголосуйте за эту же статью на Хабре Месседжер MAX следит за пользователями VPN? Реверс инжиниринг говорит — да / Хабр

Возможно привлечение внимания либо побудит их притормозить, ну либо больше людей будет в курсе.

А хороший вопрос - отправляется PLMN вставленной сим карты или используемой сети?

Да, конечно, прямо в середине статьи пример и объяснение. Вот кусок из сообщения

'operator': '25001:MTS',

Это не отвечает на мой вопрос. PLMN берётся из вставленной сим карты или используемой сети? В случае используемой сети, в роуминге будет не MCC 250.

Да, конечно, это именно из текущей сети. Для сим карты используется другой api

Если посмотреть на конфиги которые прилетают с сервера прям у работающего приложения в ru.oneme.app/app_file_prefs/settings.prefs, там есть настройка callDontUseVpnForRtp. Похоже удаленное управление для C кода, чтобы включали звонки в обход системного VPN

Если само приложение отправляет эти запросы, то разве не вариант - просто исключить само приложение из списка проксируемых средствами split tunneling?

Может быть и вариант, но попробуйте сделать это на IOS

А если на уровне днс не резолвить все домены на oneme.ru - сломается ли только эта телеметрия или сломается весь функционал мессенджера?

все запросы идут на одно соединение

Выше верно написали, это основной эндпоинт мессенджера, туда идут все запросы.

Что-то я не совсем понимаю логику структуры репорта. Почему поле “ip” только одно вместо того чтобы проверять сразу все эндпоинты и возвращать все ip массивом? Это ведь дало бы более полную картину чем один случайно выбранный эндроинт. причем я не вижу чтобы тут было упоминание от какого именно эндпоинта было получено значение ip

получается проблем можно избежать просто используя split tunneling по приложениям, если бы не одно но: чекается статус впн-подключения. можно ли это как то скрыть?

Спасибо за обзор.

Самый дешёвый android будет нашпигован зверьем по хуже. Яндексом каким-нибудь, который будет всё подряд смотреть/слушать и сливать. Низкая цена окупается данными.

Можно использовать прокси режим. Хотя, мало какой софт на андроиде поддерживает прокси. Но MAX всё равно может получить список установленного софта и увидеть v2ray. Но можно использовать консольную версию прокси приложения в Termux.

все эти яндексы и прочий мусор, который устанавливается по требованию, легко удаляется стандартными средствами. а мусора от производителя везде хватает

Даже если яндекс предоставляет лаунчер?

Но можно же не использовать его 24\7, не давать ему доступа к сети, вставить старую сим с минимальным тарифом, и прочими штуками. Потому что у меня первая мысль после прочтения статьи, так это письма “счастья” как в Китае, когда пользователю приходит письмо от местного органа управления с просьбой удалить софт vpn и оплатить штраф, или не штраф, а на первый раз предупреждение, но не суть важно, денежку можно стричь просто в промышленных масштабах

ну такое откровенное Г конечно же не стоит рассматривать

Ответ от пресс службы Max В Max пояснили Хабру: мессенджер не отправляет запросы на серверы WhatsApp и Telegram / Хабр