Все мы пользуемся динамически-компонуемыми билиотеками. Их возможности поистине великолепны. Во-первых, такая библиотека загружается в физическое адресное пространство только один раз для всех процессов. Во-вторых, можно расширять функционал своей программы, подгружая дополнительную библиотеку, которая и будет этот функционал обеспечивать. И все это без перезапуска самой программы. А еще решается проблема обновлений. Для динамически компонуемой библиотеки можно определить стандартный интерфейс и влиять на функционал и качество своей основной программы, просто меняя версию библиотеки. Такие методы повторного использования кода даже получили название «архитектура plug-in’ов». Но топик не об этом.
Кстати, нетерпеливые могут все скачать и попробовать прямо сейчас.
Конечно, редко какая динамически компонуемая библиотека в своей реализации опирается исключительно на себя, то есть вычислительные возможности процессора и память. Библиотеки используют библиотеки. Или, хотя бы, стандартные библиотеки. Как, например, программы на С\С++ используют стандартные библиотеки С\С++. Последнии, кстати, для удобства тоже организуются в динамически компонуемом виде (libc.so и libstdc++.so). Сами они хранятся в файлах особого формата. Мое исследование проводилось для ОС Linux, в которой основным форматом динамически компонуемых библиотек является ELF (Executable and Linkable Format).
Некоторое время назад я столкнулся с необходимостью перехватывать вызовы функций из одной библиотеки в другую. Просто для того, чтобы обрабатывать их особым образом. Это называется перенаправлением вызова.
Для начала сформулируем задачу на конкретном примере. Допустим, у нас есть программа с названием «test» на языке С (файл test.c) и две разделяемые библиотеки (файлы libtest1.c и libtest2.c), с неизменным содержимым, откомпилированные заранее. Эти библиотеки предоставляют по одной функции: libtest1() и libtest2() соответственно. В своей реализации каждая из них пользуется функцией puts() из стандартной библиотеки языка С.
Задача состоит в следующем:
При этом менять код или перекомпилировать сами библиотеки не разрешается, только главную программу.
Этот пример иллюстрирует две очень интересные особенности подобного перенаправления:
Легального способа решить эту задачу не существует. Единственный вариант – разобраться с ELF и самому производить необходимые изменения в памяти.
Поехали!
Лучший способ понять ELF – это набраться терпения и пару раз внимательно прочитать его спецификацию, затем написать простую программу, откомпилировать ее и детально исследовать с помощью шестнадцатеричного редактора, сравнивая увиденное со спецификацией. Такой метод исследования сразу натолкнет на мысль написать какой-нибудь простой парсер для ELF, так как появится много рутинной работы. Но, не стоит спешить. Таких утилит создано уже несколько. Для исследования возьмем файлы из предыдущего раздела:
Чтобы ответить на этот вопрос, необходимо заглянуть внутрь такого файла. Для этого существует такие утилиты:
Любой ELF файл начинается со специального заголовка. Его структуру, как и описание многих других элементов ELF, можно найти в файле /usr/include/linux/elf.h. У заголовка есть специальное поле, в котором записано смещение от начала файла таблицы заголовков секций. Каждый элемент этой таблицы описывает некоторую секцию в ELF. Секция – это наименьший неделимый структурный элемент в ELF файле. При загрузке в память, секции объединяются в сегменты. Сегменты – это наименьшие неделимые части ELF файла, которые могут быть отображены в память загрузчиком (ld-linux.so.2). Сегменты описывает таблица сегментов, смещение которой так же есть в заголовке ELF файла.
Наиболее важные из них:
Первая команда создает динамически компонуемую библиотеку libtest1.so. Вторая – libtest2.so. Обратите внимание на ключ –fPIC. Он заставляет компилятор генерировать так называемый Position Independent Code. Подробности в следующем разделе. Третья команда создает исполняемый модуль c именем test путем компиляции файла test.c и компоновки его с уже созданными библиотеками libtest1.so и libtest2.so. Последние находятся в текущем каталоге, что отражено использованием ключа –L$PWD. Компоновка с libdl.so необходима для использования функицй dlopen() и dlclose().
Для запуска программы необходимо выполнить следующие команды:
То есть, добавить динамическому компоновщику\загрузчику путь к текущему каталогу как путь для поиска библиотек. Вывод программы получим такой:
Теперь посмотрим на секции модуля test. Для этого запустим readelf с ключом –a. Ниже преведены наиболее интересные из них:
Стандартный заголовок исполняемого модуля. Магическая последовательность в первых 16 байтах. Обозначен тип модуля (в данном случае – исполняемый, а еще бывает объектный (.o) и разделяемый (.so)), архитектура (i386), рекомендуемая точка входа, смещения к заголовкам сегментов и секций и их размеры. В самом конце – смещение в таблице строк для названий секций.
Здесь можно увидеть перечень всех секций подопытного ELF файла, их тип и режим загрузки в память (R, W, X и A).
Это список сегментов – своеобразных контейнеров для секций в памяти. Также указан путь к специальному модулю – динамическому компоновщику\загрузчику. Именно ему предстоит расположить содержимое этого ELF файла в памяти.
А вот как произойдет распределение секций по сегментам во время загрузки.
Но, наиболее интересная секция, в которой хранится информация об импортируемых и экспортируемых динамически компонуемых функциях называются “.dynsym”:
Кроме прочих функций, необходимых для правильной загрузки\выгрузки программы, можно отыскать знакомые имена: libtest1, libtest2, dlopen, fprintf, puts, dlclose. Для всех них значится тип FUNC и тот факт, что они в этом модуле не определены – индекс секции помечен как UND.
Секции “.rel.dyn” и “.rel.plt” являются таблицами переразмещений для тех символов из “.dynsym”, для которых вообще необходимо переразмещение при компоновке.
В чем разница между этими таблицами с точки зрения динамической компоновки функций? Это тема следующего раздела.
Компиляция библиотек libtest1.so и libtest2.so несколько отличалась. libtest2.so компилировалась с ключом –fPIC (генерировать Position Independent Code). Посмотрим, как это отразилось на таблицах динамических символов для этих двух модулей (используем readelf):
Итак, таблицы динамических символов для обеих библиотек отличаются только порядком следования самих символов. Видно, что обе они используют неопределенную функцию puts(), а предоставляют libtest1() либо libtest2(). Как изменились таблицы переразмещений?
Для libtest1.so переразмещение для функции puts() встречается два раза в секции “.rel.dyn”. Посмотрим на эти места непосредственно в модуле при помощи дизассемблера. Необходимо отыскать функцию libtest1() в которой и происходит двойной вызов puts(). Используем objdump –D:
Имеем две относительные инструкции CALL (код E8) с операндами 0xFFFFFFFC. Относительный CALL c таким операндом лишен смысла, так как, по сути, передает управление на один байт вперед относительно адреса инструкции CALL. Если посмотреть на смещение переразмещений для puts() в секции “.rel.dyn”, то можно обнаружить, что они применяются как раз к операнду инструкции CALL. Таким образом, в обоих случаях обращения к puts(), загрузчик просто перезапишет 0xFFFFFFFC так, что CALL будет переходить на корректный адрес функции puts().
Так работает переразмещение типа R_386_PC32.
Теперь обратим внимание на libtest2.so:
Обращение к puts() упоминается только единожды, и, притом, в секции “.rel.plt”. Посмотрим на ассемблер и займемся отладкой:
Операнды инструкций CALL уже разные и осмысленные, а значит, что они на что-то указывают. Это уже не просто набивка (padding). Также полезно отметить, что перед вызовом самой puts() происходят запись 0x1FF4 (0x1BAC + 0x448) в регистр EBX. Отладчик помогает узнавать изначальное значение EBX, равное 0x448. Значит, это где-то дальше пригодится. Адрес 0x354 ведет нас к очень интересной секции “.plt”, которая, как и “.text”, помечена как исполняемая. Вот она:
По интересующему нас адресу 0x354 обнаруживаем три инструкции. В первой из них происходит безусловный переход по адресу, на который указывает EBX (0x1FF4) плюс 0x10. Произведя простые вычисления, получим значение указателя 0x2004. Эти адреса попадают в секцию “.got.plt”.
Самое интересное обнаруживается тогда, когда мы этот указатель разыменовываем и, наконец-то, получаем адрес безусловного перехода, равный 0x35A. Но, это же, по сути, следующая инструкция! Зачем было производить такие сложные манипуляции и ссылаться на секцию “.got.plt”, чтобы просто перейти на следующую инструкцию? Что вообще такое PLT и GOT?
PLT (Procedure Linkage Table) — это таблица компоновки процедур. Она присутствует в исполняемых и разделяемых модулях. Это массив заглушек, по одной на каждую импортируемую функцию.
Вызов функции по адресу PLT[n+1] приведет к косвенному переходу управления по адресу GOT[n+3]. При первом вызове GOT[n+3] указывает назад, на PLT[n+1] + 6, что представляет собой последовательность PUSH\JMP на PLT[0]. Проходя через PLT[0], компоновщик использует сохраненный стековый аргумент, чтобы определить 'n' и затем разрешает символ 'n'. Потом компоновщик исправляет значение GOT[n+3] так, чтобы оно указывало прямо на целевую подпрограмму, и, в конце концов, вызывает ее. Каждый следующий вызов PLT[n+1] будет направлен на целевую подпрограмму без подобного разрешения ее адреса через инструкцию JMP.
Первый элемент PLT особенный и используется, чтобы перейти на код динамического разрешения адреса.
Управление передается на код компоновщика. 'n' уже в стеке и туда же добавляется адрес GOT[1]. Таким образом компоновщик (находится в /lib/ld-linux.so.2) может определить, какая библиотека требует его услуг.
GOT (Global Offset Table) — это глобальная таблица смещений. Ее первые три элемента зарезервированы. При первой инициализации GOT все ее элементы, которые относятся к разрешению адресов в PLT, указывают обратно на PLT[0].
Вот эти особые элементы:
Так работает переразмещение типа R_386_JUMP_SLOT, которое использовалось в библиотеке libtest2.so. Остальные типы переразмещений относятся к статической компоновке, поэтому нам не пригодятся.
В методах разрешения вызова импортируемых функций и заключается разница между кодом, зависящим от позиции загрузки в память и не зависящим от нее (PIC).
Сделаем некоторые полезные заключения:
Замечание: ключ компиляции –fPIC обязателен для 64-битной архитектуры. То есть в 64-битных библиотеках разрешение вызовов импортируемых функций всегда осуществляется через PLT\GOT. Плюс к тому на такой архитектуре секции с переразмещениями называются “.rela.plt” и “.rela.dyn”.
Для осуществления перенаправления импортируемой функции в некоторой динамически-компонуемой библиотеке необходимо знать следующее:
Прототип функции для перенаправления на языке С получается следующий:
Вот алгоритм работы функции перенаправления:
Ниже представлен код этой функции на языке С:
Полная реализация этой функции с тестовыми примерами доступна для скачивания.
Перепишем нашу тестовую программу:
Откомпилируем:
И запустим:
Вывод получим следующий:
Что свидетельствует о полном выполнении задачи, поставленной в самом начале. Ура!
Этот очень интересный вопрос возникает при внимательном рассмотрении прототипа функции для перенаправления. После некоторого исследования мне удалось обнаружить способ определения адреса загрузки библиотеки по ее описателю, который возвращает функция dlopen(). Делается это таким макросом:
С переписыванием адресов, на которые указывают переразмещения из секции “.rel.plt” проблем не возникает. По сути, переписывается операнд инструкции JMP соответствующего элемента из секции “.plt”. А операнды такой инструкции – это просто адреса.
Интересней дела обстоят с применением переразмещений к операндам относительных инструкций CALL (код E8). Адреса перехода в них вычисляются по формуле:
Так мы можем узнать адрес функции-оригинала. Из предыдущей формулы получаем значение, которое нужно записать как аргумент для относительного CALL, чтобы осуществлять вызов нужной нам функции:
Секция “.rel.dyn” попадает в сегмент, помеченный как “R E”, а значит, записывать адреса в него просто так не получится. Нужно добавить право на запись для страницы, на которую приходится переразмещение и не забыть после перенаправления вернуть все обратно. Это делается при помощи функции mprotect(). Первый параметр этой функции – это адрес страницы, содержащей переразмещение. Он должен быть всегда кратен размеру страницы. Вычислять его не сложно: нужно только обнулить несколько младших байт адреса переразмещения (в зависимости от размера страницы):
Например, для страниц размером 4096 (0x1000) байт на 32-битной системе приведенное выше выражение преобразуется в:
Размер одной страницы, можно узнать, вызвав sysconf(_SC_PAGESIZE).
В качестве упражнения можно написать plug-in к Firefox, который будет перенаправлять на себя все сетевые вызовы, например, Adobe Flash plug-in’а (libflashplayer.so). Таким образом, можно контролировать весь трафик Adobe Flash в Internet из процесса Firefox никак не воздействуя на сетевые вызовы самого обозревателя и других plug-in’ов.
Удачи!
Желающим прочитать на английском сюда, на русском публикую впервые.
Кстати, нетерпеливые могут все скачать и попробовать прямо сейчас.
Конечно, редко какая динамически компонуемая библиотека в своей реализации опирается исключительно на себя, то есть вычислительные возможности процессора и память. Библиотеки используют библиотеки. Или, хотя бы, стандартные библиотеки. Как, например, программы на С\С++ используют стандартные библиотеки С\С++. Последнии, кстати, для удобства тоже организуются в динамически компонуемом виде (libc.so и libstdc++.so). Сами они хранятся в файлах особого формата. Мое исследование проводилось для ОС Linux, в которой основным форматом динамически компонуемых библиотек является ELF (Executable and Linkable Format).
Некоторое время назад я столкнулся с необходимостью перехватывать вызовы функций из одной библиотеки в другую. Просто для того, чтобы обрабатывать их особым образом. Это называется перенаправлением вызова.
Поподробней о перенаправлении
Для начала сформулируем задачу на конкретном примере. Допустим, у нас есть программа с названием «test» на языке С (файл test.c) и две разделяемые библиотеки (файлы libtest1.c и libtest2.c), с неизменным содержимым, откомпилированные заранее. Эти библиотеки предоставляют по одной функции: libtest1() и libtest2() соответственно. В своей реализации каждая из них пользуется функцией puts() из стандартной библиотеки языка С.
Задача состоит в следующем:
- Нужно заменить вызов функции puts() для обеих библиотек на вызов функции redirected_puts(), реализованной в главной программе (файл test.c), которая, в свою очередь, может пользоваться оригинальной puts();
- Отменить произведенные изменения, то есть сделать так, чтобы повторный вызов libtest1() и libtest2() приводил к вызову оригинальной puts().
При этом менять код или перекомпилировать сами библиотеки не разрешается, только главную программу.
Зачем это нужно?
Этот пример иллюстрирует две очень интересные особенности подобного перенаправления:
- Оно осуществляется исключительно для конкретной динамически компонуемом библиотеки, а не для всего процесса, как при использовании переменной окружения LD_PRELOAD динамического загрузчика, что позволяет остальным модулям безболезненно пользоваться оригиналом функции;
- Оно происходит во время работы программы и не требует ее перезапуска.
Легального способа решить эту задачу не существует. Единственный вариант – разобраться с ELF и самому производить необходимые изменения в памяти.
Поехали!
Кратко об ELF
Лучший способ понять ELF – это набраться терпения и пару раз внимательно прочитать его спецификацию, затем написать простую программу, откомпилировать ее и детально исследовать с помощью шестнадцатеричного редактора, сравнивая увиденное со спецификацией. Такой метод исследования сразу натолкнет на мысль написать какой-нибудь простой парсер для ELF, так как появится много рутинной работы. Но, не стоит спешить. Таких утилит создано уже несколько. Для исследования возьмем файлы из предыдущего раздела:
File test.c
#include <stdio.h>
#include <dlfcn.h>
#define LIBTEST1_PATH "libtest1.so" //position dependent code (for 32 bit only)
#define LIBTEST2_PATH "libtest2.so" //position independent code
void libtest1(); //from libtest1.so
void libtest2(); //from libtest2.so
int main()
{
void *handle1 = dlopen(LIBTEST1_PATH, RTLD_LAZY);
void *handle2 = dlopen(LIBTEST2_PATH, RTLD_LAZY);
if (NULL == handle1 || NULL == handle2)
fprintf(stderr, "Failed to open \"%s\" or \"%s\"!\n", LIBTEST1_PATH, LIBTEST2_PATH);
libtest1(); //calls puts() from libc.so twice
libtest2(); //calls puts() from libc.so twice
puts("-----------------------------");
dlclose(handle1);
dlclose(handle2);
return 0;
}
File libtest1.c
int puts(char const *);
void libtest1()
{
puts("libtest1: 1st call to the original puts()");
puts("libtest1: 2nd call to the original puts()");
}
File libtest2.c
int puts(char const *);
void libtest2()
{
puts("libtest2: 1st call to the original puts()");
puts("libtest2: 2nd call to the original puts()");
}
Из каких частей состоит ELF?
Чтобы ответить на этот вопрос, необходимо заглянуть внутрь такого файла. Для этого существует такие утилиты:
- readelf – очень мощный инструмент для просмотра содержимого секций ELF-файла
- objdump – очень похож на предыдущий, умеет дизассемблировать секции
- gdb – незаменим для отладки под Linux, особенно для просмотра мест, подлежащих переразмещению
Любой ELF файл начинается со специального заголовка. Его структуру, как и описание многих других элементов ELF, можно найти в файле /usr/include/linux/elf.h. У заголовка есть специальное поле, в котором записано смещение от начала файла таблицы заголовков секций. Каждый элемент этой таблицы описывает некоторую секцию в ELF. Секция – это наименьший неделимый структурный элемент в ELF файле. При загрузке в память, секции объединяются в сегменты. Сегменты – это наименьшие неделимые части ELF файла, которые могут быть отображены в память загрузчиком (ld-linux.so.2). Сегменты описывает таблица сегментов, смещение которой так же есть в заголовке ELF файла.
Наиболее важные из них:
- .text – содержит код модуля
- .data – инициализированные переменные
- .bss – не инициализированные переменные
- .symtab – символы модуля: функции и статические переменные
- .strtab – имена для символов модуля
- .rel.text – переразмещения для функций (для статически компонуемых модулей)
- .rel.data – переразмещения для статических переменных (для статически компонуемых модулей)
- .rel.plt – список элементов в PLT (Procedure Linkage Table), подлежащих переразмещению при динамической компоновке (используется PLT)
- .rel.dyn – переразмещения для динамически компонуемых функций (если PLT не используется)
- .got – Global Offset Table, содержит информацию о смещениях переразмещаемых объектов
- .debug – отладочная информация
gcc -g3 -m32 -shared -o libtest1.so libtest1.c
gcc -g3 -m32 -fPIC -shared -o libtest2.so libtest2.c
gcc -g3 -m32 -L$PWD -o test test.c -ltest1 -ltest2 –ldl
Первая команда создает динамически компонуемую библиотеку libtest1.so. Вторая – libtest2.so. Обратите внимание на ключ –fPIC. Он заставляет компилятор генерировать так называемый Position Independent Code. Подробности в следующем разделе. Третья команда создает исполняемый модуль c именем test путем компиляции файла test.c и компоновки его с уже созданными библиотеками libtest1.so и libtest2.so. Последние находятся в текущем каталоге, что отражено использованием ключа –L$PWD. Компоновка с libdl.so необходима для использования функицй dlopen() и dlclose().
Для запуска программы необходимо выполнить следующие команды:
export LD_LIBRARY_PATH=$PWD:$LD_LIBRARY_PATH
./test
То есть, добавить динамическому компоновщику\загрузчику путь к текущему каталогу как путь для поиска библиотек. Вывод программы получим такой:
libtest1: 1st call to the original puts()
libtest1: 2nd call to the original puts()
libtest2: 1st call to the original puts()
libtest2: 2nd call to the original puts()
-----------------------------
Теперь посмотрим на секции модуля test. Для этого запустим readelf с ключом –a. Ниже преведены наиболее интересные из них:
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8048580
Start of program headers: 52 (bytes into file)
Start of section headers: 21256 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 8
Size of section headers: 40 (bytes)
Number of section headers: 39
Section header string table index: 36
Стандартный заголовок исполняемого модуля. Магическая последовательность в первых 16 байтах. Обозначен тип модуля (в данном случае – исполняемый, а еще бывает объектный (.o) и разделяемый (.so)), архитектура (i386), рекомендуемая точка входа, смещения к заголовкам сегментов и секций и их размеры. В самом конце – смещение в таблице строк для названий секций.
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 08048134 000134 000013 00 A 0 0 1
...
[ 5] .dynsym DYNSYM 08048200 000200 000110 10 A 6 1 4
[ 6] .dynstr STRTAB 08048310 000310 0000df 00 A 0 0 1
...
[ 9] .rel.dyn REL 08048464 000464 000010 08 A 5 0 4
[10] .rel.plt REL 08048474 000474 000040 08 A 5 12 4
[11] .init PROGBITS 080484b4 0004b4 000030 00 AX 0 0 4
[12] .plt PROGBITS 080484e4 0004e4 000090 04 AX 0 0 4
[13] .text PROGBITS 08048580 000580 0001fc 00 AX 0 0 16
[14] .fini PROGBITS 0804877c 00077c 00001c 00 AX 0 0 4
[15] .rodata PROGBITS 08048798 000798 00005c 00 A 0 0 4
...
[20] .dynamic DYNAMIC 08049f08 000f08 0000e8 08 WA 6 0 4
[21] .got PROGBITS 08049ff0 000ff0 000004 04 WA 0 0 4
[22] .got.plt PROGBITS 08049ff4 000ff4 00002c 04 WA 0 0 4
[23] .data PROGBITS 0804a020 001020 000008 00 WA 0 0 4
[24] .bss NOBITS 0804a028 001028 00000c 00 WA 0 0 4
...
[27] .debug_pubnames PROGBITS 00000000 0011b8 000040 00 0 0 1
[28] .debug_info PROGBITS 00000000 0011f8 0004d9 00 0 0 1
[29] .debug_abbrev PROGBITS 00000000 0016d1 000156 00 0 0 1
[30] .debug_line PROGBITS 00000000 001827 000309 00 0 0 1
[31] .debug_frame PROGBITS 00000000 001b30 00003c 00 0 0 4
[32] .debug_str PROGBITS 00000000 001b6c 00024e 01 MS 0 0 1
...
[36] .shstrtab STRTAB 00000000 0051a8 000160 00 0 0 1
[37] .symtab SYMTAB 00000000 005920 000530 10 38 57 4
[38] .strtab STRTAB 00000000 005e50 000268 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
Здесь можно увидеть перечень всех секций подопытного ELF файла, их тип и режим загрузки в память (R, W, X и A).
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x00100 0x00100 R E 0x4
INTERP 0x000134 0x08048134 0x08048134 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x007f8 0x007f8 R E 0x1000
LOAD 0x000ef4 0x08049ef4 0x08049ef4 0x00134 0x00140 RW 0x1000
DYNAMIC 0x000f08 0x08049f08 0x08049f08 0x000e8 0x000e8 RW 0x4
NOTE 0x000148 0x08048148 0x08048148 0x00020 0x00020 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
GNU_RELRO 0x000ef4 0x08049ef4 0x08049ef4 0x0010c 0x0010c R 0x1
Это список сегментов – своеобразных контейнеров для секций в памяти. Также указан путь к специальному модулю – динамическому компоновщику\загрузчику. Именно ему предстоит расположить содержимое этого ELF файла в памяти.
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame
03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag
06
07 .ctors .dtors .jcr .dynamic .got
А вот как произойдет распределение секций по сегментам во время загрузки.
Но, наиболее интересная секция, в которой хранится информация об импортируемых и экспортируемых динамически компонуемых функциях называются “.dynsym”:
Symbol table '.dynsym' contains 17 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FUNC GLOBAL DEFAULT UND libtest2
2: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
3: 00000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses
4: 00000000 0 FUNC GLOBAL DEFAULT UND dlclose@GLIBC_2.0 (2)
5: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.0 (3)
6: 00000000 0 FUNC GLOBAL DEFAULT UND libtest1
7: 00000000 0 FUNC GLOBAL DEFAULT UND dlopen@GLIBC_2.1 (4)
8: 00000000 0 FUNC GLOBAL DEFAULT UND fprintf@GLIBC_2.0 (3)
9: 00000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.0 (3)
10: 0804a034 0 NOTYPE GLOBAL DEFAULT ABS _end
11: 0804a028 0 NOTYPE GLOBAL DEFAULT ABS _edata
12: 0804879c 4 OBJECT GLOBAL DEFAULT 15 _IO_stdin_used
13: 0804a028 4 OBJECT GLOBAL DEFAULT 24 stderr@GLIBC_2.0 (3)
14: 0804a028 0 NOTYPE GLOBAL DEFAULT ABS __bss_start
15: 080484b4 0 FUNC GLOBAL DEFAULT 11 _init
16: 0804877c 0 FUNC GLOBAL DEFAULT 14 _fini
Кроме прочих функций, необходимых для правильной загрузки\выгрузки программы, можно отыскать знакомые имена: libtest1, libtest2, dlopen, fprintf, puts, dlclose. Для всех них значится тип FUNC и тот факт, что они в этом модуле не определены – индекс секции помечен как UND.
Секции “.rel.dyn” и “.rel.plt” являются таблицами переразмещений для тех символов из “.dynsym”, для которых вообще необходимо переразмещение при компоновке.
Relocation section '.rel.dyn' at offset 0x464 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
08049ff0 00000206 R_386_GLOB_DAT 00000000 __gmon_start__
0804a028 00000d05 R_386_COPY 0804a028 stderr
Relocation section '.rel.plt' at offset 0x474 contains 8 entries:
Offset Info Type Sym.Value Sym. Name
0804a000 00000107 R_386_JUMP_SLOT 00000000 libtest2
0804a004 00000207 R_386_JUMP_SLOT 00000000 __gmon_start__
0804a008 00000407 R_386_JUMP_SLOT 00000000 dlclose
0804a00c 00000507 R_386_JUMP_SLOT 00000000 __libc_start_main
0804a010 00000607 R_386_JUMP_SLOT 00000000 libtest1
0804a014 00000707 R_386_JUMP_SLOT 00000000 dlopen
0804a018 00000807 R_386_JUMP_SLOT 00000000 fprintf
0804a01c 00000907 R_386_JUMP_SLOT 00000000 puts
В чем разница между этими таблицами с точки зрения динамической компоновки функций? Это тема следующего раздела.
Как компонуются разделяемые ELF-библиотеки?
Компиляция библиотек libtest1.so и libtest2.so несколько отличалась. libtest2.so компилировалась с ключом –fPIC (генерировать Position Independent Code). Посмотрим, как это отразилось на таблицах динамических символов для этих двух модулей (используем readelf):
Symbol table '.dynsym' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
2: 00000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses
3: 00000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.0 (2)
4: 00000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.1.3 (3)
5: 00002014 0 NOTYPE GLOBAL DEFAULT ABS _end
6: 0000200c 0 NOTYPE GLOBAL DEFAULT ABS _edata
7: 0000043c 32 FUNC GLOBAL DEFAULT 11 libtest1
8: 0000200c 0 NOTYPE GLOBAL DEFAULT ABS __bss_start
9: 0000031c 0 FUNC GLOBAL DEFAULT 9 _init
10: 00000498 0 FUNC GLOBAL DEFAULT 12 _fini
Symbol table '.dynsym' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
2: 00000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses
3: 00000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.0 (2)
4: 00000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.1.3 (3)
5: 00002018 0 NOTYPE GLOBAL DEFAULT ABS _end
6: 00002010 0 NOTYPE GLOBAL DEFAULT ABS _edata
7: 00002010 0 NOTYPE GLOBAL DEFAULT ABS __bss_start
8: 00000304 0 FUNC GLOBAL DEFAULT 9 _init
9: 0000043c 52 FUNC GLOBAL DEFAULT 11 libtest2
10: 000004a8 0 FUNC GLOBAL DEFAULT 12 _fini
Итак, таблицы динамических символов для обеих библиотек отличаются только порядком следования самих символов. Видно, что обе они используют неопределенную функцию puts(), а предоставляют libtest1() либо libtest2(). Как изменились таблицы переразмещений?
Relocation section '.rel.dyn' at offset 0x2cc contains 8 entries:
Offset Info Type Sym.Value Sym. Name
00000445 00000008 R_386_RELATIVE
00000451 00000008 R_386_RELATIVE
00002008 00000008 R_386_RELATIVE
0000044a 00000302 R_386_PC32 00000000 puts
00000456 00000302 R_386_PC32 00000000 puts
00001fe8 00000106 R_386_GLOB_DAT 00000000 __gmon_start__
00001fec 00000206 R_386_GLOB_DAT 00000000 _Jv_RegisterClasses
00001ff0 00000406 R_386_GLOB_DAT 00000000 __cxa_finalize
Relocation section '.rel.plt' at offset 0x30c contains 2 entries:
Offset Info Type Sym.Value Sym. Name
00002000 00000107 R_386_JUMP_SLOT 00000000 __gmon_start__
00002004 00000407 R_386_JUMP_SLOT 00000000 __cxa_finalize
Для libtest1.so переразмещение для функции puts() встречается два раза в секции “.rel.dyn”. Посмотрим на эти места непосредственно в модуле при помощи дизассемблера. Необходимо отыскать функцию libtest1() в которой и происходит двойной вызов puts(). Используем objdump –D:
0000043c <libtest1>:
43c: 55 push %ebp
43d: 89 e5 mov %esp,%ebp
43f: 83 ec 08 sub $0x8,%esp
442: c7 04 24 b4 04 00 00 movl $0x4b4,(%esp)
449: e8 fc ff ff ff call 44a <libtest1+0xe>
44e: c7 04 24 e0 04 00 00 movl $0x4e0,(%esp)
455: e8 fc ff ff ff call 456 <libtest1+0x1a>
45a: c9 leave
45b: c3 ret
45c: 90 nop
45d: 90 nop
45e: 90 nop
45f: 90 nop
Имеем две относительные инструкции CALL (код E8) с операндами 0xFFFFFFFC. Относительный CALL c таким операндом лишен смысла, так как, по сути, передает управление на один байт вперед относительно адреса инструкции CALL. Если посмотреть на смещение переразмещений для puts() в секции “.rel.dyn”, то можно обнаружить, что они применяются как раз к операнду инструкции CALL. Таким образом, в обоих случаях обращения к puts(), загрузчик просто перезапишет 0xFFFFFFFC так, что CALL будет переходить на корректный адрес функции puts().
Так работает переразмещение типа R_386_PC32.
Теперь обратим внимание на libtest2.so:
Relocation section '.rel.dyn' at offset 0x2cc contains 4 entries:
Offset Info Type Sym.Value Sym. Name
0000200c 00000008 R_386_RELATIVE
00001fe8 00000106 R_386_GLOB_DAT 00000000 __gmon_start__
00001fec 00000206 R_386_GLOB_DAT 00000000 _Jv_RegisterClasses
00001ff0 00000406 R_386_GLOB_DAT 00000000 __cxa_finalize
Relocation section '.rel.plt' at offset 0x2ec contains 3 entries:
Offset Info Type Sym.Value Sym. Name
00002000 00000107 R_386_JUMP_SLOT 00000000 __gmon_start__
00002004 00000307 R_386_JUMP_SLOT 00000000 puts
00002008 00000407 R_386_JUMP_SLOT 00000000 __cxa_finalize
Обращение к puts() упоминается только единожды, и, притом, в секции “.rel.plt”. Посмотрим на ассемблер и займемся отладкой:
0000043c <libtest2>:
43c: 55 push %ebp
43d: 89 e5 mov %esp,%ebp
43f: 53 push %ebx
440: 83 ec 04 sub $0x4,%esp
443: e8 ef ff ff ff call 437 <__i686.get_pc_thunk.bx>
448: 81 c3 ac 1b 00 00 add $0x1bac,%ebx
44e: 8d 83 d0 e4 ff ff lea -0x1b30(%ebx),%eax
454: 89 04 24 mov %eax,(%esp)
457: e8 f8 fe ff ff call 354 <puts@plt>
45c: 8d 83 fc e4 ff ff lea -0x1b04(%ebx),%eax
462: 89 04 24 mov %eax,(%esp)
465: e8 ea fe ff ff call 354 <puts@plt>
46a: 83 c4 04 add $0x4,%esp
46d: 5b pop %ebx
46e: 5d pop %ebp
46f: c3 ret
Операнды инструкций CALL уже разные и осмысленные, а значит, что они на что-то указывают. Это уже не просто набивка (padding). Также полезно отметить, что перед вызовом самой puts() происходят запись 0x1FF4 (0x1BAC + 0x448) в регистр EBX. Отладчик помогает узнавать изначальное значение EBX, равное 0x448. Значит, это где-то дальше пригодится. Адрес 0x354 ведет нас к очень интересной секции “.plt”, которая, как и “.text”, помечена как исполняемая. Вот она:
Disassembly of section .plt:
00000334 <__gmon_start__@plt-0x10>:
334: ff b3 04 00 00 00 pushl 0x4(%ebx)
33a: ff a3 08 00 00 00 jmp *0x8(%ebx)
340: 00 00 add %al,(%eax)
...
00000344 <__gmon_start__@plt>:
344: ff a3 0c 00 00 00 jmp *0xc(%ebx)
34a: 68 00 00 00 00 push $0x0
34f: e9 e0 ff ff ff jmp 334 <_init+0x30>
00000354 <puts@plt>:
354: ff a3 10 00 00 00 jmp *0x10(%ebx)
35a: 68 08 00 00 00 push $0x8
35f: e9 d0 ff ff ff jmp 334 <_init+0x30>
00000364 <__cxa_finalize@plt>:
364: ff a3 14 00 00 00 jmp *0x14(%ebx)
36a: 68 10 00 00 00 push $0x10
36f: e9 c0 ff ff ff jmp 334 <_init+0x30>
По интересующему нас адресу 0x354 обнаруживаем три инструкции. В первой из них происходит безусловный переход по адресу, на который указывает EBX (0x1FF4) плюс 0x10. Произведя простые вычисления, получим значение указателя 0x2004. Эти адреса попадают в секцию “.got.plt”.
Disassembly of section .got.plt:
00001ff4 <.got.plt>:
1ff4: 20 1f and %bl,(%edi)
...
1ffe: 00 00 add %al,(%eax)
2000: 4a dec %edx
2001: 03 00 add (%eax),%eax
2003: 00 5a 03 add %bl,0x3(%edx)
2006: 00 00 add %al,(%eax)
2008: 6a 03 push $0x3
...
Самое интересное обнаруживается тогда, когда мы этот указатель разыменовываем и, наконец-то, получаем адрес безусловного перехода, равный 0x35A. Но, это же, по сути, следующая инструкция! Зачем было производить такие сложные манипуляции и ссылаться на секцию “.got.plt”, чтобы просто перейти на следующую инструкцию? Что вообще такое PLT и GOT?
PLT (Procedure Linkage Table) — это таблица компоновки процедур. Она присутствует в исполняемых и разделяемых модулях. Это массив заглушек, по одной на каждую импортируемую функцию.
PLT[n+1]: jmp *GOT[n+3]
push #n @push n as a signal to the resolver
jmp PLT[0]
Вызов функции по адресу PLT[n+1] приведет к косвенному переходу управления по адресу GOT[n+3]. При первом вызове GOT[n+3] указывает назад, на PLT[n+1] + 6, что представляет собой последовательность PUSH\JMP на PLT[0]. Проходя через PLT[0], компоновщик использует сохраненный стековый аргумент, чтобы определить 'n' и затем разрешает символ 'n'. Потом компоновщик исправляет значение GOT[n+3] так, чтобы оно указывало прямо на целевую подпрограмму, и, в конце концов, вызывает ее. Каждый следующий вызов PLT[n+1] будет направлен на целевую подпрограмму без подобного разрешения ее адреса через инструкцию JMP.
Первый элемент PLT особенный и используется, чтобы перейти на код динамического разрешения адреса.
PLT[0]: push &GOT[1]
jmp GOT[2] @points to resolver()
Управление передается на код компоновщика. 'n' уже в стеке и туда же добавляется адрес GOT[1]. Таким образом компоновщик (находится в /lib/ld-linux.so.2) может определить, какая библиотека требует его услуг.
GOT (Global Offset Table) — это глобальная таблица смещений. Ее первые три элемента зарезервированы. При первой инициализации GOT все ее элементы, которые относятся к разрешению адресов в PLT, указывают обратно на PLT[0].
Вот эти особые элементы:
- GOT[0] список элементов, используемый загрузчиком
- GOT[1] указатель на таблицу переразмещений этого модуля
- GOT[2] указатель на код загрузчика из библиотеки ld-linux.so.2
- GOT[3]
- … далее следуют вспомогательные значение для косвенного вызова каждой из импортируемых функций
- GOT[3+M]
- GOT[3+M+1]
- … далее идут косвенный указатели для ссылок на глобальные переменные, по одной на каждый такой символ
Так работает переразмещение типа R_386_JUMP_SLOT, которое использовалось в библиотеке libtest2.so. Остальные типы переразмещений относятся к статической компоновке, поэтому нам не пригодятся.
В методах разрешения вызова импортируемых функций и заключается разница между кодом, зависящим от позиции загрузки в память и не зависящим от нее (PIC).
Важные выводы
Сделаем некоторые полезные заключения:
- Всю информацию об импортируемых и экспортируемых функциях можно получить в секции “.dynsym”
- Если модуль компилировался в режиме PIC (ключ -fPIC), то вызовы импортируемых функций будут осуществляться через PLT и GOT, переразмещение будет производиться лишь одинажды для каждой функции и будет применяться для первой инструкции определенного элемента в PLT. Информацию о самом переразмещении можно найти в секции “.rel.plt”
- Если ключ –fPIC при компиляции библиотеки не применялся, то переразмещения будут производится над операндом каждой относительной инструкции CALL столько раз, сколько в коде осуществляется вызовов некоторой импортируемой функции. Информацию о самом переразмещении можно найти в секции “.rel.dyn”
Замечание: ключ компиляции –fPIC обязателен для 64-битной архитектуры. То есть в 64-битных библиотеках разрешение вызовов импортируемых функций всегда осуществляется через PLT\GOT. Плюс к тому на такой архитектуре секции с переразмещениями называются “.rela.plt” и “.rela.dyn”.
Долгожданное решение
Для осуществления перенаправления импортируемой функции в некоторой динамически-компонуемой библиотеке необходимо знать следующее:
- Путь в файловой системе к этой библиотеке
- Виртуальный адрес, по которому она загрузилась
- Имя замещаемой функции
- Адрес функции-заместителя
Прототип функции для перенаправления на языке С получается следующий:
void *elf_hook(char const *library_filename, void const *library_address, char const *function_name, void const *substitution_address);
Алгоритм перенаправления
Вот алгоритм работы функции перенаправления:
- Открываем файл библиотеки.
- Запоминаем индекс символа в секции “.dynsym”, имя которого соответствует имени искомой функции.
- Просматриваем секцию “.rel.plt” и ищем в ней переразмещение для символа с указанным индексом.
- Если такой символ найден, сохраняем оригинальный адрес, чтобы потом вернуть его из функции, и записываем на место, указанное в переразмещении, адрес функции-заместителя. Это место вычисляется как сумма адреса загрузки библиотеки в память и смещения в переразмещении. Все. Осуществлена подмена адреса функции. Перенаправление будет происходить всякий раз при вызове библиотекой этой функции. Выходим из функции и возвращаем адрес оригинала.
- Если такой символ в секции “.rel.plt” не найден, то ищем его в секции “rel.dyn” по такому же принципу. Но, нужно помнить, что в секции переразмещений “rel.dyn” символ с искомым индексом может встречаться не один раз. Поэтому завершать цикл поиска после первого же перенаправления нельзя. А вот адрес оригинала можно запомнить при первом совпадении индекса и больше не вычислять – он все равно не измениться.
- Возвращаем адрес функции-оригинала или NULL, если функция с искомым именен не была найдена.
Ниже представлен код этой функции на языке С:
void *elf_hook(char const *module_filename, void const *module_address, char const *name, void const *substitution)
{
static size_t pagesize;
int descriptor; //file descriptor of shared module
Elf_Shdr
*dynsym = NULL, // ".dynsym" section header
*rel_plt = NULL, // ".rel.plt" section header
*rel_dyn = NULL; // ".rel.dyn" section header
Elf_Sym
*symbol = NULL; //symbol table entry for symbol named "name"
Elf_Rel
*rel_plt_table = NULL, //array with ".rel.plt" entries
*rel_dyn_table = NULL; //array with ".rel.dyn" entries
size_t
i,
name_index = 0, //index of symbol named "name" in ".dyn.sym"
rel_plt_amount = 0, // amount of ".rel.plt" entries
rel_dyn_amount = 0, // amount of ".rel.dyn" entries
*name_address = NULL; //address of relocation for symbol named "name"
void *original = NULL; //address of the symbol being substituted
if (NULL == module_address || NULL == name || NULL == substitution)
return original;
if (!pagesize)
pagesize = sysconf(_SC_PAGESIZE);
descriptor = open(module_filename, O_RDONLY);
if (descriptor < 0)
return original;
if (
section_by_type(descriptor, SHT_DYNSYM, &dynsym) || //get ".dynsym" section
symbol_by_name(descriptor, dynsym, name, &symbol, &name_index) || //actually, we need only the index of symbol named "name" in the ".dynsym" table
section_by_name(descriptor, REL_PLT, &rel_plt) || //get ".rel.plt" (for 32-bit) or ".rela.plt" (for 64-bit) section
section_by_name(descriptor, REL_DYN, &rel_dyn) //get ".rel.dyn" (for 32-bit) or ".rela.dyn" (for 64-bit) section
)
{ //if something went wrong
free(dynsym);
free(rel_plt);
free(rel_dyn);
free(symbol);
close(descriptor);
return original;
}
//release the data used
free(dynsym);
free(symbol);
rel_plt_table = (Elf_Rel *)(((size_t)module_address) + rel_plt->sh_addr); //init the ".rel.plt" array
rel_plt_amount = rel_plt->sh_size / sizeof(Elf_Rel); //and get its size
rel_dyn_table = (Elf_Rel *)(((size_t)module_address) + rel_dyn->sh_addr); //init the ".rel.dyn" array
rel_dyn_amount = rel_dyn->sh_size / sizeof(Elf_Rel); //and get its size
//release the data used
free(rel_plt);
free(rel_dyn);
//and descriptor
close(descriptor);
//now we've got ".rel.plt" (needed for PIC) table and ".rel.dyn" (for non-PIC) table and the symbol's index
for (i = 0; i < rel_plt_amount; ++i) //lookup the ".rel.plt" table
if (ELF_R_SYM(rel_plt_table[i].r_info) == name_index) //if we found the symbol to substitute in ".rel.plt"
{
original = (void *)*(size_t *)(((size_t)module_address) + rel_plt_table[i].r_offset); //save the original function address
*(size_t *)(((size_t)module_address) + rel_plt_table[i].r_offset) = (size_t)substitution; //and replace it with the substitutional
break; //the target symbol appears in ".rel.plt" only once
}
if (original)
return original;
//we will get here only with 32-bit non-PIC module
for (i = 0; i < rel_dyn_amount; ++i) //lookup the ".rel.dyn" table
if (ELF_R_SYM(rel_dyn_table[i].r_info) == name_index) //if we found the symbol to substitute in ".rel.dyn"
{
name_address = (size_t *)(((size_t)module_address) + rel_dyn_table[i].r_offset); //get the relocation address (address of a relative CALL (0xE8) instruction's argument)
if (!original)
original = (void *)(*name_address + (size_t)name_address + sizeof(size_t)); //calculate an address of the original function by a relative CALL (0xE8) instruction's argument
mprotect((void *)(((size_t)name_address) & (((size_t)-1) ^ (pagesize - 1))), pagesize, PROT_READ | PROT_WRITE); //mark a memory page that contains the relocation as writable
if (errno)
return NULL;
*name_address = (size_t)substitution - (size_t)name_address - sizeof(size_t); //calculate a new relative CALL (0xE8) instruction's argument for the substitutional function and write it down
mprotect((void *)(((size_t)name_address) & (((size_t)-1) ^ (pagesize - 1))), pagesize, PROT_READ | PROT_EXEC); //mark a memory page that contains the relocation back as executable
if (errno) //if something went wrong
{
*name_address = (size_t)original - (size_t)name_address - sizeof(size_t); //then restore the original function address
return NULL;
}
}
return original;
}
Полная реализация этой функции с тестовыми примерами доступна для скачивания.
Перепишем нашу тестовую программу:
#include <stdio.h>
#include <dlfcn.h>
#include "elf_hook.h"
#define LIBTEST1_PATH "libtest1.so" //position dependent code (for 32 bit only)
#define LIBTEST2_PATH "libtest2.so" //position independent code
void libtest1(); //from libtest1.so
void libtest2(); //from libtest2.so
int hooked_puts(char const *s)
{
puts(s); //calls the original puts() from libc.so because our main executable module called "test" is intact by hook
puts("is HOOKED!");
}
int main()
{
void *handle1 = dlopen(LIBTEST1_PATH, RTLD_LAZY);
void *handle2 = dlopen(LIBTEST2_PATH, RTLD_LAZY);
void *original1, *original2;
if (NULL == handle1 || NULL == handle2)
fprintf(stderr, "Failed to open \"%s\" or \"%s\"!\n", LIBTEST1_PATH, LIBTEST2_PATH);
libtest1(); //calls puts() from libc.so twice
libtest2(); //calls puts() from libc.so twice
puts("-----------------------------");
original1 = elf_hook(LIBTEST1_PATH, LIBRARY_ADDRESS_BY_HANDLE(handle1), "puts", hooked_puts);
original2 = elf_hook(LIBTEST2_PATH, LIBRARY_ADDRESS_BY_HANDLE(handle2), "puts", hooked_puts);
if (NULL == original1 || NULL == original2)
fprintf(stderr, "Redirection failed!\n");
libtest1(); //calls hooked_puts() twice
libtest2(); //calls hooked_puts() twice
puts("-----------------------------");
original1 = elf_hook(LIBTEST1_PATH, LIBRARY_ADDRESS_BY_HANDLE(handle1), "puts", original1);
original2 = elf_hook(LIBTEST2_PATH, LIBRARY_ADDRESS_BY_HANDLE(handle2), "puts", original2);
if (NULL == original1 || original1 != original2) //both pointers should contain hooked_puts() address now
fprintf(stderr, "Restoration failed!\n");
libtest1(); //again calls puts() from libc.so twice
libtest2(); //again calls puts() from libc.so twice
dlclose(handle1);
dlclose(handle2);
return 0;
}
Откомпилируем:
gcc -g3 -m32 -shared -o libtest1.so libtest1.c
gcc -g3 -m32 -fPIC -shared -o libtest2.so libtest2.c
gcc -g3 -m32 -L$PWD -o test test.c elf_hook.c -ltest1 -ltest2 -ldl
И запустим:
export LD_LIBRARY_PATH=$PWD:$LD_LIBRARY_PATH
./test
Вывод получим следующий:
libtest1: 1st call to the original puts()
libtest1: 2nd call to the original puts()
libtest2: 1st call to the original puts()
libtest2: 2nd call to the original puts()
-----------------------------
libtest1: 1st call to the original puts()
is HOOKED!
libtest1: 2nd call to the original puts()
is HOOKED!
libtest2: 1st call to the original puts()
is HOOKED!
libtest2: 2nd call to the original puts()
is HOOKED!
-----------------------------
libtest1: 1st call to the original puts()
libtest1: 2nd call to the original puts()
libtest2: 1st call to the original puts()
libtest2: 2nd call to the original puts()
Что свидетельствует о полном выполнении задачи, поставленной в самом начале. Ура!
Как узнать адрес по которому загрузилась разделяемая библиотека?
Этот очень интересный вопрос возникает при внимательном рассмотрении прототипа функции для перенаправления. После некоторого исследования мне удалось обнаружить способ определения адреса загрузки библиотеки по ее описателю, который возвращает функция dlopen(). Делается это таким макросом:
#define LIBRARY_ADDRESS_BY_HANDLE(dlhandle) ((NULL == dlhandle) ? NULL : (void*)*(size_t const*)(dlhandle))
Как записать и восстановить адрес новой функции?
С переписыванием адресов, на которые указывают переразмещения из секции “.rel.plt” проблем не возникает. По сути, переписывается операнд инструкции JMP соответствующего элемента из секции “.plt”. А операнды такой инструкции – это просто адреса.
Интересней дела обстоят с применением переразмещений к операндам относительных инструкций CALL (код E8). Адреса перехода в них вычисляются по формуле:
address_of_a_function = CALL_argument + address_of_the_next_instruction
Так мы можем узнать адрес функции-оригинала. Из предыдущей формулы получаем значение, которое нужно записать как аргумент для относительного CALL, чтобы осуществлять вызов нужной нам функции:
CALL_argument = address_of_a_function - address_of_the_next_instruction
Секция “.rel.dyn” попадает в сегмент, помеченный как “R E”, а значит, записывать адреса в него просто так не получится. Нужно добавить право на запись для страницы, на которую приходится переразмещение и не забыть после перенаправления вернуть все обратно. Это делается при помощи функции mprotect(). Первый параметр этой функции – это адрес страницы, содержащей переразмещение. Он должен быть всегда кратен размеру страницы. Вычислять его не сложно: нужно только обнулить несколько младших байт адреса переразмещения (в зависимости от размера страницы):
page_address = (size_t)relocation_address & ( ((size_t) -1) ^ (pagesize - 1) );
Например, для страниц размером 4096 (0x1000) байт на 32-битной системе приведенное выше выражение преобразуется в:
page_address = (size_t)relocation_address & (0xFFFFFFFF ^ 0xFFF) = (size_t)relocation_address & 0xFFFFF000;
Размер одной страницы, можно узнать, вызвав sysconf(_SC_PAGESIZE).
Пример использования
В качестве упражнения можно написать plug-in к Firefox, который будет перенаправлять на себя все сетевые вызовы, например, Adobe Flash plug-in’а (libflashplayer.so). Таким образом, можно контролировать весь трафик Adobe Flash в Internet из процесса Firefox никак не воздействуя на сетевые вызовы самого обозревателя и других plug-in’ов.
Удачи!
Ссылки по теме
Желающим прочитать на английском сюда, на русском публикую впервые.
- www.skyfree.org/linux/references/ELF_Format.pdf
- en.wikipedia.org/wiki/Executable_and_Linkable_Format
- vxheavens.com/lib/vsc06.html
- netwinder.osuosl.org/users/p/patb/public_html/elf_relocs.html
- www.slideshare.net/sanjivmalik/dynamic-linker-presentation
- www.codeproject.com/KB/cpp/shared_object_injection_1.aspx
- www.linuxjournal.com/article/1060