Кампания в npm: пакеты-клоны математических библиотек управляются через блокчейн

Атака на цепочку поставок NPM

Несколько вредоносных пакетов в реестре npm оказались звеньями одной операции. Она идёт с марта 2026 года. Цель - разработчики, занятые количественным анализом и проектами в сфере DeFi (децентрализованные финансы). Внешне всё выглядит как череда разрозненных инцидентов: разные имена, разные учётные записи, разные сроки публикации. Общая картина проступает лишь при сопоставлении деталей.

Описание

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

Ключ искали в самом поведении библиотеки. Загрузчик срабатывает не у всех, кто установил пакет, а лишь при одном узком условии. Он запускается после того, как разработчик решит систему линейных уравнений - рутинную задачу для тех, кто работает с численными методами. Из результата вычисления формируется пароль, из пароля выводится ключ, и только он открывает зашифрованные данные. Любая другая матрица даёт сбой проверки подлинности. Ошибка гасится внутри, а код завершается так же, как при обычной неудаче. Ответ разработчика при этом остаётся верным: вредоносная вставка не меняет результат. Подобрать нужную матрицу удалось перебором с опорой на проверку целостности данных.

Похожий пакет использовал ту же цепочку, но с другим условием запуска. Там ключ сверялся с исходными данными вызывающего кода, а не с результатом вычисления. Случайное совпадение исключено, поэтому входные данные жертве кто-то передавал извне - например, в виде демонстрационного скрипта. Через неделю после публикации вышла версия, где загрузчик тихо убрали.

Исследование JFrog показало, что дальше в дело вступает механизм закрепления. Полученный код переселяется в фоновый процесс и продолжает работу после того, как скрипт разработчика завершится. Один канал связи ведёт в корпоративный мессенджер, второй устроен сложнее.

Основным каналом управления служит не сервер, а смарт-контракт в тестовой сети Ethereum Sepolia. У контракта нет хостинга и провайдера, отключать его некому, а развёртывание в тестовой сети ничего не стоит. За полгода злоумышленники развернули четырнадцать контрактов с пяти кошельков. Все они опубликованы под тремя именами. Поиск по этим именам в обозревателе блокчейна выдаёт каждое развёртывание независимо от автора. Тринадцать вариантов исходного кода переписывали и компилировали заново. В июне три контракта работали параллельно под разными кошельками.

Контракт работал как тайник с задачами. Через него прошло более тысячи заданий, сохранённых в виде двух с лишним тысяч зашифрованных пакетов. Каждый запечатан ключом, который могут вывести только две стороны: оператор и конкретная заражённая машина. Открыть пакеты не удалось никому, кроме адресатов. Полезные нагрузки отправляли одной сборкой сразу на множество машин. Один документ объёмом около пятнадцати килобайт ушёл шестьдесят четыре раза за пять дней.

Второй канал работает через мессенджер Slack. Отдельный агент опрашивает нужный канал каждые десять секунд и реагирует только на сообщения одного конкретного пользователя. Команда приходит как текст и собирается на машине жертвы из нескольких частей, поскольку мессенджер ограничивает длину сообщения. Отдельная команда служит сигналом к самоудалению: агент стирает свой след, останавливает родительский процесс и завершается. К тому же ключи двух каналов независимы, поэтому взлом одного ничего не даёт для другого.

Масштаб аудитории обеспечивала ферма загрузок на инфраструктуре GitHub. Три публичных аккаунта держали по десять репозиториев с одинаковым набором файлов: зашифрованный список целей, драйвер и декодер. Каждая сборка скачивала нужный пакет заданное число раз с интервалом в секунду. Полученные данные сразу выбрасывались. Запрос нужен был только для того, чтобы система учёта зафиксировала загрузку. Список целей зашифровали, чтобы репозиторий не называл жертв открыто, но ключ положили в тот же репозиторий. По восстановленным сведениям, отдельные пакеты накрутили на десятки миллионов загрузок. Помимо этого, минимум один пакет с двумя с половиной миллионами загрузок раскрутили иным способом, которого найти не удалось.

Атрибуция опирается на редкие совпадения. Злоумышленники заводили отдельную учётную запись под каждый пакет, чередовали почтовые сервисы и бросали аккаунты сразу после удаления пакета. Выдал их один псевдоним, использованный на разных платформах. Публичные репозитории заражённых пакетов оказались клонами настоящих проектов с сохранённой историей правок. Тысячи коммитов оригинальной библиотеки остались на месте. Вредоносный код в них ни разу не попал: его добавляют только на этапе публикации. Проверка репозитория поэтому ничего не показывает.

Ни одну похищенную полезную нагрузку расшифровать не удалось. Что именно поручали заражённым машинам, неизвестно. Защита строится на нескольких простых правилах. Если в проекте используется математическая библиотека, проверьте, нет ли в пакете файлов, которых нет в оригинале. Признаком состоявшегося заражения служит посторонняя строка в лицензионном файле. На стороне реестра надёжный сигнал - миллион загрузок при полном отсутствии зависимых пакетов. Рабочую станцию, где мог сработать триггер, разумно считать скомпрометированной и сменить все доступные с неё учётные данные. Операторы публикуют чистую версию, а вооружают её примерно через сутки, поэтому свежий пакет без вредоносных вставок ещё нельзя считать безопасным.

Индикаторы компрометации

Package

  • @andrewstory18/is-real-odd 2.0.3
  • @ignacionunez91/keccak24 1.0.7
  • @oliviamcdaniel12/safer-buffer 2.2.0, 2.2.1
  • crypto-hasher 3.1.2, 3.1.3
  • events-channel 2.3.1, 2.3.2, 2.4.1
  • events-router 2.1.4
  • events-runtime 3.1.3, 3.2.0, 3.2.1, 3.2.3, 3.2.4, 3.3.0
  • graphcore-js 2.3.2, 2.3.4, 2.4.1, 2.4.2
  • graphlib-js 1.2.0, 1.2.1, 1.2.2, 1.3.2, 1.3.3, 1.3.4
  • indexed-btree 2.1.2
  • mathmain 1.0.0
  • mathsbase 1.0.1
  • math-universe 1.0.0, 1.0.1, 1.0.2
  • matrixflow-js 3.2.1
  • matrixkit-js 1.0.0
  • modern-events 1.3.3, 1.3.4, 1.4.0, 1.4.1, 1.4.2, 1.4.3, 1.4.4, 1.5.0, 1.5.1, 1.5.2
  • mutex-core 2.1.2
  • mutex-forge 2.0.0, 2.0.1, 2.0.2
  • mutex-lite 1.4.2
  • mutex-plus 3.0.2
  • mutex-thread 1.3.0
  • ordered-btree 3.2.2
  • quick-events 2.1.3
  • sort-btree 2.1.6, 2.2.0

Ethereum Sepolia

  • 0x2707BbD5CF9b2F7cE37e4913BADC0349272d48cf
  • 0x2a8536AdA44816Cb049778d24b75d795987D90b1
  • 0x315d47b401aC29e97893dec1c85aBBB2791ef2aA
  • 0x475B8659A33FB0C2bDB4c60ffdb92bC24d5c4015
  • 0x4b225b742eb31AD0DfD6bc00F8C55650b4394c18
  • 0x4C0c9B92BB9647fAf45022BA55f02bB0950196de
  • 0x661e50E19f05E3c0d04fD75891456D1F0A24508D
  • 0x906f019AC38bB572a8f7d8b2cA662D624EDebaff
  • 0x9E4dF8F253Eb439dd9538Fda30cC03B38fD3a631
  • 0xc0445F1b679DC46280A0f03F451bdf613b5A0feA
  • 0xd5A4620278788AcCf388B7Ea5f35f849465728E7
  • 0xDe34270921D37124fd0b64ea3aE4c5062B467418
  • 0xE390863Dac96a7118C71227C2b099B50cF602D31

Base Sepolia

  • 0xac0bfC4C48A679b667732128278EACBA1c191894

Telegram

  • 8717417715:AAGZ-24bqk9QAUh chat -1003968723972
  • 8961878831:AAG4WTbRUcbXI5U chat -1003952553968, -1004489630130
  • 8836581068:AAF1e0v1nbMEShE operator monitoring tool, alerts to @server_alert_0630

Комментарии: 0