html текст
All interests
  • All interests
  • Design
  • Food
  • Gadgets
  • Humor
  • News
  • Photo
  • Travel
  • Video
Click to see the next recommended page
Like it
Don't like
Add to Favorites

Перенаправление функций в Mach-O библиотеках

В предыдущей статье был описан метод перехвата вызовов для разделяемых библиотек ELF. А сейчас мы посмотрим как сделать то же самое с библиотеками в формате Mach-O.

Вкратце напомню ситуацию. Имеем программу под Mac OS X, которая пользуется множеством сторонних динамически-компонуемых библиотек, которые, в свою очередь, также пользуются функциями друг друга.

Задача следующая: перехватить вызов некоторой функции из одной библиотеки к другой, и в обработчике вызвать оригинал.

Как обычно, нетерпеливые могут все скачать и попробовать прямо сейчас.

Для наглядности — воображаемый пример: у нас есть программа с названием «test» на языке С (файл test.c) и разделяемая библиотека (файл libtest.c), с неизменным содержимым, откомпилированные заранее. Эта библиотека предоставляет одну функцию: libtest(). В своей реализации каждая из них пользуется функцией puts() из стандартной библиотеки языка С (поставляется вместе с Mac OS, содержится в libSystem.B.dylib). Посмотрим на схематическое изображение описываемой ситуации:

Задача состоит в следующем:
  1. Нужно заменить вызов функции puts() для библиотеки libtest.dylib на вызов функции hooked_puts(), реализованной в главной программе (файл test.c), которая, в свою очередь, может пользоваться оригинальной puts();
  2. Отменить произведенные изменения, то есть сделать так, чтобы повторный вызов libtest() приводил к вызову оригинальной puts().
При этом менять код или перекомпилировать сами библиотеки не разрешается, только главную программу. Само перенаправление вызова должно осуществляться только для конкретной библиотеки и налету, без перезапуска программы.

Кратко о Mach-O


Лучший способ понять Mach-O – это посмотреть на картинку ниже.

Похоже, человечество еще не сумело изобразить его структуру более наглядно. В первом приближении все выглядит примерно так:
  1. Заголовок — здесь хранится информация о целевой архитектуре и различные опции дальнейшей интерпретации содержимого файла.
  2. Команды загрузки — сообщают как и куда загружать части Mach-O: сегменты (см. ниже), таблицы символов, а также — от каких библиотек зависит этот файл, чтобы сперва загрузить их
  3. Сегменты — описывают регионы памяти, куда загружать секции с кодом или данными.
Утилиты-парсеры

Для второго приближения придется познакомится с некоторыми утилитами:
  • 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 — указывает зависимость от некоторой сторонней библиотеки
Например (32- и 64-битные версии соответственно):

Наиболее важные сегменты следующие:
  • __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 — заглушки для вызовов импортируемых функций
Забегая вперед, отмечу, что в одном Mach-O в качестве таблицы импорта может быть либо __IMPORT,__jump_table (32 бита, Mac OS 10.5), либо __DATA,__la_symbol_ptr (64 бита, либо Mac OS 10.6 и старше).

Секции в сегментах имеют следующую структуру:
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, можно сформулировать алгоритм поиска нужного элемента в таблице импорта.

Алгоритм перенаправления


Опишем все действия словами, так как код, несмотря на обилие комментариев, может оказать не столь понятным:
  1. Отыскиваем таблицу символов и строк по данным из команды загрузки LC_SYMTAB.
  2. Узнаем из команды загрузки LC_DYSYMTAB с какого элемента таблицы символов начинается подмножество неопределенных символов (поле iundefsym).
  3. Ищем целевой символ по имени среди подмножества неопределенных символов в таблице символов.
  4. Запоминаем индекс целевого символа от начала таблицы символов.
  5. Отыскиваем таблицу косвенных символов по данным из команды загрузки LC_DYSYMTAB (поле indirectsymoff).
  6. Узнаем индекс, с которого начинается отображение таблицы импорта (содержимого секции __DATA, __la_symbol_ptr (либо __IMPORT, __jump_table — будет что-то одно)) на таблицу косвенных символов (поле reserved1).
  7. Начиная с этого индекса просматриваем таблицу косвенных символов и ищем в ней значение, соответствующее индексу целевого символа в таблице символов.
  8. Запоминаем, каким по счету с начала отображения таблицы импорта на таблицу косвенных символов попался целевой символ. Сохраненное значение — это и есть индекс нужного элемента в таблице импорта.
  9. По данным из секции __la_symbol_ptr (либо __jump_table) находим таблицу импорта (поле offset).
  10. Имея индекс целевого элемента в ней, переписываем адрес (для __la_symbol_ptr) на необходимое нам значение (либо меняем инструкцию CALL/JMP на JMP с операндом — адресом необходимой нам функции (для __jump_table)).
Замечу, что работать с таблицами символов, строк и косвенных символов необходимо только, загрузив их из файла. А читать содержимое секций, описывающих таблицы импорта, и, естественно, производить само перенаправление, уже в памяти. Это связано с тем, что таблицы символов и строк могут отсутствовать или не отображать действительное положение вещей в целевом Mach-O. Ведь до нас там поработал динамический загрузчик и благополучно сохранил себе все необходимые данные о символах, не размещая сами таблицы.

Реализация перенаправления


Настало время превратить изложенные мысли в код. Для оптимизации поиска нужных элементов Mach-O при каждом перенаправлении, разобъем всю операцию на три этапа:
  1. void *mach_hook_init(char const *library_filename, void const *library_address);
    На основании самого файла Mach-O и его отображения в памяти, эта функция возвращает некий непрозрачный описатель, за которым скрывается смещения к таблице импорта, таблица символов, строк и отображение косвенных (indirect) символов из таблицы динамических символов, а также ряд полезных индексов для этого модуля. Вот этот описатель:
    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)
    };
    
  2. mach_substitution mach_hook(void const *handle, char const *function_name, mach_substitution substitution);
    Эта функция, по имеющемуся описателю библиотеки, имени целевого символа и адреса перехватчика осуществляет само перенаправление по описанному выше алгоритму.
  3. void mach_hook_free(void *handle);
    Так осуществляется очистка любого описателя, который вернула mach_hook_init().

С учетом этих прототипов тестовую программку придется переписать:
#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()

Вывод программы свидетельствует о полном выполнении задачи, поставленной в самом начале.

Полезные ссылки

Удачи!
Читать дальше
Twitter
Одноклассники
Мой Мир

материал с habrahabr.ru

1

      Add

      You can create thematic collections and keep, for instance, all recipes in one place so you will never lose them.

      No images found
      Previous Next 0 / 0
      500
      • Advertisement
      • Animals
      • Architecture
      • Art
      • Auto
      • Aviation
      • Books
      • Cartoons
      • Celebrities
      • Children
      • Culture
      • Design
      • Economics
      • Education
      • Entertainment
      • Fashion
      • Fitness
      • Food
      • Gadgets
      • Games
      • Health
      • History
      • Hobby
      • Humor
      • Interior
      • Moto
      • Movies
      • Music
      • Nature
      • News
      • Photo
      • Pictures
      • Politics
      • Psychology
      • Science
      • Society
      • Sport
      • Technology
      • Travel
      • Video
      • Weapons
      • Web
      • Work
        Submit
        Valid formats are JPG, PNG, GIF.
        Not more than 5 Мb, please.
        30
        surfingbird.ru/site/
        RSS format guidelines
        500
        • Advertisement
        • Animals
        • Architecture
        • Art
        • Auto
        • Aviation
        • Books
        • Cartoons
        • Celebrities
        • Children
        • Culture
        • Design
        • Economics
        • Education
        • Entertainment
        • Fashion
        • Fitness
        • Food
        • Gadgets
        • Games
        • Health
        • History
        • Hobby
        • Humor
        • Interior
        • Moto
        • Movies
        • Music
        • Nature
        • News
        • Photo
        • Pictures
        • Politics
        • Psychology
        • Science
        • Society
        • Sport
        • Technology
        • Travel
        • Video
        • Weapons
        • Web
        • Work

          Submit

          Thank you! Wait for moderation.

          Тебе это не нравится?

          You can block the domain, tag, user or channel, and we'll stop recommend it to you. You can always unblock them in your settings.

          • shoumikhin
          • домен habrahabr.ru

          Get a link

          Спасибо, твоя жалоба принята.

          Log on to Surfingbird

          Recover
          Sign up

          or

          Welcome to Surfingbird.com!

          You'll find thousands of interesting pages, photos, and videos inside.
          Join!

          • Personal
            recommendations

          • Stash
            interesting and useful stuff

          • Anywhere,
            anytime

          Do we already know you? Login or restore the password.

          Close

          Add to collection

             

            Facebook

            Ваш профиль на рассмотрении, обновите страницу через несколько секунд

            Facebook

            К сожалению, вы не попадаете под условия акции