io_uring руткит: какие артефакты он оставляет в памяти и как их найти

В апреле 2025 года ARMO выложила на GitHub proof-of-concept руткит Curing. Он выполнял файловые и сетевые операции целиком через интерфейс io_uring — и оставался невидимым для Falco и Tetragon, двух основных runtime-инструментов безопасности Linux. Через несколько месяцев Picus Security задокументировала RingReaper — уже боевой постэксплуатационный агент с той же механикой и полным набором тактик: разведка, сбор данных, сокрытие следов. Русскоязычные материалы по теме ограничиваются пересказом новости без единой команды для детектирования. Здесь — разбор того, что происходит на уровне ядра, конкретные артефакты, которые io_uring руткит всё равно оставляет в памяти, и пошаговый процесс их обнаружения.
Зачем атакующему io_uring руткит: место в цепочке атаки
Руткит появляется в цепочке атаки после первоначального проникновения и повышения привилегий. Задача — закрепиться в системе и скрыть присутствие от мониторинга: получать команды от C2-сервера (Command and Control — управляющий сервер атакующего, через который он раздаёт задачи и забирает результаты), собирать данные, удерживать доступ как можно дольше. Традиционные Linux-руткиты решали это через перехват системных вызовов (syscall hooking) или загрузку вредоносного модуля ядра (LKM — Loadable Kernel Module, по сути кусок кода, который встраивается прямо в работающее ядро). Оба подхода хорошо изучены и детектируются современными инструментами.
io_uring руткит работает иначе: он не модифицирует ядро и не подменяет функции. Вместо этого он использует легитимный механизм ядра так, что инструменты мониторинга не видят его операций. Это не эксплуатация уязвимости — это архитектурный пробел в подходе к детектированию. Разница принципиальная: патчить нечего, а проблема есть.
Зачем это атакующему в деньгах: по данным расследования Sandfly Security реального инцидента с LD_PRELOAD-руткитом, стелс-руткиты на Linux чаще всего применяются для криптомайнинга в связке с SSH-брутфорсером, хотя фреймворк легко переориентировать на вымогательство или шпионаж. IBM X-Force Threat Intelligence Index 2025 фиксирует, что infostealers (32%) опережают ransomware как самый распространённый тип вредоносного ПО — io_uring руткит идеально ложится в сценарий скрытой эксфильтрации данных.
Обход EDR через io_uring — как это работает на уровне ядра
io_uring — интерфейс асинхронного ввода-вывода, появившийся в ядре Linux 5.1 (2019). Чтобы понять, почему он создаёт проблему для EDR (Endpoint Detection and Response — система, которая следит за происходящим на хосте и реагирует на подозрительную активность), нужно разобрать обычную схему мониторинга.
Обычная схема: приложение хочет прочитать файл → вызывает системный вызов read() → ядро обрабатывает запрос. EDR-агенты перехватывают эти вызовы через eBPF-программы (extended Berkeley Packet Filter — механизм, который позволяет запускать проверочный код прямо внутри ядра без его пересборки), seccomp-фильтры или привязки к трейспойнтам. Каждый read(), write(), connect() виден и анализируется.
Схема io_uring: приложение и ядро делят два кольцевых буфера (ring buffers) в общей памяти:
- SQ (Submission Queue) — очередь подачи. Приложение записывает сюда запросы на операции в виде структур SQE (Submission Queue Entry).
- CQ (Completion Queue) — очередь завершения. Ядро складывает сюда результаты выполненных операций.
Аналогия для понимания: в здании есть охранник, который проверяет каждого посетителя — это syscall-мониторинг. io_uring — отдельная дверь с почтовым ящиком. Вы кладёте конверт с заданием, уходите, позже забираете конверт с результатом. Охранник на входе вас не видел.
Каждая SQE-запись содержит: opcode (код операции — чтение, запись, подключение к сокету), файловый дескриптор цели, адрес буфера данных и их размер. Приложение заполняет пакет SQE и одним вызовом io_uring_enter() передаёт их ядру. Ядро выполняет все операции и кладёт результаты в CQ. Конкретные системные вызовы read(), write(), connect() при этом не выполняются. По данным ARMO, io_uring поддерживает около 60 различных операций — от файлового I/O до сетевых подключений.
Почему Falco и Tetragon слепнут
Falco и Tetragon — два главных инструмента runtime-безопасности для Linux- и Kubernetes-окружений. Оба используют eBPF-программы, привязанные к системным вызовам: отслеживают запуск процессов, файловые операции, сетевую активность. Когда Curing выполнял все действия через io_uring, ни один перехватчик не сработал. Перехватывать было нечего.
Исследователи ARMO назвали это «большим пробелом в инструментарии безопасности Linux». Коммерческие EDR для Linux в большинстве случаев используют тот же подход — проблема системная.
Google ограничил io_uring в ChromeOS, Android и на своих продуктовых серверах ещё в 2023 году. В ядре 6.6 появился sysctl-параметр io_uring_disabled для полного отключения. Но на серверах с высоконагруженным I/O — СУБД, хранилища — отключить io_uring значит потерять ощутимую производительность. Типичная дилемма: безопасность или скорость, и серебряной пули нет.
Реверс-инжиниринг руткитов: Curing и RingReaper
Curing — PoC-демонстрация от ARMO
Curing реализует базовый сценарий: приём команд от C2 через TCP-соединение на порт 8888 и чтение/передача файлов (в демонстрации — /etc/shadow). Все сетевые и файловые операции проходят через io_uring, ни одного «видимого» syscall’а. Подразумевается, что руткит устанавливается после компрометации и получения root-привилегий.
RingReaper — боевой агент с полным TTP-покрытием
По данным Picus Security, RingReaper — полноценный постэксплуатационный агент. Его техники привязаны к классификации MITRE ATT&CK (открытая база тактик и техник, которую используют и атакующие, и защитники; каждая техника имеет уникальный T-код, например T1057 — по нему можно найти описание, детекты и примеры):
| Тактика | Техника (T-код) | Что делает через io_uring |
|---|---|---|
| Discovery | Process Discovery (T1057) | Асинхронно читает /proc для перечисления процессов и их владельцев |
| Discovery | System Network Connections Discovery (T1049) | Перечисляет активные соединения через ядерные таблицы |
| Discovery | System Owner/User Discovery (T1033) | Перечисляет PTS-сессии и залогиненных пользователей через /dev/pts |
| Collection | Data from Local System (T1005) | Читает /etc/passwd и другие чувствительные файлы |
| Privilege Escalation | Exploitation for Privilege Escalation (T1068) | Ищет SUID-бинарники для эскалации привилегий |
| Defense Evasion | Hide Artifacts (T1564) | Самоудаление бинарника после выполнения |
Каждая операция в обычном исполнении вызвала бы readdir(), open(), read(), unlink() — и была бы перехвачена EDR. Через io_uring они превращаются в SQE-записи, невидимые для syscall-мониторинга.
Анализ памяти Linux руткита — артефакты, которые он оставляет
io_uring обманывает инструменты на основе перехвата syscall’ов. Но он не обманывает физику работы ядра. Каждая операция оставляет следы, доступные для форензики (цифровой криминалистики — дисциплины, которая занимается сбором и анализом цифровых доказательств после инцидента).
Файловые дескрипторы io_uring в /proc
Для инициализации io_uring процесс обязан вызвать io_uring_setup() — единственный системный вызов, которого не избежать. Он создаёт файловый дескриптор типа anon_inode:[io_uring], видимый в /proc/[pid]/fd/. В /proc/[pid]/fdinfo/[fd_number] — информация о размере SQ и CQ, текущих указателях head/tail.
На типичном Linux-сервере io_uring используют 1-2 процесса: СУБД, storage-демоны, высоконагруженные веб-серверы. Если дескриптор io_uring появляется у бинарника из /tmp, /dev/shm или у процесса с нехарактерным именем — это прямой индикатор аномалии. Запомните этот момент: io_uring_setup() — та точка, где руткит всё-таки «светится».
Кольцевые буферы в ядерной памяти
SQ и CQ — memory-mapped области ядра. Каждая SQE содержит opcode операции (определён в заголовке ядра <linux/io_uring.h>), файловый дескриптор цели, адрес буфера и размер данных. Как один из участников дискуссии на OpenNet точно заметил: «данные в момент io_uring_enter очевидно доступны — информация лежит в памяти в кольцевом буфере, анализируй не хочу».
Модуль LiME (Linux Memory Extractor — модуль ядра, который снимает дамп оперативной памяти без остановки системы) позволяет зафиксировать полный образ RAM. В дампе через crash-utility можно найти struct io_ring_ctx — контекстную структуру io_uring с указателями на оба буфера. Стандартных плагинов Volatility3 для парсинга io_uring пока нет, но при наличии символов ядра (vmlinux или пакета debuginfo) crash-utility позволяет пройти по структурам вручную. Да, это муторно. Но это работает.
Сетевые соединения и метаданные файлов
io_uring обходит перехват connect(), но созданный сокет регистрируется в ядерных таблицах. TCP-соединение с C2 видно в /proc/net/tcp и через ss -tnp. Руткит уровня пользователя не может скрыть сокет — для этого нужен LKM-руткит с перехватом на уровне ядра. Curing и RingReaper — userspace, значит, сокеты видны.
При чтении файла через io_uring обновляется atime (время последнего доступа, если файловая система не смонтирована с noatime). При записи — mtime и ctime. Руткит без контроля ядра не может подавить это обновление.
«Удалённый» бинарник в памяти
RingReaper использует самоудаление через io_uring (T1564). Но если процесс продолжает работать после unlink бинарника, его исполняемый образ доступен через /proc/[pid]/exe — путь отображается как /path/to/binary (deleted). Карта памяти видна через /proc/[pid]/maps. По данным Elastic Security Labs, даже packed-бинарники (упакованные, например, UPX) восстанавливаются из /proc/[pid]/exe работающего процесса — и по их опыту расследований, packed-бинарник на Linux «в 99% случаев является вредоносным».
Обнаружение io_uring малвари — пошаговый алгоритм
Предпосылки: Linux-хост с ядром 5.1+, root-доступ. Для трассировки — bpftrace (установка: apt install bpftrace на Debian/Ubuntu). Для дампа памяти — собранный модуль LiME под текущее ядро.
Шаг 1. Найти процессы с io_uring-дескрипторами
Зачем: определить, какие процессы используют io_uring. На обычном сервере таких единицы, и все известны.
for pid in $(ls /proc | grep -E '^[0-9]+$'); do
if ls -la /proc/$pid/fd 2>/dev/null | grep -q 'io_uring'; then
echo "PID=$pid CMD=$(cat /proc/$pid/cmdline | tr '\0' ' ')"
echo " EXE=$(readlink /proc/$pid/exe 2>/dev/null)"
fi
done
Ожидаемый результат на чистом сервере: пустой вывод или 1-2 процесса СУБД. Неизвестные PID — повод перейти к шагу 2.
Шаг 2. Проверить подозрительный процесс
Для каждого подозрительного PID — четыре быстрые проверки:
readlink /proc/<PID>/exe— путь к бинарнику. Видите(deleted)или путь в/tmp,/dev/shm— серьёзный индикатор.ss -tnp | grep <PID>— сетевые соединения. ESTABLISHED на внешний IP с нестандартным портом — вероятна C2-связь.cat /proc/<PID>/fdinfo/<FD>(FD — номер io_uring-дескриптора) — размеры SQ/CQ и указатели.head -30 /proc/<PID>/maps— карта памяти. Анонимные rwx-сегменты (чтение+запись+исполнение одновременно) часто указывают на распакованный в памяти код. rwx на анонимном сегменте — это почти всегда либо JIT, либо что-то нехорошее.
Шаг 3. Трассировать операции в реальном времени
Зачем: увидеть, какие именно операции процесс выполняет через io_uring прямо сейчас. Это отличит легитимный I/O базы данных от чтения /etc/shadow и подключения к внешнему IP.
# Требуется bpftrace, ядро с io_uring tracepoints (5.6+)
# Поля args зависят от версии ядра
bpftrace -e 'tracepoint:io_uring:io_uring_submit_sqe
/pid == TARGET_PID/ {
printf("opcode=%d\n", args->opcode);
}'
Замените TARGET_PID на PID подозрительного процесса. В выводе opcode — код операции из <linux/io_uring.h>. Соответствие кодов операциям (IORING_OP_READ, IORING_OP_CONNECT и т.д.) зависит от версии ядра — загляните в заголовочный файл вашей системы.
Шаг 4. Снять дамп памяти
Зачем: зафиксировать состояние памяти для офлайн-анализа, включая содержимое кольцевых буферов. Критично, если руткит использует самоудаление — после завершения процесса артефакты из /proc исчезнут безвозвратно.
Команда: insmod lime.ko "path=/tmp/memory.lime format=lime". Дамп анализируется на отдельной чистой машине. В нём ищем: структуры io_ring_ctx через crash-utility (kmem -S для slab-аллокатора), сетевые структуры struct sock с корреляцией по PID, содержимое SQE-записей для восстановления истории операций руткита.
Детектирование Linux руткитов: KRSI и LSM-хуки
Рекомендация ARMO и Elastic Security Labs: для обнаружения io_uring руткита недостаточно мониторить системные вызовы. Нужен мониторинг на уровне LSM (Linux Security Modules — фреймворк безопасности ядра, через который работают SELinux и AppArmor; он «сидит» глубже, чем syscall-интерфейс).
KRSI (Kernel Runtime Security Instrumentation) — механизм ядра 5.7+, позволяющий прикреплять BPF-программы к LSM-хукам. В отличие от syscall-перехвата, LSM-хуки срабатывают на уровне самих операций независимо от интерфейса:
| LSM-хук | Что отслеживает | Ловит io_uring |
|---|---|---|
security_file_open |
Открытие файла | Да |
security_socket_connect |
Установка сетевого соединения | Да |
security_socket_sendmsg |
Отправка данных через сокет | Да |
Дополнительные меры для защиты:
- sysctl
io_uring_disabled(ядро 6.6+): значение 1 запрещает io_uring непривилегированным пользователям, значение 2 — полное отключение. Если io_uring не нужен рабочим нагрузкам — отключайте без раздумий. - seccomp-профили: фильтрация
io_uring_setup,io_uring_enter,io_uring_registerдля контейнеров без потребности в io_uring. - SELinux/AppArmor: политики, ограничивающие
io_uring_setupдля конкретных процессов.
Проблема глубже, чем io_uring. Вся парадигма «мониторим системные вызовы — ловим угрозы» сломалась в тот момент, когда ядро получило альтернативный канал взаимодействия с пользовательским пространством. KRSI закрывает текущий пробел, но это реактивная мера: ядро развивается быстрее, чем инструменты безопасности адаптируются.
По данным Elastic Security Labs, даже статические сигнатуры антивирусов ненадёжны — добавление одного нулевого байта к бинарнику руткита ломает детектирование у части движков. Единственный рабочий подход — многослойный: поведенческий анализ на уровне LSM + форензика памяти + мониторинг сетевых соединений, где каждый слой ловит то, что пропускает другой.
На практике я вижу одну и ту же картину: команды, которые полагаются на один инструмент, стабильно пропускают инциденты. Falco видит syscall’ы, но не видит io_uring. Антивирус видит сигнатуры, но не видит тривиально модифицированный бинарник. Даже KRSI не поможет, если руткит уже на уровне ядра через LKM. Выход — в комбинации: KRSI + анализ /proc + периодический дамп памяти через LiME. Это не красивая теория, а минимальный рабочий стек, который реально ловит то, что не видят одиночные решения. Если вы в начале пути в DFIR — на IB Basics фундамент закрывается за пару месяцев, после чего такие разборы превращаются из теории в рабочие инструкции.
Эту тему и смежные навыки разбирают на практике в курсе «Профессия Реверс-инженер» Codeby Academy.
