Релокации в Linux: PLT/GOT за 60 секунд
Когда программа вызывает библиотечную функцию (например, sleep) в Linux, программа понятия не имеет, где эта функция лежит в памяти. ASLR каждый раз меняет адрес libc. Как она её находит?
Здесь работают два механизма в связке: PIE (Position Independent Executable) и ASLR (Address Space Layout Randomization).
PIE – это свойство исполняемого файла, которое позволяет ему загружаться по любому адресу в памяти. Без PIE программа была бы «привязана» к фиксированному адресу и ASLR не работал бы для кода самой программы (только для библиотек). PIE делает возможным применение ASLR ко всей программе целиком.
ASLR – это механизм операционной системы, который случайным образом выбирает адрес загрузки программы и библиотек при каждом запуске. Благодаря PIE программа может загружаться по этому случайному адресу.
Но даже зная, что программа загружена по случайному адресу, как она вызывает функции из динамических библиотек (например, printf, sleep или puts), адрес которых тоже неизвестен?
Ответ – релокации, PLT, GOT и ленивая привязка. В этой шпаргалке разложил всё по полочкам:
- Куда на самом деле ведёт call.
- Как работает GOT и PLT.
- В чём разница между первым и вторым вызовом.
- Команды для GDB и objdump.
- Ленивая привязка против BIND_NOW.
Карточки
1. Что узнаете из заметки
Как программа в Linux находит функцию, если ее адрес неизвестен до запуска?
- PLT (Procedure Linkage Table) – таблица заглушек для функций из динамических библиотек.
- GOT (Global Offset Table) – таблица, куда динамический компоновщик позже запишет реальные адреса.
Куда на самом деле ведёт call sleep@plt и как работают релокации в Linux – наша цель в этой заметке.
2. Зачем это знать?
До знакомства с темой:
- Непонятно, почему отладчик GDB прыгает в PLT, а не в динамическую библиотеку (например, libc).
- Непонятно, почему stepi ведёт в странный код.
- Не вижу реальный адрес библиотечной функции (например, sleep).
После прочтения заметки:
- Полный контроль отладки.
- Узнаете, где смотреть адреса.
- Увидите ленивое связывание в действии.
- Почувствуете себя настоящим хакером.
3. Пример простой программы
Разберемся с релокациями на примере простой программы, которая вызывает функцию sleep() (из стандартной динамической библиотеки libc) и завершается:
Так выглядит дизассемблированное представление функции main нашей программы:
Компилятор не знает адрес sleep(), поэтому вставляет заглушку:
4. Первый вызов функции sleep
Когда main вызывает sleep в первый адрес, в GOT ещё нет настоящего адреса. Магия первого вызова функции:
5. Повторный вызов библиотечной функции
При повторном вызове функции GOT уже содержит настоящий адрес из внешней библиотеки:
6. Полезные команды
Небольшая шпаргалка для GDB, objdump и readelf:
7. Ленивая привязка и BIND_NOW
Особенности ленивой привязки:
- Релокация при первом вызове.
- Более быстрый старт программы.
- Первый вызов функции медленнее.
Особенности разрешения всех релокаций при загрузке программы (BIND_NOW):
- Все релокации разрешены при загрузке программы.
- Медленнее запуск программы.
- Все вызовы функций одинаково быстрые.
8. Итоги
Теперь вы знаете:
- Куда на самом деле ведёт call sleep@plt.
- Как работает GOT и PLT.
- В чём разница между первым и вторым вызовом библиотечной функции.
- Какие команды использовать.
Нужно больше подробностей, подробных примеров, живой отладки в GDB? Читайте статью в журнале Хакер: «Релокации. Разбираемся, как Linux-программа находит функцию во время выполнения».
Добро пожаловать в кузницу!