Утром 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); // убивает все соединения
}

Данный механизм вызывается несколькими способами:

  1. Скрытым push-уведомлением от сервера Telega c полями type=dc_update_version и force_relogin=true (См. класс TelegaPushHandler)

  2. Скрытым push-уведомлением от сервера Telega c полями type=dc_force_switch и force_reconnect=true (См. класс TelegaPushHandler)

  3. При переходе по ссылке tg://dc_event?force_relogin=true или tg://dc_event?force_reconnect=true (См. класс DCDeepLinkHandler)

  4. Промо-баннер с предложением о "миграции" (См. классы PromoRestClient, DahlBannerCell, DCMigrationHelper)

Последний механизм показывает пользователю промо-баннер со следующим текстом:

Перезайдите в приложение, чтобы ускорить соединение

Мы переходим на выделенные серверы для максимальной скорости и стабильности звонков, чатов и загрузки медиа. Вам нужно только перезайти в свой аккаунт.

Мы не знаем, показывался ли данный баннер при активации MITM 18 марта, но по данному сообщению видно, что Telega ведёт себя как обычное вредоносное ПО — предлагает пользователю скорость и стабильность, а под капотом ворует данные от их аккаунтов Telegram и устанавливает постоянную "прослушку".

Так как Telega со своим dc-proxy контроллирует хэндшейк, это означает, что они могут провести классическую атаку MITM (Man-in-the-middle, "человек посередине") — договориться с клиентом об одном ключе шифрования, а с настоящим сервером Telegram о другом ключе шифрования, и, будучи посередине между клиентом и сервером, просматривать, сохранять и модифицировать весь трафик.

Схема MITM-атаки Telega|697
Схема MITM-атаки Telega

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 объяснили эту функциональность "родительским контролем" (якобы, родитель может запретить ребёнку открывать то, что запрещено), но просмотр исходного кода показывает, что "чёрные списки" существуют отдельно и применяются глобально для всех пользователей.

Как это работает:

  1. Настройка для каждого пользователя blacklist_filter_enabled (ключ JSON в TelegaUserConfig, полученный из собственного API настроек Telega, по умолчанию: false)

  2. При включении каждое открытие чата/профиля/истории вызывает:

    POST https://api.telega.info/v1/api/blacklist/filter
    Body: { "targets": [{ "type": "user|channel|chat|bot", "id": 123456 }] }
    Response: { "blacklisted": [{ "type": "user", "id": 123456 }] }
    
  3. Если цель в черном списке → отображается BlacklistedOverlay, контент полностью скрыт

  4. Результаты кэшируются локально в moderation_list SharedPreferences

При этом в BlacklistedOverlay отображается следующий текст:

Материалы недоступны
Этот [чат/канал/бот] недоступен в связи с нарушениями правил платформы

создавая у пользователя впечатление, что контент был заблокирован администрацией Telegram (платформы), а не жуликами-админами Telega (всего лишь безобидный клиент от маленького стартапа из Казани).

Где конкретно применяется эта логика:

Местоположение

Эффект

ChatActivity.checkIsBlacklisted

блокирует открытие чата

ProfileActivity.checkIsBlacklisted

блокирует просмотр профиля

PeerStoriesView.checkIsBlacklisted

блокирует просмотр историй

Ключевые классы:

  1. ru.dahl.messenger.data.rest.ModerationService
    → Делает запрос HTTP POST api/blacklist/filter

  2. ru.dahl.messenger.data.repository.ModerationRepository
    → Локальный кэш + логика удаленной проверки

  3. ru.dahl.messenger.data.entity.BlacklistRequestObject
    → Формирует объект запроса { targets: [{ type, id }] }

  4. ru.dahl.messenger.data.entity.BlacklistResponseObject
    → Обрабатывает объект ответа { blacklisted: [{ type, id }] }

  5. ru.dahl.messenger.data.entity.TargetType
    → Перечисление: USER, CHANNEL, CHAT, BOT

  6. ru.dahl.messenger.data.entity.TelegaUserConfig
    → Конфигурация для каждого пользователя с blacklistFilterEnabled

  7. ru.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, чтобы быть в курсе событий!