Стандартные команды ls, ps и netstat предоставляют пользователю не действительную информацию о системе, а только то, что им разрешено показывать. Чтобы выявить истинное положение дел, требуется глубокое изучение: понимание механизмов загрузки библиотек, структур GOT и PLT. В данной статье мы разберемся, как увидеть информацию, скрытую от стандартного взгляда.

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

Понимание механизма релокаций

Процесс релокаций определяет способ, которым приложение через таблицы PLT и GOT обнаруживает функции из библиотек и как динамическая ленивая привязка заполняет GOT фактическими адресами из libc. Этот механизм можно отследить в реальном времени с помощью отладчика GDB.

Основная идея LD_PRELOAD

Но рассмотрим это с иной стороны: что произойдет, если в таблице GOT вместо адреса из libc окажется адрес функции из нашей собственной библиотеки? Что если мы хотим, чтобы системные утилиты выполняли не то, что ожидает пользователь? Именно на таком принципе основывается механизм LD_PRELOAD.

Что будет рассмотрено в статье

Мы изучим технологию работы пользовательских руткитов:

  • Разберемся в механизме перехвата: как LD_PRELOAD управляет загрузкой нашей библиотеки вместо стандартной libc, и увидим это в действии через GDB.
  • Столкнемся с практическими сложностями при реализации перехвата: проблемы бесконечной рекурсии, ошибки segmentation fault и необходимость использования dlsym для корректной работы.
  • Изучим ограничения механизма и поймем, почему LD_PRELOAD не работает с программами, имеющими флаг setuid, и каким образом злоумышленники обходят эту защиту через уязвимости.
  • Установим и модифицируем руткит BEURK для скрытия критических компонентов системы.
  • Изучим способы выявления пользовательских руткитов с помощью различных инструментов и техник анализа.

История механизма LD_PRELOAD

Механизм LD_PRELOAD появился в операционной системе SunOS в конце 1980-х и начале 1990-х годов как утилита для разработчиков, позволяющая тестировать новые версии библиотек без необходимости переcompилировать приложения. Никто не предполагал, что этот механизм впоследствии станет основой для целого класса вредоносных программ.

Структура экспериментальной среды

Для проведения всех экспериментов подготовлена единая лабораторная среда со следующей структурой:

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.

Как работает перехват функций

Суть механизма LD_PRELOAD заключается в использовании переменной окружения, которую понимает динамический компоновщик (ld-linux). Библиотеки, указанные в этой переменной, загружаются в адресное пространство процесса до загрузки всех остальных библиотек. Если в такой библиотеке присутствует функция с аналогичным именем и сигнатурой, что и в других загружаемых библиотеках (например, libc, libssl), динамический компоновщик при разрешении символов запишет в таблицу GOT адрес нашей функции вместо оригинальной. На этом основывается весь механизм перехвата.

Практический пример с функцией sleep

Рассмотрим простой пример. Тестовая программа содержит вызов функции sleep с параметром 30 секунд. Библиотека-перехватчик определяет собственную функцию sleep с идентичной сигнатурой, которая выводит сообщение о перехвате и возвращает управление без выполнения исходного кода.

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

Отладка через GDB

Для наблюдения процесса используется отладчик GDB. Мы устанавливаем переменную окружения LD_PRELOAD непосредственно в сеансе отладки и отслеживаем три критических состояния таблицы GOT для функции sleep:

  • Исходное состояние, прежде чем компоновщик начнет загружать библиотеки.
  • Состояние после загрузки всех библиотек, но до первого вызова функции.
  • Состояние после выполнения функции.

Первая точка останова устанавливается на _start — самую раннюю точку входа. На самом деле, _start расположена не в нашем приложении, а в динамическом компоновщике ld-linux-x86-64.so.2, который загружается в первую очередь.