惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - 三生石上(FineUI控件)
雷峰网
雷峰网
爱范儿
爱范儿
P
Proofpoint News Feed
Security Archives - TechRepublic
Security Archives - TechRepublic
Latest news
Latest news
The Hacker News
The Hacker News
Cyberwarzone
Cyberwarzone
博客园 - 【当耐特】
Project Zero
Project Zero
小众软件
小众软件
T
Tailwind CSS Blog
量子位
博客园 - 聂微东
I
Intezer
美团技术团队
S
SegmentFault 最新的问题
T
Tor Project blog
Spread Privacy
Spread Privacy
V
Vulnerabilities – Threatpost
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Jina AI
Jina AI
罗磊的独立博客
B
Blog RSS Feed
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
T
Troy Hunt's Blog
有赞技术团队
有赞技术团队
Google DeepMind News
Google DeepMind News
宝玉的分享
宝玉的分享
C
Cisco Blogs
L
LINUX DO - 热门话题
Last Week in AI
Last Week in AI
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
AI
AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Microsoft Azure Blog
Microsoft Azure Blog
L
LINUX DO - 最新话题
Know Your Adversary
Know Your Adversary
GbyAI
GbyAI
Engineering at Meta
Engineering at Meta
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Recent Commits to openclaw:main
Recent Commits to openclaw:main
L
Lohrmann on Cybersecurity
The Register - Security
The Register - Security
L
LangChain Blog
博客园 - 叶小钗
T
Tenable Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Не угодили «Лаборатории Касперского»: как интеграция с Telegram превратила ZentrySpace во вредоносное ПО
KatNoName20 · 2026-05-05 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение8 мин

Охват и читатели712

Кейс

Не успели мы анонсировать долгожданную интеграцию ZentrySpace с Telegram, как случилось то, к чему нас жизнь точно не готовила — зловещее уведомление у скачивающих от «Лаборатории Касперского» о наличии трояна в приложении. По мотивам недавних реальных атак в Telegram, в борьбе с которыми Касперский преуспел, наши потенциальные пользователи, конечно же, насторожились. После получения серии отзывов о том, что ZentrySpace вредоносный и подозрительный, мы начали разбираться в том, что же могло пойти не так.

Скрытый текст

Спойлер: даже Telegram Desktop периодически получает false positives от антивирусов. Мы к нему и присоединились.

Контекст: что мы делаем и что изменили

ZentrySpace — десктопное приложение на Electron (TypeScript + React). Мы добавили интеграцию с Telegram через официальную библиотеку TDLib (Telegram Database Library) — ту самую, на которой работает официальный Telegram Desktop. Для работы с ней из Node.js используется пакет tdl.

TDLib поставляется в виде нативной разделяемой библиотеки под каждую платформу: tdjson.dll на Windows, libtdjson.dylib на macOS, libtdjson.so на Linux. Размер бинарника — около 30 МБ.

Архитектура: как TDLib живёт в Electron-приложении

Electron-приложение состоит из нескольких типов процессов. Кратко:

  1. Main process — Node.js, управляет окнами, системными API, доступом к ФС

  2. Renderer process — Chromium, рендерит UI, изолирован от системы

  3. Utility process — изолированный Node.js-процесс для тяжёлых/нативных задач

Изначально мы запускали TDLib прямо в main process. Это самый простой путь.

Первоначальная реализация: TDLib в main process

Вся инициализация происходила при старте приложения. Сначала резолвим путь к нативной библиотеке в зависимости от платформы и окружения:

// main.ts
function getTdjsonFileName(): string {
  switch (process.platform) {
    case 'darwin': return 'libtdjson.dylib';
    case 'win32':  return 'tdjson.dll';
    case 'linux':  return 'libtdjson.so';
    default:       return 'libtdjson.so';
  }
}
function resolveTdjsonPath(): string | null {
  if (app.isPackaged) {
    // Production: берём из ресурсов приложения
    return path.join(process.resourcesPath, 'tdlib', getTdjsonFileName());
  }
  // Development: берём из vendor/
  const platform = process.platform;
  const arch = process.arch;
  let subDir: string | null = null;
  if (platform === 'darwin') {
    subDir = arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
  } else if (platform === 'win32') {
    subDir = arch === 'ia32' ? 'win32-ia32' : 'win32-x64';
  } else if (platform === 'linux') {
    subDir = arch === 'arm64' ? 'linux-arm64' : 'linux-x64';
  }
  if (!subDir) return null;
  return path.join(app.getAppPath(), 'vendor', 'tdlib', subDir, getTdjsonFileName());
}

Далее создаём сервис и инициализируем его в методе initializeExternalInstances():

private initializeExternalInstances() {
  const userDataPath = path.join(app.getPath('userData'), 'telegram');
  const tdjsonPath = resolveTdjsonPath();
  this.telegramService = new ElectronTdLibClientService({
    dbBaseDir: userDataPath,
    tdjsonPath: tdjsonPath ?? undefined
  });
}

Сам ElectronTdLibClientService при первом вызове init(accountId) конфигурировал tdl и создавал TDLib-клиент напрямую в main process:

// Старая версия ElectronTdLibClientService
import * as tdl from 'tdl'
private async configureTdLibOnce(): Promise<void> {
  if (this.configured) return;
  tdl.configure({ tdjson: this.options.tdjsonPath, verbosityLevel: 1 });
  this.configured = true;
}
async init(accountId: string): Promise<void> {
  await this.configureTdLibOnce();
  const dbDir = path.join(this.dbBaseDir, accountId);
  const client = tdl.createClient({
    apiId: Number(TG_API_ID),
    apiHash: TG_API_HASH,
    databaseDirectory: dbDir,
    filesDirectory: path.join(dbDir, 'files'),
    tdlibParameters: {
      use_message_database: true,
      use_chat_info_database: true,
      system_language_code: 'en',
      device_model: 'Zentry Desktop',
      application_version: '1.0.0',
    },
  });
  client.on('update', (update) => {
    this.emit('update', { type: 'raw-tdlib-update', payload: update });
  });
  this.clients.set(accountId, client);
}

Handlers обращались к клиенту через TelegramClientContext, который изолировал их от прямой зависимости на tdl:

// handlers/TelegramClientContext.ts
interface TelegramClientContext {
  getClient(): Promise<TdClientLike>;
  readonly service: ElectronTdLibClientService;
  readonly accountId: string;
}
// пример использования в TelegramAuthHandler.ts
async getAuthState(ctx: TelegramClientContext) {
  const client = await ctx.getClient();
  return client.invoke({ _: 'getAuthorizationState' });
}

Всего таких вызовов client.invoke() — более 30 штук: получение чатов, отправка сообщений, загрузка медиа, авторизация и т.д. Все они исполнялись в main process.

Что именно Касперский заблокировал и почему

Детекция называется PDM:Trojan.Win32.Generic — это срабатывание модуля PDM (Proactive Defense Module). Это принципиально важно: PDM — поведенческий анализ. Он смотрит не на сигнатуры и не на сертификаты, а на то, что процесс делает в runtime.

При инициализации TDLib происходит следующее:

  1. Приложение вызывает LoadLibraryW на tdjson.dll из нестандартной директории (resources/tdlib/)

  2. DLL создаёт SQLite-базы данных в %AppData%\ZentrySpace\telegram\{accountId}\

  3. DLL немедленно открывает зашифрованные TCP-соединения с серверами Telegram (MTProto-протокол)

  4. Всё это происходит за 1–2 секунды после запуска

Именно такой паттерн характерен для банковских Троянов: подгрузить DLL → записать данные в AppData → установить зашифрованный канал с C2-сервером. PDM видит этот паттерн и блокирует — не потому что наш файл плохой, а потому что поведение неотличимо от реального Трояна.

Попытка исправить архитектурой: TDLib в Utility Process

Мы предположили, что проблема в том, что main process — «сердце» приложения — напрямую загружает нативную DLL и открывает сеть. Electron предоставляет специальный механизм для таких случаев — Utility Process: изолированный дочерний Node.js-процесс без доступа к UI-API.

Идея: вынести TDLib в отдельный worker, а в main process оставить только IPC-прокси.

Создали tdlib-worker.ts — полноценный изолированный процесс:

// telegram/tdlib-worker.ts
let tdl: typeof import('tdl') | null = null;

// Ленивый импорт — tdl не загружается пока не придёт сообщение 'configure'
async function loadTdl(): Promise<typeof import('tdl')> {
  if (!tdl) tdl = await import('tdl');
  return tdl;
}

const clients = new Map<string, any>();
let configured = false;
const parentPort = process.parentPort!;

// Конфигурируем TDLib и загружаем нативную библиотеку
async function handleConfigure(msg: { tdjsonPath: string; verbosity: number }) {
  if (configured) { send({ type: 'configured' }); return; }
  const lib = await loadTdl();
  lib.configure({ tdjson: msg.tdjsonPath, verbosityLevel: msg.verbosity });
  configured = true;
  send({ type: 'configured' });
}

// Создаём TDLib-клиент для аккаунта
async function handleCreateClient(msg: { accountId: string; apiId: number; apiHash: string;
  databaseDirectory: string; filesDirectory: string; useTestDc: boolean; tdlibParameters: any }) {
  const lib = await loadTdl();
  const client = lib.createClient({
    apiId: msg.apiId,
    apiHash: msg.apiHash,
    databaseDirectory: msg.databaseDirectory,
    filesDirectory: msg.filesDirectory,
    useTestDc: msg.useTestDc,
    tdlibParameters: msg.tdlibParameters,
  });
  // Все обновления пробрасываем в main process через IPC
  client.on('update', (update: any) => {
    send({ type: 'update', accountId: msg.accountId, payload: update });
  });
  clients.set(msg.accountId, client);
  send({ type: 'client-created', accountId: msg.accountId });
}

// Пробрасываем invoke()-вызовы к TDLib API
async function handleInvoke(msg: { id: string; accountId: string; params: any }) {
  const client = clients.get(msg.accountId);
  try {
    const result = await client.invoke(msg.params);
    send({ type: 'invoke-result', id: msg.id, result });
  } catch (err) {
    send({ type: 'invoke-error', id: msg.id, error: String(err) });
  }
}

В main process ElectronTdLibClientService превратился в менеджер воркера с proxy-клиентом:

// ElectronTdLibClientService.ts (новая версия)
import { utilityProcess, type UtilityProcess } from 'electron/main';

export class ElectronTdLibClientService extends EventEmitter {
  private worker: UtilityProcess | null = null;

  private ensureWorker(): void {
    if (this.worker) return;
    const workerPath = path.join(__dirname, 'tdlib-worker.js');
    // Запускаем TDLib в изолированном utility process
    this.worker = utilityProcess.fork(workerPath);
    this.worker.on('message', (msg) => this.handleWorkerMessage(msg));
    this.worker.on('exit', (code) => {
      console.error('[TDLib Worker] exited with code', code);
      this.worker = null;
      // Отклоняем все pending вызовы
      for (const [id, { reject }] of this.pendingInvokes) {
        reject(new Error('TDLib worker process exited'));
      }
    });
  }

  async init(accountId: string): Promise<void> {
    this.ensureWorker();
    // Конфигурируем воркер при первом вызове
    if (!this.workerConfigured) {
      this.sendToWorker({ type: 'configure',
        tdjsonPath: this.options.tdjsonPath, verbosity: 1 });
      await this.configuredPromise; // ждём подтверждения
    }
    // Просим воркер создать клиент
    this.sendToWorker({ type: 'create-client', accountId, apiId: ..., apiHash: ...,
      databaseDirectory: dbDir, filesDirectory: filesDir, ... });
    await createdPromise;
    // Создаём proxy-объект — все invoke() уйдут в воркер через IPC
    const proxyClient = new TdLibProxyClient(accountId,
      (msg) => this.sendToWorker(msg), this.pendingInvokes, this.invokeIdCounter);
    this.clients.set(accountId, proxyClient);
  }
}

TdLibProxyClient реализует тот же интерфейс TdClientLike, что и настоящий tdl-клиент — все 30+ хендлеров не потребовали изменений:

class TdLibProxyClient implements TdClientLike {
  async invoke(params: Record<string, any>): Promise<unknown> {
    const id = String(++this.idCounter.value);
    return new Promise((resolve, reject) => {
      this.pendingInvokes.set(id, { resolve, reject });
      // Отправляем в utility process, ждём invoke-result/invoke-error
      this.sendToWorker({ type: 'invoke', id, accountId: this.accountId, params });
    });
  }
}

Почему даже это не помогло

После рефакторинга Касперский продолжил блокировку. Причина проста: PDM анализирует всю цепочку процессов, а не только главный.

Он видит следующую картину:

  1. ZentrySpace.exe запускает дочерний процесс (utility process)

  2. Дочерний процесс загружает нативную DLL

  3. Дочерний процесс открывает зашифрованные сетевые соединения

С точки зрения поведенческого анализа это даже более подозрительно — родительский процесс скрывает вредоносную активность за дочерним. Именно такую технику используют троянские загрузчики.

Что ещё точно не поможет

  • EV-сертификат (проверено на себе).

  • Регистрация приложения в реестре Windows — антивирусники не проверяют это.

  • Обфускация (преднамеренное усложнение кода для затруднения его аналитики)  — будет только хуже.

С этой же PDM:Trojan.Win 32.Generic сталкивались Rocket.Chat, Jitsi Meet, OpenCode — у всех действующие EV‑сертификаты, у всех одна и та же проблема.

У официального Telegram Desktop этой проблемы нет по двум причинам: TDLib там статически слинкован в главный исполняемый файл (нет отдельной DLL, нет LoadLibraryW), и у Telegram годами накопленная репутация в облачной базе Касперского (KSN). У нас ни того, ни другого.

Как решили проблему мы

Шаг 1: Отправка файла через Virusdesk

Через форму virusdesk.kaspersky.com отправили исполняемый файл с описанием: что это за приложение, почему ему нужна TDLib, почему такое поведение легитимно. Ответ пришёл в течение нескольких дней, false positive подтверждён.

Шаг 2: Технический отчёт с TRACE‑файлами

Для PDM‑детекций Касперский отдельно просит собирать и присылать trace‑файлы: gsf.trace и avp.trace. Именно они позволяют аналитикам понять конкретный поведенческий паттерн и добавить точечное исключение в базу. Инструкция: support.kaspersky.com/common/diagnostics/15898

Шаг 3: Программа Allowlist

Подали заявку в kaspersky.com/partners/allowlist‑program. После включения приложения в программу оно автоматически получает кредит доверия во всех продуктах Kaspersky, и проблема не повторится при обновлениях.

Таким образом, нащ продукт абсолютно безопасен, а вы теперь знаете, как не попасть в подобную ловушку.