Содержание статьи
- Структура проекта и инструментарий
- Механика LD_PRELOAD
- Суть механизма
- Готовим программу и перехватчик
- Наблюдаем перехват через GDB
- Практические примеры перехвата функций
- Перехват puts
- Перехват strcmp: обходим проверку пароля
- Перехват getuid
- От setuid к руту
- Где находится LD_PRELOAD
- Ограничения LD_PRELOAD
- Реальный путь к root
- Закрепление: руткит BEURK
- Как искать руткиты
- BEURK в матрице MITRE ATT&CK
- Настройка и сборка
- Установка и демонстрация скрытия
- Анализ через strace
- Обход руткита: статическая компиляция
- Персистентность
- Обнаружение и удаление
- Заключение
- Ключевые выводы
- Что дальше
ls, ps, netstat — показывают ему не реальность, а то, что им позволено. Чтобы найти правду, нужно смотреть глубже: в механизмы загрузки библиотек, GOT, PLT. В этой статье мы научимся видеть то, что скрыто от глаз.warning
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
Механизм релокаций определяет, как программа через PLT и GOT находит библиотечные функции и как ленивая привязка заполняет GOT реальными адресами из libc.. Этот процесс можно наблюдать вживую через отладчик GDB.
info
Подробнее о релокациях читай в статье «Релокации. Разбираемся, как Linux-программа находит функцию во время выполнения».
Но что, если посмотреть на это с другой стороны: что, если в GOT окажется не адрес из libc, а адрес из нашей собственной библиотеки? Что, если мы хотим, чтобы стандартные утилиты делали не то, чего ожидает пользователь?
Именно так работает механизм LD_PRELOAD. В этой статье мы разберем технику работы userland-руткитов:
- Поймем механику перехвата, а именно как
LD_PRELOADзаставляет компоновщик загружать нашу библиотеку вместоlibc, и увидим это черезGDBв реальном времени. - Столкнемся с практическими проблемами при перехвате функций: рекурсией, segfault и необходимостью использовать
dlsymдля корректного перехвата. - Изучим ограничения и выясним, почему
LD_PRELOADне работает с setuid-программами, а также как атакующие обходят это через уязвимости (на примере CVE-2025-32463 в sudo). - Установим руткит BEURK и модифицируем его для скрытия критических артефактов вроде файла
/.etc/ ld. so. preload - Разберем методы обнаружения userland-руткитов. Опираясь на два из трех принципов охоты на руткиты (утечки данных и несогласованные ответы), я покажу, как использовать strace, статически скомпилированный BusyBox и auditd для выявления руткита. Третий принцип (влияние на систему) проявляется на реальных и нагруженных системах, поэтому в рамках статьи мы рассмотрим его только в теории.
Механизм LD_PRELOAD появился в SunOS в конце 1980-х — начале 1990-х годов как инструмент для разработчиков, позволяющий тестировать новые версии библиотек без пересборки программ. Никто не предполагал, что этот механизм станет основой для целого класса вредоносного ПО!
Структура проекта и инструментарий
Для воспроизведения всех экспериментов я подготовил единую лабораторию. Вот ее структура:
ld_preload/
├── Makefile
├── src/
│ ├── check_pass.c # Программа для перехвата strcmp
│ ├── empty_sleep.c # Программа для перехвата sleep
│ ├── hook_getuid.c # Перехватчик getuid
│ ├── hook_puts.c # Перехватчик puts
│ ├── hook_strcmp.c # Перехватчик strcmp
│ ├── hook.c # Перехватчик sleep
│ └── show_uid.c # Программа для перехвата getuid
├── bin/ # Скомпилированные программы
└── lib/ # Скомпилированные библиотеки-перехватчики
В директории src/ лежат исходники — тестовые программы и библиотеки‑перехватчики. После сборки исполняемые файлы попадают в bin/, а разделяемые библиотеки — в lib/. Исходный код ты найдешь в разделах, связанных с этими программами.
Makefile позволяет быстро все собрать одной командой:
CC = gcc
CFLAGS = -fPIC -shared
all: bin/empty_sleep bin/check_pass bin/show_uid lib/libhook.so lib/libhook_strcmp.so lib/libhook_getuid.so lib/libhook_puts.so# Программа со sleepbin/empty_sleep: src/empty_sleep.c | bin $(CC) src/empty_sleep.c -o bin/empty_sleep -Wl,-z,lazy
# Программа с проверкой пароляbin/check_pass: src/check_pass.c | bin $(CC) src/check_pass.c -o bin/check_pass -Wl,-z,lazy
# Программа для проверки setuidbin/show_uid: src/show_uid.c | bin $(CC) src/show_uid.c -o bin/show_uid -Wl,-z,lazy
# Библиотеки-перехватчикиlib/libhook.so: src/hook.c | lib $(CC) $(CFLAGS) src/hook.c -o lib/libhook.so
lib/libhook_strcmp.so: src/hook_strcmp.c | lib $(CC) $(CFLAGS) src/hook_strcmp.c -o lib/libhook_strcmp.so -ldl
lib/libhook_puts.so: src/hook_puts.c | lib $(CC) $(CFLAGS) src/hook_puts.c -o lib/libhook_puts.so
lib/libhook_getuid.so: src/hook_getuid.c | lib $(CC) $(CFLAGS) src/hook_getuid.c -o lib/libhook_getuid.so
bin lib: mkdir -p $@clean: rm -rf bin lib
.PHONY: all cleanФлаг -Wl, включает ленивую привязку — это позволяет наблюдать изменение GOT именно в момент первого вызова функции, а не при загрузке программы. Флаги -shared и -fPIC создают разделяемую библиотеку, которую динамический компоновщик сможет загрузить по любому адресу в памяти.
Механика LD_PRELOAD
Суть механизма
LD_PRELOAD — это переменная окружения, которую понимает динамический компоновщик (ld-linux.). Библиотеки, указанные в ней, загружаются в адресное пространство процесса до всех остальных. Если в этой библиотеке есть функция с тем же именем, что и в любой загружаемой библиотеке (например, libc, libssl, libcurl), компоновщик при разрешении символов запишет в GOT адрес нашей функции, а не оригинальной. На этом и строится перехват.
В этой статье мы сосредоточимся на перехвате функций из libc — это самый распространенный случай, так как большинство программ используют стандартную библиотеку C для работы с файлами, сетью и процессами.
Готовим программу и перехватчик
Подопытная программа src/ предельно проста — она вызывает sleep( и завершается:
#include <unistd.h>int main() { sleep(30); return 0;}Библиотека‑перехватчик src/ определяет функцию с таким же именем и сигнатурой, как у оригинального sleep из libc:
#include <stdio.h>#include <unistd.h>unsigned int sleep(unsigned int seconds) { printf("[HOOK] sleep(%u) перехвачен! Оригинал не вызывается.\n", seconds); return 0;}Сигнатура unsigned должна точно совпадать с той, что объявлена в <. Если сигнатуры разойдутся, компоновщик не свяжет нашу функцию с вызовом sleep в программе и перехвата не произойдет.
Обе программы мы уже собрали через make на предыдущем шаге; empty_sleep лежит в bin/, libhook. — в lib/.
Наблюдаем перехват через GDB
Запускаем отладчик GDB и прямо в нем устанавливаем переменную окружения LD_PRELOAD, чтобы она действовала только на время отладки:
gdb ./empty_sleep
(gdb) set environment LD_PRELOAD=../lib/libhook.so
Дальше наша задача — зафиксировать три состояния GOT для функции sleep:
- В самом начале, до того, как компоновщик начал загружать библиотеки.
- После загрузки библиотек, но до того, как
sleepбыл вызван. - После первого вызова
sleep.
Компоновщик еще ничего не загружал
Ставим точку останова на _start. Это самая ранняя точка, куда может попасть отладчик. На самом деле _start находится не в нашей программе, а в динамическом компоновщике ld-linux-x86-64. — он загружается первым:
(gdb) break _start
(gdb) run
Breakpoint 1, 0x00007ffff7fdfa80 in _start () from /lib64/ld-linux-x86-64.so.2
Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
