TL;DR Внутри клиента exteraGram на базе официального клиента Telegram работает внутренняя система динамического обновления компонентов. Через неё авторы могут в любой момент загрузить на ваш телефон код прямо из Telegram-канала, обойти окно установки (can_not_skip) и выполнить его в едином адресном пространстве приложения (с его UID и полными правами). В исследованной цепочке загрузки я не обнаружил проверки цифровой подписи. При компрометации канала или инфраструктуры обновлений это создаёт возможность массовой доставки вредоносного кода пользователям.
Как всё началось Был обычный вечер, делать было откровенно нечего. Я сам сидел на ExteraGram, и вдруг стало интересно - а как оно вообще внутри устроено? (Спойлер: после того, что я там увидел, я его снес).

Ну immat0x1 говорил, так и сделаю. На момент написания поста последняя версия exteraGram 12.9.0 от 31 июля 2026 года
Открываю терминал, натравливаю JADX на свежий APK, жду декомпиляции. Открываю исходники и... немного впадаю в ступор. Авторы обмазали клиент защитой по полной программе. Весь Java-код приложения завернут в жесточайший обфускатор LSParanoid. Вдобавок, Python-модули SDK скомпилированы через Cython в нативные либы .so, а потом прогнаны через OLLVM с Control Flow Flattening, чтобы совсем отбить желание туда лезть.
Потом я зашел в их чат и увидел забавную фразу от лид-разраба:

Тут всё встало на свои места. Когда ты генерируешь сложную архитектуру нейросетями, а потом просто заворачиваешь это в жесточайший обфускатор, решив, что это спасёт от взлома - получается то, что мы имеем.
Найдя классический бэкдор и RCE (Remote Code Execution) прямо в продакшен-сборке, я собрал детальный репорт и отправил его в Google Android Security Team. И знаете, что они мне ответили?

То есть, по мнению инженеров Google, скачивание обфусцированных бинарников из Telegram-постов и их запуск с доступом к JVM без песочницы — это не малварь. Что ж, раз официальные инстанции дали зеленый свет такому подходу, давайте я просто покажу, как это работает изнутри.
Что они говорят и что есть в коде
Все номера строк ниже — это вывод декомпилятора JADX на оригинальном APK версии 12.9.0 (сборка 20260731). Никакой ручной деобфускации, показываю всё как есть, прямо с тонной шифратора LSParanoid.
В чатах авторы очень любили рассказывать, какие у них крутые защиты. Разработчик bleizix писал на форуме:

По факту, они накрутили защиту, как в коммерческих DRM:
Слой первый и второй (Cython + OLLVM): Python-модули плагинов они скомпилировали в бинарники .so, а потом прогнали через кастомный OLLVM (Control Flow Flattening). Паранойя в Java (LSParanoid): Вся Java-часть загрузчика наглухо зашифрована. Но самое смешное — они так увлеклись всей этой обфускацией, что забыли банально сделать strip нативных бинарников.
Они накрутили Control Flow Flattening и потратили ресурсы на сборку, но при этом забыли сделать базовую вещь — strip. Это как поставить на входную дверь сейфовый замок толщиной в метр, но оставить ключи под ковриком с надписью ‘Welcome’. Утилита strip выпиливает отладочную информацию и символы из нативных бинарников. Её отсутствие полностью обесценивает сложную OLLVM-обфускацию, предоставляя реверс-инженеру внутренние пути и имена функций на блюдечке.
Вся их претензия на элитарность и «запредельную защиту» сыпется от одной команды в терминале. Достаточно посмотреть строки в бинарнике metadata_parser.so (один из плагинов), который должен был быть супер-защищенным. Забытый strip выдает всё с потрохами:

Мало того, что мы видим точную версию Cython, так еще и палится конкретный форк OLLVM с гитхаба (luluovo1/ollvm-ndk30), который они использовали для обфускации. И там же рядом торчат пути сборки их SDK:

В официальном патчноуте 12.9.0 они с гордостью заявляли:

Я полез искать этот безопасный режим. Оказалось, в коде это просто один if (safeMode) return;, который отключает загрузку. Никаких реальных песочниц на уровне ОС, никаких ограничений прав через SecurityManager. Либо плагины выключены, либо у них есть полный доступ ко всему адресному пространству процесса.
Загрузчик кода прямо из Telegram
Авторы потратили кучу времени на настройку LSParanoid со всей этой кастомной математикой, но во всей этой многослойной броне забыли прописать самую базу: проверку того, что именно они скачивают.
Открывая PythonPluginsEngine.java, мы не видим ни одной нормальной строки. Всё зашифровано:

LSParanoid прячет строки, заменяя их на вызовы метода getString(long seed). Если заглянуть в их классы DeobfuscatorHelper и RandomHelper, мы увидим всю их математику в открытом виде.
Во-первых, переданный длинный seed прогоняется через миксер в стиле SplitMix64 (XOR с битовыми сдвигами и умножением на гигантские константы вроде 7109453100751455733L). Во-вторых, внутренний стейт генератора дробится не на 64-битные куски, а на 16-битные short-блоки. Для генерации каждого байта происходит магия битовых сдвигов: блоки ксорятся между собой (^), после чего циклически сдвигаются влево и вправо функцией rotl на жестко заданные смещения (9, 10, 13).
Полученный из этого хаоса псевдослучайный ключ снова XOR-ится. Но с чем? Деобфускатор лезет в гигантский заранее сгенерированный массив бессмысленных строк (chunks), берет оттуда символ по индексу (используя деление и остаток от деления на 8191) и применяет XOR. Таким образом генерируется вся строка, символ за символом.
deobf.py
import os import re import struct def to_int(val: int) -> int: val &= 0xFFFFFFFF return val - 0x100000000 if val >= 0x80000000 else val def to_short(val: int) -> int: val &= 0xFFFF return val - 0x10000 if val >= 0x8000 else val def rotl_short(val: int, shift: int) -> int: val_int = to_int(to_short(val)) unsigned = val_int & 0xFFFFFFFF return to_short((unsigned >> (32 - shift)) | (val_int << shift)) def calc_seed(val: int) -> int: val &= 0xFFFFFFFFFFFFFFFF j2 = ((val ^ (val >> 33)) * 7109453100751455733) & 0xFFFFFFFFFFFFFFFF mult = ((j2 ^ (j2 >> 28)) * -3808689974395783757) & 0xFFFFFFFFFFFFFFFF return mult >> 32 def next_state(state: int) -> int: state &= 0xFFFFFFFFFFFFFFFF s1 = to_short(state & 0xFFFF) s2 = to_short((state >> 16) & 0xFFFF) s_sum = to_short(to_int(s1) + to_int(s2)) s_rotl = to_short(to_int(rotl_short(s_sum, 9)) + to_int(s1)) s_xor = to_short(to_int(s2) ^ to_int(s1)) a_b = (rotl_short(s_xor, 10) & 0xFFFFFFFFFFFFFFFF) | ((s_rotl & 0xFFFFFFFFFFFFFFFF) << 16) d_val = (a_b << 16) & 0xFFFFFFFFFFFFFFFF e_val = to_short(to_int(rotl_short(s1, 13)) ^ to_int(s_xor)) g_val = to_short(to_int(e_val) ^ (to_int(s_xor) << 5)) return d_val | (g_val & 0xFFFFFFFFFFFFFFFF) def read_char(idx: int, chunks: list[list[int]], state: int) -> tuple[int, int]: chunk = chunks[idx // 8191][idx % 8191] nxt = next_state(state) res = ((chunk << 32) & 0xFFFFFFFF00000000) ^ nxt return res, nxt def decrypt_string(string_id: int, chunks: list[list[int]]) -> str: raw_id = string_id & 0xFFFFFFFFFFFFFFFF nxt1 = next_state(calc_seed(raw_id & 0xFFFFFFFF)) nxt2 = next_state(nxt1) part1 = (raw_id >> 32) ^ ((nxt1 >> 32) & 0xFFFF) part2 = (nxt2 >> 16) & 0xFFFF0000 start_idx = to_int(part1 ^ part2) header, current_state = read_char(start_idx, chunks, nxt2) length = (header >> 32) & 0xFFFF chars = [] for offset in range(length): ch_data, current_state = read_char(start_idx + offset + 1, chunks, current_state) chars.append((ch_data >> 32) & 0xFFFF) return struct.pack(f'<{len(chars)}H', *chars).decode('utf-16-le') def parse_java_chars(raw: str) -> list[int]: cleaned = re.sub(r'\\u([0-9a-fA-F]{4})', lambda m: chr(int(m.group(1), 16)), raw) escapes = {'\\n': '\n', '\\r': '\r', '\\t': '\t', '\\"': '"', '\\\\': '\\', "\\'": "'", '\\b': '\b', '\\f': '\f'} for src, dst in escapes.items(): cleaned = cleaned.replace(src, dst) chars = [] for ch in cleaned: cp = ord(ch) if cp <= 0xFFFF: chars.append(cp) else: cp -= 0x10000 chars.append(0xD800 + (cp >> 10)) chars.append(0xDC00 + (cp & 0x3FF)) return chars def escape_string(text: str) -> str: table = {'\\': '\\\\', '"': '\\"', '\n': '\\n', '\r': '\\r', '\t': '\\t'} res = [] for ch in text: if ch in table: res.append(table[ch]) elif ord(ch) < 32 or ord(ch) == 127: res.append(f'\\u{ord(ch):04x}') else: res.append(ch) return "".join(res) def load_chunks(target_dir: str) -> dict[str, list[list[int]]]: chunks_db = {} array_re = re.compile(r'String\[\]\s+\w+\s*=\s*(?:new\s+String\[\]\s*)?\{([^}]+)\}') str_re = re.compile(r'"((?:[^"\\]|\\.)*)"') for root, _, files in os.walk(target_dir): for file in files: if not file.endswith(".java") or "Helper" in file: continue path = os.path.join(root, file) try: with open(path, encoding="utf-8", errors="ignore") as f: content = f.read() except OSError: continue for match in array_re.finditer(content): raw_strings = str_re.findall(match.group(1)) if any(len(s) > 1000 for s in raw_strings): cls_name = file[:-5] chunks_db[cls_name] = [parse_java_chars(s) for s in raw_strings] return chunks_db def deobfuscate_dir(target_dir: str = "sources"): chunks_db = load_chunks(target_dir) if not chunks_db: return call_re = re.compile(r'([A-Za-z0-9_$.]+)?getString\s*\(\s*(?:\(\s*long\s*\)\s*)?(-?\s*\d+)L?\s*\)') ternary_re = re.compile(r'([A-Za-z0-9_$.]+)?getString\s*\(\s*([^?]+?)\s*\?\s*(-?\s*\d+)L?\s*:\s*(-?\s*\d+)L?\s*\)') for root, _, files in os.walk(target_dir): for file in files: if not file.endswith(".java") or "Deobfuscator" in file: continue path = os.path.join(root, file) try: with open(path, encoding="utf-8", errors="ignore") as f: content = f.read() except OSError: continue if "getString(" not in content: continue keys_to_try = [k for k in chunks_db if k in content] + [k for k in chunks_db if k not in content] def replace_call(m: re.Match) -> str: full_match, raw_id = m.group(0), m.group(2) try: str_id = int(raw_id.replace(" ", "")) if "Deobfuscator" not in full_match and abs(str_id) <= 0xFFFFFFFF: return full_match for key in keys_to_try: try: decrypted = decrypt_string(str_id, chunks_db[key]) return f'"{escape_string(decrypted)}"' except Exception: continue return full_match except ValueError: return full_match def replace_ternary(m: re.Match) -> str: full_match = m.group(0) condition = m.group(2).strip() try: id1 = int(m.group(3).replace(" ", "")) id2 = int(m.group(4).replace(" ", "")) dec1, dec2 = None, None for key in keys_to_try: if dec1 is None: try: dec1 = decrypt_string(id1, chunks_db[key]) except Exception: pass if dec2 is None: try: dec2 = decrypt_string(id2, chunks_db[key]) except Exception: pass if dec1 is not None and dec2 is not None: break if dec1 is not None and dec2 is not None: return f'{condition} ? "{escape_string(dec1)}" : "{escape_string(dec2)}"' return full_match except ValueError: return full_match new_content = call_re.sub(replace_call, content) new_content = ternary_re.sub(replace_ternary, new_content) if new_content != content: with open(path, "w", encoding="utf-8") as f: f.write(new_content) if __name__ == "__main__": deobfuscate_dir()
Написав простенький скрипт, который копирует их же логику из RandomHelper, мы мгновенно снимаем всю эту «непробиваемую» броню. И вот что мы видим под капотом парсера обновлений

ИИ, который «не думает»
Отдельного упоминания заслуживает их хваленый встроенный AI. В коде com/exteragram/messenger/ai/network/Client.java они написали свой собственный парсер для вырезания тегов … из ответов нейросетей!
Вместо того, чтобы нормально настроить API-запрос к моделям вроде DeepSeek так, чтобы они не возвращали процесс рассуждения, разработчики решили просто вырезать мысли нейросети по-живому прямо из стрингов:

Вы только представьте эту картину: нейросеть пыжится, генерирует сложные логические цепочки, тратит токены и время пользователя. А на стороне мобильного клиента сидит while (true) с захардкоженными индексами +7 и +8 и неистово выпиливает весь этот мыслительный процесс ржавым лобзиком. Искусственный интеллект, может, и умеет рассуждать, но пацанам из ExteraGram это не нужно — их помощник должен отвечать сразу, без раздумий. Иллюзия всезнания спасена костылем из четырех строчек!
Инжектнуть код в Telegram? Легко!
Я стал ковырять загрузчик Updater. Оказалось, обновы прилетают прямо из постов в Telegram-канале:

Все обновления скачиваются напрямую из постов. Если произойдет компрометация инфраструктуры (например, утечка токена бота, публикующего релизы, или угон сессии администратора канала), атакующий сможет опубликовать пост со своим вредоносным SDK.
Клиент не проверяет цифровую подпись скачанного архива и не валидирует ID чата источника (chat_id) на жестко зашитый константный канал. Он просто парсит пост и слепо жрет то, что прилетело — и этот бинарник улетит напрямую в телефоны сотен тысяч пользователей.
Но это еще не всё. Самое смешное в UpdaterUtils.java - это то, как выглядит сам диалог установки этого потенциального бэкдора. Пока ваш телефон распаковывает непроверенный APK, на экране принудительно крутится стикер утки:

Вас хакают, а на экране танцует утенок. Гениально.
Бэкдор “для своих”
В файле BadgesController.java (который выдает бейджики Trusted, Developer, Supporter) я нашел тупо захардкоженный ID пользователя:

То есть чувак с ID 2562664432 автоматически считается “Trusted” (доверенным). Любой плагин от него система проглотит без предупреждений. Вот это безопасность уровня enterprise!
Исполнение питона прямо в памяти
Но и это не самое страшное. Я пошел глубже и добрался до плагинов — PythonPluginsEngine.java. Разрабы решили, что скачивать обновления апк-шек им мало, поэтому они встроили загрузку питоновских плагинов (SDK) напрямую из Telegram-постов.

Код просто ищет посты с зашифрованными тегами python_sdk_stable через MTProto. И тут я заметил две вещи, из-за которых вся их защита просто обнуляется.
Нет проверки подписи
Я просмотрел всю цепочку и не нашел вообще никакой проверки того, кто сгенерировал этот архив. Тихая установка. В парсере апдейтов есть интересный флаг:

Чтобы доказать, что этот флаг приводит к принудительной установке в обход настроек пользователя, проследим за его использованием при проверке обновлений:

Это значит, что если в посте будет стоять can_not_skip=true, клиент вообще ничего не спросит. Диалоговое окно просто скипается, и архив улетает прямиком в savePythonSdkArchive. (Справедливости ради, для 100% подтверждения “тихой установки” нужен динамический патчинг ответа в рантайме, но статика говорит сама за себя).
В мире нормальной разработки флаг force_update используется для критических исправлений нулевого дня, и даже тогда пользователь получает уведомление. В ExteraGram этот флаг просто отключает кнопку “Отмена”, потому что кто-то очень не хочет, чтобы вы увидели вопрос “Вы точно хотите установить обновление?”. Демократия? Нет, это добровольно-принудительный апдейт с утёнком на фоне.
Затем архивы тупо перезаписывают старые файлы:

Кстати, загрузка исполняемого кода в обход Google Play напрямую нарушает политику Device and Network Abuse.
Доступ ко всему
Инъекция кода через MVEL Файл ClassProxy.java (строка 631) предоставляет метод executeMvelMethod, который выполняет Java-выражения через библиотеку org.mvel2.MVEL.

Я проанализировал полный стек вызовов от точки входа плагина до MVEL.executeExpression(). Ни на одном уровне не передаётся экземпляр ParserContext с ограничениями. Более того, в коде явно передаются obj и obj2 в качестве переменных java и python, что прямо указывает на намерение дать полный доступ. Если бы разработчики хотели безопасности, они бы вручную создали контекст (whitelist) и передали его в compileExpression. Этого нет.
Они не просто передают ссылку на инстанс приложения. Они буквально сказали MVEL: “Вот тебе, брат, полный доступ к JVM. Работай, я доверяю тебе, как родному”. Это не архитектура, это незащищенный секс с интернетом. Песочница? Нет, это открытое поле с табличкой «Здесь не воруют, честное слово».
PoC: Как плагин угоняет базу данных
Плагинам не нужно изощряться с хаками памяти. «Песочницы» в Android-плагинах по умолчанию нет. Плагины выполняются в едином адресном пространстве процесса приложения, с его UID, получая полный доступ к файловой системе (sandbox Android).
Если вы напишете Python-плагин для их клиента, вам достаточно дернуть executeMvelMethod через стандартный мост Chaquopy/JNI. Вот как выглядит элементарный Proof-of-Concept на Python:
# Минимальный рабочий пример внутри плагина ExteraGram from java.lang import Class from java.lang import Object from java.lang import String # Получаем класс через рефлексию (плагин уже в процессе Telegram!) ClassProxy = Class.forName("com.exteragram.messenger.plugins.utils.ClassProxy") # Формируем MVEL-нагрузку (читаем базу юзеров Telegram) mvel_payload = """org.telegram.messenger.MessagesStorage.getInstance(0).getDatabase().queryFinalized("SELECT uid, name FROM users LIMIT 1", new Object[0]);""" # Ищем метод method = ClassProxy.getMethod("executeMvelMethod", [Object, Object, String, Class.forName("[Ljava.lang.String;"), Class.forName("[Ljava.lang.Object;")]) # Выполняем. База в ваших руках. result = method.invoke(None, [None, None, mvel_payload, None, None]) print("Stolen DB Data:", result)
Но это ещё цветочки. Поскольку плагин работает внутри процесса мессенджера, он имеет доступ ко всем файлам приложения. Вы можете не просто читать переписку из базы, а напрямую скопировать файл tgnet.dat (или ключи авторизации MTProto) и отправить его на свой сервер. Получив эти ключи, атакующий может полностью угнать вашу Telegram-сессию и клонировать аккаунт на своем устройстве. Никаких СМС, никаких 2FA-паролей - просто полный перехват аккаунта.
Криптография уровня «невидимых пробелов»
Когда я смотрел, как они хранят секреты, я наткнулся на фантастическую вещь. Я нашел класс InvisibleEncryptor.java (буквально «Невидимый Шифратор»).
Этот кусок кода даёт 100% подтверждение моему выводу из начала статьи: код им реально писала нейронка. Разработчики и сами публично хвастались, что крупные изменения у них делаются “клодиком” (Claude AI).
Конечно, технически это примитивная обфускация и стеганография, а не криптография (не надо сравнивать это с AES). Но раз уж автор пафосно назвал класс InvisibleEncryptor, давайте судить его по всей строгости. Этот алгоритм берет секретный текст и конвертирует каждый байт… в невидимые Unicode-символы (zero-width spaces). Для школьного фокуса это прикольно, но для production-кода мессенджера — это позор.
Вот кусок из их YandexMapsProvider.java:
// Там внутри кавычек идут невидимые пробелы, в которых "спрятан" ключ private static final String YANDEX_API_KEY = InvisibleEncryptor.decode("\u2001\u2002\u206a\u200c\u2000\u206e...");
Их концепция защиты — это буквально: “превратим секретный ключ в невидимые пробелы, хакер откроет файл, увидит пустоту, решит, что у него сломался монитор, и уйдет в монастырь”. (Спойлер: если вы думаете, что \u2001 это какая-то хитрая защита — нет, это просто символ с названием “EM QUAD”. Он существует в Unicode уже лет 30. Очевидно, разработчики открыли Википедию, увидели слова “zero-width spaces” и решили, что теперь они криптографы).
Они на полном серьёзе прячут этим алгоритмом не только второстепенные API-ключи, но и бэкапы пользователей! Любому плагину с доступом к внутренним API достаточно просто дёрнуть этот же самый публичный метод InvisibleEncryptor.decode(), чтобы получить всё в открытом виде.
Самая ирония в том, что пока они прятали ключи от Яндекс.Карт в невидимые пробелы, главные API-ключи от Telegram (APP_ID и APP_HASH) и конфигурация Firebase лежат в абсолютно открытом виде в дефолтном BuildConfig.java и strings.xml. Бронированная дверь на кладовке со швабрами, а входная дверь в квартиру снята с петель.
DevServer: локальный сервер в проде
В PythonPluginsEngine.java есть метод, который запускает модуль:

В проде оставлен код запуска локального сервера (start_server). Разработчики могут возразить, что он закрыт флагом pluginsDevMode. Но у плагинов же есть MVEL. Любой загруженный скрипт может переопределить этот флаг прямо в памяти через рефлексию.
А если вы думаете: "Ну ок, сервер запустится, но как же порты и NAT? К телефону снаружи не подключиться!" - вы мыслите категориями защиты, а не атаки.
Вредоносному коду не нужно, чтобы вы подключались к нему снаружи. Сервер поднимается локально, на самом устройстве (обычно на 127.0.0.1 или 0.0.0.0). Плагин сам, находясь внутри защищенного периметра приложения, может инициировать обратное подключение (reverse shell) к серверу злоумышленника, обходя любые NAT и файрволы, или просто использовать этот локальный TCP-сокет для межпроцессного взаимодействия с другими малварями на устройстве.
Как защититься
Чтобы статья не была просто детективом, вот практическое руководство для разработчиков ExteraGram (и любых других форков), как исправить эту зияющую дыру:
Проверка цифровой подписи (Signature Verification). Любой скачанный архив должен валидироваться публичным ключом. Если подпись не сошлась, файл удаляется до распаковки.
Проверка источника (chat_id). Клиент должен проверять, что сообщение с обновлением пришло из канала с фиксированным ID, который нельзя подменить даже при компрометации аккаунта. Иначе злоумышленник может отправить сообщение от имени бота из другого чата.
Изоляция плагинов. Вместо MVEL с полным доступом к JVM используйте изолированный WebView с загруженным JavaScript, где через addJavascriptInterface вы контролируете, какие именно методы доступны (например, только работа с UI). Либо, если нужен тяжеловес, используйте V8 (LiquidCore).
Хранение секретов. Прекратите использовать невидимые пробелы. Ключи нужно прятать в NDK/JNI (нативный слой) или использовать Android Keystore.
Очистка релизов. Выпиливайте тестовые бэкдоры (DevServer) из production-сборок на этапе компиляции (например, через ProGuard/R8 правила).
Что в сухом остатке
Я собрал всё, что нашел, в одну картинку. У нас есть механизм тихой загрузки апдейтов. У нас есть выполнение этого кода без изоляции памяти. И у нас нет проверки того, что этот код вообще оригинальный.

Главная проблема здесь даже не сами ошибки. Ошибки делают все. Проблема в том, что авторы просто не понимают, как работает безопасность. Зато накрутили обфускаторов.
Когда архитектура содержит подобные фундаментальные дыры в доставке исполняемого кода, просьба к пользователям «просто отключить системный антивирус» выглядит как минимум самонадеянно. Искусственный интеллект может сгенерировать архитектуру, но он не обеспечит её безопасность.
Что делать, если вы пользовались ExteraGram
Для разработчиков советы были выше, а вот руководство к действию для обычных пользователей:
Немедленно удалите клиент со своего устройства.
Зайдите в официальный Telegram -> Настройки -> Конфиденциальность -> Активные сеансы -> Найдите сеанс ExteraGram и нажмите «Завершить сеанс». (Просто удалить приложение недостаточно, сессия остается активной на серверах Telegram, и в случае угона ключей к ней могут получить доступ).
Никогда не отключайте Google Play Protect по просьбе разработчиков сторонних клиентов.
P.S. Автор аудита - 15-летний разработчик. Этот разбор был выполнен исключительно в образовательных целях. Мораль: никакие OLLVM и LSParanoid не спасут вас, если ваша архитектура состоит из костылей, а безопасность ограничивается невидимыми пробелами. ИИ не написал ни слова в этой статье, но использовался для форматирования уже написанного текста
P.P.S. Отдельная благодарность lead-разработчику ExteraGram (@immat0x1) за искренние рассказы про разработку «клодиком», баны за технические вопросы — вы лучший мотиватор в моей жизни. Огромная благодарность за предоставленный билд: pixwet, bleizix, alexeyzavar, ireina, yzewe, hehcker, ampliation, iwill, kvuco, "i miss her".
UPD 1: Через час, автор официально ответил в чате exteraGram Forum.

"притянутая за уши статья", сочувствую автору если он не понимает собственную логику

То есть тим лид прямо признает что их система плагинов дырявое корыто. Вопрос к вам. Вы бы доверили ваш Telegram аккаунт проекту с таким человеком во главе?

Ставлю 10 баксов на то что они в следующем обновлении попытаются исправить некоторые баги(через тот же claude, хотя я думаю у них ии не додумает).
Комментарии (7)

alex1478
11.08.2026 15:37Всегда считал и продолжаю считать, что основная задача таких клиентов - скамить пользователя. Либо собирая ценную для автора информацию о пользователях, либо удовлетворяя вуайеристкие расстройства автора. Не удивлюсь, если окажется что в клиенте есть механизмы взаимодействия с командным сервером, например через секретный чат

Cating65801 Автор
11.08.2026 15:37Ну оно скачивает конфиги из личного телеграмм канала(@XSd6GEcz5ZXMu82UvXQc), и если есть обновление скачивает сдк, конфиг для приложения

veselcraft
11.08.2026 15:37Позор этому клиенту и их неповоротливым разработчикам. Общался с ними намедни, они непробиваемые, даже если ты юридически и логически прав. А раньше пользовался им и рекламил за деньги...

Andrey4ik
11.08.2026 15:37Как всегда разрабы экстеры и аюграма обосрались
Юзайте награм икс и жизнь будет лучше
Автору статьи респект!

yozora
11.08.2026 15:37Open-Source чудоэкстераграм. Вайбкод-решение с блекджеком и RCE... Так еще и потенциальный supply-chain в меру того что это чудо вайбкод. После новостей со всякими NekoGram, CherryGram, [вставить слово]Gram, которые то сливают номера, то воруют сессии, доверять стоит исключительно официальному клиенту. Официально разработчик заявлял что исходный код закрыт для того чтобы всякие гении не копировали клиент с их аналоговнетовыми функциями и не вшивали туда вирусы. И вот у них через расширения от "разных" разработчиков могут быть вирусы. Только этими самыми "разными" разработчиками могут быть и сами владельцы клиента ;)
P.S. 28 января ничего не произошло
llimonix
Интересная статья, спасибо автору. Экстере стоит позаботиться о защите клиента и не хвастаться тем, что они используют клодик, который не имеет критического мышления и допускает кучу неточностей не видя всей картины целиком.
Можно ли считать данный клиент очередной жертвой вайбкодинга?