来源: https://habr.com/ru/articles/1006666/
而且,这个标题并非标题党。通过对俄罗斯即时通讯软件MAX进行逆向工程,我们证实了最糟糕的假设。
最近网上开始出现关于 MAX Messenger 向 Telegram 和 WhatsApp 发送奇怪请求的报道,引发了人们对这些请求的性质和目的的猜测。但猜测是一回事,了解真相又是另一回事。这可能是某种集成功能,也可能是一个随机的分析模块。为了弄清真相并与大家分享,我决定深入研究一下客户端,了解它的功能和原因。
简而言之——它包含一个间谍模块,由MAX开发人员创建,用于监控VPN用户。他们试图使该模块无法被屏蔽,并添加了远程控制功能。
准备
由于 MAX 客户端不包含调试信息且难以逆向工程,我决定首先查看测试对象发出的网络请求。为此,我们需要:
- 我使用了mitmproxy,并启用了 WireGuard 模式(
--mode wireguard),因为它允许拦截所有流量。 - 我使用的是Android Studio自带的Android模拟器。
- 将 mitmproxy 根证书上传到模拟器的系统存储中。
- 适用于安卓系统的 WireGuard 客户端。我使用的是官方版本。启动后,mitmproxy/mitmweb 会显示一个二维码和 WG 客户端的配置信息。
MAX_(RS)_v.26.4.3(6552)(8.0-15.0)(arm7a,arm64-8a,x86,x86-64)MAX Messenger 本身。在我的研究中,我使用了在 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 | 命令(可能路由到不同的服务) |
01 |
1 | 请求序列号 (SEQ) |
0006 |
0x0006 | 操作码(OPCODE) |
01 |
1 | 压缩标志 |
000087 |
135 | 有效载荷尺寸 |
data |
— | MessagePack格式的数据 |
因此,为了分析流量,我不得不为 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- 连接类型
| 代码 | 意义 |
|---|---|
0 |
连接类型未知 |
1 |
无连接 |
2 |
无线上网 |
3 |
手机慢 |
4 |
移动快 |
hosts- 要检查的主机列表及其检查状态。值可以是:
| 代码 | Ping(ICMP) | TCP:443 | 结果 |
|---|---|---|---|
0 |
失败 | 失败 | 主机完全不可用 |
1 |
好的 | 失败 | ping 通了,但是 HTTPS 不可用。 |
2 |
失败 | 好的 | 无 ping 操作,HTTPS 可用 |
3 |
好的 | 好的 | ping 通了,HTTPS 可用 |
ip— 显然,这是客户端的 IP 地址。此外,不同的事件可能会收到来自不同来源的 IP 地址。operator— 该行包含运营商的PLMN代码,由移动国家代码和运营商代码组成:- MCC:(
250俄语) - 跨国公司:(
01在本例中为 MTS)
- MCC:(
vpn— 指示系统中 VPN 连接是否处于活动状态的标志。此标志仅受限于手机本身是否正在使用 VPN 软件(原生 Android API)。
还发现,该模块可通过服务器远程启用和禁用 host-reachability 。登录/会话刷新时,会返回一个包含标志的配置,从而可以针对特定帐户启用此功能 。
这是如何运作的
- 应用程序启动时,会获取并打乱源 IP 地址列表:
- https://ipv4-internet.yandex.net/api/v0/ip
- https://ipv6-internet.yandex.net/api/v0/ip
- https://ifconfig.me/ip
- https://api.ipify.org
- https://checkip.amazonaws.com
- https://ip.mail.ru/
- IP 地址以异步方式获取,超时时间为 3000 毫秒,并且
127.0.0.1忽略响应。 - 同时,使用以下命令查询目标主机:
- ping(ICMP)
- 连接 TCP:443 (通过 HTTPS 检查可用性)。超时时间相同,为 3000 毫秒。
- 当您最小化/展开应用程序时,数据会发送到
api.oneme.ru消息中心。HOST_REACHABILITY
值得注意的是,这不是一个被放弃的模块;它正在不断发展,并且有迹象表明他们计划添加一个功能齐全的模块,用于从服务器执行命令,将其变成Roskomnadzor 的袖珍审计员。
不同版本中,被检查的主机列表也会有所不同。例如,有时会启用对 Telegram 和 WhatsApp 的检查,有时则会禁用(但并未移除;APK 代码中始终包含这些主机)。我相信目前正在进行测试和改进,之后应该可以轻松地将此模块切换到完全远程控制模式。
详细的逆向工程图,链接在剧透下方。
本节不包含屏幕截图,因为乍一看它仍然像是一堆难以理解的、掺杂着小代码的杂乱无章的代码。
我已经指定了 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 地址(并将其丢弃)127.0.0.1 。
调用 GET_HOST_REACHABILITY 时:
该任务在应用程序启动时初始化:HostReachabilityTask 调用 new xb7().m22182c() (sources/one/p010me/android/OneMeApplication.java 和 sources/p000/C0136c6.java )
xb7.m22182c() 仅当 PMS 标志host-reachability (sources/p000/xb7.java:164 和sources/p000/j06.java:457 )启用时,才会在 p3i 中注册监听器。PMS 密钥本身位于sources/ru/p026ok/tamtam/android/prefs/PmsKey.java
然后,每次应用程序切换到前台时,p3i 都会调用 vb7 mo462j() ,对于 xb7 来说,会启动 vb7 协程(如果之前的协程尚未激活)。(sources/p000/p3i.java:102 和sources/p000/dk6.java:92 )
在 vb7 中,开始检查/报告之前会有 3000 毫秒的延迟。
它具体是如何检查的?
- ping 操作通过标准
InetAddress.isReachable(ub7 → jy2(case 3))执行。 - TCP 连接到主机:443,超时时间为 3000 毫秒 (
tb7 → xb7.m22181a → sq2(case 25))
代码映射发生在sources/p000/rb7.java ,但使用 smali,jadx 在这里显示得歪歪扭扭。
如果不存在连接,connection_type 为 1;zw3.f79639a 如果存在连接,则为枚举值。vpn是通过 ConnectivityManager 的 ( and )
运算符检查 NetworkCapabilities.TRANSPORT_VPN 的值,否则为空。sources/p000/vb7.java:125 sources/p000/hw3.java:186
TelephonyManager.getNetworkOperator() + ":" + getNetworkOperatorName() "undefined"
定向遥控
PMS 信息作为配置的一部分,在用户登录时 从服务器返回。该响应会被解析成不同sources/p000/qea.java:900 的值。因此,服务器可能会针对不同的用户/会话返回不同的值。此配置会在用户注销时被清除sources/p000/olc.java:34 。
这一切意味着什么?
这一切似乎显而易见,但我们来谈谈其中的细微差别:
- 这绝非巧合。他们喜欢谈论开源分析模块,但事实并非如此。这个模块显然是VK内部开发的,而且资源被封锁和限制的情况表明,他们正是此次审计的目标。
- 这些数据并非发送到单独的分析域,而是与即时通讯软件的主流量混合在一起,因此,如果不屏蔽该即时通讯软件,就无法屏蔽此类分析数据。此外,其协议也无法被自动化工具解码。
- 验证方法(ping + tcp:443)直接测试资源是否在 TSPU 上被成功阻止。TSPU 不会阻止 ping 请求,但会限制对特定端口和协议的访问。
- 显然,IP地址的选择并非随机,而是俄罗斯服务和外国服务各占一半。为什么?为了抓捕那些设置流量路由却不进行本地流量隧道传输的“聪明人”。
- 当前的远程控制功能及其似乎不可避免的改进,将国家信使变成了国家间谍软件工具。
- 这种方法非常适合捕获和阻止个人(私人)VPN服务器,这些服务器通常具有相同的输入和输出IP。
- 这种方法非常适合将特定 VPN 服务的用户与特定人员联系起来(我不会展开讨论接下来的内容)。
- 能够针对个人或群体启用此功能令人非常担忧。
- 发送运营商的PLMN代码可以很好地表明用户可能位于俄罗斯。然而,与地理位置信息不同,目前无法禁止收集移动运营商信息。
他们可能想把数百万台设备变成扫描仪,以检验封锁措施的效果,并找出那些绕过封锁的人。
我看到有人这样想:“为什么俄罗斯联邦储蓄银行做不到这一点,或者它已经做到了?”也许它已经做到了,但想想普通人在俄罗斯联邦储蓄银行应用程序上花费的时间,与现代即时通讯软件(本质上就是一个社交网络)相比,又有多少时间呢?
那我该怎么办?
看来最简单的解决办法就是删除它。
如果由于某种原因无法删除,那么你实际上只有几种选择(除了技术上比较复杂的选择,但这取决于个人喜好):
- 如果您使用的是安卓系统 ,可以将应用安装在单独的、隔离的工作区中。通常情况下,这样的工作区不会继承主配置文件中的 VPN 连接。
- 三星 ——Knox安全文件夹
- 小米/红米/POCO — 第二空间
- 华为/荣耀 — 私人空间
- 通用选项,包括Pixel / Motorola / Nothing :庇护所、岛屿、孤立
- 如果你用的是iOS系统或者不想冒险,那么为此目的购买一部单独的、价格低廉的手机或许更划算。截至撰写本文时,最便宜的安卓手机售价约为5000卢布。
- 屏蔽所有列出的获取 IP 地址的服务。但这并不可靠;随时可能新增获取 IP 地址的服务。
当然,也要告诉你的朋友们。记住,即使你没有什么可隐瞒的,这也可能会剥夺你基本的日常生活便利和访问大部分互联网的权利。
附注:本文中人工智能没有撰写一个字,而是用于格式化已撰写的文本。
更新
我在此不做任何评论,但建议在答案本身的评论区进行讨论。
更新 2
这些奇怪的查询最早出现在 Habr 网站 2026 年 1 月 20 日的报道中,随后又出现在 YouTube 上 2026 年 1 月 9 日的报道中。
此外,还有一项针对 iPhone 的独立检查。
UDP 3
在今天发布的 26.7.1 版本(RuStore 版)更新中,该即时通讯软件已禁用向 WhatsApp 和 Telegram 发送请求的功能 。此次更新很可能是在昨天的文章发布后进行的。
然而,调用 WhatsApp 和 Telegram 的代码(该类名为 WhatsApp和 Telegram)并未被移除 on7 。整个模块仍然处于活动状态。
个人点评:
公民是拿来监控的,公民中绝不能有对普金怀有二心之人