В сети начали появляться сообщения о странных обращениях мессенджера MAX к Telegram и WhatsApp, из-за чего в сети начали выдвигаться предположения касательно природы и целей этих запросов. Но одно дело предполагать, другое дело знать. Мало ли это какая-то интеграция или случайный аналитический модуль. Поэтому чтобы понять самому и рассказать вам я решил посмотреть внутрь клиента и понять что и зачем он делает.
TL;DR - содержит шпионский модуль, который сделали разработчики MAX для слежки за теми кто использует VPN, они постарались сделать этот модуль не блокируемым и прикрутили удаленное управление.
Подготовка
Так как клиент MAX не содержит отладочной информации и его реверс инжиниринг затруднен, то на первом этапе я решил просто посмотреть какие сетевые запросы делает подопытный. Для этого нам понадобится:
- mitmproxy. Я использовал режим wireguard (
--mode wireguard), так как он позволяет перехватывать вообще весь трафик. - Эмулятор Android. Я использовал Android Emulator, который идет в комплекте с Android Studio.
- Загрузить корневой сертификат mitmproxy в системное хранилище эмулятора.
- WireGuard клиент на android. Я использовал официальный. При старте mitmproxy/mitmweb показывает qr код и конфигурацию для wg клиента.
- Собственно сам мессенджер MAX. В исследовании я использовал версию
MAX_(RS)_v.26.4.3(6552)(8.0-15.0)(arm7a,arm64-8a,x86,x86-64), которую нашел на 4pda. - 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)
- MCC:
-
vpn— флаг, показывающий активно ли vpn подключение в системе. Этот флаг ограничен только статусом используется ли VPN ПО на самом телефоне (нативный Android API)
Так же было обнаружено, что этот модуль включается и отключается удаленно сервером. При логине/обновлении сессии возвращается конфигурация, которая содержит флаг host-reachability, что делает возможным включение этой функции таргетно для отдельных аккаунтов.
Как это работает
-
При старте приложения берется список адресов источников ip и перемешивается:
-
IP добывается асинхронно с таймаутом 3000ms, причем ответ
127.0.0.1игнорируется -
Параллельно опрашиваются хосты назначения, используя:
- ping (ICMP)
- connect TCP:443 (проверка доступности по HTTPS). Таймаут такой же - 3000ms
-
При сворачивании/разворачивании приложения данные отправляются на
api.oneme.ruсообщениемHOST_REACHABILITY
Отдельно хочется отметить, что это не заброшенный модуль, он развивается от версии к версии и есть признаки того, что планируется приделать к нему полноценный модуль выполнения команд с сервера чтобы превратить его в карманный Ревизор для Роскомнадзора.
Так же от версии к версии список проверяемых хостов меняется. К примеру проверка Telegram и WhatsApp то включается, то выключается (но не удаляется, apk всегда содержит эти хосты в своем коде). Я считаю что сейчас идет тестирование и обкатка, после чего ничего не стоит переключить этот модуль в полностью удаленно управляемый режим.
Эта часть не будет содержать скриншотов, так как это все равно на первый взгляд выглядит как непонятная мешанина кода с примесями smali.
Я указал точную версию apk и укажу названия классов, в которым обрабатывается то или иное, каждый сможет найти и перепроверить.
Отправной точкой поиска будет поиск в коде HOST_REACHABILITY, который без труда находится в public final vb7. vb7 ссылается на значения из public abstract class zb7, который содержит C строки, который JADX неверно интерпретировал как массив int8. Вот так выглядит его контент на самом деле:
| Переменная | Значение |
|---|---|
| f77818a | gstatic.com |
| f77820c | mtalk.google.com |
| f77822e | calls.okcdn.ru |
| f77824g | gosuslugi.ru |
| f77826i | main.telegram.org |
| f77828k | mmg.whatsapp.net |
| f77830m | https://ipv4-internet.yandex.net/api/v0/ip |
| f77832o | https://ipv6-internet.yandex.net/api/v0/ip |
| f77834q | https://ifconfig.me/ip |
| f77836s | https://api.ipify.org |
| f77838u | https://checkip.amazonaws.com |
| f77840w | https://ip.mail.ru/ |
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)
Что все это значит?
Ну, кажется все и так очевидно, но давайте поговорим о нюансах:
- Это точно получилось не случайно. Они любят рассказывать о opensource модулях аналитики, но это не тот случай. Очевидно этот модуль был разработан внутри VK и наличие заблокированных и ограниченных ресурсов говорит нам о том, что они и являются целью проверки.
- Эти данные отправляются не на отдельный аналитический домен, а смешиваются с основным трафиком мессенджера так, чтобы заблокировать эту аналитику не заблокировав мессенджер было невозможно. Отдельным бонусом идет то, что их протокол не декодируется автоматическими инструментами.
- Методика проверки (ping + tcp:443) это прямая проверка успешности блокировки ресурса на ТСПУ. ТСПУ не режет пинги, но ограничивает доступ к конкретным портам/протоколам.
- Очевидно, что выбор источников получения IP не случаен, это 50/50 российские и зарубежные сервисы. Зачем? Чтобы ловить умников, которые настроили маршрутизацию трафика и не заворачивают в туннель местный трафик.
- Текущие функции удаленного управления и кажущаяся неизбежность их совершенствования превращает национальный мессенджер в государственный шпионский инструмент (spyware)
- Этот подход очень хорош для отлавливания и блокировки личных (приватных) ВПН серверов, у которых обычно одинаковый входной и выходной ip.
- Этот подход очень хорош для привязывания пользователей конкретных впн сервисов к конкретным людям (я не буду развивать тему что из этого следует).
- Возможность включать эту функцию таргетно для отдельных людей или групп очень настораживает.
- Отправка PLMN кода оператора будет являться неплохим маркером того, что пользователь скорее всего в РФ. При этом, в отличии от геолокации, запретить собирать информацию о мобильном операторе не получится.
Вероятно, они хотят превратить миллионы устройств в сканеры успешности своих блокировок и поиска тех, кто их обходит.
Я видел мысли вроде “А почему сбербанк так не может или, может, уже делает”. Может и делает, но подумайте сколько времени средний человек проводит в приложении сбербанка, а сколько в современном мессенджере, который фактически является соцсетью?
А что делать то?
Ну кажется решение простейшее - удалите его.
Если не можете удалить по каким-либо обстоятельствам, то у вас буквально пара вариантов (кроме технически сложных, но тут кто на что горазд):
- Если у вас Android, можно установить приложение в отдельное, изолированное рабочее пространство. Обычно такое пространство не наследует VPN соединение основного профиля.
- Samsung — защищённая папка Knox
- Xiaomi / Redmi / POCO — Второе пространство
- Huawei / Honor — PrivateSpace (Личное пространство)
- Универсальный вариант, в том числе для Pixel / Motorola / Nothing: Shelter, Island, Insular
- Если у вас IOS или совсем не хотите риска, то, вероятно, стоит купить для этого отдельный самый дешевый телефон. Самый дешевый android на момент написания статьи в DNS стоит около 5 тысяч рублей.
- Заблокировать все перечисленные сервисы получения ip адресов. Но это ненадежно, в любой момент могут добавить новые.
Ну и конечно - рассказать друзьям. Помните, даже если вам нечего скрывать, то это может лишить самого обычного бытового комфорта и доступа в большую часть интернета.