Руткиты изнутри: типы, работа, защита

Я расскажу, как устроены руткиты в живую: не в теории, а на уровне того, как они прячутся в ядре, что делают с системными вызовами и почему обычные утилиты их не видят. Плюс разберём, как от них защищаться, какие есть инструменты и как написать простую проверку на C, которая покажет, включён ли Secure Boot.
Что такое руткит
Руткит это набор техник, цель которых: остаться незаметным в системе и при этом иметь контроль над тем, что система говорит о себе. Это не вирус в классическом смысле, это инструмент сокрытия. Он может быть установлен как легитимная программа, может прийти вместе с другим ПО, может быть внедрён атакующим после получения доступа. Важно понять одну вещь, с которой начинается вся тема. Руткит не пытается «быть сильнее» системы. Он пытается стать частью системы, чтобы система сама его скрывала.
Уровни привилегий и типы руткитов
Прежде чем перейти к технике, вспомним про кольца защиты. В x86 это ring 0 и ring 3, промежуточные почти не используются.
Ring | Что там работает | Уровень доступа |
Ring 0 | Ядро ОС, драйверы | Полный доступ ко всему |
Ring 3 | Приложения | Только через системные вызовы |
Отсюда и типы руткитов, от наименее привилегированных к наиболее.
Тип | Уровень | Что делает | Насколько глубоко прячется |
Application | ring 3 | Подменяет приложения, внедряется в процессы | Легко обнаруживается |
User‑mode | ring 3 | Хуки API, перехват библиотек | Средне |
Bootkit | загрузчик | Заражение MBR или UEFI | Очень глубоко |
Kernel‑mode | ring 0 | LKM, драйвер ядра | Глубоко |
Memory | ring 0 | Патч ядра в памяти, без модуля | Очень глубоко |
Hardware | прошивка | Живёт в железе, не в ОС | Практически не ловится |
Вот эта схема ниже показывает то же самое визуально. Кольца, от внешнего к внутреннему, и типы руткитов, которые живут на каждом уровне.


На картинках видно главное: чем ближе к железу, тем сложнее обнаружить. Hardware руткит находится вообще вне операционной системы, поэтому из ОС его не увидеть.
Как руткит работает в ядре
Дальше про Linux, потому что там проще показать механику, а Windows разберём отдельно в разделе про WinAPI.
Загрузка модуля
В Linux ядро монолитное, но расширяемое. Есть Loadable Kernel Module, LKM. Это объектный файл, который можно подгрузить в работающее ядро.
#include <linux/module.h>
#include <linux/kernel.h>
static int __init rk_init(void) {
printk(KERN_INFO "loaded\n");
return 0;
}
static void __exit rk_exit(void) {
printk(KERN_INFO "unloaded\n");
}
module_init(rk_init);
module_exit(rk_exit);
MODULE_LICENSE("GPL");Что тут важно. init и exit это не просто пометки, это указание ядру, что код можно выгрузить из памяти после выполнения. module_init и module_exit связывают наши функции с механизмом загрузки. MODULE_LICENSE обязателен, иначе ядро загрузит модуль, но пометит его как проприетарный и откажет в доступе к некоторым символам. Собирается это через Makefile:
obj-m += rootkit.o # говорим kbuild, что мы собираем модуль
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) cleanПосле загрузки модуль виден в lsmod. Значит, первая задача руткита: спрятаться.
Скрытие модуля из списка
Ядро хранит список модулей в двусвязном списке. Можно вырезать себя из этого списка.
static struct list_head *prev;
static void hide(void) {
prev = THIS_MODULE->list.prev;
list_del(&THIS_MODULE->list);
}Смысл в одной строке: list_del разрывает связи нашего узла с соседями, и список модулей больше про нас не знает. После этого lsmod и /proc/modules тебя не покажут, потому что они читают именно этот список. Но модуль остаётся в /sys/module/, поэтому хороший руткит чистит и это.
Перехват системных вызовов
Спрятаться мало, надо ещё управлять тем, что система рассказывает о себе. Для этого перехватывают системные вызовы. В старых ядрах было просто: есть таблица sys_call_table, меняешь указатель на свой обработчик.
// сохраняем указатель на оригинальную функцию open
static asmlinkage long (*orig_open)(const char __user *, int, umode_t);
// наш обработчик, который встанет вместо оригинала
static asmlinkage long hook_open(const char __user *path, int flags, umode_t mode) {
// если в имени файла есть слово secret, притворяемся что файла нет
if (strstr(path, "secret"))
return -ENOENT; // ENOENT это «файл не найден»
// во всех остальных случаях отдаём работу оригиналу
return orig_open(path, flags, mode);
}Ключевое здесь asmlinkage и __user. Первое говорит компилятору, что аргументы приходят через стек, как это принято в системных вызовах. Второе это маркер для анализатора, что указатель пришёл из пользовательского пространства и его нельзя просто так разыменовывать. Без этих пометок код либо не соберётся, либо упадёт в рантайме.
Сейчас таблица read‑only, и просто так её не переписать. Современные методы другие.
Метод | Работает на новых ядрах | Заметность |
Запись в cr0 | Нет | Высокая |
kprobes | Да | Средняя |
ftrace | Да | Низкая |
Хуки на VFS | Да | Низкая |
Скрытие файлов и процессов
Когда перехват есть, скрывать файлы и процессы становится делом техники. Для файлов перехватывают iterate_dir или filldir, для процессов вырезают запись из списка задач.
static int hook_filldir(struct dir_context *ctx, const char *name, int namlen,
loff_t offset, u64 ino, unsigned d_type) {
// если имя совпадает с тем, что мы прячем, не отдаём запись пользователю
if (strncmp(name, "hidden", namlen) == 0)
return 0; // 0 значит «пропустить эту запись»
// всё остальное отдаём оригинальному обработчику
return orig_filldir(ctx, name, namlen, offset, ino, d_type);
}filldir это функция, которую ядро вызывает для каждой записи в директории, когда пользователь делает ls или читает через getdents. Мы вклиниваемся в этот процесс и выкидываем то, что не хотим показывать. return 0 здесь означает «запись обработана, идём дальше», а не ошибка.
Windows и WinAPI
На Windows картина другая, но идея та же. Рутткит работает через драйвер ядра, который загружается как сервис. Дальше он может:
Перехватывать Native API, это функции
Nt*иZw*, которые лежат ниже WinAPIПатчить SSDT, System Service Descriptor Table, аналог
sys_call_tableСтавить inline‑хуки прямо в код функций ядра
Фильтровать то, что возвращают функции перечисления процессов и файлов
WinAPI это верхний уровень, то, чем пользуются приложения. Но под ним есть нативный уровень, и именно там работают руткиты. Если руткит перехватил NtQuerySystemInformation, то любые программы, которые через WinAPI смотрят список процессов, увидят то, что руткит решил им показать.
Уровень | Что там | Кто вызывает |
WinAPI |
| Приложения |
Native API |
| WinAPI внутри |
Ядро | Драйверы, SSDT | Native API внутри |
Именно поэтому современные антируткиты не доверяют WinAPI и лезут напрямую в ядро или сравнивают то, что им говорит система, с тем, что видно через другие каналы.
Методы защиты
Защита от руткитов строится на одной идее: перекрёстная проверка. Если руткит спрятал что‑то в одном месте, оно осталось в другом, и на этом его ловят.
Утилиты | Платформа | Что делает |
rkhunter | Linux | Сравнивает хеши, ищет подозрительные файлы и модули |
chkrootkit | Linux | Проверяет известные сигнатуры руткитов |
Volatility | Linux, Windows | Анализ дампа памяти |
GMER | Windows | Поиск скрытых драйверов и хуков |
Sysinternals Autoruns | Windows | Показывает всё, что грузится с системой |
rkhunter стоит держать на любой машине, где ты что‑то тестируешь. Он не гарантирует, что найдёт всё, но ловит классические схемы.
Secure Boot и почему он важен
Secure Boot это механизм UEFI, который проверяет подпись загрузчика и ядра до того, как они запустятся. Если подпись не совпадает с доверенной, загрузка останавливается. Это закрывает целый класс руткитов на уровне загрузчика.
Проверить, включён ли Secure Boot, можно разными способами. Вот простой пример на Cи, который читает переменную EFI через sysfs.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
// открываем файл переменной SecureBoot в sysfs
// GUID в имени файла это стандартный идентификатор переменной SecureBoot
FILE *f = fopen("/sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c", "rb");
if (!f) {
// если файла нет, значит система загрузилась в legacy режиме
printf("EFI variable not accessible (legacy BIOS or no permission)\n");
return 1;
}
// читаем первые 8 байт: 4 байта атрибутов и 4 байта значения
unsigned char buf[8];
size_t n = fread(buf, 1, sizeof(buf), f);
fclose(f);
if (n < 5) {
printf("unexpected data size\n");
return 1;
}
// пятый байт (индекс 4) это значение переменной
// 1 значит Secure Boot включён, 0 значит выключен
if (buf[4] == 1)
printf("Secure Boot: enabled\n");
else
printf("Secure Boot: disabled\n");
return 0;
}Что тут важно. Формат переменной EFI такой: первые 4 байта это атрибуты, дальше идёт значение. В нашем случае значение однобайтовое, поэтому смотрим на buf[4]. Если бы переменная была другого типа, данные лежали бы дальше, но у SecureBoot это именно один байт. Сборка:
gcc check_sb.c -o check_sb
sudo ./check_sbПрава нужны, потому что efivars не читаются из‑под обычного пользователя.
Что проверяем | Где смотреть | Что означает |
Secure Boot включён | efivars, байт 1 | Загрузчик проверяется |
Secure Boot выключен | efivars, байт 0 | Загрузчику доверяют без проверки |
Нет efivars | Файл отсутствует | Legacy BIOS, Secure Boot недоступен |
Перекрёстная проверка
Помимо утилит, есть более надёжный подход: сравнивать то, что говорит система, с тем, что видно в обход системы. Например:
Список процессов из
psсравнить со списком в/procнапрямую через syscallСписок модулей из
lsmodсравнить с содержимым/sys/module/Список сетевых соединений из
netstatсравнить с содержимым/proc/net/Код функций ядра сравнить с эталоном через хеши
Разница между двумя источниками это и есть след руткита.
Проверка | Что сравниваем | Что ловим |
Процессы |
| Скрытые процессы |
Модули |
| Скрытые модули |
Сеть |
| Скрытые соединения |
Сеть | Хеши функций | Inline‑хуки |
Что делать, если руткит найден
Порядок действий примерно такой.
Не выключать машину. Память содержит следы, которые на диске уже не останутся.
Снять дамп памяти через LiME или AVML, если это Linux, или WinPmem, если Windows.
Проанализировать дамп через Volatility, искать скрытые модули, хуки, аномалии.
Снять образ диска, если нужно идти дальше.
Сравнить хеши системных файлов с эталонными.
Если машина боевая, изолировать от сети, дальше по процедуре инцидента.
Это стандартный порядок, который используют в DFIR. Без снятия дампа памяти ты теряешь большую часть следов.
Итог
Руткит это не что‑то магическое, а сумма конкретных техник: скрыть модуль, перехватить вызов, спрятать файл, спрятать процесс. Каждая техника оставляет след в другом месте, и на этом строится защита. Защита от руткитов это не одна утилита, а набор: Secure Boot закрывает загрузчик, rkhunter и chkrootkit ловят классику, Volatility разбирает дампы памяти, перекрёстные проверки находят несоответствия. По отдельности ни один метод не даёт гарантии. Вместе они дают шанс заметить то, что иначе осталось бы невидимым.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.