Как работают руткиты и можно ли им противодействовать на примере Singularity
- вторник, 17 февраля 2026 г. в 00:00:07
Всем привет. Экспрементируя со способами закрепления на Linux системах в рамках разработки своей системы мониторига безопасности, я наткнулся на руткит с открытым исходным кодом Singularity. Он использует большое количество методов для сокрытия себя от обнаружения, а открытый исходный исходный код позволяет досконально изучить эти методы. В данной статье я подробно расскажу вам, с помощью каких подходов руткиты закрепляются на Linux системах на примере Singularity.
Не вдаваясь в подробности, руткит - это программное обеспечение, целью которого является сокрытие следов своего фукнционирования, а также, следов фукнционирования других необходимых программ. Руткиты обычно применяются с целью привилегированного закрепления на системе. Почему привилегированного? Так как цель руткита скрывать свою работу, то для этого нужны высокие права, например, в Linux права ядра. Но зачем? Эти права нужны чтобы подменять функционирование самой системы, например, в Linux достаточно перехватывать и модифицировать системный вызовы.
Резюмируя, под руткитом будем понимать программное обеспечение, работающее на уровне операционной системы и скрывающее следы своей деятельности.
Singularity - один из представителей программного обеспечения описанного выше, работающий через ftrace. Какие у него фишки? Данный руткит позиционирует себя, как программное средство активно противодействующее обнаружению даже со стороны механизмов мониторинга на уровне ядра, например, eBPF. Кроме того, Singularity предлагает удобный способ для добавления процессов, которые необходимо скрывать, через отправку сигнала kill -59 на целевой процесс или установку переменной окружения MAGIC=mtz (можно изменить при сборке) перед запуском процесса. Давайте подробнее разберём какие механизмы использует Singularity для сокрытия.
В документации к руткиту описаны следующие хуки на системные вызовы:
static struct ftrace_hook hooks[] = { HOOK("__x64_sys_kill", hook_kill, &orig_kill), HOOK("__x64_sys_getpgid", hook_getpgid, &orig_getpgid), HOOK("__x64_sys_getpgrp", hook_getpgrp, &orig_getpgrp), HOOK("__x64_sys_getsid", hook_getsid, &orig_getsid), HOOK("__x64_sys_sched_getaffinity", hook_sched_getaffinity, &orig_sched_getaffinity), HOOK("__x64_sys_sched_getparam", hook_sched_getparam, &orig_sched_getparam), HOOK("__x64_sys_sched_getscheduler", hook_sched_getscheduler, &orig_sched_getscheduler), HOOK("__x64_sys_sched_rr_get_interval", hook_sched_rr_get_interval, &orig_sched_rr_get_interval), HOOK("__x64_sys_sysinfo", hook_sysinfo, &orig_sysinfo), HOOK("__x64_sys_pidfd_open", hook_pidfd_open, &orig_pidfd_open), };
Данный хук обрабатывает передачу сигнала в процесс и работает следующим образом:
если сигнал -59, то процессу выдаются рут-права и он добавляется в список скрываемых;
если сигнал не равен -59 и он не входит в сатндартные, то возвращается ошибка;
если сигнал стандартный, то
если процесс скрываемый, то вовзращаем ошибку что процесс не найден;
иначе передаём в процесс сигнал;
Исходный код представлен в файле modules/become_root.c -> hook_kill
Принцип работы остальных хуков фактически идентичен и представляет из себя следующий алгоритм:
ловится событие;
если целевой процесс необходимо скрывать, то возвращается ошибка, что цель не была найдена;
в противном случае, вызывается оригинальная функция-обработчик.
Отслеживаются следующие события:
getpgid - получение pgid процесса;
getpgrp - получение pgid вызывающего процесса;-
getsid - получение sid процесса;
getaffinity - получение маски привязки к процессору;
getparam - получение параметров процесса;
getscheduler - получение политики планировщика;
get_interval - получение кванта времени;
sysinfo - получение системной информации;
pidfd_open - создание файлового дескриптора на процесс.
Исходный код представлен в файле modules/become_root.c
Чтобы противодейтсвовать обнаружению при обходе файловой системы Singularity обрабатывает следующие системные вызовы:
Источник modules/hiding_chdir.c:
sys_chdir... - переход с директорию;
Источник modules/hiding_directory.c
sys_getdents... - чтение записей директории из файлового дескрипора;
Источник modules/hiding_readlink.c
sys_readlink - чтение файла, на который указывает ссылка;
sys_readlinkat - чтение файла, на который указывает ссылка по относительному пути;
Источник modules/hiding_stat.c
sys_stat - чтение метаданных файла;
sys_lstat - чтение метаданных ссылки;
newstat, newlstat, newfstatat - внутренний вариант чтения метаданных файла;
Источник modules/open.c
sys_openat - открытие файла по относительному пути;
sys_access, sys_faccessat - проверка прав на файл.
Принцип работы данных хуков тоже до банальности прост:
если наш путь принадлежит скрываемым, то возвращаем ошибку что данный файл не найден;
в противном случае запускаем оригинал фукнции.
Для противодействия логированию Singularity перехватывает следующие вызовы:
write, pwrite, tee и другие write-подобные системные вызовы - записывают данные из userspace в файл;
do_syslog - чтение из kernel.log;
read, pread, readv, preadv - чтение из файла.
Источники:
modules/hooks_write.c
modules/clear_taint_dmesg.c
В функционал зашито удаление строк taint и singularity из буфера обмена, записи в файлы, сокеты и так далее. Кроме того, просматривается изменение ftrace_enabled, для того чтобы невозможно было отключить ftrace и тем самым обезвредить руткит.
Для противодействия аудиту руткит перехватывает следующие вызовы:
audit_log_start - начинает новую audit-запись и возращает буфер для данных. В данном системном вызове Singularity предпринимает следующий функционал:
если текущий процесс скрыт, то возращает NULL (пустой буфер);
netlink_unicast - отправляет сообщение одному получателю. В данном системном вызове Singularity предпринимает следующий функционал:
если в payload есть pid, который необходимо скрывать, то пакет дропается;
recvmsg / recvfrom - принимают сообщения из сокета и передают его в userspace. В данном системном вызове Singularity предпринимает следующий функционал:
после оригинального вызова удаляет данные о скрытых процессах.
Источник modules/audit.c
Для противодействия средствам защиты основанным на технологии BPF, Singularity перехватывает следующие системные вызовы:
bpf_ringbuf_output, reserve, submit - отправляет данные из eBPF в userspace. Руткит блокирует вызовы у которых pid принадлежит процессам, которые необходимо скрывать;
bpf_map_lookup_elem, update_elem - поиск и обновления ключа в мапе eBPF. Руткит делает так, что если ключом является pid, который необходимо скрывать, то он возвращает NULL, что эквивалентно тому, что элемент не существует;
bpf_prog_run - исполняет программу BPF. Руткит блокирует исполнение, если программа запускается в контексте процессов, которые необходимо скрывать;
perf_event_output, perf_trace_run_bpf_submit - отправка perf-событий. Руткит блокирует отправку, если процесс скрыт.
Источник modules/bpf_hook.c
Кроме всего выше перечисленного, руткит скрывает себя из списка модулей ядра (Источник modules/hide_module.c) и байпассит проверку целостности LKRG (Источник modules/lkrg_bypass.c)
По итогу всего вышеперечисленного мы видим, что у нас на разборе руткит, который блокирует почти все способы его обнаружить как с помощью ручного анализа так и с помощью средств аудита и логирования. Кроме того, Singularity защищает себя от выгрузки. В совокупности, это нам даёт ситуацию при которой обнаружить руткит очень-очень сложно, а вытравить его из системы без физического доступа к ней практически нереально.
Все тесты проводились на виртуальной машине Ubuntu Server 24.04
По фактам, перечисленным выше становится очевидно, что самое лучшее, что мы можем сделать для противодействия данному руткиту это не допустить его, в противном случае, бороться с ним будет практически бесполезно.
Но как не допустить его запуск?
sudo nano /etc/default/grub
->ищем строчку GRUB_CMDLINE_LINUX_DEFAULT= -> добавляем в неё module.sig_enforce=1
Далее перезапускаем систему:
sudo update-grub && sudo reboot
После данной простой манипуляции система будет выбрасывать ошибку на попытку установить неподписанный модуль. Но если у человека есть права root значит, что он может эту настройку отключить обратно, когда увидит ошибку, что же делать в таком случае?
Представленный руткит, как уже было показано ранее, сам работает через хуки на системные вызовы с помощью ftrace. Однако, это не единственный механизм. В данном разделе я напишу простую программу на eBPF для отслеживания и запрета установки модулей ядра.
Более подробное описание программ на eBPF можно прочитать в другой моей статье
Для начала организуем проект:
mkdir bpf_tracer && cd bpf_tracer go mod init tarcer mkdir ebpf
После напишем саму BPF-программу на C:
//bpf_insmod.c #include <linux/bpf.h> #include <bpf/bpf_helpers.h> struct event { char comm[16]; }; struct { __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); __uint(max_entries, 128); } events SEC(".maps"); SEC("kprobe/__x64_sys_init_module") int block_init_module(struct pt_regs *ctx) { char comm[16]; bpf_get_current_comm(&comm, sizeof(comm)); bpf_printk("Blocked init_module from %s\n", comm); struct event evt = {}; __builtin_memcpy(evt.comm, comm, sizeof(comm)); bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &evt, sizeof(evt)); bpf_override_return(ctx, -1); return 0; } SEC("kprobe/__x64_sys_finit_module") int block_finit_module(struct pt_regs *ctx) { char comm[16]; bpf_get_current_comm(&comm, sizeof(comm)); bpf_printk("Blocked finit_module from %s\n", comm); struct event evt = {}; __builtin_memcpy(evt.comm, comm, sizeof(comm)); bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &evt, sizeof(evt)); bpf_override_return(ctx, -1); return 0; } char _license[] SEC("license") = "GPL";
В данном коде мы используем kprobe, а не tracepoint. Почему так? Потому что через tracepoint нельзя менять вывод системного вызова, а чтобы запретить загрузку модуля нам как раз нужно его поменять.
Далее пишем обёртку на Go:
package main //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -output-dir ebpf -target bpf -go-package=ebpf EbpfMonitoring bpf_insmod.c -- -I. -O2 -Wall -g import ( "C" "bytes" "encoding/binary" "fmt" "log" "os" "os/signal" "strings" "syscall" "github.com/cilium/ebpf/link" "github.com/cilium/ebpf/perf" "github.com/cilium/ebpf/rlimit" "tracer/ebpf" ) type Event struct { Comm [16]byte } func main() { if err := rlimit.RemoveMemlock(); err != nil { log.Fatalf("Failed to remove memlock: %v", err) } objs := ebpf.EbpfMonitoringObjects{} if err := ebpf.LoadEbpfMonitoringObjects(&objs, nil); err != nil { log.Fatalf("Failed to load eBPF objects: %v", err) } defer objs.Close() kp1, err := link.Kprobe("__x64_sys_init_module", objs.BlockInitModule, nil) if err != nil { log.Fatalf("Failed to attach kprobe for init_module: %v", err) } defer kp1.Close() kp2, err := link.Kprobe("__x64_sys_finit_module", objs.BlockFinitModule, nil) if err != nil { log.Fatalf("Failed to attach kprobe for finit_module: %v", err) } defer kp2.Close() rd, err := perf.NewReader(objs.Events, os.Getpagesize()) if err != nil { log.Fatalf("Failed to open perf buffer: %v", err) } defer rd.Close() fmt.Println("eBPF program running...") sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) go func() { var e Event for { record, err := rd.Read() if err != nil { log.Printf("Failed to read from perf buffer: %v", err) continue } if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &e); err != nil { log.Printf("Failed to decode event: %v", err) continue } fmt.Printf("Blocked insmod: File=%s\n", strings.TrimRight(string(e.Comm[:]), "\x00"), ) } }() <-sigChan }
Следующим шагом генерируем код и собираем программу:
sudo go generate sudo go build
Далее тестируем что всё работает:


Исследуя возможность обнаружения Singularity на системе, я нашёл следующий способ (-59 в команде kill можно менять при сборке, но исключительно для примера будем использовать стандартный сигнал -59). Работает он следующим образом:
запускаем bash-скрипт на фоне который что-то пишет в некоторый файл (например, каждую секунду пишет date >> output.txt;
посылаем по pid нашего скрипта сигнал -59;
если в течение минуты наш скрипт не отключился и продолжает писать в файл, однако, самого процесса НЕ видно в ps aux, то можно сделать вывод, что руткит его скрывает.
Практический пример:
пишем баш-скрипт, который в цикле что-то пишет в файл output.txt:
#!/bin/bash while [[ 1 ]]; do date >> output.txt sleep 1 done
запускаем его в фоне и запоминаем его пид:
sudo ./script.sh &
пробуем послать сигнал -59 на данный pid:
sudo kill -59 <PID>
немного ждём и проверяем пишется ли инфорамция о дате в файл output.txt:
tail -f output.txt
если пишется, то это может быть сигналом о то, что руткит работает на вашей системе.
Однако, данный способ не гарантирует сто процентный результат.
В данной статье на примере Singularity был разобран типичный функционал для руткитов:
сокрытие процессов;
сокрытие файлов и директорий;
противодействие средствам мониторинга, аудита и защиты.
Кроме того, были приведены примеры противодействия работе руткитов, заключающиеся в недопущении загрузки руткита. В скором времени расширенный функционал по противодействию руткитам будет интегрирован в мою систему мониторинга безопасности).
В конце данной статьи хотелось бы подытожить, что лучший способ защиты от руткита - это недопущение его внедрение в вашу систему, в противном случае, вытравить его является очень сложной задачей.
Всем спасибо за прочтение данной статьи.
GitHub руткита Singularity: https://github.com/MatheuZSecurity/Singularity;
Статья про руткит Singularity (помечено как устаревшее): https://blog.kyntra.io/Singularity-A-final-boss-linux-kernel-rootkit;
Моя статья про eBPF: https://habr.com/en/articles/972602/;
Калавера Д., Фонтана Л. "BPF для мониторинга Linux": https://www.piter.com/collection/linux/product/bpf-dlya-monitoringa-linux.