Утром 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 |
|---|---|---|
|
|
|
| - |
|
|
|
|
|
|
|
Хэш ключа генерировался следующей командой: 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 о другом ключе шифрования, и, будучи посередине между клиентом и сервером, просматривать, сохранять и модифицировать весь трафик.

TL;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 (всего лишь безобидный клиент от маленького стартапа из Казани).
Где конкретно применяется эта логика:
Местоположение | Эффект |
|---|---|
| блокирует открытие чата |
| блокирует просмотр профиля |
| блокирует просмотр историй |
Ключевые классы:
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, чтобы быть в курсе событий!
