Задумывались ли вы о том, что находится внутри зависимостей, которые так или иначе подтягиваются в ваш код? Взять чужую библиотеку сейчас — норма жизни, но чем это обернется с точки зрения безопасности?

Последние истории с node‑ipc и CTX заставили задуматься о том, что лежит внутри этих репозиториев. Оказалось, не только легитимный код. Там есть и попытки заработать без особых усилий, просто собирая информацию, и даже полноценные стиллеры. Причем негативных изменений стало больше после известных событий.

Почему мы обратили внимание на зависимости

Меня зовут Леонид Безвершенко, я — младший исследователь угроз информационной безопасности в команде Глобального центра исследований и анализа угроз (GReAT) подразделения Threat Research «Лаборатории Касперского». Это исследование, а также наш инструмент для поиска закладок в репозиториях и, конечно, сама статья написаны в соавторстве с моим коллегой по GReAT, главным экспертом по исследованию угроз информационной безопасности Игорем Кузнецовым.

В марте 2022 года мы с Игорем решили отвлечься от привычного исследования бинарщины — рассмотрения apt, реверса malware — и заглянуть в opensource. В этом направлении нас подтолкнула новость о node-ipc, который используется в большом количестве других пакетов (в том числе в Unity Hub) в качестве зависимости.

17 марта появилась информация, что пакет удаляет файлы у пользователей, IP которых бьется как русский или белорусский.

Когда в проблеме начали разбираться и в конечном счете обратились к автору, тот сначала отмалчивался, а потом заявил, что это его способ выразить протест. Иными словами, это не история взлома, а пример того, как автор сам злонамеренно превратил свой пакет в троян. 

Мы предположили, что подобные истории наверняка можно найти и в других opensource-пакетах. Эта идея лежит на поверхности. Выполняя install, мы особо не смотрим, что скачиваем. А все распространенные современные языки поддерживают парадигму «скачай и запусти какую-нибудь зависимость».

  • Node: “node install%package%”

  • Python: “pip install%package%”

  • Rust: curl …rustup.sh | /bin/sh; cargo run

  • Go: go get github.com/… В этом случае пакет скачивается с GitHub и можно посмотреть на него до того, как он соберется. Но вряд ли кто‑то так делает…

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

Например, в Python есть пакет, который должен заниматься математикой. Но с вероятностью 1 к 100 он открывает у пользователя порносайт.

Спустя некоторое время по секьюрити комьюнити прошла еще одна история о Rust-пакете rustdecimal. Пакет скопировали с опечаткой в имени, добавив в новую версию malware. Автор оригинального пакета был в курсе, но сообщество не знает, как работать с такими атаками.

Так мы пришли к идее проверки opensource-пакетов, которые часто подтягивают в качестве зависимостей.

Там должно быть что-то…

Идея была довольно простая. Надо собрать все пакеты, изменившиеся с февраля 2022 года (потому что это горячее время), посмотреть разницу с предыдущей версией и проанализировать в поисках закладок. Изначально было понятно, что правила детектирования придется исправлять, поэтому обнаруженные изменения мы решили складывать в базу, чтобы потом их можно было легко пересканировать.

 Под эту идею мы расчехлили свою заначку — виртуальную машину с 4 ТБ дискового пространства:

  • 4 ТБ HDD

  • 16 ГБ RAM

  • Ubuntu

  • Python & PostgreSQL

Мы решили начать с двух репозиториев: npm (node.js) и PyPI (python) — они довольно популярные, да и в новостях уже появлялась информация о находках.

Обобщенная архитектура написанного нами сервиса выглядит так:

Четыре модуля работают асинхронно:

  • Первый взаимодействует с API репозиториев (npm или PyPI в нашем случае). Он берет данные о новых релизах пакетов — сверяет не только сами версии, но и описание, ищет новых контрибьюторов и т. п.

  • Второй скачивает новый релиз.

  • Третий сравнивает новую и предыдущую версии пакетов. Это классический инструмент Diff. Назовем его Differ.

  • Четвертый — сканер. Мы подобрали набор хантинговых правил, который нам действительно помог.

Но не все так просто…

Прежде чем рассказывать о находках, остановимся на сложностях, с которыми пришлось столкнуться.

Первая проблема заключалась в том, что, когда мы собрали весь этот асинхронный сетап, он стал работать слишком быстро. Нас начали банить.

Пришлось различными эвристическими способами подбирать sleep.

Мы с этим справились и даже не сильно замедлились. Вышли на поток. Задача была — получать обновления первыми, чтобы что-то зарепортить до того, как пакет скачают другие пользователи.

Следующая проблема скрывалась в Differ-е. На одном из пакетов размером не более 900 МБ он завис на несколько часов. А ведь это был не самый большой размер пакета — встречаются файлы по 2 или 3 ГБ. Мы прогнали на этом пакете обычный Diff из Linux, и тот отработал за несколько секунд.

Мы предположили, что проблема в нашем коде и его надо дебажить. Но после всех изысканий оказалось, что виновата стандартная библиотека Python — difflib, которая сравнивает файлы и прочие объекты. Оказывается, она не оптимизирована и для нашей задачи не подходит — с ней мы просто не смогли бы выйти на поток.

В качестве альтернативы мы нашли diff-match-patch от Google. Сами ее не тестировали, но поверили комментатору со Stackoverflow, который заявлял, что она в 20 раз быстрее. 

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

Следующая проблема, с которой мы встретились, также была связана с Differ-ом.

Мы увидели, что наш робот поднимается и падает из-за того, что не хватает оперативной памяти. Размер пакета — 186 МБ, что странно. Но когда мы его разархивировали, там оказалось 3 ГБ. Пакет назывался ‘similar-persian-words’ и, как ни странно, содержал в себе JSON со всеми персидскими словами. Им действительно пользуются, у него есть загрузки.

Понятно, что здесь была проблема арифметики. У нас было 16 ГБ оперативной памяти, а размер пакета — 3 ГБ. Итого: 3 ГБ — предыдущая версия, 3 ГБ — новая версия и еще сам Differ требовал 3 ГБ. После этого анализа мы нашли и исправили ошибку в своем коде — он не очень правильно считал хеш от разницы (не чанками). Естественно, пришлось и объем памяти на виртуальной машине увеличить.

Что мы обнаружили

А теперь поговорим про найденные проблемы.

Всего за 5 месяцев, с февраля 2022 года, в двух репозиториях мы проанализировали 2 184 498 пакетов. Из них:

  • 97% — npm

  • 3% — PyPI

По графику ниже видно, что разработчики open source по выходным отдыхают. А разработчики PyPI на фоне разработчиков npm отдыхают почти всегда.

 Еще немного статистики:

  • мы скачали 4 873 242 уникальные версии пакетов;

  • в хранилище в сжатом виде пакеты вместе с изменениями занимают 6 ТБ.

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

Наш набор правил (в том числе на обфускацию) обнаружил 6200 подозрительных изменений. Это примерно 0,1% от всех проанализированных расхождений. Сюда включены все выявленные в пакетах политические лозунги, проявления активизма, удаление файлов и т. п.

Мы посмотрели вглубь и нашли довольно странные вещи.

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

Выглядит подозрительно. Все бы ничего, если бы не домены, куда это все отсылается, — interactsh, burpcollaborator или pydream. Возможно, читателю они знакомы. Это бесплатные эндпоинты для разработчиков и тестеров.

Есть и более подозрительные вещи, в частности скрипты конкретно под Windows, которые собирают еще больше информации, вплоть до наличия административных прав у пользователя. 

Этот скрипт тоже отсылает всю информацию, которую только можно запаковать, но никакого дополнительного кода не исполняет. Понятно, что следующая версия могла прийти уже с полной нагрузкой. Но тем не менее чаще мы видели только такие «отстуки».

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

В качестве объяснения своего поступка он даже приложил ссылку, где за подобное заплатили 3 тысячи долларов. 

Помимо любителей такого заработка мы нашли огромное количество пакетов — 1915 штук — которые так или иначе обфусцированы. В основном это obfuscate.io.

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

После фильтрации осталось 638 пакетов от горе-хакеров.

Среди них есть и вполне «безобидные» истории, которые легко отлавливаются одним из правил. Например, часто эти горе-хакеры используют бесплатные хостинги для питон-скриптов, чтобы реализовать админку, собрать информацию и пойти с ней к вендору.

Но дальше появились более интересные находки.

Больше всего (60+) мы нашли программ, которые воруют кредиты Discord. Понятно, что до кучи они собирают информацию по кредитным картам, троянят сам Discord. Но почему-то пользователи этого мессенджера — основная таргет-аудитория.  

Еще одна интересная находка была в удаленных пакетах.

Как правило, в новостях об обнаруженном malware публикуется целый список пакетов, подробно в которые никто не смотрит. Мы наткнулись на такой python-пакет aiohttp-proxies-forked, благо у нас он был уже отложен к моменту выхода публикации (и удаления пакета). Внутри мы обнаружили прекрасный асинхронный бэкдор под Windows, причем завернутый не в exe (https://blog.sonatype.com/this-week-in-malware-npm-malware-exfiltrates-windows-sam-amazon-ec2-credentials). 

 Он был написан на Python.

 Логирование в этом пакете на чистейшем русском.

Более свежая находка — и, пожалуй, одна из самых интересных — пакет, загружающий полноценный стиллер.

Прочитав очередной отчет Checkmarx с большим количеством вредоносных пакетов, мы обновили свои правила и просканировали все, что было обнаружено к этому моменту. И таким образом наткнулись на два пакета, которые были гораздо интереснее всего списка Checkmarx, — ultrarequests и pyrequests. Они мимикрируют под один из самых известных пакетов — requests.

Фокус заключался в том, что злоумышленник полностью скопировал описание легитимного пакета, так что на сайте PyPi мы видим оригинальную статистику — 48 тысяч звезд GitHub, 8 тысяч форков, 230 миллионов скачиваний. Увидев эту статистику, человек подумает, что ничего плохого в пакете быть не может. 

Внутри этот пакет почти ничем не отличается от оригинального requests, кроме одного файла — exceptions.py. В нем в одном из классов ошибок сделан злой прелоад в base64, который декодируется и загружает стиллер. Он, в свою очередь, ворует данные кредитных карт, ищет файлы с паролями, ворует cookie и сохраненные пароли и т. п.

Мы сразу же сообщили об этом пакете в PyPi, но быстрого ответа не получили. Зато когда написали твит, он разлетелся очень быстро (https://twitter.com/bzvr_/status/1557807795877171201).

После того как мы сообщили о пакете в базу открытых уязвимостей Snyk.io, его почти сразу забанили. А на PyPi пакет прожил две недели. Только потом нам ответили из ассоциации Python, что очень нам благодарны за находку, и удалили его. 

Любопытно, что по графику скачиваний этого пакета после твита видно, что его популярность выросла почти на 50%. То есть мы в принципе его разрекламировали.

Что мы будем делать дальше

Наше исследование — лишь вершина айсберга. Мы попробовали текстовый diff, нашли некоторое количество закладок. Но мир open source огромен. Плюс есть еще много языков, которые мы не успели подключить, — для начала это Rust и Golang. И мы уже работаем над этим.  

Следующая идея — следить за всем GitHub. И это бесконечная история. Нам каждый день прилетают какие-то детекты, с ними нужно что-то делать, но рук не хватает. Хочется сказать: «Горшочек не вари», но мы продолжаем. Уверен, что у нас еще будут интересные находки, и именно вы можете стать тем, кто их обнаружит. Присоединяйтесь к нашим командам Threat Research и командам Security Services. Сможете заниматься расследованием кибершпионажа, поиском глобального malware, программами-вымогателями и прочими трендами мира киберпреступлений. Это новые интереснейшие задачи каждый день.

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

Комментарии (28)


  1. IvanGanev
    00.00.0000 00:00
    +14

    А как на счет того что бы особо пристально следить за конкретными разработчиками которые попались на подозрительной деятельности? Условно говоря, если в проекте были коммиты от автора node‑ipc - то есть смысл проверить такой коммит и в целом подозрительно относиться к проекту в котором апрувят коммиты от таких людей.


    1. Didimus
      00.00.0000 00:00
      +5

      Можно ещё физически устранять их. Тогда количество таких кейсов начнёт стремительно сокращаться

      А имущество передавать тем, кто раскрыл.


      1. ovalsky
        00.00.0000 00:00
        +1

        Смотрите !!! У Didimus'а обнаружены вредоносы! А на его компе их тысячи!
        *Размахиваю флешкой над трибуной*


  1. ShashkovS
    00.00.0000 00:00
    +14

    Python c PyPi ещё, терпимо: в типичном среднем проекте 20-60 зависимостей. А вот экосистема javascript — это что-то... Смотрю наши проекты: приложение на vuejs — 3220 уникальных пакетов-версий, на реакте — 6386, бек на ноде — жалкие 772. Понятно, что с такими числами хотя бы минимально прикинуть, что за проекты ты используешь, нет никакой реальной возможности.

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

    Если честно, то я удивлён, что реальных атак по большому счёту так мало, так как при наличие целеустремлённости сначала внедрить зависимость на свою полезную мелкую библиотеку, а потом подновить её, добавив зловредной функциональности — не особо большой фокус.
    А ведь ещё можно бывают пакеты с бинарными артефактами...


    1. funca
      00.00.0000 00:00

      На самом деле крупные корпорации вкладывают кучу денег в безопасность. Например, в статье упоминают Checkmarx. Это гигантский сервис, включающий в себя функции статического анализа приложений SAST (static application security testing), динамического DAST, композиционного анализа SCA (software composition analysis) и т.п.

      Последнее в автоматическом режиме проводит анализ зависимостей приложения - да вот этих самых over 9k зависимостей (и зависимостей зависимостей) какого-либо hello world на react.js - и на выходе даёт отчёты о совместимости лицензий, наличии уязвимостей в коде со ссылками на CVE и многое другое. Интернировав такие проверки в CI/CD, компании начинают оперативно получать информацию о возможных инцидентах.

      В общем, содержимое публичных репозиториев постоянно сканируется и при обнаружении инцидентов платежеспособная часть сразу получает уведомления. Далее что-то выносится в паблик, в виде твитов, статей или бесплатной помощи - баг репортов в опенсорсные проекты. В целом экосистема находится под постоянным контролем и поэтому довольно легко переносит разные катаклизмы, всплески различного malware, protestware (https://github.com/open-source-peace/protestware-list), и т.п.


    1. nin-jin
      00.00.0000 00:00
      -5

      Смотрю на наши проекты: 160 пакетов в node_modules на все 70 проектов. Возможно вам стоит почистить свои ряды?


      1. funca
        00.00.0000 00:00
        +3

        Ваш подход улучшает ситуацию в определенном домене. Но представьте ситуацию на уровне компании, когда количество собственных репозиториев начинает измеряется тысячами, а счёт технологий идёт на десятки: go, rust, c++, python, c#, java, swift, js, ts, sh, ansible, terraform, docker и куча других. У каждого есть свой механизм определения зависимостей и желание что-то вытянуть снаружи. Тут 100 или 10000 уже не имеет принципиального значения - руками за всем не уследить и нужен комплексный подход.


        1. slonopotamus
          00.00.0000 00:00
          +5

          То что в одном месте/аспекте всё плохо ещё не повод переставать заниматься улучшениями в другом. При этом реклама $mol меня тоже уже притомила.


          1. nin-jin
            00.00.0000 00:00
            -14

            Не $mol, а $hyoo в данном случае. У вас какая-то нездоровая фиксация на $mol.


        1. nin-jin
          00.00.0000 00:00

          За сотней руками вполне себе уследить. А за желание тянуть что попало снаружи надо бить по рукам.


          1. funca
            00.00.0000 00:00
            +3

            Все постоянно меняется внутри и периодически меняется снаружи. Сегодня мейтейнер адекватный, через год съедет с катушек. Безопасность это вопрос рисков. А риски это штука вероятностная. В том смысле, что негативное развитие ситуации это не если, а когда . Минимизировать зависимости полезно не только в плане security. Но само по себе это не является решением данной проблемы. Даже если вы можете контролировать набор своих непосредственных зависимостей, то на зависимости зависимостей влиять уже не можете.


            1. nin-jin
              00.00.0000 00:00
              -4

              Ещё как смогу, ведь я же их и мейнтейню.


              1. Newbilius
                00.00.0000 00:00

                Т.е. вы узкая часть развития некой экосистемы или проекта, и скорость развития там ограничена вашем личным временем и ничьим ещё. Не для любого проекта приемлемо иметь узкое место в размере одного человека.


                1. nin-jin
                  00.00.0000 00:00
                  +1

                  Не ограничена. Свой вклад может внести любой. Даже вы.


                  1. patricksafarov
                    00.00.0000 00:00
                    +1

                    Все-таки между «могут вносить» и «вносят» есть разница.


                    1. nin-jin
                      00.00.0000 00:00

                      Всё-таки между "ограничением снизу" и "ограничением сверху" есть разница.


      1. atd
        00.00.0000 00:00
        +5

        Зря заминусили, мысль верная, наверное за резкость высказывания.

        Если сформулировать помягче и на языке бизнеса: каждый новый пакет (зависимость) — это не asset, а liability, и рассматривать его с этой точки зрения. Тогда и не будет случаться node_modules.jpg


  1. stackjava
    00.00.0000 00:00

    Для бэка обычно безопасники открывают доступ во вне только для конкретных доменов по заявкам (сайты партнеров, госушные и проч), что решает проблему сброса инфы на какие-то мутные сайты.

    Но идея,конечно, правильная, подтягивая 100500 зависимостей, даже не думаешь нарваться на пасхалочку.


  1. savostin
    00.00.0000 00:00
    +2

    А как можно "заглядывать" в свои зависимости? Ну первый уровень, то что явно прописано в моём package.json, ещё можно проверить. Но каждый из них тянет свои зависимости, они свои и так до бесконечности. В итоге можно откуда-то из глубин получить зловред и хрен его найдешь. Что-то не так в корне всей этой кухни...


    1. funca
      00.00.0000 00:00
      +2

      Ну вручную системно это не сделать. У OWASP неплохо описана эта проблема безопасности, которую создают зависимости (причем не только программные), с примерами соответствующих подходов и инструментов для снижения рисков https://owasp.org/www-community/Component_Analysis


  1. chemtech
    00.00.0000 00:00

    Спасибо за пост. Можно ли поменять цвета на графике "Number of releases by day of week"?

    Плохо что на сайте PyPi можно выставлять свои значения в статистике — звезды GitHub, форки, скачивания.


    1. Gugic
      00.00.0000 00:00

      Там скорее всего вставляется просто ссылка на гитхаб


  1. olegtsss
    00.00.0000 00:00

    Мне кажется ваш труд прямо коррелирует с PT PyAnalysis от Positive Technologies.
    https://habr.com/ru/company/pt/blog/715754/


  1. fire64
    00.00.0000 00:00

    Да такая беда есть везде, где используются репозитории.

    Unity, Gradle и др.


  1. Protos
    00.00.0000 00:00

    Очередное исследование, а сервис то есть, который можно прикупить по подписке?


  1. ovalsky
    00.00.0000 00:00

    А ваш продукт полностью закрытый? Он не будет открытым инструментом, что бы им пользовались и дополняли его?


  1. upcFrost
    00.00.0000 00:00
    +1

    Для разработчиков, тянущих в проект зависимости на каждый чих, есть отдельный котёл в аду. Стоит рядом с котлом для админов, гоняющих curl | sudo bash


  1. Simpre_falta_algo
    00.00.0000 00:00
    +1

    С моей врождённой подозрительностью не приходится удивляться....

    По вашему мнению, Леонид, избежать этого можно только начав контролировать и опенсорс? То есть убив саму идею опенсорса?

    Или оставим все же свободу, в том числе и попасть в неприятную ситуацию?