И этот заголовок - не кликбейт. Подвергнув реверс инжинирингу клиент российского мессенджера MAX удалось подтвердить самые худшие предположения.
В сети начали появляться сообщения о странных обращениях мессенджера 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
Состоит из
|
Поле |
Значение |
Описание |
|---|---|---|
|
|
10 |
Версия протокола |
|
|
0x0100 |
Command (вероятно, маршрутизация на разные сервисы) |
|
|
1 |
Порядковый номер запроса (SEQ) |
|
|
0x0006 |
Опкод (OPCODE) |
|
|
1 |
Флаг сжатия |
|
|
135 |
Размер полезной нагрузки |
|
|
— |
Данные в формате MessagePack |
По поводу msgpack - привет ребятам из VK, это их любимая игрушка.
Так что для анализа трафика пришлось написать аддон для mitmproxy, который распаковывает трафик на лету.
maxproto_dump.py
import lz4.block
import msgpack
import pprint
from datetime import datetime
from mitmproxy import tcp
from mitmproxy import ctx
def unpack_packet(data: bytes):
if len(data) < 10:
return None
ver = int.from_bytes(data[0:1], 'big')
cmd = int.from_bytes(data[1:3], 'big')
seq = int.from_bytes(data[3:4], 'big')
opcode = int.from_bytes(data[4:6], 'big')
packed_len = int.from_bytes(data[6:10], 'big', signed=False)
comp_flag = packed_len >> 24
payload_length = packed_len & 0xFFFFFF
if payload_length == 0:
return {
"ver": ver, "cmd": cmd, "seq": seq, "opcode": opcode,
"payload": "[Empty Payload / System Message / ACK]"
}
payload_bytes = data[10:10 + payload_length]
if comp_flag != 0:
compressed_data = payload_bytes
try:
payload_bytes = lz4.block.decompress(compressed_data, uncompressed_size=1048576)
except lz4.block.LZ4BlockError as e:
return {
"ver": ver, "cmd": cmd, "seq": seq, "opcode": opcode,
"payload": f"[Error: LZ4 Decompression failed - {e}]"
}
try:
payload = msgpack.unpackb(payload_bytes, raw=False, strict_map_key=False)
except Exception as e:
payload = f"[Error: MessagePack unpack failed - {e}]"
return {
"ver": ver,
"cmd": cmd,
"seq": seq,
"opcode": opcode,
"payload": payload
}
class MaxProtoDumper:
def tcp_message(self, flow: tcp.TCPFlow):
host = ""
if flow.server_conn and flow.server_conn.sni:
host = flow.server_conn.sni
elif flow.server_conn and flow.server_conn.address:
host = flow.server_conn.address[0]
if "oneme.ru" not in host and "155.212" not in host:
return
message = flow.messages[-1]
raw_bytes = message.content
direction = "C->S" if message.from_client else "S->C"
parsed = unpack_packet(raw_bytes)
if not parsed:
return
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
if isinstance(parsed["payload"], (dict, list)):
formatted_payload = pprint.pformat(parsed["payload"], indent=2)
else:
formatted_payload = str(parsed["payload"])
log_msg = (
f"n[{timestamp}]n{direction}n"
f"VER: {parsed['ver']} | CMD: {parsed['cmd']} | SEQ: {parsed['seq']} | OPCODE: {hex(parsed['opcode'])}n"
f"Payload Data:n{formatted_payload}n"
f"{'='*50}"
)
ctx.log.info(log_msg)
with open("maxproto_decoded.txt", "a", encoding="utf-8") as f:
f.write(log_msg + "n")
addons = [
MaxProtoDumper()
]
Данные дампятся в консоль и в 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- Тип соединения
|
Код |
Значение |
|---|---|
|
|
Тип соединения неизвестен |
|
|
Нет соединения |
|
|
Wi-Fi |
|
|
Mobile slow |
|
|
Mobile fast |
-
hosts- список хостов для проверки и статус этой проверки. Значения могут быть такие
|
Код |
Ping (ICMP) |
TCP:443 |
Итог |
|---|---|---|---|
|
|
FAIL |
FAIL |
хост недоступен полностью |
|
|
OK |
FAIL |
пинг есть, HTTPS недоступен |
|
|
FAIL |
OK |
пинга нет, HTTPS доступен |
|
|
OK |
OK |
пинг есть, HTTPS доступен |
-
ip— Очевидно, IP адрес клиента. Причём в разных событиях может приходить IP, полученный из разных источников. -
operator— строка содержит PLMN код оператора, состоящий из мобильного кода страны и кода оператора:-
MCC:
250(RUS) -
MNC:
01(в данном случае - MTS)
-
-
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 |
|
|
f77832o |
|
|
f77834q |
|
|
f77836s |
|
|
f77838u |
|
|
f77840w |
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 адресов. Но это ненадежно, в любой момент могут добавить новые.
Ну и конечно - рассказать друзьям. Помните, даже если вам нечего скрывать, то это может лишить самого обычного бытового комфорта и доступа в большую часть интернета.
PS - ИИ не написал ни слова в этой статье, но использовался для форматирования уже написанного текста
UPD
Ответ пресс службы мессенджера MAX
Я оставлю это без комментариев тут, предлагаю обсудить ответ в комментариях к самому ответу.
UPD 2
Первые упоминания этих странных запросов появились на хабре 20.01.26 и 09.01.26 на YouTube
UDP 3
С сегодняшним обновлением 26.7.1 (RuStore) в мессенджере отключили отправку запросов к WhatsApp и Telegram. Вероятно билд подготовили после вчерашней статьи.
Но тем не менее из кода обращения к WhatsApp и Telegram не удалены, класс называется on7. Модуль в целом остался активен.
Автор: runetfreedom
