
Обнаружитель руткитов для Linux на основе eBPF, использующий многоканальный кросс-просмотровый анализ (sched_switch, NMI, /proc) для обнаружения DKOM, подмены точек трассировки и сокрытия процессов с верификацией целостности на уровне оборудования.
Системный анализ целостности процессов и перекрёстный просмотр
«Я спою, так сияй ярко, SPiCa...»
SPiCa — это детектор руткитов для Linux на основе eBPF, написанный на Rust. Название происходит от песни Hatsune Miku SPiCa и звезды, на которую она ссылается — Спики (Альфа Девы), самой яркой точки в созвездии Девы. То, что невооружённому глазу кажется одной звездой, на самом деле является спектроскопической двойной: две звезды во взаимной орбите, неразличимые как отдельные объекты без измерения их спектров. SPiCa применяет тот же принцип к наблюдению за ядром: несколько независимых каналов измеряют одно и то же состояние ядра с помощью физически различных механизмов, и руткит, подавляющий один из них, раскрывается другими.
Отказ от ответственности: Значительные части этой кодовой базы были сгенерированы или рефакторизованы с помощью GLM. Было проведено тщательное тестирование и итеративное проектирование, но перед использованием в производстве проверьте код на безопасность и производительность.
SPiCa разработан для победы над ограниченным eBPF-противником — злоумышленником с повышенными привилегиями (CAP_BPF или CAP_SYS_ADMIN), который загружает привилегированную программу eBPF в ядро. Этот противник принципиально слабее руткита в виде LKM, поскольку верификатор BPF накладывает жёсткие ограничения:
| Ограничение | Влияние на противника |
|---|---|
| Ограниченные циклы (~1 млн инструкций на программу) | Не может сканировать всю память ядра |
| Безопасность указателей (прямое разыменование требует доказуемо корректной, ограниченной цели) | Не может следовать произвольным указателям |
| Ограниченные хелперы (белый список для каждого типа программы) | Не может вызывать kmalloc, vmalloc или любую функцию выделения памяти ядра |
Нет произвольной записи в ядро (bpf_probe_write_kernel сильно ограничен) | Не может изменять task_struct, массивы функций точек трассировки или байт-код программы BPF |
| Доступ к картам ограничен загруженными ссылками | Не может получить доступ к картам других программ (если они не перечислены по ID — см. §7) |
Поскольку LKM блокируются шлюзом LSM после загрузки, этот ограниченный противник является реалистичной остаточной угрозой. Механизмы защиты от обхода в SPiCa настроены именно на эту угрозу — каждая защита честно сообщает, что она покрывает, а что нет.
init_module. SPiCa поднимает планку, блокируя более лёгкий вектор LKM, но не ограничивает противника сверху.SPiCa — это последний рубеж в стеке эшелонированной защиты, а не замена вышележащим уровням.
SPiCa запускает четыре программы eBPF, прикреплённые к хукам ядра, а также пользовательский движок обнаружения, который перекрёстно коррелирует их вывод с представлением системы о самой себе (/proc).
| Канал | Механизм | Что его подавляет | Стоимость подавления |
|---|---|---|---|
/proc | Перечисление каталогов procfs | DKOM (отвязка процесса от списка задач) | Низкая — хорошо известный метод руткитов |
sched_switch | Точка трассировки eBPF на планировщике | Открепление программы от массива функций; фильтрация контекста | Средняя — требуется запись в ядро в массив функций |
| NMI | Прерывание аппаратного счётчика производительности → событие eBPF perf | Перехват IDT/обработчика NMI | Почти гарантированная паника ядра — аппаратное прерывание, не маскируемое |
Ключевое архитектурное свойство: руткит не может подавить все три канала одновременно, чтобы само подавление не стало обнаруживаемым или дестабилизирующим. Подавление NMI требует модификации IDT (таблицы дескрипторов прерываний), что приводит к панике на большинстве ядер. Это «живая бомба» — единственный путь атакующего к полной слепоте с высокой вероятностью приведёт к краху системы.
sched sees it, /proc doesn't → DKOM NMI finds sched's heartbeat stopped → TAMPER sched goes globally silent, /proc != empty → SILENT
Каждый класс обнаружения — это дифференциальный вердикт: расхождение между двумя или более каналами. Движок обнаружения — это чистая функция над снимком реестра + /proc + метками времени каналов — без ввода-вывода, без побочных эффектов, полностью модульно тестируемый.
### Редизайн NMI: от наблюдения к целостности
В исходном дизайне NMI был вторым каналом наблюдения за процессами, который производил выборку ЦП и сообщал, какая задача выполняется. Это было избыточно: `sched_switch` уже наблюдает за планированием, а NMI получал те же данные через другой механизм. Избыточность стоила ~1000+ событий в кольцевом буфере/сек/ЦП данных о процессах, которые в 99,999% случаев подтверждали «да, планировщик делает то, что делает планировщик».
В переработанной архитектуре **NMI перепрофилирован с наблюдения за процессами на проверку целостности точек трассировки.** Он больше не сообщает, какой процесс находится на ЦП. Вместо этого он проверяет, что `sched_switch` действительно выполняется, считывая разделяемое сердцебиение в `.bss`. Это:
1. Устраняет ~99% трафика NMI в кольцевом буфере (почти нулевые события в установившемся режиме)
2. Напрямую обнаруживает отсоединение точек трассировки, подавление и сбои BTF/присоединения (исходная ошибка BTF — см. [§11](#11-the-btf-bug-incident))
3. Выполняется из аппаратного прерывания, вне пути диспетчеризации точек трассировки — невосприимчив к `bpf_override_return`, перехвату kprobe и манипуляциям с массивом funcs
4. Читает глобальные переменные `.bss` через прямой доступ к памяти, а не через помощники BPF — невосприимчив к `fmod_ret` на вспомогательных функциях
---
## 3. Канал наблюдения sched\_switch