В предыдущей статье был описан метод перехвата вызовов для разделяемых библиотек ELF. А сейчас мы посмотрим как сделать то же самое с библиотеками в формате Mach-O.
Вкратце напомню ситуацию. Имеем программу под Mac OS X, которая пользуется множеством сторонних динамически-компонуемых библиотек, которые, в свою очередь, также пользуются функциями друг друга.
Задача следующая: перехватить вызов некоторой функции из одной библиотеки к другой, и в обработчике вызвать оригинал.
Как обычно, нетерпеливые могут все скачать и попробовать прямо сейчас.
Для наглядности — воображаемый пример: у нас есть программа с названием «test» на языке С (файл test.c) и разделяемая библиотека (файл libtest.c), с неизменным содержимым, откомпилированные заранее. Эта библиотека предоставляет одну функцию: libtest(). В своей реализации каждая из них пользуется функцией puts() из стандартной библиотеки языка С (поставляется вместе с Mac OS, содержится в libSystem.B.dylib). Посмотрим на схематическое изображение описываемой ситуации:
Задача состоит в следующем:
Лучший способ понять Mach-O – это посмотреть на картинку ниже.
Похоже, человечество еще не сумело изобразить его структуру более наглядно. В первом приближении все выглядит примерно так:
Для второго приближения придется познакомится с некоторыми утилитами:
По большому счету, чтобы обычному пользователю разобраться с Mach-O, достаточно поиграть с MachOView на различных примерах. Но, этого недостаточно для программирования Mach-O, поскольку неизвестны точные структуры заголовков, команд загрузки, сегментов, секций, таблиц символов и точное описание их полей. Но, это не большая беда, при наличии спецификации. А она всегда доступна на официальном сайте Apple. А при наличии установленных средств разработки, можно заглянуть в заголовочные файлы из /usr/include/mach-o (особенно loader.h).
Кроме того, стоит помнить, что, хотя содержимое файла и размещается в памяти в точно в таком же порядке, как оно есть на диске, но во время загрузки компоновщик может удалять некоторые части таблицы символов, всю таблицу строк и проставлять значения реальных смещений в памяти там, где это потребуется, в то время, как в файле, эти значения могут вообще быть обнулены или соответствовать смещению на диске.
Структура заголовка проста (привожу для 32-битной архитектуры, 64-битная не сильно отличается):
Все начитается с магического значения (0xFEEDFACE или наоборот, в зависимости от соглашения относительно порядка байт в машинных словах). Затем указан тип архитектуры процессора, количество и размер команд загрузки и флаги, описывающие прочие особенности.
Например:
Существенные команды загрузки перечислены ниже:
Наиболее важные сегменты следующие:
После которых могут идти еще много различных полей, в зависимости от типа команды.
Например:
Самые интересные секции в перечисленных сегментах такие:
Секции в сегментах имеют следующую структуру:
Имеем имя сегмента и самой секции, размер, смещение в файле и адрес в памяти, по которому динамический загрузчик ее разместил. Кроме того, присутствует и другая, специфическая для конкретной секции информация.
Например:
Безусловно, стоит упомянуть, что, в следствии неоднократной плавной смены компанией Apple своих целевых архитектур (Motorola -> IBM -> Intel), исполняемые файлы и библиотеки «научились» хранить сразу несколько вариантов исполняемого кода. В общем случае, такие файлы называют fat binary. По сути, это несколько Mach-O, собранных в одном файле, но заголовок у него особый. Он содержит информацию о количестве и типе поддерживаемых архитектур и смещения к каждой из них. По такому смещению находятся обычные Mach-O со структурой, описанной выше.
Вот как это выглядит на языке С:
Где под magic скрывается 0xCAFEBABE (или наоборот — помним про разный порядок байт в машинных словах на разных процессорах). А после, незамедлительно следует ровно nfat_arch структур типа:
Собственно, названия полей говорят сами за себя: тип процессора, смещение в файле конкретного Mach-O, размер и выравнивание.
Для исследования работы вызова импортируемой функции возьмем следующие файлы на языке С:
Ограничимся процессорами Intel. Пускай у нас Mac OS 10.5. Добавим эти файлы в новый Xcode-проект, скомпилируем (32-битную версию) и запустим в отладочном режиме, остановившись на строчке, где в функции libtest() библиотеки libtest.dylib происходит вызов функции puts(). Вот ассемблерный листинг для libtest():
Выполним еще одну инструкцию:
И посмотри на нее в памяти:
Это и есть та ячейка таблицы импорта (в данном случае — ячейка __IMPORT, __jump_table), которая служит трамплином для вызова динамического загрузчика (функция __dyld_stub_binding_helper_interface), если используется позднее связывание (lazy binding), либо прыгает сразу на целевую функцию. Что подтверждается последующим вызовом puts():
И в памяти:
Итак, мы видим, что динамический загрузчик заменил инструкцию косвенного вызова CALL (0xE8) на инструкцию косвенного перехода JMP (0xE9). Стало быть, для перенаправления элементов __jump_table нам достаточно будет прописывать вместо их изначального содержимого инструкцию косвенного перехода на начало функции-подстановки.
Еще интересный момент. Почему для перехода на динамический загрузчик (он же компоновщик) не используется JMP? Да потому, что CALL, сохраняющий адрес возврата в стеке, поможет компоновщику определить, какой элемент таблицы импорта его вызвал. А, значит, и вычислить, что это был за символ и разрешить его, поменяв CALL на себя на косвенный JMP на требуемую функцию.
Теперь перенесем проект на Mac OS 10.6 и скомпилируем fat binary для 32- и 64-битных архитектур. На всякий случай, в Xcode это можно сделать так:
Компилируем, запускаем 64-битный вариант (просто для примера; таблица импорта на Snow Leopard будет одинаковая и для 32-бит) и останавливается снова на вызове puts():
И снова простой CALL. Смотрим дальше:
Вот тут уже заметно различие с обычным __IMPORT, __jump_table.
Добро пожаловать в __TEXT, __symbol_stub1. Данная таблица представляет из себя набор инструкций JMP для каждой импортируемой функции. В нашем случае там только одна такая инструкция, представленная выше. Каждая такая инструкция осуществляет переход на адрес, указанный в соответствующей ячейке таблицы __DATA, __la_symbol_ptr. Последняя и является таблицей импорта для этого Mach-O.
Но, продолжим исследование. Если заглянуть по адресу, на который собирается произойти переход:
То мы увидим следующее:
Мы попадаем в секцию __TEXT, __stub_helper. По сути, это PLT (Procedure Linkage Table) для Mach-O. Первой инструкцией (в данном случае — это LEA в связке с R11, а могла быть и простая PUSH) динамический компоновщик запоминает, что за символ требует переразмещения, вторая инструкция всегда ведет на один и тот же адрес — начало функции __dyld_stub_binding_helper, которая и займется связыванием:
После того, как динамический компоновщик выполнит переразмещения для puts(), соответствующая ячейка в __DATA, __la_symbol_ptr будет иметь вид:
А это уже и есть адрес функции puts() из модуля libSystem.B.dylib. То есть, подменив его каким-то своим адресом, мы получим требуемый эффект перенаправления вызова.
Итак. На данном этапе мы на конкретном примере выяснили, как происходит динамическое связывание, какие бывают таблицы импорта в Mach-O и из каких элементов они состоят. Теперь приступим к разбору Mach-O!
Нужно по имени символа найти соответствующую ему ячейку в таблице импорта. Алгоритм этого действия несколько нетривиален.
Во-первых, нужно найти сам символ в таблице символов. Последняя представляет из себя массив следующих структур:
Где n_un.n_strx — смещение в байтах от начала таблицы строк имени этого символа. Остальное касается типа символа, секции, в котором он находится и так далее. Словом, вот ее несколько последних элементов для нашей подопытной библиотеки libtest.dylib (32-битная версия):
Таблица строк — это цепочка имен, каждое из которых завершается нулем. Однако, стоит обратить внимание, что к каждому имени компилятор добавляет в начало нижнее подчеркивание "_", поэтому, например имя «puts» будет выглядеть в таблице строк как "_puts".
Вот пример:
Узнать место нахождения таблицы символов и строк можно из соответствующей команды загрузки (LC_SYMTAB):
Однако, таблица символов неоднородна. В ней существует несколько разделов. Один из них нам особо интересен — это неопределенные (undefined) символы, то есть те, которые компонуются динамически. Кстати, MachOView подсвечивает таковые синеватым фоном. Для того, чтобы определить какая часть таблицы символов отражает подмножество неопределенных символов, нужно заглянуть в команду загрузки динамических символов (LC_DYSYMTAB):
Вот ее представление на языке С:
А теперь, очень важный момент: найдя символ по его имени, самое главное для нас — запомнить его индекс в таблице символов от ее начала. Поскольку из числовых значений этих индексов состоит другая важная таблица — таблица косвенных (indirect) символов. Найти ее можно по значению dysymtab_command.indirectsymoff, а количество индексов определяет dysymtab_command.nindirectsyms.
В нашем тривиальном случае эта таблица состоит всего из одного элемента (в реальной жизни их намного больше):
И в конце концов, давайте посмотрим на секцию __IMPORT, __jump_table, некоторый элемент которой и нужно отыскать в конечном итоге. Она выглядит вот так:
Поле section.reserved1 для этой секции имеет очень важное значение (MachOView назвал его Indirect Sym Index). Оно означает индекс в таблице косвенных символов, с которого начинается взаимно однозначное соответствие с элементами __jump_table. А мы помним, что элементы в таблице косвенных символов представляют собой индексы в таблице символов. Улавливаете, к чему я клоню?
Но, перед тем, как окончательно собрать все осколки знаний воедино, для полноты картины бегло посмотрим на ситуацию в Snow Leopard, где роль таблицы импорта играет __DATA, __la_symbol_ptr. На самом деле, различия не особо ощутимы.
Вот команда загрузки символов:
А вот и ее последние элементы:
На синеватом фоне видны два неопределенных символа, что соответствует данным из команды загрузки динамических символов (LC_DYSYMTAB):
Да и в таблице косвенных символов уже не один элемент, а четыре:
Однако, если посмотреть на поле reserved1 заветной секции __la_symbol_ptr, можно обнаружить, что взаимно однозначное отражение ее элементов на таблицу косвенных символов начитается не с начала последней, а с четвертого элемента (индекс равен 3):
Само же содержимое таблицы импорта, что описывает секция __la_symbol_ptr, будет такое:
Узнав обо всех этих тонкостях Mach-O, можно сформулировать алгоритм поиска нужного элемента в таблице импорта.
Опишем все действия словами, так как код, несмотря на обилие комментариев, может оказать не столь понятным:
Настало время превратить изложенные мысли в код. Для оптимизации поиска нужных элементов Mach-O при каждом перенаправлении, разобъем всю операцию на три этапа:
С учетом этих прототипов тестовую программку придется переписать:
Полная реализация тестового примера вместе с алгоритмом перенаправления и файлом проекта доступна для скачивания.
и опробовать примерно так:
Вывод программы свидетельствует о полном выполнении задачи, поставленной в самом начале.
Вкратце напомню ситуацию. Имеем программу под Mac OS X, которая пользуется множеством сторонних динамически-компонуемых библиотек, которые, в свою очередь, также пользуются функциями друг друга.
Задача следующая: перехватить вызов некоторой функции из одной библиотеки к другой, и в обработчике вызвать оригинал.
Как обычно, нетерпеливые могут все скачать и попробовать прямо сейчас.
Для наглядности — воображаемый пример: у нас есть программа с названием «test» на языке С (файл test.c) и разделяемая библиотека (файл libtest.c), с неизменным содержимым, откомпилированные заранее. Эта библиотека предоставляет одну функцию: libtest(). В своей реализации каждая из них пользуется функцией puts() из стандартной библиотеки языка С (поставляется вместе с Mac OS, содержится в libSystem.B.dylib). Посмотрим на схематическое изображение описываемой ситуации:
Задача состоит в следующем:
- Нужно заменить вызов функции puts() для библиотеки libtest.dylib на вызов функции hooked_puts(), реализованной в главной программе (файл test.c), которая, в свою очередь, может пользоваться оригинальной puts();
- Отменить произведенные изменения, то есть сделать так, чтобы повторный вызов libtest() приводил к вызову оригинальной puts().
Кратко о Mach-O
Лучший способ понять Mach-O – это посмотреть на картинку ниже.
Похоже, человечество еще не сумело изобразить его структуру более наглядно. В первом приближении все выглядит примерно так:
- Заголовок — здесь хранится информация о целевой архитектуре и различные опции дальнейшей интерпретации содержимого файла.
- Команды загрузки — сообщают как и куда загружать части Mach-O: сегменты (см. ниже), таблицы символов, а также — от каких библиотек зависит этот файл, чтобы сперва загрузить их
- Сегменты — описывают регионы памяти, куда загружать секции с кодом или данными.
Утилиты-парсеры
Для второго приближения придется познакомится с некоторыми утилитами:
- otool — представляет собой консольную программу, поставляемую вместе с системой. Она способна отображать содержимое различных частей файла: заголовков, команд загрузки, сегментов, секций и прочее. Особо полезно добавлять при вызове ключ -v (verbose).
- MachOView — распространяется под GPL, имеет GUI, работает только на Mac OS 10.6 и выше. Позволяет просматривать полное содержимое Mach-O, дополняет информацию по некоторым разделам, на основании данных из других частей, что очень удобно.
По большому счету, чтобы обычному пользователю разобраться с Mach-O, достаточно поиграть с MachOView на различных примерах. Но, этого недостаточно для программирования Mach-O, поскольку неизвестны точные структуры заголовков, команд загрузки, сегментов, секций, таблиц символов и точное описание их полей. Но, это не большая беда, при наличии спецификации. А она всегда доступна на официальном сайте Apple. А при наличии установленных средств разработки, можно заглянуть в заголовочные файлы из /usr/include/mach-o (особенно loader.h).
Кроме того, стоит помнить, что, хотя содержимое файла и размещается в памяти в точно в таком же порядке, как оно есть на диске, но во время загрузки компоновщик может удалять некоторые части таблицы символов, всю таблицу строк и проставлять значения реальных смещений в памяти там, где это потребуется, в то время, как в файле, эти значения могут вообще быть обнулены или соответствовать смещению на диске.
Структура заголовка проста (привожу для 32-битной архитектуры, 64-битная не сильно отличается):
struct mach_header
{
uint32_t magic;
cpu_type_t cputype;
cpu_subtype_t cpusubtype;
uint32_t filetype;
uint32_t ncmds;
uint32_t sizeofcmds;
uint32_t flags;
};
Все начитается с магического значения (0xFEEDFACE или наоборот, в зависимости от соглашения относительно порядка байт в машинных словах). Затем указан тип архитектуры процессора, количество и размер команд загрузки и флаги, описывающие прочие особенности.
Например:
Существенные команды загрузки перечислены ниже:
- LC_SEGMENT — содержит различную информацию о некотором сегменте: размер, количество секций, смещение в файле и, после загрузки, в памяти
- LC_SYMTAB — загружает таблицу символов и строк
- LC_DYSYMTAB — создает таблицу импорта, данные о символах берутся из таблицы символов
- LC_LOAD_DYLIB — указывает зависимость от некоторой сторонней библиотеки
Наиболее важные сегменты следующие:
- __TEXT — исполняемый код и други данные только для чтения
- __DATA — данные, доступные для записи; в том числе и таблицы импорта, которые имеют свойство изменяться динамическим загрузчиком во время позднего связывания
- __OBJC — различная информация стандартной библиотеки языка Objective-C времени выполнения
- __IMPORT — таблица импорта исключительно для 32-битной архитектуры (у меня генерировалась только на Mac OS 10.5)
- __LINKEDIT — здесь динамический загрузчик располагает свои данные для уже загруженных модулей: таблицы символов, строк и прочее
struct load_command
{
uint32_t cmd; //числовой код команды
uint32_t cmdsize; //размер текущей команды в байтах
};
После которых могут идти еще много различных полей, в зависимости от типа команды.
Например:
Самые интересные секции в перечисленных сегментах такие:
- __TEXT,__text — собственно код
- __TEXT,__cstring — константные строки (в двойных кавычках)
- __TEXT,__const — различные константы
- __DATA,__data — инициализированные переменные (строки и массивы)
- __DATA,__la_symbol_ptr — таблица указателей на импортируемые функции
- __DATA,__bss — неинициализированные статические переменные
- __IMPORT,__jump_table — заглушки для вызовов импортируемых функций
Секции в сегментах имеют следующую структуру:
struct section
{
char sectname[16];
char segname[16];
uint32_t addr;
uint32_t size;
uint32_t offset;
uint32_t align;
uint32_t reloff;
uint32_t nreloc;
uint32_t flags;
uint32_t reserved1;
uint32_t reserved2;
};
Имеем имя сегмента и самой секции, размер, смещение в файле и адрес в памяти, по которому динамический загрузчик ее разместил. Кроме того, присутствует и другая, специфическая для конкретной секции информация.
Например:
Fat binary
Безусловно, стоит упомянуть, что, в следствии неоднократной плавной смены компанией Apple своих целевых архитектур (Motorola -> IBM -> Intel), исполняемые файлы и библиотеки «научились» хранить сразу несколько вариантов исполняемого кода. В общем случае, такие файлы называют fat binary. По сути, это несколько Mach-O, собранных в одном файле, но заголовок у него особый. Он содержит информацию о количестве и типе поддерживаемых архитектур и смещения к каждой из них. По такому смещению находятся обычные Mach-O со структурой, описанной выше.
Вот как это выглядит на языке С:
struct fat_header
{
uint32_t magic;
uint32_t nfat_arch;
};
Где под magic скрывается 0xCAFEBABE (или наоборот — помним про разный порядок байт в машинных словах на разных процессорах). А после, незамедлительно следует ровно nfat_arch структур типа:
struct fat_arch
{
cpu_type_t cputype;
cpu_subtype_t cpusubtype;
uint32_t offset;
uint32_t size;
uint32_t align;
};
Собственно, названия полей говорят сами за себя: тип процессора, смещение в файле конкретного Mach-O, размер и выравнивание.
Подопытная программа
Для исследования работы вызова импортируемой функции возьмем следующие файлы на языке С:
File test.c
void libtest(); //from libtest.dylib
int main()
{
libtest(); //calls puts() from libSystem.B.dylib
return 0;
}
File libtest.c
#include <stdio.h>
void libtest() //just a simple library function
{
puts("libtest: calls the original puts()");
}
Исследуем динамическую компоновку
Ограничимся процессорами Intel. Пускай у нас Mac OS 10.5. Добавим эти файлы в новый Xcode-проект, скомпилируем (32-битную версию) и запустим в отладочном режиме, остановившись на строчке, где в функции libtest() библиотеки libtest.dylib происходит вызов функции puts(). Вот ассемблерный листинг для libtest():
Выполним еще одну инструкцию:
И посмотри на нее в памяти:
Это и есть та ячейка таблицы импорта (в данном случае — ячейка __IMPORT, __jump_table), которая служит трамплином для вызова динамического загрузчика (функция __dyld_stub_binding_helper_interface), если используется позднее связывание (lazy binding), либо прыгает сразу на целевую функцию. Что подтверждается последующим вызовом puts():
И в памяти:
Итак, мы видим, что динамический загрузчик заменил инструкцию косвенного вызова CALL (0xE8) на инструкцию косвенного перехода JMP (0xE9). Стало быть, для перенаправления элементов __jump_table нам достаточно будет прописывать вместо их изначального содержимого инструкцию косвенного перехода на начало функции-подстановки.
Еще интересный момент. Почему для перехода на динамический загрузчик (он же компоновщик) не используется JMP? Да потому, что CALL, сохраняющий адрес возврата в стеке, поможет компоновщику определить, какой элемент таблицы импорта его вызвал. А, значит, и вычислить, что это был за символ и разрешить его, поменяв CALL на себя на косвенный JMP на требуемую функцию.
Теперь перенесем проект на Mac OS 10.6 и скомпилируем fat binary для 32- и 64-битных архитектур. На всякий случай, в Xcode это можно сделать так:
Компилируем, запускаем 64-битный вариант (просто для примера; таблица импорта на Snow Leopard будет одинаковая и для 32-бит) и останавливается снова на вызове puts():
И снова простой CALL. Смотрим дальше:
Вот тут уже заметно различие с обычным __IMPORT, __jump_table.
Добро пожаловать в __TEXT, __symbol_stub1. Данная таблица представляет из себя набор инструкций JMP для каждой импортируемой функции. В нашем случае там только одна такая инструкция, представленная выше. Каждая такая инструкция осуществляет переход на адрес, указанный в соответствующей ячейке таблицы __DATA, __la_symbol_ptr. Последняя и является таблицей импорта для этого Mach-O.
Но, продолжим исследование. Если заглянуть по адресу, на который собирается произойти переход:
То мы увидим следующее:
Мы попадаем в секцию __TEXT, __stub_helper. По сути, это PLT (Procedure Linkage Table) для Mach-O. Первой инструкцией (в данном случае — это LEA в связке с R11, а могла быть и простая PUSH) динамический компоновщик запоминает, что за символ требует переразмещения, вторая инструкция всегда ведет на один и тот же адрес — начало функции __dyld_stub_binding_helper, которая и займется связыванием:
После того, как динамический компоновщик выполнит переразмещения для puts(), соответствующая ячейка в __DATA, __la_symbol_ptr будет иметь вид:
А это уже и есть адрес функции puts() из модуля libSystem.B.dylib. То есть, подменив его каким-то своим адресом, мы получим требуемый эффект перенаправления вызова.
Итак. На данном этапе мы на конкретном примере выяснили, как происходит динамическое связывание, какие бывают таблицы импорта в Mach-O и из каких элементов они состоят. Теперь приступим к разбору Mach-O!
Поиск элемента в таблице импорта
Нужно по имени символа найти соответствующую ему ячейку в таблице импорта. Алгоритм этого действия несколько нетривиален.
Во-первых, нужно найти сам символ в таблице символов. Последняя представляет из себя массив следующих структур:
struct nlist
{
union
{
int32_t n_strx;
} n_un;
uint8_t n_type;
uint8_t n_sect;
int16_t n_desc;
uint32_t n_value;
};
Где n_un.n_strx — смещение в байтах от начала таблицы строк имени этого символа. Остальное касается типа символа, секции, в котором он находится и так далее. Словом, вот ее несколько последних элементов для нашей подопытной библиотеки libtest.dylib (32-битная версия):
Таблица строк — это цепочка имен, каждое из которых завершается нулем. Однако, стоит обратить внимание, что к каждому имени компилятор добавляет в начало нижнее подчеркивание "_", поэтому, например имя «puts» будет выглядеть в таблице строк как "_puts".
Вот пример:
Узнать место нахождения таблицы символов и строк можно из соответствующей команды загрузки (LC_SYMTAB):
Однако, таблица символов неоднородна. В ней существует несколько разделов. Один из них нам особо интересен — это неопределенные (undefined) символы, то есть те, которые компонуются динамически. Кстати, MachOView подсвечивает таковые синеватым фоном. Для того, чтобы определить какая часть таблицы символов отражает подмножество неопределенных символов, нужно заглянуть в команду загрузки динамических символов (LC_DYSYMTAB):
Вот ее представление на языке С:
struct dysymtab_command
{
uint32_t cmd;
uint32_t cmdsize;
uint32_t ilocalsym;
uint32_t nlocalsym;
uint32_t iextdefsym;
uint32_t nextdefsym;
uint32_t iundefsym;
uint32_t nundefsym;
uint32_t tocoff;
uint32_t ntoc;
uint32_t modtaboff;
uint32_t nmodtab;
uint32_t extrefsymoff;
uint32_t nextrefsyms;
uint32_t indirectsymoff;
uint32_t nindirectsyms;
uint32_t extreloff;
uint32_t nextrel;
uint32_t locreloff;
uint32_t nlocrel;
};
Здесь dysymtab_command.iundefsym — это индекс в таблице символов, с которого начинается подмножество неопределенных символов. dysymtab_command.nundefsym — количество неопределенных символов. Поскольку то, что мы ищем, является заведомо неопределенным символом, то и искать его в таблице символов нужно только в этом подмножестве. А теперь, очень важный момент: найдя символ по его имени, самое главное для нас — запомнить его индекс в таблице символов от ее начала. Поскольку из числовых значений этих индексов состоит другая важная таблица — таблица косвенных (indirect) символов. Найти ее можно по значению dysymtab_command.indirectsymoff, а количество индексов определяет dysymtab_command.nindirectsyms.
В нашем тривиальном случае эта таблица состоит всего из одного элемента (в реальной жизни их намного больше):
И в конце концов, давайте посмотрим на секцию __IMPORT, __jump_table, некоторый элемент которой и нужно отыскать в конечном итоге. Она выглядит вот так:
Поле section.reserved1 для этой секции имеет очень важное значение (MachOView назвал его Indirect Sym Index). Оно означает индекс в таблице косвенных символов, с которого начинается взаимно однозначное соответствие с элементами __jump_table. А мы помним, что элементы в таблице косвенных символов представляют собой индексы в таблице символов. Улавливаете, к чему я клоню?
Но, перед тем, как окончательно собрать все осколки знаний воедино, для полноты картины бегло посмотрим на ситуацию в Snow Leopard, где роль таблицы импорта играет __DATA, __la_symbol_ptr. На самом деле, различия не особо ощутимы.
Вот команда загрузки символов:
А вот и ее последние элементы:
На синеватом фоне видны два неопределенных символа, что соответствует данным из команды загрузки динамических символов (LC_DYSYMTAB):
Да и в таблице косвенных символов уже не один элемент, а четыре:
Однако, если посмотреть на поле reserved1 заветной секции __la_symbol_ptr, можно обнаружить, что взаимно однозначное отражение ее элементов на таблицу косвенных символов начитается не с начала последней, а с четвертого элемента (индекс равен 3):
Само же содержимое таблицы импорта, что описывает секция __la_symbol_ptr, будет такое:
Узнав обо всех этих тонкостях Mach-O, можно сформулировать алгоритм поиска нужного элемента в таблице импорта.
Алгоритм перенаправления
Опишем все действия словами, так как код, несмотря на обилие комментариев, может оказать не столь понятным:
- Отыскиваем таблицу символов и строк по данным из команды загрузки LC_SYMTAB.
- Узнаем из команды загрузки LC_DYSYMTAB с какого элемента таблицы символов начинается подмножество неопределенных символов (поле iundefsym).
- Ищем целевой символ по имени среди подмножества неопределенных символов в таблице символов.
- Запоминаем индекс целевого символа от начала таблицы символов.
- Отыскиваем таблицу косвенных символов по данным из команды загрузки LC_DYSYMTAB (поле indirectsymoff).
- Узнаем индекс, с которого начинается отображение таблицы импорта (содержимого секции __DATA, __la_symbol_ptr (либо __IMPORT, __jump_table — будет что-то одно)) на таблицу косвенных символов (поле reserved1).
- Начиная с этого индекса просматриваем таблицу косвенных символов и ищем в ней значение, соответствующее индексу целевого символа в таблице символов.
- Запоминаем, каким по счету с начала отображения таблицы импорта на таблицу косвенных символов попался целевой символ. Сохраненное значение — это и есть индекс нужного элемента в таблице импорта.
- По данным из секции __la_symbol_ptr (либо __jump_table) находим таблицу импорта (поле offset).
- Имея индекс целевого элемента в ней, переписываем адрес (для __la_symbol_ptr) на необходимое нам значение (либо меняем инструкцию CALL/JMP на JMP с операндом — адресом необходимой нам функции (для __jump_table)).
Реализация перенаправления
Настало время превратить изложенные мысли в код. Для оптимизации поиска нужных элементов Mach-O при каждом перенаправлении, разобъем всю операцию на три этапа:
На основании самого файла Mach-O и его отображения в памяти, эта функция возвращает некий непрозрачный описатель, за которым скрывается смещения к таблице импорта, таблица символов, строк и отображение косвенных (indirect) символов из таблицы динамических символов, а также ряд полезных индексов для этого модуля. Вот этот описатель:void *mach_hook_init(char const *library_filename, void const *library_address);
struct mach_hook_handle { void const *library_address; //base address of a library in memory char const *string_table; //buffer to read string_table table from file struct nlist const *symbol_table; //buffer to read symbol table from file uint32_t const *indirect_table; //buffer to read the indirect symbol table in dynamic symbol table from file uint32_t undefined_symbols_count; //number of undefined symbols in the symbol table uint32_t undefined_symbols_index; //position of undefined symbols in the symbol table uint32_t indirect_symbols_count; //number of indirect symbols in the indirect symbol table of DYSYMTAB uint32_t indirect_symbols_index; //index of the first imported symbol in the indirect symbol table of DYSYMTAB uint32_t import_table_offset; //the offset of (__DATA, __la_symbol_ptr) or (__IMPORT, __jump_table) uint32_t jump_table_present; //special flag to show if we work with (__IMPORT, __jump_table) };
Эта функция, по имеющемуся описателю библиотеки, имени целевого символа и адреса перехватчика осуществляет само перенаправление по описанному выше алгоритму.mach_substitution mach_hook(void const *handle, char const *function_name, mach_substitution substitution);
Так осуществляется очистка любого описателя, который вернула mach_hook_init().void mach_hook_free(void *handle);
С учетом этих прототипов тестовую программку придется переписать:
#include <stdio.h>
#include <dlfcn.h>
#include "mach_hook.h"
#define LIBTEST_PATH "libtest.dylib"
void libtest(); //from libtest.dylib
int hooked_puts(char const *s)
{
puts(s); //calls the original puts() from libSystem.B.dylib, because our main executable module called "test" remains intact
return puts("HOOKED!");
}
int main()
{
void *handle = 0; //handle to store hook-related info
mach_substitution original; //original data for restoration
Dl_info info;
if (!dladdr((void const *)libtest, &info)) //gets an address of a library which contains libtest() function
{
fprintf(stderr, "Failed to get the base address of a library!\n", LIBTEST_PATH);
goto end;
}
handle = mach_hook_init(LIBTEST_PATH, info.dli_fbase);
if (!handle)
{
fprintf(stderr, "Redirection init failed!\n");
goto end;
}
libtest(); //calls puts() from libSystem.B.dylib
puts("-----------------------------");
original = mach_hook(handle, "puts", (mach_substitution)hooked_puts);
if (!original)
{
fprintf(stderr, "Redirection failed!\n");
goto end;
}
libtest(); //calls hooked_puts()
puts("-----------------------------");
original = mach_hook(handle, "puts", original); //restores the original relocation
if (!original)
{
fprintf(stderr, "Restoration failed!\n");
goto end;
}
libtest(); //again calls puts() from libSystem.B.dylib
end:
mach_hook_free(handle);
handle = 0; //no effect here, but just a good advice to prevent double freeing
return 0;
}
Полная реализация тестового примера вместе с алгоритмом перенаправления и файлом проекта доступна для скачивания.
Тестовый запуск
и опробовать примерно так:
user@mac$ arch -i386 ./test
libtest: calls the original puts()
-----------------------------
libtest: calls the original puts()
HOOKED!
-----------------------------
libtest: calls the original puts()
user@mac$ arch -x86_64 ./test
libtest: calls the original puts()
-----------------------------
libtest: calls the original puts()
HOOKED!
-----------------------------
libtest: calls the original puts()
Вывод программы свидетельствует о полном выполнении задачи, поставленной в самом начале.
Полезные ссылки
- Mac OS X ABI Mach-O File Format Reference
- Mach-O Programming Topics
- Dynamic Linking: ELF vs. Mach-O
- Dynamic symbol table duel: ELF vs Mach-O, round 2
- Runtime binary loading via the dynamic loader on Apple Mac OS X
- Let your Mach-O fly — Black Hat
- Advanced Mac OS X Physical Memory Analysis — Black Hat
- Breaking Mac OS X