- Группа (Ex)Cobalt остается одной из наиболее активных APT-групп, атакующих российские организации.
- Группа стала чаще получать первоначальный доступ к инфраструктуре целей через подрядные организации.
- Для обеспечения скрытого на Linux-системах группа использует руткит уровня ядра PUMAKIT (развитие руткитов Facefish, Kitsune и Megatsune).
- Альтернативным методом закрепления на Linux-системах является модификация легитимных системных файлов, для чего применяется инструмент Octopus.
Авторы:

Владислав Лунин
Ведущий специалист группы исследования сложных угроз TI-департамента, Positive Technologies
Станислав Пыжов
Ведущий специалист группы исследования сложных угроз TI-департамента, Positive Technologies
Игорь Ширяев
Старший специалист департамента комплексного реагирования на киберугрозы, Positive Technologies
Максим Шаманов
Младший специалист группы исследования сложных угроз TI-департамента, Positive Technologies

Кирилл Навощик
Младший специалист группы исследования сложных угроз TI-департамента, Positive Technologies
Ключевые моменты
Введение
Группа (Ex)Cobalt является одним из наиболее активных акторов, атакующих российские организации с целью похищения конфиденциальных данных, а также с целью деструктивного воздействия на инфраструктуру. Наряду с хорошо известными и проверенными инструментами — бэкдором Cobint, шифровальщиками Babuk и Lockbit, а также сетевыми туннелями, такими как GSocket, Revsocks, — группа продолжает создавать и развивать свои дополнительные инструменты (Pumakit, Octopus), обзор которых представлен в настоящей статье.
1. Обзор активности группы
В 2024 году и первой половине 2025 года наиболее активной группировкой, атаковавшей российские организации, стала группировка (Ex)Cobalt. Согласно материалам департамента комплексного реагирования на киберугрозы экспертного центра безопасности Positive Technologies (PT ESC IR), за указанный период расследован 21 инцидент с участием группировки. При этом среднее количество инцидентов за полугодие (7–8 инцидентов) сохраняется и (Ex)Cobalt остается одной из наиболее активных группировок.
Рисунок 1. Количество инцидентов с участием группировки (Ex)Cobalt
В своих атаках APT-группа (Ex)Cobalt в основном использует одни и те же широко известные инструменты, но в то же время активно осваивает новые. Среди известных инструментов специалисты департамента отмечают ПО для туннелирования GSocket, Revsocks, программы-шифровальщики Babuk (нацеленный на узлы на базе Linux и гипервизоры VMware ESXi) и Lockbit (нацеленный на узлы на базе Windows). При этом, несмотря на стойкость криптографических алгоритмов, применяемых в этих шифровальщиках, иногда события развиваются не по плану хакеров. Если процесс выполнения ПО на узлах удается остановить (при экстренном выключении узла, при обнаружении процесса средствами ИБ, при сбоях в операционной системе), то шифрование прерывается и данные с поврежденного носителя (или из файла-контейнера на диске виртуальной машины) удается частично восстановить — как для извлечения важных документов, так и для извлечения артефактов ОС, содержащих следы вредоносной активности. В практике PT ESC IR подобные успешные случаи встречались.

Также участники группировки (Ex)Cobalt продолжают использовать свой фирменный инструмент — бэкдор Cobint. Мы подробно разбирали это ВПО в 2024 году и с того времени наблюдаем его в ходе расследования практически каждого инцидента с участием данной группировки.
2. Новые инструменты и техники
При этом хакеры не стоят на месте и постоянно модифицируют свои инструменты и техники. С декабря 2024 года мы отмечаем некоторые отличия, в частности в векторе первоначального доступа. Если ранее (Ex)Cobalt использовали в основном общедоступные эксплойты, направленные на эксплуатацию уязвимостей в популярном ПО (в частности, в почтовом сервере Microsoft Exchange), то в последних кейсах мы все чаще видим использование похищенных у небольших подрядчиков учетных данных VPN-сервисов более крупных организаций и доступ через опубликованные RDP-сервисы. Кроме того, хакеры стараются вести себя максимально скрытно, собирать информацию об атакованной инфраструктуре — и развивают активные действия через два-три месяца после проникновения, чтобы журналы систем безопасности ротировались и не возникало явных корреляций.
2.1. Хищение учетных данных и сообщений в Telegram
Одним из способов добычи учетных записей является хищение учетных данных и истории сообщений в мессенджере Telegram путем получения доступа к каталогу tdata на устройстве жертвы. Данная атака не является новой, ее подробно разбирали и мы в 2024 году, и наши коллеги в 2023 году. Хакеры, получив доступ к устройству, копируют каталог учетной записи и подключаются к ней. При этом новая сессия в параметрах безопасности может не отображаться. При ретроспективном анализе зараженных систем следы такой активности были видны в различных артефактах файловой системы:
- USN Journal: 25.12.2024 19:07:09 FileDelete|Close .\Users\%username%\AppData\Roaming\Telegram Desktop\tdata.7z 56956458584 Archive
- ShellBag: BagMRU\3\18\0\0\0\1\0\1,0,432,0,Desktop\Computers and Devices\172.27.0.10\172.27.0.10\c$\Users\%username%\AppData\Roaming\Telegram Desktop\tdata,Directory,tdata,0,,,,15.03.2024 20:40:45,414850,11,1,15.03.2024 20:40:45,15.03.2024 20:40:45,False,NTFS file system
- JumpList: [somehost]\Users\%username%\AppData\Roaming\Microsoft \Windows\Recent\AutomaticDestinations\f01b4d95cf55d32, a.automaticDestinations-ms,21.05.2024 08:02:48, \\[REDACTED]\c$\Users\%username%\AppData\Roaming\Telegram Desktop\tdata,1,False,eb1cd5ad-debe-11ee-bcba-e0d55e4368c4,,"VistaAndAboveIdListDataBlock, EnvironmentVariableDataBlock, TrackerDataBaseBlock, PropertyStoreDataBlock"
Если подобные события имели место, то необходимо выполнить следующие действия:
- Завершить все активные сессии на всех устройствах, оставив одну на доверенном мобильном устройстве, а затем повторно войти на других необходимых устройствах.

- Установить сложный локальный пароль для доступа к учетной записи, так как именно этот пароль защищает профиль учетной записи на устройстве.
Рисунок 4. Установка локального пароля для доступа к учетной записи Telegram
2.2. Flogon-кейлоггер
Другим способом получения учетных записей в арсенале (Ex)Cobalt является использование стилера, встраиваемого в структуру веб-сервиса Microsoft Outlook Web Access — в компонент Flogon. Данный стилер не является уникальным, однако ранее мы не фиксировали его применение данной группировкой. В рассмотренных нашей командой инцидентах вредоносный код внедрялся в легитимную функцию clkLgn(), что позволяло перехватывать вводимые пользователем учетные данные с последующей передачей их на сервер злоумышленника. Передача происходила в параметрах GET- или в теле POST-запросов.

Все вариации данного стилера были подробно разобраны ранее.
2.3. Шеллы с расширением .epf («1С»)
Еще один интересный способ закрепления в атакованной инфраструктуре, ранее не наблюдавшийся в арсенале группировки (Ex)Cobalt, — размещение шеллов в виде отдельных файлов с расширением *.epf, которые реализуют механизм внешних обработок в программных продуктах «1С».
Впервые наша команда столкнулась с подобным способом в начале 2025 года, и выявить его удалось при анализе файлов кэша службы удаленного рабочего стола (RDP). В ходе реконструкции графических файлов RDP-сессии скомпрометированного пользователя были обнаружены следы обращения к различным инструментам работы с программным продуктом «1С:Предприятие», а также ссылки на загрузку различных шеллов.

В ходе дальнейшего анализа артефактов сессий RDP удалось также получить данные о некоторых командах, выполненных с использованием полученного шелла:

2.4. Руткит PUMAKIT
Наиболее интересным инструментом группировки (Ex)Cobalt, обнаруженным в ходе расследований, является руткит PUMAKIT, который маскируется под легитимные компоненты ОС и скрывает свое присутствие.
Ранее инструмент был описан в публикации Elastic Security Labs: коллеги описали дроппер и часть функциональности LKM-руткита. Позднее исследователи из Solar 4RAYS опубликовали собственный анализ, в котором описали особенности работы бэкдора и механизм кражи данных.
В настоящей статье мы более подробно рассмотрим некоторые функции данного инструмента, а также проследим этапы его развития.

Обнаружить его удалось при помощи изучения аномалий во временных метках файлов. Для этого в инструментах, разработанных командой департамента комплексного реагирования на киберугрозы PT ESC, применяется алгоритм сопоставления временных меток файлов, который выявляет отклонения от времени создания или модификации файлов в каталоге.


Таким образом был определен круг подозрительных файлов в служебных каталогах ОС. Затем путем ручного анализа удалось выявить файл, маскировавшийся под компонент операционной системы (cron), размер которого отличался от размера легитимного файла более чем в 20 раз.
2.4.1. Первая стадия: cron
Найденный файл, подменявший системный файл cron, представлял собой дроппер и состоял из двух основных компонентов — оригинального cron (tgt, target) и установщика вредоносного модуля (wpn, weapon). Вместо записи исполняемых файлов на диск дроппер загружал их в анонимные файловые дескрипторы (/memfd:wpn и /memfd:tgt), а затем запускал комбинацией функций fork и execveat, обеспечивая их выполнение без сохранения в файловой системе. Таким образом, оригинальная функциональность cron сохранялась, что не вызывало подозрений, а вредоносная нагрузка незаметно выполнялась при каждом запуске системы.
Получив первоначальный доступ к системе с повышенными привилегиями, злоумышленники определяли версию ядра и выбирали установщик модуля, совместимый с конфигурацией системы жертвы. Это обеспечивало корректную интеграцию модуля в целевую среду и гарантировало его успешное выполнение.
2.4.2. Вторая стадия: weapon
Основная задача wpn — установка LKM-руткита, совместимого с системой жертвы. Для сокрытия активности загрузчик маскируется под системный процесс /usr/sbin/sshd.
Логика установки начинается с идентификации узла: вычисляется его уникальный идентификатор, формирующийся следующим образом:
- с помощью Netlink-сокета в ядро отправляется запрос для получения информации о сетевых интерфейсах;
- MAC-адреса обнаруженных интерфейсов последовательно сохраняются, за исключением начинающихся с «docker», «veth» или «br»;
- полученные адреса объединяются в общий буфер, разделенный символами новой строки «\n»;
- к получившемуся результату дописывается локальное имя системы и подается на вход алгоритма хеширования MD5;
- вычисленное значение сохраняется в качестве agent_id системы.
Как только был получен и сохранен идентификатор системы — формируется команда sh -c «dmesg | grep ’ecure boot enabled’», которая выполняется с помощью интерпретатора командной строки. Результат ее выполнения позволяет определить состояние Secure Boot — механизма защиты, предотвращающего загрузку неподписанных или измененных загрузочных образов:
- если Secure Boot активен — выполнение немедленно прерывается;
- иначе — установка модуля ядра считается возможной: инициируется обращение к системному файлу /lib/modules/<версия_ядра>/build/Module.symvers для проверки соответствия экспортируемых символов ядра (функций, переменных) символам модуля.
При успешном доступе к файлу выполняется последовательное чтение его записей, при этом для каждой сохраняются контрольная сумма (cyclic redundancy check, CRC) и само имя символа.
2.4.2.1. Формирование собственного списка Module.symvers
Если не удалось получить информацию напрямую — создается собственная версия экспортируемых символов ядра:
- Процесс обращается к двум файлам — /proc/version и /proc/cmdline, извлекая из них информацию о версии ядра.
- В каталоге /boot выполняется поиск файла, начинающегося с «vmlinuz-».
- Оставшаяся часть имени, определяющая версию ядра, сравнивается с теми версиями, которые были получены на шаге 1.
Если все три версии ядра совпали — создается файл с именем /tmp/script.sh, в который записывается скрипт для распаковки сжатого файла ядра.
#!/bin/sh c() { if file "$1" | grep -q "ELF"; then exit 0 else return 1 fi } d() { for p in tr "$1\n$2" "\n$2=" < "$i" | grep -abo "^$2" do p=${p%%:*} tail -c+$p "$i" | $3 > $r 2>/dev/null c $r done } i=$1 r="/tmp/vmlinux" [[ -z $vmlinuz_path ]] || exit 0 d '\037\213\010' xy gunzip d '\3757zXZ\000' abcde unxz d 'BZh' xy bunzip2 d '\135\0\0\0' xxx unlzma d '\211\114\132' xy 'lzop -d' d '\002!L\030' xxx 'lz4 -d' d '(\265/\375' xxx unzstd c $i exit 1
Листинг 1. Базовый скрипт для распаковки сжатого файла ядра- Данный скрипт запускается с помощью команды bash /tmp/script.sh “/boot/vmlinuz-<KERNEL_RELEASE>”.
- В результате выполнения скрипта в каталоге /tmp появляется разархивированный файл ядра.
- Сам скрипт удаляется.
После получения файла ядра выполняется перебор его таблицы заголовков для получения и сохранения размеров и смещений конкретных секций, таких как:
- __kcrctab_gpl: таблица контрольных сумм для символов, экспортируемых под лицензией GPL. Каждая запись содержит CRC, соответствующую символу из __ksymtab_gpl;
- __ksymtab_gpl: таблица символов, используется для разрешения ссылок на символы в модулях, совместимых с лицензией GPL. Каждая запись имеет поля:
- value — адрес экспортируемого символа;
- name — указатель на строку в секции __ksymtab_strings, содержащую имя символа;
- __ksymtab: список всех экспортированных символов, кроме тех, которые помечены GPL. Структурно идентична __ksymtab_gpl.
Перед переходом к извлечению информации о символах ядра с использованием перечисленных заголовков из секции .rodata считывается версия разархивированного ядра (рис. 11). С ее помощью определяется размер единичной записи в перечисленных выше секциях и идентификатор одного из четырех имеющихся обработчиков, который должен быть установлен для корректного парсинга и сохранения записей.

2.4.2.2. Проверка совместимости
После того как была получена информация об используемых символах ядра, независимо от способа ее получения, выполняется проверка совместимости этих символов с символами модуля, хранящимися в секции versions: поочередно каждая запись из списка системы сравнивается с соответствующей в секции модуля до тех пор, пока не будет найдено соответствие. Если хоть одна из них не была найдена в системе, было превышено количество итераций или CRC отличается — выполнение будет завершено.
В ином случае будет выполнена загрузка LKM-руткита в систему жертвы с помощью системного вызова init_module.
2.4.2.3. Отладочный режим
При детальном рассмотрении в загрузчике руткита был обнаружен отладочный режим, позволяющий получить расширенные сведения о процессе установки. Учитывая его сложность и множество зависимостей, мы предполагаем, что данный режим используется атакующей стороной в том случае, если при стандартном запуске дроппера (без дополнительных аргументов) не удается установить вредоносный модуль.
Для его активации злоумышленники выполняют два последовательных шага:
- Запускают дроппер с предварительно установленной переменной окружения HUINDER и одним из следующих аргументов:
- —extract-target или -et для извлечения tgt.bin;
- —extract-weapon или -ew для извлечения wpn.bin.
- После получения файла wpn.bin запускают его с аргументами -f, -v и -t (порядок аргументов не имеет значения) с правами суперпользователя.
После выполнения описанных выше действий загрузчик запускается в режиме отладки, в котором вместо непосредственной установки модуля осуществляется проверка его совместимости с системой. В этом режиме атакующему возвращается либо подтверждение возможности установки, либо подробное описание причин, по которым установка невозможна. Примеры возможных ошибок и их выводов показаны на рис. 12–14.



Кроме того, при запуске загрузчика в режиме отладки будет использоваться специальная версия скрипта для распаковки ядра, которая отличается от обычной добавленным журналированием, проверкой наличия утилит для распаковки (чтобы определить отсутствующие разархиваторы) и дополнительной проверкой на наличие данных в файле, что делает процесс более информативным (рис. 14).
#!/bin/sh
c() {
if [[ ! -s "$1" ]]; then
return 1
fi
if file "$1" | grep -q "ELF"; then
echo "OK"
exit 0
else
echo "NOT ELF: $1"
return 1
fi
}
d() {
echo "Try: $1, $2, $3"
IFS=' ' read -r dcmd dargs <<< "$3"
for p in tr "$1\n$2" "\n$2=" < "$i" | grep -abo "^$2"
do
p=${p%%:*}
echo "Check: $i.$p"
if ! command -v "$dcmd" &> /dev/null; then
echo "Warning: Decompressor '$dcmd' not available. Skipping..."
return 0
fi
tail -c+$p "$i" | $3 > $r 2>/dev/null
c $r
done
}
i=$1
r="/tmp/vmlinux"
[[ -z $vmlinuz_path ]] || exit 0
d '\037\213\010' xy gunzip
d '\3757zXZ\000' abcde unxz
d 'BZh' xy bunzip2
d '\135\0\0\0' xxx unlzma
d '\211\114\132' xy 'lzop -d'
d '\002!L\030' xxx 'lz4 -d'
d '(\265/\375' xxx unzstd
c $i
exit 1
Листинг 2. Расширенный скрипт для распаковки сжатого файла ядра

2.4.3. Третья стадия: LKM audit
При инициализации модуля выполняется несколько подготовительных шагов, необходимых для последующей работы руткита. Сначала выполняется регистрация и снятие kprobe, чтобы извлечь адрес kallsyms_lookup_name, обычно не экспортируемый напрямую. Полученный адрес сохраняется и используется для определения расположения таблицы системных вызовов.
После завершения подготовительных операций отключается защита записи в памяти ядра — для этого модифицируется содержимое регистра управления CR0: временно сбрасывается бит Write Protect, что позволяет записывать в ранее защищенные области памяти ядра (рис. 16).

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

Полный перечень подменяемых системных вызовов: execveat, execve, newfstatat, mmap, openat, newlstat, getdents64, newfstat, getsid, newstat, getpgid, close, rmdir, open, getdents, write, kill, read.
Почти все системные вызовы, перехватываемые вредоносным модулем, следуют единой логике: обработчик сначала анализирует переданные аргументы, а затем принимает решение о модификации возвращаемого пользователю ответа.
Например, вызовы newlstat и newstat, предназначенные для получения информации о файлах, в обычных условиях возвращают полный набор данных, тогда как их обновленные версии определяют, к какому объекту файловой системы запрашивается доступ, и, если это требуется, скрывают файлы вредоноса, подменяя возвращаемые значения. Ниже приведен список скрываемых от пользователя объектов:
- /proc
- /sys/module/audit/initstate
- /sys/module/audit/holders
- /sys/module/audit/refcnt
- /sys/module/audit
- /usr/share
Отдельного внимания заслуживают переопределенные вызовы rmdir, read и write, содержащие ключевую для дальнейшего использования модуля логику.
2.4.3.1. Механизм стилера: перехват системных вызовов write и read
Для реализации механизма стилера осуществляется подмена системных вызовов write и read. Переопределенный write анализирует передаваемые на запись данные с целью обнаружения конфиденциальной информации, в частности строк, содержащих ключевые слова «password» или «passphrase». Переопределенный вызов read, в свою очередь, предназначен для перехвата и сохранения закрытых криптографических ключей PEM-формата, считываемых процессом при установке защищенного соединения.
В обоих случаях модуль сохраняет в памяти сами перехваченные данные и их тип, а также данные о процессе, инициировавшем системный вызов.
Помимо таких общих фраз, как «password», «login», «passphrase» и «...PRIVATE KEY...», мы обнаружили, что стилер дополнительно перехватывает данные, содержащие разные варианты написания слов «пользователя:» и «пароль:».
Факт поиска строк на русском языке сигнализирует о фокусе атакующих на русскоязычном сегменте интернета.
Особого внимания заслуживает механизм внедрения SSH-ключа, обеспечивающий атакующему устойчивый удаленный доступ. Для этого руткит перехватывает вызовы open и openat, отслеживая обращения к файлу authorized_keys — стандартному компоненту OpenSSH, расположенному в каталоге ~/.ssh/ конкретного пользователя и определяющему, какие публичные ключи разрешают доступ к соответствующей учетной записи без ввода пароля. При попытке чтения файла (например, в процессе подключения или его проверки) руткит на лету модифицирует содержимое: к оригинальным данным дописывается заранее подготовленный публичный ключ. При этом сам файл на диске остается неизменным: подмена производится исключительно в памяти, в момент вызова read.
В типичном сценарии атаки злоумышленник, имея на руках приватный ключ, инициирует SSH-подключение. При проверке ~/.ssh/authorized_keys сервер читает подмененный в памяти список ключей, распознает внедренный ключ как доверенный и открывает сессию без запроса пароля. После этого атакующий получает интерактивный доступ от имени целевой учетной записи и может скрытно выполнять команды и разворачивать дополнительные инструменты. Скрытность сохраняется до тех пор, пока загружен модуль руткита.
Даже смена пароля и отключение парольной аутентификации не устраняют угрозу: доступ по внедряемому ключу сохраняется и остается активным.
Наибольшую угрозу представляет ситуация, при которой публичный ключ добавляется в authorized_keys пользователя root: это дает атакующим максимальные привилегии и полный контроль над целевым узлом.
Исходя из представленной функциональности, можно предположить, что основным назначением руткита является закрепление в системе, а также перехват учетных данных, обрабатываемых в процессе аутентификации при установлении SSH-соединений. Эти данные впоследствии будут использованы не только для закрепления на начальном скомпрометированном узле, но и для дальнейшего перемещения внутри инфраструктуры жертвы.
2.4.3.2. Механизм взаимодействия с руткитом через переопределение системного вызова rmdir
Ключевую роль в работе руткита играет переопределенный системный вызов rmdir, который выступает внутренним каналом взаимодействия между пользовательской частью бэкдора и установленным модулем ядра. Инициатором этих вызовов является сам бэкдор, передавая в качестве аргумента не путь к каталогу, а специально сформированную строку-аргумент.
В основе данного механизма лежит перехват системного вызова: при обращении к rmdir перед стандартной операцией удаления руткит проверяет переданный путь и, если он начинается с «zarya», интерпретирует последующую часть строки как управляющую команду. Данная команда должна строго соответствовать определенной структуре, которая представлена на рис. 18.
rmdir zarya_[command]_[ argument]
Полный список команд, доступных атакующему, представлен в табл. 1.
Таблица 1. Перечень команд для управления
| Команда | Аргумент | Описание |
| v | Атакующие используют символ-заглушку «0», но может быть и любой другой | Отображает версию установленного модуля |
| d | Извлекает данные, собранные стилером, и выводит их в пространство пользователя. После чего удаляет эти данные из памяти | |
| с | Копирует встроенную конфигурацию для установки соединения (данные из секции .puma-config) в пространство пользователя | |
| t | Выполняет тестовый вызов без возврата ошибки | |
| u | Восстанавливает отображение скрытого модуля, возвращая его в список загруженных модулей | |
| 9 | Отображает в пространство пользователя таблицу соответствий PID ↔ IP-адрес, связывающую их между собой (рис. 19) | |
| 0 | Повышает привилегии у вызвавшего процесса | |
| 1 | Идентификатор процесса (PID) | Принимает строку с идентификатором процесса, проверяет наличие соответствующей записи во внутренней таблице и при ее отсутствии добавляет переданный PID в список скрываемых. Данная команда также проверяет актуальность текущих записей, удаляя сведения о завершившихся процессах и их IP-адресах. Используется совместно с командой «5» при установке соединения |
| 5 | IP-адрес | Команда принимает IP-адрес в виде строки, преобразовывает его в 32-битное число и, при отсутствии соответствующей записи, добавляет его во внутреннюю таблицу скрываемых подключений (см. команду «9»). Используется совместно с командой «1» при установке соединения |
| k | Идентификатор процесса (PID) | Устанавливает PID руткита |
Было установлено, что, помимо перечисленных выше команд, при вызове rmdir с аргументом zarya или zarya_ (без указания конкретной команды) у инициировавшего вызов процесса повышаются привилегии.

Анализ множества семплов показал, что в более поздних версиях функциональность модуля была расширена. В частности, была добавлена логика сокрытия используемых портов — аналогичная той, которая была описана для IP-адресов. Также были добавлены команды для управления портами (табл. 2).
Таблица 2. Добавленные в новых версиях команды
| Команда | Аргумент | Описание | Замечание |
| 7 | Локальный порт | Добавляет локальный порт в список скрываемых (см. рис. 20), если значение уникально | Вызов rmdir с данными командами выполняется при получении соответствующей команды с C2-сервера (см. описание полезной нагрузки бэкдора) |
| 8 | Локальный порт | Удаляет локальный порт из списка скрываемых |

2.4.3.3. Использование ftrace-хуков
Помимо перехвата системных вызовов, данный руткит использует механизм ftrace для установки хуков на заранее определенный набор функций ядра Linux. Адреса этих функций вычисляются динамически с помощью ранее полученного указателя на kallsyms_lookup_name. Вычисленные адреса используются для корректной установки хуков.
Руткит дополнительно анализирует каждую целевую функцию, определяя в ее коде участок, наиболее подходящий для внедрения хука. После чего с помощью функций ftrace_set_filter_ip и register_ftrace_function регистрирует обработчик, перенаправляющий выполнение функций на их подмененную реализацию.
Полный перечень функций ядра, подвергающихся перехвату, можно разделить на две группы: полностью отключаемые руткитом и те, поведение которых переопределяется. В первую группу входят функции, связанные с механизмами контроля доступа: selinux_file_open, selinux_file_permission, avc_has_perm, file_map_prot_check, selinux_inode_setattr, selinux_inode_permission, selinux_socket_bind и selinux_socket_connect. Обработчики для них полностью заменяют оригинальную логику на простую заглушку, которая всегда завершает выполнение без ошибок (return 0). В итоге любые проверки или действия, которые должны были выполняться, фактически отключаются, поскольку система считает, что они завершились успешно.
Ко второй группе относятся функции sk_diag_fill, tpacket_rcv, tcp4_seq_show, kernel_clone или _do_fork (в зависимости от версии модуля PUMAKIT), а также nf_hook_slow. В более поздних версиях к этому списку добавляется функция inet_sk_diag_fill. Обработчики для каждой из них не блокируют выполнение оригинального кода полностью, а осуществляют предварительный анализ входных аргументов. Во всех перечисленных случаях обработчики извлекают IP-адрес из передаваемых в функцию аргументов и сравнивают их с внутренним списком IP-адресов, хранящимся в структуре руткита (см. рис. 19 и 20). Если среди переданных аргументов обнаруживается совпадение с одним из имеющихся в структуре адресов — оригинальная функция ядра не вызывается, а обработчик вместо этого сразу возвращает нулевой результат, тем самым подавляя выполнение исходной функции. Такой механизм переопределения позволяет эффективно скрывать нелегитимные подключения, процессы и сетевую активность, обеспечивая их невидимость для средств мониторинга и анализа.
Особого внимания заслуживает функция nf_hook_slow, поскольку перехват ее вызова позволяет злоумышленнику обойти работу файрвола на уровне ядра, исключая проверку пакетов механизмами фильтрации.
В стандартной реализации Netfilter функция nf_hook_slow выполняет последовательный вызов всех зарегистрированных в ядре хуков, включая обработчики iptables и nftables. Именно эти хуки принимают окончательное решение о том, пропустить, заблокировать или перенаправить пакет.
Однако в данном случае внедренный обработчик анализирует пакет до его передачи в оригинальную функцию, извлекая исходный и целевой IP-адреса и сравнивая их с внутренним списком, хранящимся в рутките. Если хотя бы один из них совпадает — обработчик возвращает NF_ACCEPT (1), сразу принудительно разрешая прохождение пакета без его передачи в nf_hook_slow.
Такой подход позволяет атакующему полностью скрывать трафик от механизмов мониторинга и обходить политики файрвола, поскольку пакеты, принудительно принимаемые обработчиком, минуют стандартные процессы фильтрации и анализа. Таким образом, файрвол не имеет возможности зафиксировать, проанализировать или заблокировать такой трафик. В результате злоумышленник может беспрепятственно устанавливать скрытые соединения, не оставляющие следов в системах мониторинга и журналирования, полностью избегая наложенных ограничений безопасности.
2.4.3.4. Удаление из списка модулей и инъекция бэкдора libs.so
После завершения установки хуков и подмены адресов системных вызовов на собственные обработчики руткит принимает меры по сокрытию своего присутствия в системе. Для этого он удаляет себя из списка загруженных модулей ядра, модифицируя указатели в двусвязном списке, хранящим все активные модули. В результате стандартные инструменты мониторинга, такие как команда lsmod или просмотр файла /proc/modules, перестают отображать модуль руткита.
Далее руткит запускает отдельный поток, выполняющий функцию, отвечающую за инъекцию и дальнейшее поддержание бэкдора. В ней в бесконечном цикле производится проверка существования процесса бэкдора с помощью функций find_get_pid и pid_task. Если целевой процесс отсутствует или с момента последней проверки прошло более пяти секунд — проверка выполняется повторно и при необходимости руткит инициирует запуск бэкдора, обеспечивая его постоянное присутствие в системе. Для этого с помощью механизма запуска пользовательских процессов из ядра call_usermodehelper через оболочку /bin/sh выполняются две команды:
- truncate -s 0 /usr/share/zov_f/zov_latest;
- cat /dev/null 1>/dev/null.
При этом, помимо самих команд, call_usermodehelper также получает указатель на массив переменных окружения:
- SHELL=sh
- HOME=/
- LD_PRELOAD=/lib64/libs.so
- PATH=/sbin:/bin:/usr/sbin:/usr/bin
Среди этих переменных особое значение имеет LD_PRELOAD=/lib64/libs.so, обеспечивающая инъекцию бэкдора.
В результате первая команда обнуляет определенный файл, используемый бэкдором (его работа будет рассмотрена далее), фактически скрывая его содержимое, пока он находится в неактивном состоянии. Вторая команда, по сути, не выполняет никаких значимых действий: она просто считывает пустой файл и перенаправляет вывод в /dev/null. Однако ее запуск необходим для выполнения с заданным окружением, которое позволяет активировать инъекцию бэкдора без заметных следов в системе.
Итак, описанный выше механизм обеспечивает автоматическое восстановление и постоянное присутствие в системе.
2.4.4. Четвертая стадия: libs.so
После того как LKM-руткит вызывает call_usermodehelper с модифицированным окружением (переменная LD_PRELOAD указывает на встроенный в рутките файл libs.so), бэкдор немедленно загружается в адресное пространство созданного процесса и начинает выполнение. Поскольку вызванная команда завершается практически сразу, бэкдор дополнительно предпринимает шаги, направленные на закрепление в системе и обеспечение возможности длительной автономной работы.
Для этого выполняется превращение процесса в демон, позволяя ему работать в фоновом режиме без привязки к терминалу и управляющей сессии. Достигается это двумя последовательными вызовами функции fork: первый создает дочерний процесс и сразу завершает родительский, разрывая исходную связь с запускающим процессом. Затем дочерний процесс вновь вызывает fork, а следом за ним и setsid, чтобы создать новую сессию, полностью отсоединенную от управляющего терминала. Эти действия окончательно разрывают связь бэкдора с его первоначальным контекстом запуска, гарантируя, что процесс перестает зависеть от сигналов или жизненного цикла родительского процесса.
Вслед за этим бэкдор изменяет текущий рабочий каталог на корневой (chdir("/")), чтобы исключить зависимость от исходного пути, устанавливает маску прав доступа в значение umask(0), предотвращая возможные ограничения при создании файлов, и закрывает все открытые файловые дескрипторы (включая стандартные потоки ввода, вывода и ошибок), предотвращая случайный вывод данных в консоль.
Для получившегося в результате процесса с помощью вызова функции getpid определяется PID, а после, с помощью полученного значения, выполняется вызов rmdir, переопределенного руткитом, с параметром zarya_k_<PID> — для связывания между собой бэкдора и руткита.
После этого бэкдор переходит к следующему этапу своей работы: он проверяет наличие конфигурационных данных, необходимых для установки соединения с C2-сервером, в секции .konfig, расположенной в памяти процесса. В случае отсутствия этих данных бэкдор извлекает необходимую конфигурацию из секции .puma-config, расположенной в памяти ранее загруженного LKM-модуля. Для извлечения и последующего заполнения собственной конфигурационной секции используется механизм, основанный на вызове функции rmdir, но уже с аргументом zarya_c_0. Особого внимания заслуживает структура извлекаемой конфигурации, представленная на рис. 21.

В конфигурации могут быть указаны следующие данные.
Таблица 3. Перечень команд для управления
| Название записи | Тип структуры записи | Описание | Значение по умолчанию |
| ping_interval_s | Int32 | Интервал между попытками опроса C2-сервера при отсутствии установленного соединения | 5 секунд |
| hibernate_s | Int32 | Время сна при неудачном подключении к C2-серверу (после использования должно быть повторно получено) | 0 секунд |
| session_timeout_s | Int32 | Время жизни сессии | 3 секунды |
| c2_timeout_s | Int32 | Максимальное время, в течение которого бэкдор подключен к одному серверу | 43 200 секунд (12 часов) |
| jitter_s | Int32 | Величина случайного отклонения от ping | 50 |
| tag | String | Тег жертвы | «x» |
| cert | Binary | Отпечаток сертификата (SHA-256), используемого для подключения к серверу | Отсутствует |
| sni | String | Доменное имя в сертификате, используемом для подключения к C2-серверу | |
| dns_s | Int32 | Время жизни кэшированных DNS-записей | 3600 секунд |
| gw_p | String | Порт, на котором бэкдор будет работать в режиме прокси-сервера | Отсутствует |
| s_a<№> | String | Адрес C2-сервера | |
| s_p<№> | String | Порт C2-сервера | |
| s_c<№> | String | Протокол C2-сервера |
Важно отметить, что конфигурация бэкдора способна включать в себя до 32 записей об управляющих серверах (s_*<№>).
Получив необходимые данные конфигурации, бэкдор формирует уникальный идентификатор зараженного узла (agent_id). Алгоритм его вычисления аналогичен тому, который был описан в загрузчике. Сформированное значение служит для однозначного определения устройства при последующем взаимодействии с C2-сервером.
2.4.4.1. Сохранение данных
Следующим шагом становятся извлечение и обработка данных, ранее полученных стилером. Для этого бэкдор выполняет вызов rmdir с аргументом zarya_d_0. Данный вызов выполняется циклически — не более восьми раз подряд либо до момента, пока его ответы не перестанут содержать информацию. Следует отметить, что при каждом обращении с таким аргументом получаемые данные удаляются из памяти модуля, благодаря чему на каждом следующем шаге возвращается новая, еще не обработанная информация.
В ранних версиях модуля структура перехваченных данных содержит такие поля, как тип перехваченных данных, UID процесса (данные которого были перехвачены) и непосредственно сама конфиденциальная информация (см. рис. 22).

Сохраненный и переданный в бэкдор UID используется в качестве ключа для получения полной информации о соответствующей учетной записи системы: сначала бэкдор пытается обратиться к системному файлу /etc/passwd, чтобы, используя полученный идентификатор, извлечь информацию о зарегистрированном в системе пользователе. Если доступ к файлу невозможен или нужная запись не обнаружена — UID преобразуется в строковый формат и используется для формирования запроса к демону кэширования учетных данных — Name Service Cache Daemon. На основе полученных данных бэкдор формирует строку с информацией о пользователе, используя нулевой байт (0×00) в качестве разделителя.

В более поздних версиях модуля, помимо UID, злоумышленники также сохраняют EUID и SUID процесса.

Добавленные идентификаторы также используются в качестве ключей для извлечения информации о пользователе из файла /etc/passwd. Однако в отличие от более ранних версий, бэкдор сохраняет не всю запись целиком, а только имя пользователя, соответствующее каждому из указанных идентификаторов (так как эти идентификаторы могут указывать на разных пользователей). Полученные значения формируются в строку в специальном формате.

Следует отметить, что более поздние версии бэкдора имеют обратную совместимость со старыми версиями модуля (сохраняющими исключительно UID). Такая возможность была добавлена вследствие того, что злоумышленники не предусмотрели обновление модуля ядра, в отличие от бэкдора (описание полезной нагрузки бэкдора представлено в табл. 5). Для таких случаев применяется специальный формат строки (рис. 26).

В более поздних версиях модуля, перед тем как полученные от руткита данные будут записаны в журнал, бэкдор хеширует их с помощью алгоритма FNV-1: если такой хеш уже присутствует в памяти — увеличивается счетчик обращений и запись пропускается, а если его нет, то он добавляется в таблицу (или замещает наименее используемый при переполнении). Таким образом злоумышленники исключают запись повторяющихся данных.
Если же запись является уникальной — сведения о пользователе, перехваченные данные и информация о процессе-источнике кодируются в Base64 и записываются в журнал в порядке, определяемом типом перехваченных данных. Каждая запись начинается с временной метки, за которой следует сформированная CSV-строка, элементы которой разделены запятыми.

Получившаяся запись добавляется в конец файла /usr/share/zov_f/zov_logs.txt. Перед записью бэкдор обязательно проверяет источник, с которым связаны перехваченные данные. Если оказывается, что обработке подвергается собственный процесс ведения журнала — запись немедленно прекращается, что исключает саможурналирование и зацикливание записей.
2.4.4.2. Взаимодействие с С2-сервером
После того как перехваченные данные были записаны в журнал, процесс предпринимает попытку установить соединение с C2-сервером для последующего взаимодействия. Для этого, при отсутствии активного соединения, бэкдор сравнивает текущее время с сохраненной временной меткой последнего успешного взаимодействия с C2-сервером:
- При отсутствии ранее установленных соединений бэкдор фиксирует текущее время в качестве отправной точки и инициирует цикл перебора доступных конфигураций подключения, полученных из ранее заполненной секции .konfig. Для каждой записи перед попыткой установить соединение выполняется DNS-разрешение, по результатам которого в случае успеха сохраняется временная метка. Цикл ограничен 100 итерациями и продолжается до тех пор, пока хотя бы одна из записей не будет успешно разрешена.
- Если метка последнего успешного взаимодействия с сервером установлена — вычисляет время, прошедшее с момента предыдущего сеанса связи. Если это значение не превышает c2_timeout_s, процесс пытается установить соединение, используя последнюю использованную конфигурацию. В противном случае выбирается следующая запись из секции .konfig, для которой проводится проверка актуальности DNS-разрешения: если с момента последнего запроса прошло менее dns_s секунд — используется ранее полученный адрес. Иначе инициируется новое разрешение, по завершении которого обновляется временная метка.
- В обоих случаях перед инициализацией соединения по очереди вызываются команды:
- rmdir zarya_1_<PID> — для скрытия текущего PID процесса;
- rmdir zarya_5_<IP> — для скрытия IP-адреса разрешенного C2-сервера.

Для обеспечения регулярного взаимодействия с сервером в бэкдоре предусмотрен механизм внутренних таймеров, контролирующих периодичность обращений (см. структуру конфигурации). При инициализации соединения формируется пустое сообщение с заголовком, содержащим основную информацию о зараженной системе (команда 4097).
Таблица 4. Базовый заголовок для всех сообщений
| Поле структуры сообщения | Добавляемые данные |
| agent_id | Уникальный идентификатор жертвы |
| v | Версия бэкдора |
| pv | Версия модуля |
| p | PID процесса |
| log_t | Время, прошедшее с момента последней записи в zov_logs.txt |
| uptime | Общее время работы руткита |
| cmd_id | Заголовок отправленных данных |
| cmd_type | Номер выполненной команды |
| jitter_s | Верхняя граница величины случайной задержки, добавляемой к основному интервалу |
| tag | Тег жертвы |
| dpi | Уникальный идентификатор сообщения, представляющий собой случайное значение из диапазона [0, 999] |
Важно отметить, что каждое сообщение, отправляемое на C2-сервер, начинается с этого заголовка и дополняется результатами выполнения соответствующих команд (см. табл. 5).
После формирования и отправки стартового сообщения бэкдор переходит в режим ожидания ответа от C2-сервера, включающего тип команды (cmd_type), идентификатор передаваемых данных (cmd_id) и непосредственно полезную нагрузку. Форматы отправляемых и получаемых сообщений, а также описание функциональных возможностей бэкдора приведены в табл. 5.
Таблица 5. Возможности бэкдора
| Тип команды (cmd_type) | Описание команды | Комментарий | Структура ответного сообщения |
| 4097 | Отправляет на C2-сервер блок данных с основной информацией о зараженной системе (heartbeating) | Каждое сообщение начинается с этого блока | Только заголовок (см. табл. 4), в котором будет указан тип выполненной команды |
| 4100 | Получает и записывает новую версию бэкдора в /usr/share/zov_f/zov_latest | — | |
| 4101 | Запускает реверс-шелл | Используется псевдотерминал (pty) | |
| 4103 | Удаляет или добавляет (см. табл. 2) локальный порт для работы в режиме прокси | Номер порта и выбранная команда поступают с C2-сервера | |
| 4098 | Отправляет содержимое файла /usr/share/zov_f/zov_logs.txt | При превышении порога в 8 МБ файл очищается | «logs» + <содержимое zov_logs.txt> |
| 4096 | Собирает и отправляет системную информацию | Вызов команд с помощью /bin/sh:
| Полученные в результате данные группируются:
|
| 4099 | Выполняет произвольную шелл-команду и отправляет ее результат | Команды выполняются с помощью /bin/sh | «result» + <stdout> «output» + <stderr> |
| 4102 | Обновляет внутренние таймеры | Обновляемые параметры:
| Значения конфигурационных параметров:
|
2.4.4.3. Используемые сертификаты
В ходе анализа доменов, использовавшихся для C2-серверов, было установлено, что ряд из них применяет весьма примечательный SSL-сертификат (рис. 29). Он интересен тем, что в качестве Issuer и Subject злоумышленники указали организацию «FSB» и адрес «2 Bolshaya Lubyanka Street».
Ранее, в ходе одного из расследований, мы встречали использование данного сертификата группировкой (Ex)Cobalt. Поэтому мы связываем руткит PUMAKIT с данной группировкой и рассматриваем его как часть ее инструментария.

Данный сертификат был использован следующими С2-серверами:
- cckitsfrp1.n3x1lo.pro
- qdkitsorp2.n3x1lo.pro
- cddcvesfhfp1.wris.monster
- deefveskiip2.wris.monster
- cumfpo90sing.agddns.net
- procdia42ecte.agddns.net
- viedeu98.agddns.net
- laipros50.agddns.net
- fira24sonstablee.agddns.net
- chronback49in.duckdns.org
Злоумышленники маскируют сертификат, используемый для шифрования взаимодействия бэкдора с C2-сервером, при помощи Server Name Indication (SNI), расширения протокола TLS. Настоящий сертификат выдается по имени, указанному в поле sni конфигурации бэкдора. Таким образом, они могут избежать обнаружения других C2-серверов по сертификату и получения других сертификатов без знания имени сервера из конфигурации. Сертификат является заглушкой, которая возвращается сервером в том случае, если не указано имя сервера в запросе. Она не используется при взаимодействии с C2-сервером.
Отпечаток сертификата (SHA-256) и соответствующие им SNI, использовавшиеся злоумышленниками и обнаруженные нами в конфигурационной секции, представлены в таблице 6.
Таблица 6. Информация о сертификатах
| SHA-256 | SNI |
| 29AD1A06DCA85041E793A8BF2F966B6A7CF3F35904FB3E8648A7F97D9A211F8D | run.sssddd.org |
| 0B3B6E06CB7B6C25066B0DAEA6CF2D6EEA57D33FB58EF66EBAEF2107BA2A92B0 | zfs.wefwe.net |
2.4.5. Эволюция
Помимо руткита PUMAKIT, группировка (Ex)Cobalt регулярно использует и другой инструмент — Facefish, частично повторяющий его функциональность. В ходе расследований инцидентов наши специалисты неоднократно фиксировали случаи одновременного применения двух этих инструментов внутри одной и той же инфраструктуры.
Ситуация дополнительно усложняется тем, что ранее злоумышленники использовали еще один руткит — Kitsune, который был подробно изучен нашими коллегами из BI.ZONE.
Сопоставив возможности всех трех инструментов и временные метки их появления, можно четко проследить, как эволюционировал инструментарий группировки с течением времени.
Таблица 7. Сравнение инструментов
| Facefish | Kitsune | PUMAKIT | |
| Функции |
|
|
|
| Первое обнаружение | Февраль 2021 г. | Февраль 2022 г. | Март 2024 г. |
| Тип руткита | Userland | Userland | Kernel |
| Компоненты |
|
|
|
| Способ закрепления в системе | Дроппер сохраняет userland-руткит в файл /lib64/libs.so и прописывает его в /etc/ld.so.preload | Userland-руткит сохраняется злоумышленниками в файл /lib64/libselinux.so (как в рутките Azazel) и прописывается в /etc/ld.so.preload | Подмена системного сервиса cron |
| Способ активации | При вызове функции bind() процессом sshd | При вызове функции bind() процессом sshd | Активируется сразу при старте подмененного cron-сервиса |
| Формат пакетов для общения с C2-сервером | Собственный формат пакетов | BSON | BSON |
| Хранение конфигурации | Хранится в «хвосте» дроппера | Хранится в файле /etc/config__hhide | Хранится в виде ELF-секции руткита, в некоторых версиях в файле /usr/share/zov_f/zov_config |
| Механизмы скрытности | Отсутствуют | Реализован механизм скрытия записи в /etc/ld.so.preload. Скрывает файлы, процессы и сетевые соединения, связанные с ними, на уровне пользователя через перехват функций libc | Полноценный руткит уровня ядра. Скрывает:
|
При этом мы считаем, что Facefish не был разработан группировкой (Ex)Cobalt, а был приобретен на черном рынке или получен в результате утечки. В пользу данного предположения говорит тот факт, что используемая в Fasefish конфигурация расположена в конце файла и не упаковывается UPX, а просто зашифровывается (рис. 30). Таким образом, не имея исходного кода, злоумышленники могли беспрепятственно менять параметры конфигурации.

Однако предоставляемой Facefish функциональности, по всей видимости, оказалось недостаточно, вследствие чего группировка разработала собственный руткит — Kitsune, во многом наследующий возможности своего предшественника. Примечательно, что в Facefish и в Kitsune запуск бэкдора привязан к вызову функции bind(), что указывает на прямую преемственность архитектурных решений.
В то же время имеются все основания предполагать, что Kitsune был создан на основе исходного кода руткита Azazel: помимо схожих архитектурных решений, шифрование строк, заложенное в Azazel (XOR с байтом 0xFE), сохранилось в Kitsune и в дальнейшем перешло в его следующую версию (рис. 31 и 32).


Переход от руткита Kitsune к PUMAKIT не был резким: между ними существовала промежуточная версия, известная как Megatsune. И хотя Megatsune по-прежнему оставался руткитом уровня пользователя, а его архитектура и логика напрямую наследовали решения Kitsune — именно на Megatsune злоумышленники начали тестировать бэкдор, позднее реализованный в PUMAKIT, сохранив ту же логику работы, набор команд и структуру конфигурации.
На основании проведенного анализа мы объединяем эти три инструмента в одно семейство — PUMA, предполагаемая цепочка эволюции которого представлена на рис. 33.

2.4.6. Методы обнаружения и борьбы с руткитом в своей системе
В ходе анализа мы выявили в LKM-модуле важную уязвимость, проэксплуатировав которую можно обнаружить установленный модуль PUMAKIT через его же интерфейс: руткит не проверяет, какой именно процесс вызывает переопределенный rmdir для выполнения команд. Таким образом, вызвав rmdir с определенными аргументами, можно однозначно определить присутствие руткита в системе.
Вы можете легко проверить свою систему на наличие руткит-модуля, воспользовавшись нашим скриптом для детекта.
import ctypes
import subprocess
import os
SYS_rmdir = 84
buffer_size = 16
path_buf = ctypes.create_string_buffer(buffer_size)
ctypes.memmove(path_buf, b"zarya.u\0", 7)
libc = ctypes.CDLL("libc.so.6", use_errno=True)
ret = libc.syscall(SYS_rmdir, path_buf)
try:
proc = subprocess.Popen(
"lsmod | grep audit",
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True
)
output, _ = proc.communicate()
if output.strip():
path_buf2 = ctypes.create_string_buffer(buffer_size)
ctypes.memmove(path_buf2, b"zarya.t.0\0", 9)
ret2 = libc.syscall(SYS_rmdir, path_buf2)
if ret2 == 0:
print(f"Puma detected on this machine, module info:\n{output.strip()}")
else:
print("Puma wasn't detected at this machine")
else:
print("Puma wasn't detected at this machine")
except Exception as e:
print("Error:", e)
Листинг 3. Скрипт для обнаружения скрытого модуля руткита
Уязвимость трудноустранима, поскольку злоумышленники, используя возможности бэкдора, могут обновлять лишь его код, но не код загруженного в систему модуля ядра. Для этого потребуется значительно больше усилий.
Кроме того, чтобы убедиться в отсутствии вмешательства руткита в файл authorized_keys, выполните в своей системе следующий скрипт.
import difflib
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
def read_with_binary(binary_path: str, target_file: str) -> bytes:
try:
res = subprocess.run(
[binary_path, target_file],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
check=True
)
return res.stdout
except subprocess.CalledProcessError:
print(f"Ошибка: не удалось запустить {binary_path}", file=sys.stderr)
sys.exit(1)
def detect_in_memory_modification(authorized_keys: Path):
clean = read_with_binary("/bin/cat", str(authorized_keys))
with tempfile.TemporaryDirectory() as td:
fake_ssh = Path(td) / "ssh"
shutil.copy2("/bin/cat", fake_ssh)
fake_ssh.chmod(0o755)
modified = read_with_binary(str(fake_ssh), str(authorized_keys))
if clean != modified:
print("Обнаружено вмешательство руткита!")
clean_lines = clean.decode(errors="ignore").splitlines()
mod_lines = modified.decode(errors="ignore").splitlines()
diff = difflib.ndiff(clean_lines, mod_lines)
added = [
line[2:]
for line in diff
if line.startswith('+ ') and line[2:].strip() != ''
]
if added:
print("\nДобавленные строки:")
for l in added:
print(f" + {l}")
else:
print("Изменения обнаружены, но добавленных строк не найдено.")
else:
print("Не обнаружено подмены содержимого authorized_keys при запуске ssh-процесса.")
if __name__ == "__main__":
path = sys.argv[1] if len(sys.argv) > 1 else os.path.expanduser("~/.ssh/authorized_keys")
ak = Path(path)
if not ak.is_file():
print(f"Ошибка: файл {ak} не найден.", file=sys.stderr)
sys.exit(1)
detect_in_memory_modification(ak)
Листинг 4. Скрипт для проверки возможной подмены содержимого файла authorized_keys
Скрипт последовательно читает файл authorized_keys через настоящий /bin/cat и через его копию, переименованную в ssh, сравнивает оба вывода и при обнаружении различий выводит только те строки (ключи), которые руткит «подмешивает» на лету.
Отключение локального файрвола, реализованное в рутките, наглядно демонстрирует, насколько легко злоумышленники могут обойти встроенные средства защиты, если те функционируют исключительно внутри целевой системы. Чтобы противостоять подобным атакам, критически важно выносить фильтрацию трафика за пределы потенциально скомпрометированной среды. Одним из надежных решений может стать использование внешнего межсетевого экрана нового поколения, например PT NGFW — продукта, который не только отслеживает и блокирует подозрительный трафик на уровне приложений, но и позволяет своевременно выявлять сложные сетевые атаки, включая попытки обхода традиционных механизмов защиты.
2.5. Octopus и его щупальца
Еще одним заслуживающим внимания элементом инструментария группы является реализованный на Rust инструмент Octopus, который может применяться для повышения привилегий и закрепления в скомпрометированной Linux-системе.
2.5.1. Локальные крейты
В реализации инструмента применялся ряд недоступных публично крейтов1. Приведем их краткое описание.
1 Crate, контейнер — модуль, обособленная единица компиляции в языке Rust.
Таблица 8. Локальные крейты Octopus
| Крейт | Назначение |
| octopus | Обертка для управления функциональностью, предоставляемой крейтами, приведенными ниже |
| octorepl | Реализация read–eval–print loop (REPL) на основе крейта clap |
| octosys | Реализация взаимодействия с операционной системой |
| octoproc | Реализация взаимодействия с оболочкой во время запуска эксплойтов |
| octolog | Реализация журналирования |
| leech | Реализация библиотеки патчинга на основе крейта gimli, также присутствует возможность сетевого взаимодействия |
| baron | Реализация CVE-2021-3156 (уязвимость heap overflow в утилите sudo), переписанная на Rust |
| route4-filter | Реализация CVE-2022-2588 (уязвимость double free в функции route4_change), переписанная на Rust |
| looney_tunables | Реализация CVE-2023-4911 (уязвимость buffer overflow в ld.so), переписанная на Rust |
| infect | Реализация заражения исполняемых файлов |
| gtfo | Реализация поиска GTFOBins |
2.5.2. Поток управления
Поток управления базируется на локальном крейте octorepl, который основан на крейте clap и реализует собственный REPL. Если Octopus будет запущен без аргументов, откроется интерактивная оболочка — REPL. При вызове команды help из оболочки будет выведен список команд и их описание.

В режиме REPL нет возможности запускать часть команд, которые доступны только в режиме запуска через интерфейс командной строки.

Если в режиме REPL попытаться ввести команду, которая доступна только в командной строке, оболочка подсветит такую команду красным цветом, в ином случае зеленым. Стоит отметить, что в оболочке есть автодополнение команд, а также что за дополнительной информацией можно обратиться на внутреннюю вики.

2.5.3. Команды
Инструмент Octopus содержит следующий набор команд.
Таблица 9. Команды Octopus
| Команда | Описание |
| detect | Проверяет, какие эксплойты повышения привилегий работают в системе. Эксплойты берутся из команды do |
| system-id | Генерирует идентификатор жертвы посредством сбора информации о системе и хеширования ее алгоритмом MD5 |
| do | Запуск набора эксплойтов для повышения привилегий:
|
| su | Сканирование системы для обнаружения компонентов, содержащих уязвимости, позволяющие повысить привилегии. Использует команду detect |
| glue | Модификация легитимных утилит, установленных в системе, для обеспечения запуска вшитой полезной нагрузки |
| netpatch | Патчинг бинарных файлов, для перехвата соединения и восстановления себя |
| netpatch-update | Обновление бинарных файлов, ранее пропатченных с помощью команды netpatch |
| check-bin | Проверка бинарного файла на роль кандидата для заражения |
| gtfo | Поиск GTFOBins |
| help | Вывод информации о командах |
2.5.4. Полезная нагрузка
Octopus несет в себе 4 полезных нагрузки:
- Руткит libzst.so.0 (мы дали название ему Spawner). Руткит для межпроцессорного взаимодействия.
- Руткит libsockopt.so.1 (мы дали название ему TransMarker). Руткит для межпроцессорного взаимодействия.
- Руткит libsockopt.so.2 (mosquito). Руткит для межпроцессорного взаимодействия.
- Бэкдор mycelium. Инструмент для удаленного управления зараженной системой.
Полезная нагрузка находятся в секции .rodata в сериализованном (CBOR) и сжатом (zstd) виде.

import zstandard
import cbor2
import sys
import json
dctx = zstandard.ZstdDecompressor()
with open(sys.argv[1], 'rb') as ifh, open(sys.argv[1] + '.dec', 'wb') as ofh:
dctx.copy_stream(ifh, ofh)
with open(sys.argv[1] + '.dec', 'rb') as fp, open(sys.argv[1] + '.des', 'w') as fw:
obj = cbor2.load(fp)
json.dump(obj, fw, indent = 4, sort_keys = True, default = repr)
Листинг 5. Скрипт на Python для декодирования полезной нагрузки
После десериализации будет получен JSON-объект, хранящий тело полезной нагрузки, а также индекс и название (при наличии).

2.5.5. Команда glue
Команда отвечает за модификацию легитимных утилит, установленных в системе, для обеспечения запуска вшитой полезной нагрузки.

Для использования команды должна быть указана как минимум одна из опций --bin, --bin-network или --package-managers. В противном случае будет выведено сообщение об ошибке.

Для каждого переданного команде glue файла будет выполнена проверка на наличие библиотеки libc.so.6 в зависимостях. Более того, Octopus должен иметь доступ к атрибуту security.selinux (если такой имеется у библиотеки).
В случае успешных проверок, в зависимости от того, передавался ли аргумент --glue, Octopus попытается либо считать файл (если в glue передавался путь до библиотеки, при этом важно, чтобы путь начинался с символа /), либо найти его по имени среди встроенных библиотек.
Если glue не удалось по какой-либо причине найти, Octopus выдаст сообщение об ошибке.
Далее Octopus сериализует конфигурацию, необходимую для работы руткитов, используя последовательно алгоритм CBOR и сжатие с помощью zstd. Сериализацию проходят параметры, переданные команде в качестве аргументов. Затем Octopus записывает необходимые руткиты в каталог, в котором находится libc.so.6. В каждом записываемом рутките дополнительно создается секция с типом SHT_NOTE, в которую помещается сериализованная ранее конфигурация.
Крейт, который непосредственно отвечает за патчинг, назван leech.

Суть используемого метода патчинга состоит в следующем. В выбранный легитимный исполняемый файл внедряется дополнительная зависимость от вредоносного модуля, который является библиотекой .so.
Опишем механику внедрения зависимости. Leech пересобирает секции исходного бинарного файла, добавляя в секцию .dynamic (содержит сведения о динамически загружаемых модулях) ссылку на строку с именем внедряемого вредоносного модуля (дописывается в секцию .dynstr). Вначале leech парсит исходный бинарный файл, собирая информацию о его сегментах и секциях. В первую очередь это секция .interp (специальная секция, которая содержит путь к нужному компоновщику), а также секции с типом SHT_NOTE. Секции SHT_NOTE, как правило, несут дополнительную информацию о файле, однако существует такая секция с именем .note.ABI-tag, которая, согласно спецификации, должна присутствовать в каждом ELF-файле.

Далее leech ищет индекс секции, содержащей таблицу символов (данная секция называется .dynstr и имеет тип SHT_STRTAB), и индекс секции, в которой находится информация, необходимая для динамического связывания (секция имеет имя .dynamic и тип SHT_DYNAMIC).
Затем, в соответствии с собранной информацией, создается новый сегмент с типом PT_LOAD, куда помещаются собранные секции. А в секции .dynamic дополнительно создается поле с типом DT_NEEDED и указателем на строку с именем руткита, которое предварительно дописывается в секцию .dynstr.
Таким образом, при запуске пропатченного файла компоновщик автоматически подгрузит руткит (так как это необходимая зависимость). В результате модификаций создается временный файл с именем <original_parent_dir>/.~<original_name>.tmp, куда записывается пропатченный ELF с учетом перестроения.
В случае корректного запуска данный временный файл переименовывается в оригинальный (с помощью системного вызова rename), тем самым замещая его.
2.5.6. Команда netpatch
Команда netpatch внедряет полезную нагрузку в один из сервисов, функционирующих в системе.

Внедрение происходит через модификацию исполняемого файла целевого сервиса. В качестве полезной нагрузки по умолчанию применяется бэкдор mycelium. Также есть возможность использовать в качестве полезной нагрузки встроенный бэкдор mosquito (libsockopt.so.2). Для его применения необходимо указать опцию --mosquito при выполнении команды netpatch.
При использовании опции --mosquito значительно уменьшаются возможности Octopus: нельзя использовать опцию --connect-to и не поддерживаются спавнеры (например, сервис cron) и пакетные менеджеры.
В опцию --connect-to передается значение в формате host:port, к этому порту будет подключаться полезная нагрузка (в данном случае mycelium).
Слово «спавнер» в контексте Octopus означает программу-демон, пропатченную библиотекой libzst.so.0, которая с определенной периодичностью (согласно справке к команде glue, в интервале от --interval до --interval-spread) будет запускать нагрузку (в данном случае это mycelium).
Стоит отметить, что бэкдор mycelium работает на дистрибутивах, версия ядра которых не ниже 2.6.27.

При запуске команды netpatch будет выведена информация о доступных в системе сетевых интерфейсах, перебор которых осуществляется с помощью инструмента Netlink.

Выводится также информация об известных и неизвестных сервисах, а также о названиях пакетов, которые их поставляют.

Под известными подразумеваются следующие сервисы:
- cron (crond),
- sshd,
- postgres,
- nginx,
- apache,
- lighttpd,
- mariadb.
Контроль над тем, что необходимо пропатчить, осуществляется либо с помощью опции —filter, либо с помощью непосредственного считывания строки во время запуска команды.
Если команда netpatch запущена c полезной нагрузкой по умолчанию (mycelium), то при указании опции —connect-to для данной нагрузки кодируется конфигурация и генерируется TOKEN (см. рис. 49).
Начальная конфигурация для mycelium представляет собой набор аргументов, с которыми он запускается.

Конфигурация сериализуется алгоритмом CBOR. Затем генерируется случайная гамма размером 5 байт и сериализованная конфигурация гаммируется. К зашифрованной конфигурации дописывается сгенерированная гамма, и все это кодируется алгоритмом Base64.

Закодированная конфигурация mycelium будет дописана в секцию с конфигурацией руткита, который записывается командой glue.

После того как все переданные сервисы были пропатчены, Octopus заменяет MD5-хеш-суммы пропатченных сервисов. Например, для сервиса openssh в системе с dpkg изменится сумма в файле /var/lib/dpkg/info/openssh-server.md5sum.
Для запуска mycelium Octopus вызывает fork и в процессе-потомке использует вызов execveat.
Стоит заметить, что вшитая полезная нагрузка на диск не записывается. Она хранятся в скрытом файле с именем kernel, который создается с помощью системного вызова memfd_create.
Выводы
Анализ активности группы (Ex)Cobalt показывает её стремление к сохранению устойчивого и скрытного присутствия в скомпрометированной инфраструктуре. Группа изменила тактику первоначального доступа, сместив фокус внимания с эксплуатации 1-day уязвимостей в доступных из Интернета корпоративных сервисах (например, Microsoft Exchange) на проникновению в инфраструктуру основной цели через подрядные организации.
(Ex)Cobalt подбирает инструментарий под прикладное программное обеспечение жертвы, например, использует вредоносные шеллы для «1С», а также разрабатывает и развивает собственные инструменты, например, руткит PUMAKIT и средство для закрепления и повышения привилегий Octopus. Мы отмечаем богатый функционал данных инструментов, а также их относительную сложность по сравнению с вредоносными инструментами других групп.
Указанные факторы позволяют рассматривать (Ex)Cobalt как одну из наиболее опасных группировок, атакующих российские организации. Для эффективного противодействия подобным угрозам необходимо выстраивать комплекс мер: мониторинг ИБ, процессы по инвентаризации инфраструктуры и управлению уязвимостями и так далее. В случае обнаружения угрозы рекомендуется подключать экспертные команды по реагированию на инциденты, которые способны оперативно оценить масштаб заражения, проанализировать инструментарий и техники злоумышленников и составить план по противодействию атаке.
Матрица MITRE ATT&CK
Индикаторы компрометации
Файловые сигнатуры
rule apt_linux_UA_Excobalt__Backdoor__Octopus {
strings:
$code1 = {E8 ?? ?? ?? ?? 48 85 C0 ?? ?? 8A 0A 30 08 EB ??}
$code2 = {8B 8? ?? ?? ?? ?? 3D 00 CA 9A 3B 75 ??}
$s1 = "octopus"
$s2 = "octosys"
$s3 = "leech"
$s4 = "infect"
$s5 = "netpatch"
$s6 = "glue"
$s7 = "failed.patch"
$s8 = "Failed to patch"
$s9 = "TOKEN"
$s10 = "mosquito"
$s11 = "could not enumerate interfaces:"
$s12 = "IP addresses"
$s13 = "libzst.so.0"
$s14 = "libsockopt.so.1"
$s15 = "libsockopt.so.2"
condition:
((uint32(0) == 0x464c457f) and (9 of($s*)) and (any of($code*)))
}
rule apt_linux_UA_Excobalt__Rootkit__TransMarker {
strings:
$s1 = "Transferring fd "
$s2 = "libleech error: "
$s3 = ". Skipping task"
$s4 = "Task::expires_in"
$s5 = "Marker::service"
$code = {48 8D 35 ?? ?? ?? ?? 4C 8B 35 ?? ?? ?? ?? 6A ?? 5B 48 89 DF 41 FF D6 48 89 05 ?? ?? ?? ?? 48 8D 35 ?? ?? ?? ?? 48 89 DF 41 FF D6 48 89 05 ?? ?? ?? ?? 48 8D 35 ?? ?? ?? ?? 48 89 DF 41 FF D6 48 89 05}
condition:
(uint32(0) == 0x464c457f) and all of them
}
rule apt_linux_UA_Excobalt__Rootkit__Mosquito {
strings:
$s1 = "run/dbus/auxiliary_bus_socket"
$s2 = "/var/run/dbus/auxiliary_bus_socket"
$s3 = "lrex"
$s4 = "ssh"
$s5 = "minicbor"
$code = {48 8D BC 24 ?? ?? ?? ?? 48 8B 1D ?? ?? ?? ?? BA ?? ?? ?? ?? 31 F6 FF D3 4C 8D BC 24 ?? ?? ?? ?? BA ?? ?? ?? ?? 4C 89 FF 31 F6 FF D3 8A 05 ?? ?? ?? ?? 89 EB 84 C0 0F 84}
$code1 = {81 BC 24 ?? ?? ?? ?? 52 50 32 33 48 8B 04 24 48 8B 74 24 ?? 0F 85}
condition:
(uint32(0) == 0x464c457f) and all of them
}
rule apt_linux_UA_Excobalt__Rootkit__Spawner {
strings:
$s1 = "Initializing interceptors"
$s2 = "Init context"
$s3 = "Init glue"
$s4 = "Init completed"
$code = {48 8D 35 ?? ?? ?? ?? 48 8D 9C 24 ?? ?? ?? ?? 6A ?? 5A 48 89 DF FF 15 ?? ?? ?? ?? 48 8D 05 ?? ?? ?? ?? 48 89 44 24 ?? 48 C7 44 24 ?? ?? ?? ?? ?? 48 8B 03 4C 39 E0 0F 85}
condition:
(uint32(0) == 0x464c457f) and all of them
}
rule apt_linux_UA_Excobalt__Backdoor__Mycelium {
strings:
$s1 = "mycelium error: "
$s2 = "last heartbeat was too long ago"
$s3 = "Options"
$s4 = "FOREGROUND"
$s5 = "Do not daemonize"
$s6 = "mycelium"
$s7 = "connect_to"
$code = {31 D2 49 39 D6 74 ?? 48 39 F7 75 ?? 48 89 CE 48 89 C7 48 39 C8 74 ?? 44 8A 07 48 FF C7 45 30 44 15 ?? 48 FF C2 EB}
condition:
(uint32(0) == 0x464c457f) and all of them
}
rule apt_linux_UA_Excobalt__Dropper__PumaKit__Weapon {
strings:
$s1 = "[+] agent_id: "
$s2 = "[+] v240513"
$s3 = "ecure boot enabled"
$s4 = "[!!!] Secure boot enabled"
$s5 = "/lib/modules/%s/build/Module.symvers"
$s6 = "BOOT_IMAGE=" fullword
$s7 = " xy gunzip"
$s8 = " xxx unzstd"
$s9 = "[+] puma is compatible"
$s10 = "/usr/share/zov_f"
$s11 = "zov_logs.txt"
$s12 = "zov_latest"
$s13 = "__ksymtab"
$s14 = "/tmp/vmlinux" fullword
$s15 = "/tmp/script.sh" fullword
$s16 = "%99s %99s" fullword
$s17 = "HUINDER" fullword
$s18 = "--extract-weapon" fullword
$s19 = "tgt" fullword
$s20 = "wpn" fullword
$s21 = "--extract-target" fullword
$s22 = "/usr/bin/sshd -t" fullword
$s23 = "-et" fullword
$s24 = "-ew" fullword
condition:
(uint32(0) == 0x464c457f) and 6 of them
}
rule apt_linux_UA_Excobalt__Backdoor__Pumakit__ZovLatest__Strings {
strings:
$command_1 = "%s_d_0"
$command_2 = "%s_c_0"
$command_3 = "%s_k_%d"
$command_4 = "%s_%c_%d"
$command_5 = "%s_%c_%s"
$string_1 = "zarya" xor
$string_2 = "/bin/bash" xor
$string_3 = "/usr/share/zov_f/zov_logs.txt" xor
$string_4 = "/usr/share/zov_f/zov_latest" xor
$string_5 = "HISTFILE=/dev/null" xor
$string_6 = "TERM=xterm" xor
$string_7 = "agent_id" xor
$string_8 = "cmd_id" xor
$string_9 = "cmd_type" xor
$string_10 = "cmd_t" xor
$string_11 = "uptime" xor
$string_12 = "logs" xor
$string_13 = "path" xor
$string_14 = "shell" xor
$string_15 = "PRIVATE KEY" xor
$string_16 = "BEGIN" xor
$string_17 = "END" xor
$string_18 = "ping_interval_s" xor
$string_19 = "c2_timeout_s" xor
$string_20 = "session_timeout_s" xor
$string_21 = "cat /etc/*-release 2>&1" xor
$string_22 = "uname -a 2>&1" xor
$string_23 = "hostname 2>&1" xor
$string_24 = "users 2>&1" xor
$string_25 = "ifconfig 2>&1" xor
condition:
(uint32(0) == 0x464c457f) and (10 of ($string_*)) and (any of ($command_*))
}
rule apt_linux_UA_Excobalt__Rootkit__Pumakit__auditModule__Strings {
strings:
$s1 = "p_init" fullword
$s2 = "p_exit" fullword
$s3 = "is_pid_running" fullword
$s4 = "sys_call_table" fullword
$s5 = "name=audit" fullword
$s6 = ".puma-config"
$s7 = "PUMA %s"
$s8 = "Kitsune PID %ld"
$s9 = "----------+-----------------+----------+"
$s10 = "%-10s|%-17pI4|%-10d"
$s11 = "ssh-rsa"
$s12 = "LD_PRELOAD=/lib64/libs.so"
$s13 = "/usr/share/zov_f"
$s14 = "kit_so_len" fullword
condition:
(uint32(0) == 0x464c457f) and 6 of them
}
rule apt_linux_UA_Excobalt__Rootkit__Pumakit__auditModule {
strings:
$command_0 = { 40 80 ?? 30 0F 8? }
$command_1 = { 40 80 ?? 31 0F 8? }
$command_5 = { 40 80 ?? 35 0f 8? }
$command_9 = { 40 80 ?? 39 0F 85 }
$command_c = { 40 80 ?? 63 0F 84 ?? ?? ?? ?? 0F 8F }
$command_d = { 40 80 ?? 64 0F 8? }
$command_k = { 40 80 ?? 6B 0F 8? }
$command_t = { 40 80 ?? 7? 0F 8? }
$command_u = { 40 80 ?? 75 0F 8? }
$hook_check_standart_prolog = { 74 ?? 31 ?? ?? 81 ?? ?? 55 48 89 E5 0F 94 ?? 89 }
$hook_cmp_call = { 80 7C ?? ?? E8 75 }
$hook_res_addr = { 48 63 ?? ?? ?? 48 01 ?? 48 01 }
condition:
(uint32(0) == 0x464c457f) and (all of ($hook*)) and (4 of ($command_*))
}

