PT Expert Security Center

(Ex)Cobalt. Обзор инструментов группы в атаках за 2024–2025 годы

(Ex)Cobalt. Обзор инструментов группы в атаках за 2024–2025 годы

Авторы:

Владислав Лунин

Владислав Лунин

Ведущий специалист группы исследования сложных угроз TI-департамента, Positive Technologies

Станислав Пыжов

Станислав Пыжов

Ведущий специалист группы исследования сложных угроз TI-департамента, Positive Technologies

Игорь Ширяев

Игорь Ширяев

Старший специалист департамента комплексного реагирования на киберугрозы, Positive Technologies

Максим Шаманов

Максим Шаманов

Младший специалист группы исследования сложных угроз TI-департамента, Positive Technologies

Кирилл Навощик

Кирилл Навощик

Младший специалист группы исследования сложных угроз TI-департамента, Positive Technologies

Ключевые моменты

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

Введение

Группа (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 подобные успешные случаи встречались.

Рисунок 2. Успешно восстановленные данные
Рисунок 2. Успешно восстановленные данные

Также участники группировки (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"

Если подобные события имели место, то необходимо выполнить следующие действия:

  • Завершить все активные сессии на всех устройствах, оставив одну на доверенном мобильном устройстве, а затем повторно войти на других необходимых устройствах.
Рисунок 3. Завершение активных Telegram-сессий на всех устройствах
Рисунок 3. Завершение активных Telegram-сессий на всех устройствах
  • Установить сложный локальный пароль для доступа к учетной записи, так как именно этот пароль защищает профиль учетной записи на устройстве.

Рисунок 4. Установка локального пароля для доступа к учетной записи Telegram

2.2. Flogon-кейлоггер

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

Рисунок 5. Модифицированный файл flogon.js
Рисунок 5. Модифицированный файл flogon.js

Все вариации данного стилера были подробно разобраны ранее.

2.3. Шеллы с расширением .epf («1С»)

Еще один интересный способ закрепления в атакованной инфраструктуре, ранее не наблюдавшийся в арсенале группировки (Ex)Cobalt, — размещение шеллов в виде отдельных файлов с расширением *.epf, которые реализуют механизм внешних обработок в программных продуктах «1С».

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

Рисунок 6. Реконструкция RDP-сессии со следами загрузки различных шеллов
Рисунок 6. Реконструкция RDP-сессии со следами загрузки различных шеллов

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

Рисунок 7. Реконструкция RDP-сессии со следами использования шелла для «1С».png
Рисунок 7. Реконструкция RDP-сессии со следами использования шелла для «1С».png

2.4. Руткит PUMAKIT

Наиболее интересным инструментом группировки (Ex)Cobalt, обнаруженным в ходе расследований, является руткит PUMAKIT, который маскируется под легитимные компоненты ОС и скрывает свое присутствие.

Ранее инструмент был описан в публикации Elastic Security Labs: коллеги описали дроппер и часть функциональности LKM-руткита. Позднее исследователи из Solar 4RAYS опубликовали собственный анализ, в котором описали особенности работы бэкдора и механизм кражи данных.

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

Рисунок 8. Диаграмма взаимодействия компонентов PUMAKIT
Рисунок 8. Диаграмма взаимодействия компонентов PUMAKIT

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

Рисунок 9. Результаты работы инструмента для поиска аномалий
Рисунок 9. Результаты работы инструмента для поиска аномалий
Рисунок 10. Обнаруженные аномалии во временных метках
Рисунок 10. Обнаруженные аномалии во временных метках

Таким образом был определен круг подозрительных файлов в служебных каталогах ОС. Затем путем ручного анализа удалось выявить файл, маскировавшийся под компонент операционной системы (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.

Логика установки начинается с идентификации узла: вычисляется его уникальный идентификатор, формирующийся следующим образом:

  1. с помощью Netlink-сокета в ядро отправляется запрос для получения информации о сетевых интерфейсах;
  2. MAC-адреса обнаруженных интерфейсов последовательно сохраняются, за исключением начинающихся с «docker», «veth» или «br»;
  3. полученные адреса объединяются в общий буфер, разделенный символами новой строки «\n»;
  4. к получившемуся результату дописывается локальное имя системы и подается на вход алгоритма хеширования MD5;
  5. вычисленное значение сохраняется в качестве 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

Если не удалось получить информацию напрямую — создается собственная версия экспортируемых символов ядра:

  1. Процесс обращается к двум файлам — /proc/version и /proc/cmdline, извлекая из них информацию о версии ядра.
  2. В каталоге /boot выполняется поиск файла, начинающегося с «vmlinuz-».
  3. Оставшаяся часть имени, определяющая версию ядра, сравнивается с теми версиями, которые были получены на шаге 1.
  4. Если все три версии ядра совпали — создается файл с именем /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. Базовый скрипт для распаковки сжатого файла ядра

  5. Данный скрипт запускается с помощью команды bash /tmp/script.sh “/boot/vmlinuz-<KERNEL_RELEASE>”.
  6. В результате выполнения скрипта в каталоге /tmp появляется разархивированный файл ядра.
  7. Сам скрипт удаляется.

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

  • __kcrctab_gpl: таблица контрольных сумм для символов, экспортируемых под лицензией GPL. Каждая запись содержит CRC, соответствующую символу из __ksymtab_gpl;
  • __ksymtab_gpl: таблица символов, используется для разрешения ссылок на символы в модулях, совместимых с лицензией GPL. Каждая запись имеет поля: 
    • value — адрес экспортируемого символа; 
    • name — указатель на строку в секции __ksymtab_strings, содержащую имя символа; 
  • __ksymtab: список всех экспортированных символов, кроме тех, которые помечены GPL. Структурно идентична __ksymtab_gpl.

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

Рисунок 11. Запись о версии ядра в секции .rodata
Рисунок 11. Запись о версии ядра в секции .rodata

2.4.2.2. Проверка совместимости

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

В ином случае будет выполнена загрузка LKM-руткита в систему жертвы с помощью системного вызова init_module.

2.4.2.3. Отладочный режим

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

Для его активации злоумышленники выполняют два последовательных шага: 

  1. Запускают дроппер с предварительно установленной переменной окружения HUINDER и одним из следующих аргументов:
    • —extract-target или -et для извлечения tgt.bin;
    • —extract-weapon или -ew для извлечения wpn.bin.
  2. После получения файла wpn.bin запускают его с аргументами -f, -v и -t (порядок аргументов не имеет значения) с правами суперпользователя.

После выполнения описанных выше действий загрузчик запускается в режиме отладки, в котором вместо непосредственной установки модуля осуществляется проверка его совместимости с системой. В этом режиме атакующему возвращается либо подтверждение возможности установки, либо подробное описание причин, по которым установка невозможна. Примеры возможных ошибок и их выводов показаны на рис. 12–14.

Рисунок 12. Ошибка: используется Secure Boot
Рисунок 12. Ошибка: используется Secure Boot
Рисунок 13. Ошибка: несовпадение экспортируемых символов
Рисунок 13. Ошибка: несовпадение экспортируемых символов
Рисунок 14. Предполагаемая последовательность действий злоумышленника
Рисунок 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. Расширенный скрипт для распаковки сжатого файла ядра

Рисунок 15. Работа расширенной версии скрипта
Рисунок 15. Работа расширенной версии скрипта

2.4.3. Третья стадия: LKM audit

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

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

Рисунок 16. Отключение защиты записи
Рисунок 16. Отключение защиты записи

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

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

Рисунок 17. Переопределение системных вызовов
Рисунок 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» при установке соединения
5IP-адресКоманда принимает IP-адрес в виде строки, преобразовывает его в 32-битное число и, при отсутствии соответствующей записи, добавляет его во внутреннюю таблицу скрываемых подключений (см. команду «9»). Используется совместно с командой «1» при установке соединения
kИдентификатор процесса (PID)Устанавливает PID руткита

Было установлено, что, помимо перечисленных выше команд, при вызове rmdir с аргументом zarya или zarya_ (без указания конкретной команды) у инициировавшего вызов процесса повышаются привилегии.

Рисунок 19. Пример отображения команды «9»
Рисунок 19. Пример отображения команды «9»

Анализ множества семплов показал, что в более поздних версиях функциональность модуля была расширена. В частности, была добавлена логика сокрытия используемых портов — аналогичная той, которая была описана для IP-адресов. Также были добавлены команды для управления портами (табл. 2).

Таблица 2. Добавленные в новых версиях команды

КомандаАргументОписаниеЗамечание
7Локальный портДобавляет локальный порт в список скрываемых (см. рис. 20), если значение уникальноВызов rmdir с данными командами выполняется при получении соответствующей команды с C2-сервера (см. описание полезной нагрузки бэкдора)
8Локальный портУдаляет локальный порт из списка скрываемых
Рисунок 20. Пример отображения обновленной команды «9»
Рисунок 20. Пример отображения обновленной команды «9»

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 выполняются две команды:

  1. truncate -s 0 /usr/share/zov_f/zov_latest;
  2. 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.

Рисунок 21. Структура конфигурационной секции модуля .puma-config
Рисунок 21. Структура конфигурационной секции модуля .puma-config

В конфигурации могут быть указаны следующие данные.

Таблица 3. Перечень команд для управления

Название записиТип структуры записиОписаниеЗначение по умолчанию
ping_interval_sInt32Интервал между попытками опроса C2-сервера при отсутствии установленного соединения5 секунд
hibernate_sInt32Время сна при неудачном подключении к C2-серверу (после использования должно быть повторно получено)0 секунд
session_timeout_sInt32Время жизни сессии3 секунды
c2_timeout_sInt32Максимальное время, в течение которого бэкдор подключен к одному серверу43 200 секунд (12 часов)
jitter_sInt32Величина случайного отклонения от ping50
tagStringТег жертвы«x»
certBinaryОтпечаток сертификата (SHA-256), используемого для подключения к серверуОтсутствует
sniStringДоменное имя в сертификате, используемом для подключения к C2-серверу
dns_sInt32Время жизни кэшированных DNS-записей3600 секунд
gw_pStringПорт, на котором бэкдор будет работать в режиме прокси-сервераОтсутствует
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).

Рисунок 22. Структура данных, возвращаемых в пространство пользователя при вызове rmdir с аргументом zarya_d_0 (в ранних версиях)
Рисунок 22. Структура данных, возвращаемых в пространство пользователя при вызове rmdir с аргументом zarya_d_0 (в ранних версиях)

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

Рисунок 23. Сохраняемая информация о пользователе (usrData)
Рисунок 23. Сохраняемая информация о пользователе (usrData)

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

Рисунок 24. Структура данных, возвращаемых в пространство пользователя при вызове rmdir с аргументом zarya_d_0 (в поздних версиях)
Рисунок 24. Структура данных, возвращаемых в пространство пользователя при вызове rmdir с аргументом zarya_d_0 (в поздних версиях)

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

Рисунок 25. Сохраняемая информация о пользователе (usrData)
Рисунок 25. Сохраняемая информация о пользователе (usrData)

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

Рисунок 26. Поддержка предыдущих версий модуля (usrData)
Рисунок 26. Поддержка предыдущих версий модуля (usrData)

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

Если же запись является уникальной — сведения о пользователе, перехваченные данные и информация о процессе-источнике кодируются в Base64 и записываются в журнал в порядке, определяемом типом перехваченных данных. Каждая запись начинается с временной метки, за которой следует сформированная CSV-строка, элементы которой разделены запятыми.

Рисунок 27. Данные для записи в zov_logs.txt
Рисунок 27. Данные для записи в zov_logs.txt

Получившаяся запись добавляется в конец файла /usr/share/zov_f/zov_logs.txt. Перед записью бэкдор обязательно проверяет источник, с которым связаны перехваченные данные. Если оказывается, что обработке подвергается собственный процесс ведения журнала — запись немедленно прекращается, что исключает саможурналирование и зацикливание записей.

2.4.4.2. Взаимодействие с С2-сервером

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

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

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

Таблица 4. Базовый заголовок для всех сообщений

Поле структуры сообщенияДобавляемые данные
agent_idУникальный идентификатор жертвы
vВерсия бэкдора 
pvВерсия модуля 
pPID процесса
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:

  • «cat /etc/*-release»;
  • «uname -a»;
  • «ip a»;
    «ifconfig»; 
  • «hostname»;
  • «users»

Полученные в результате данные группируются:

  • «os» + <data>
  • «uname» + <data>
  • «ip1» + <data>
  • «ip2» + <data>
  • «hostname» + <data>
  • «tag» + <data>
4099Выполняет произвольную шелл-команду и отправляет ее результатКоманды выполняются с помощью /bin/sh«result» + <stdout>
«output» + <stderr>
4102Обновляет внутренние таймеры

Обновляемые параметры:

  • «c2_timeout_s»;
  • «ping_interval_s»;
  • «session_timeout_s»;
  • «jitter_s»;
  • «dns_s»;
  • «hibernate_s»

Значения конфигурационных параметров:

  • «c2_timeout_s»;
  • «ping_interval_s»;
  • «jitter_s»;
  • «session_timeout_s»;
  • «dns_s»;
  • «tag»;
  • «cert»;
  • «sni»;
  • «s_a<№>»;
  • «s_p<№>»;
  • «s_t<№>»;

2.4.4.3. Используемые сертификаты

В ходе анализа доменов, использовавшихся для C2-серверов, было установлено, что ряд из них применяет весьма примечательный SSL-сертификат (рис. 29). Он интересен тем, что в качестве Issuer и Subject злоумышленники указали организацию «FSB» и адрес «2 Bolshaya Lubyanka Street».

Ранее, в ходе одного из расследований, мы встречали использование данного сертификата группировкой (Ex)Cobalt. Поэтому мы связываем руткит PUMAKIT с данной группировкой и рассматриваем его как часть ее инструментария.

Рисунок 29. Используемый сертификат
Рисунок 29. Используемый сертификат

Данный сертификат был использован следующими С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-256SNI
29AD1A06DCA85041E793A8BF2F966B6A7CF3F35904FB3E8648A7F97D9A211F8Drun.sssddd.org
0B3B6E06CB7B6C25066B0DAEA6CF2D6EEA57D33FB58EF66EBAEF2107BA2A92B0zfs.wefwe.net

2.4.5. Эволюция

Помимо руткита PUMAKIT, группировка (Ex)Cobalt регулярно использует и другой инструмент — Facefish, частично повторяющий его функциональность. В ходе расследований инцидентов наши специалисты неоднократно фиксировали случаи одновременного применения двух этих инструментов внутри одной и той же инфраструктуры.

Ситуация дополнительно усложняется тем, что ранее злоумышленники использовали еще один руткит — Kitsune, который был подробно изучен нашими коллегами из BI.ZONE.

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

Таблица 7. Сравнение инструментов

 FacefishKitsunePUMAKIT
Функции
  • Кража учетных данных SSH-пользователей через перехват функций ssh/sshd
  • Сбор и отправка сведений об узле
  • Исполнение произвольных команд
  • Реверс-шелл
  • В новых версиях был также реализован перехват функции getdelim: при чтении конфигурационного файла /etc/ssh/sshd_config будет добавляться дополнительный порт, на котором сервис SSH принимает входящие соединения
  • Кража различных учетных данных, включая данные SSH-пользователей 
  • Сбор и отправка сведений об узле
  • Исполнение произвольных команд
  • Реверс-шелл
  • Получение информации путем выполнения заготовленных команд
  • Кража различных учетных данных, включая данные SSH-пользователей 
  • Сбор и отправка сведений об узле
  • Исполнение произвольных команд
  • Реверс-шелл
  • Получение информации путем выполнения заготовленных команд
  • Бэкдор отделен от руткита, появилась возможность обновление бэкдора
  • Проксирование трафика
  • Добавление собственных ключей в файл authorized_keys
Первое обнаружениеФевраль 2021 г.Февраль 2022 г.Март 2024 г.
Тип руткитаUserlandUserlandKernel
Компоненты
  • Дроппер
  • Userland-руткит с функциями бэкдора и стилера
  • Userland-руткит с функциями бэкдора и стилера
  • Дроппер
  • Загрузчик LKM-руткита
  • LKM-руткит с функциями стилера
  • Бэкдор
Способ закрепления в системеДроппер сохраняет userland-руткит в файл /lib64/libs.so и прописывает его в /etc/ld.so.preloadUserland-руткит сохраняется злоумышленниками в файл /lib64/libselinux.so (как в рутките Azazel) и прописывается в /etc/ld.so.preloadПодмена системного сервиса cron
Способ активацииПри вызове функции bind() процессом sshdПри вызове функции bind() процессом sshdАктивируется сразу при старте подмененного cron-сервиса
Формат пакетов для общения с C2-серверомСобственный формат пакетовBSONBSON
Хранение конфигурацииХранится в «хвосте» дроппераХранится в файле /etc/config__hhideХранится в виде ELF-секции руткита, в некоторых версиях в файле /usr/share/zov_f/zov_config
Механизмы скрытностиОтсутствуютРеализован механизм скрытия записи в /etc/ld.so.preload. Скрывает файлы, процессы и сетевые соединения, связанные с ними, на уровне пользователя через перехват функций libc

Полноценный руткит уровня ядра. Скрывает:

  • процессы,
  • файлы и каталоги,
  • сетевые соединения

При этом мы считаем, что Facefish не был разработан группировкой (Ex)Cobalt, а был приобретен на черном рынке или получен в результате утечки. В пользу данного предположения говорит тот факт, что используемая в Fasefish конфигурация расположена в конце файла и не упаковывается UPX, а просто зашифровывается (рис. 30). Таким образом, не имея исходного кода, злоумышленники могли беспрепятственно менять параметры конфигурации.

Рисунок 30. Расположение конфигурации в Facefish
Рисунок 30. Расположение конфигурации в Facefish

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

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

Рисунок 31. Механизм шифрования строк в Azazel
Рисунок 31. Механизм шифрования строк в Azazel
Рисунок 32. Механизм расшифровки строк в бэкдоре PUMAKIT
Рисунок 32. Механизм расшифровки строк в бэкдоре PUMAKIT

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

На основании проведенного анализа мы объединяем эти три инструмента в одно семейство — PUMA, предполагаемая цепочка эволюции которого представлена на рис. 33.

Рисунок 33. Эволюция семейства PUMA
Рисунок 33. Эволюция семейства PUMA

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 из оболочки будет выведен список команд и их описание.

Рисунок 34. Список команд, доступных в REPL
Рисунок 34. Список команд, доступных в REPL

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

Рисунок 35. Список команд, доступных в командной строке
Рисунок 35. Список команд, доступных в командной строке

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

Рисунок 36. Отображение недоступных команд в REPL
Рисунок 36. Отображение недоступных команд в REPL

2.5.3. Команды

Инструмент Octopus содержит следующий набор команд.

Таблица 9. Команды Octopus

КомандаОписание
detectПроверяет, какие эксплойты повышения привилегий работают в системе. Эксплойты берутся из команды do
system-idГенерирует идентификатор жертвы посредством сбора информации о системе и хеширования ее алгоритмом MD5
do

Запуск набора эксплойтов для повышения привилегий:

  • pwnkit — запуск эксплойта CVE-2021-4034;
  • looney-tunables — запуск эксплойта CVE-2023-4911;
  • baron — запуск эксплойта CVE-2021-3156;
  • route4-filter — запуск эксплойта CVE-2022-2588
suСканирование системы для обнаружения компонентов, содержащих уязвимости, позволяющие повысить привилегии. Использует команду detect
glueМодификация легитимных утилит, установленных в системе, для обеспечения запуска вшитой полезной нагрузки
netpatchПатчинг бинарных файлов, для перехвата соединения и восстановления себя
netpatch-updateОбновление бинарных файлов, ранее пропатченных с помощью команды netpatch
check-binПроверка бинарного файла на роль кандидата для заражения
gtfoПоиск GTFOBins
helpВывод информации о командах

2.5.4. Полезная нагрузка

Octopus несет четыре экземпляра полезной нагрузки:

  • Руткит libzst.so.0 (мы дали ему название Spawner). Инструмент для внедрения «спавнера» в системный сервис (см. ниже в разделе 2.5.6).
  • Руткит libsockopt.so.1 (мы дали ему название TransMarker). Компактный реверс-шелл. 
  • Руткит libsockopt.so.2 (mosquito). Небольшой бэкдор для удаленного управления узлом.
  • Бэкдор mycelium. Бэкдор, обладающий богатой функциональностью, от поднятия сокет-сервера, сбора информации о системе, записи-чтения файлов, журналирования своих действий до сканирования узлов в сети жертвы.

Полезная нагрузка находятся в секции .rodata в сериализованном (CBOR) и сжатом (zstd) виде.

Рисунок 37. Вшитая полезная нагрузка
Рисунок 37. Вшитая полезная нагрузка
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-объект, хранящий тело полезной нагрузки, а также индекс и название (при наличии).

Рисунок 38. Разархивированная и десериализованная полезная нагрузка
Рисунок 38. Разархивированная и десериализованная полезная нагрузка

2.5.5. Команда glue

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

Рисунок 39. Параметры команды glue
Рисунок 39. Параметры команды glue

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

Рисунок 40. Запуск команды glue без параметров
Рисунок 40. Запуск команды glue без параметров

Для каждого переданного команде glue файла будет выполнена проверка на наличие библиотеки libc.so.6 в зависимостях. Более того, Octopus должен иметь доступ к атрибуту security.selinux (если такой имеется у библиотеки).

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

Если glue не удалось по какой-либо причине найти, Octopus выдаст сообщение об ошибке.

Далее Octopus сериализует конфигурацию, необходимую для работы руткитов, используя последовательно алгоритм CBOR и сжатие с помощью zstd. Сериализацию проходят параметры, переданные команде в качестве аргументов. Затем Octopus записывает необходимые руткиты в каталог, в котором находится libc.so.6. В каждом записываемом рутките дополнительно создается секция с типом SHT_NOTE, в которую помещается сериализованная ранее конфигурация.

Крейт, который непосредственно отвечает за патчинг, назван leech.

Рисунок 41. Название крейта, в котором реализована функция патчинга
Рисунок 41. Название крейта, в котором реализована функция патчинга

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

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

Рисунок 42. Сбор информации о бинарном файле
Рисунок 42. Сбор информации о бинарном файле

Далее 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 внедряет полезную нагрузку в один из сервисов, функционирующих в системе.

Рисунок 43. Параметры команды netpatch
Рисунок 43. Параметры команды 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.

Рисунок 44. Строка о минимальной версии ядра для запуска mycelium
Рисунок 44. Строка о минимальной версии ядра для запуска mycelium

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

Рисунок 45. Доступные сетевые интерфейсы
Рисунок 45. Доступные сетевые интерфейсы

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

Рисунок 46. Запущенные сервисы
Рисунок 46. Запущенные сервисы

Под известными подразумеваются следующие сервисы: 

  • cron (crond),
  • sshd,
  • postgres,
  • nginx,
  • apache,
  • lighttpd,
  • mariadb.

Контроль над тем, что необходимо пропатчить, осуществляется либо с помощью опции —filter, либо с помощью непосредственного считывания строки во время запуска команды.

Если команда netpatch запущена c полезной нагрузкой по умолчанию (mycelium), то при указании опции —connect-to для данной нагрузки кодируется конфигурация и генерируется TOKEN (см. рис. 49).

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

Рисунок 47. Команды mycelium и их описание
Рисунок 47. Команды mycelium и их описание

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

Рисунок 48. Шифрование конфигурации для mycelium
Рисунок 48. Шифрование конфигурации для mycelium

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

Рисунок 49. Передаваемая конфигурация для mycelium
Рисунок 49. Передаваемая конфигурация для mycelium

После того как все переданные сервисы были пропатчены, 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_*))
}


Вердикты продуктов Positive Technologies

(Ex)Cobalt. Обзор инструментов группы в атаках за 2024–2025 годы