избегать LD_PRELOAD: обернуть библиотеку и предоставить функциональность, запрошенную из libc

У меня есть разделяемая библиотека, скажем, somelib.so, которая использует ioctl из libc (согласно objdump).

Моя цель — написать новую библиотеку, охватывающую somelib.so и предоставляющую пользовательский ioctl. Я хочу избежать предварительной загрузки библиотеки, чтобы гарантировать, что только вызовы в somelib.so используют пользовательский файл ioctl.

Вот мой текущий фрагмент:

typedef int (*entryfunctionFromSomelib_t) (int par, int opt);
typedef int (*ioctl_t) (int fd, int request, void *data);
ioctl_t real_ioctl = NULL;

int ioctl(int fd, int request, void *data )
{
    fprintf( stderr, "trying to wrap ioctl\n" );
    void *handle = dlopen( "libc.so.6", RTLD_NOW );
    if (!handle)
        fprintf( stderr, "Error loading libc.so.6: %s\n", strerror(errno) );

    real_ioctl = (ioctl_t) dlsym( handle, "ioctl" );
    return real_ioctl( fd, request, data);
}

int entryfunctionFromSomelib( int par, int opt ) {
    void *handle = dlopen( "/.../somelib.so", RTLD_NOW );
    if (!handle)
        fprintf( stderr, "Error loading somelib.so: %s\n", strerror(errno) );

    real_entryfunctionFromSomelib = entryfunctionFromSomelib_t dlsym( handle, "entryfunctionFromSomelib" );
    return real_entryfunctionFromSomelib( par, opt );
}

Однако это не работает в том смысле, что вызовы ioctl формы somelib.so не перенаправляются в мою пользовательскую реализацию ioctl. Как я могу заставить завернутый somelib.so сделать это?

======================

Дополнительная информация добавлена ​​после поста @Nominal Animal:

Вот некоторая информация из mylib.so (somelib.so после редактирования), полученная через readelf -s | grep functionname:

   246: 0000000000000000   121 FUNC    GLOBAL DEFAULT  UND dlsym@GLIBC_2.2.5 (11)
 42427: 0000000000000000   121 FUNC    GLOBAL DEFAULT  UND dlsym@@GLIBC_2.2.5


   184: 0000000000000000    37 FUNC    GLOBAL DEFAULT  UND ioctl@GLIBC_2.2.5 (6)
 42364: 0000000000000000    37 FUNC    GLOBAL DEFAULT  UND ioctl@@GLIBC_2.2.5

После «исправления» mylib.so он также показывает новую функцию как:

   184: 0000000000000000    37 FUNC    GLOBAL DEFAULT  UND iqct1@GLIBC_2.2.5 (6)

Я «версировал» и экспортировал символы из моей библиотеки wrap_mylib, для которой теперь отображается readelf:

25: 0000000000000d15   344 FUNC    GLOBAL DEFAULT   12 iqct1@GLIBC_2.2.5
63: 0000000000000d15   344 FUNC    GLOBAL DEFAULT   12 iqct1@GLIBC_2.2.5

Однако, когда я пытаюсь выполнить dlopen wrap_mylib, я получаю следующую ошибку:

symbol iqct1, version GLIBC_2.2.5 not defined in file libc.so.6 with link time reference

Возможно, это потому, что mylib.so пытается dlsym iqct1 из libc.so.6 ?


person bonanza    schedule 23.03.2017    source источник
comment
Можете ли вы перекомпилировать исходный mylib.so?   -  person Daniel Jour    schedule 24.03.2017
comment
@DanielJour нет. Я просто понимаю, что, возможно, название mylib глупо, поскольку у меня нет исходников и я не могу его перекомпилировать. Извините за путаницу!   -  person bonanza    schedule 24.03.2017


Ответы (1)


Если objcopy binutils может изменять динамические символы, а mylib.so является динамической библиотекой ELF, мы могли бы использовать

mv  mylib.so  old.mylib.so
objcopy --redefine-sym ioctl=mylib_ioctl  old.mylib.so  mylib.so

переименовать имя символа в библиотеке с ioctl на mylib_ioctl, чтобы мы могли реализовать

int mylib_ioctl(int fd, int request, void *data);

в другой библиотеке или объекте, связанном с окончательными двоичными файлами.

К сожалению, эта функция не реализована (по состоянию на начало 2017 г. по крайней мере ).


Мы можем решить эту проблему с помощью уродливого хака, если имя замещающего символа будет точно такой же длины, как исходное имя. Имя символа представляет собой строку (с предшествующим и последующим нулевым байтом) в файле ELF, поэтому мы можем просто заменить его, используя, например, GNU-сед:

LANG=C LC_ALL=C sed -e 's|\x00ioctl\x00|\x00iqct1\x00|g' old.mylib.so > mylib.so

Это заменяет имя с ioctl() на iqct1(). Это явно меньше оптимального, но здесь кажется самым простым вариантом.

Если вы обнаружите, что вам нужно добавить информацию о версии к функции iqct1(), которую вы реализуете, с помощью GCC вы можете просто добавить строку, аналогичную

__asm__(".symver iqct1,iqct1@GLIBC_2.2.5");

где версия следует за символом @.


Вот практический пример, показывающий, как я проверял это на практике.

Во-первых, давайте создадим mylib.c, представляющий исходные коды для mylib.c (которых нет в операционной системе, иначе простое изменение исходных кодов и перекомпиляция библиотеки решат проблему):

#include <unistd.h>
#include <errno.h>

int myfunc(const char *message)
{
    int retval = 0;

    if (message) {
        const char *end = message;
        int         saved_errno;
        ssize_t     n;

        while (*end)
            end++;

        saved_errno = errno;

        while (message < end) {
            n = write(STDERR_FILENO, message, (size_t)(end - message));
            if (n > 0)
                message += n;
            else {
                if (n == -1)
                    retval = errno;
                else
                    retval = EIO;
                break;
            }
        }

        errno = saved_errno;
    }

    return retval;
}

Единственная экспортируемая функция — myfunc(message), как указано в mylib.h:

#ifndef   MYLIB_H
#define   MYLIB_H

int myfunc(const char *message);

#endif /* MYLIB_H */

Давайте скомпилируем mylib.c в динамическую разделяемую библиотеку mylib.so:

gcc -Wall -O2 -fPIC -shared mylib.c -Wl,-soname,libmylib.so -o mylib.so

Вместо write() из библиотеки C (это функция POSIX, такая же, как ioctl(), а не стандартная функция C), мы хотим использовать mywrt() нашей собственной разработки в нашей собственной программе. Приведенная выше команда сохраняет исходную библиотеку как mylib.so (при внутреннем имени libmylib.so), поэтому мы можем использовать

sed -e 's|\x00write\x00|\x00mywrt\x00|g' mylib.so > libmylib.so

изменить имя символа, сохранив измененную библиотеку как libmylib.so.

Далее нам нужен тестовый исполняемый файл, предоставляющий функцию ssize_t mywrt(int fd, const void *buf, size_t count); (прототип такой же, как у write(2), которую он заменяет. test.c:

#include <stdlib.h>
#include <stdio.h>
#include "mylib.h"

ssize_t mywrt(int fd, const void *buffer, size_t bytes)
{
    printf("write(%d, %p, %zu);\n", fd, buffer, bytes);
    return bytes;
}
__asm__(".symver mywrt,mywrt@GLIBC_2.2.5");

int main(void)
{
    myfunc("Hello, world!\n");

    return EXIT_SUCCESS;
}

Строка .symver указывает версию GLIBC_2.2.5 для mywrt.

Версия зависит от используемой библиотеки C. В данном случае я запустил objdump -T $(locate libc.so) 2>/dev/null | grep -e ' write$', что дало мне

00000000000f66d0  w   DF .text  000000000000005a  GLIBC_2.2.5 write

предпоследнее поле которого является необходимой версией.

Поскольку символ mywrt необходимо экспортировать для использования в динамической библиотеке, я создал test.syms:

{
    mywrt;
};

Чтобы скомпилировать исполняемый файл теста, я использовал

gcc -Wall -O2 test.c -Wl,-dynamic-list,test.syms -L. -lmylib  -o test

Поскольку libmylib.so находится в текущем рабочем каталоге, нам нужно добавить текущий каталог в путь поиска динамической библиотеки:

export LD_LIBRARY_PATH=$PWD:$LD_LIBRARY_PATH

Затем мы можем запустить наш тестовый двоичный файл:

./test

Он выведет что-то вроде

write(2, 0xADDRESS, 14);

потому что это то, что делает функция mywrt(). Если мы хотим проверить немодифицированный вывод, мы можем запустить mv -f mylib.so libmylib.so и повторно запустить ./test, который затем выведет только

Hello, world!

Это показывает, что этот подход, хотя и зависит от очень грубой двоичной модификации файла разделяемой библиотеки (с использованием sed -- но только потому, что objcopy (пока) не поддерживает --redefine-sym для динамических символов), на практике должен работать очень хорошо.

Это также прекрасный пример того, как открытый исходный код превосходит проприетарные библиотеки: количество усилий, уже затраченных на попытку исправить эту незначительную проблему, по крайней мере на порядок выше, чем это было бы переименуйте вызов ioctl в исходниках библиотеки, например, mylib_ioctl() и перекомпилируйте его.


Вставка dlsym() (из <dlfcn.h>, как стандартизовано в POSIX.1-2001) в окончательный двоичный файл кажется необходимым в случае OP.

Предположим, что исходная динамическая библиотека изменена с помощью

sed -e 's|\x00ioctl\x00|\x00iqct1\x00|g;
        s|\x00dlsym\x00|\x00d15ym\x00|g;' mylib.so > libmylib.so

и мы реализуем две пользовательские функции как что-то вроде

int iqct1(int fd, unsigned long request, void *data)
{
    /* For OP to implement! */
}
__asm__(".symver iqct1,iqct1@GLIBC_2.2.5");

void *d15ym(void *handle, const char *symbol)
{
    if (!strcmp(symbol, "ioctl"))
        return iqct1;
    else
    if (!strcmp(symbol, "dlsym"))
        return d15ym;
    else
        return dlsym(handle, symbol);
}
__asm__(".symver d15ym,d15ym@GLIBC_2.2.5");

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

{ i1ct1; d15ym; };

в противном случае реализация должна быть такой же, как в практическом примере, показанном ранее в этом ответе.

Поскольку фактическим прототипом ioctl() является int ioctl(int, unsigned long, ...);, нет никаких гарантий, что это будет работать для всех общих применений ioctl(). В Linux второй параметр имеет тип unsigned long, а третий параметр является либо указателем, либо длинным, либо длинным без знака — во всех архитектурах Linux указатели и длинные/беззнаковые длинные имеют одинаковый размер — поэтому он должен работать, если только драйвер, реализующий ioctl(), также закрыт, и в этом случае вы просто обливаетесь шлангом и ограничены либо надеждой, что это сработает, либо переключением на другое оборудование с надлежащей поддержкой Linux и драйверами с открытым исходным кодом.

Вышеупомянутые частные случаи являются как исходными символами, так и жестко привязанными к заменяемым функциям. (Я называю эти символы замещенными, а не вставленными, потому что мы действительно заменяем символы, которые вызывает mylib.so, вместо того, чтобы вставлять вызовы ioctl() и dlsym().)

Это довольно жестокий подход, но помимо использования sed из-за отсутствия поддержки динамического переопределения символов в objcopy, он достаточно надежен и понятен в отношении того, что делается и что на самом деле происходит.

person Nominal Animal    schedule 23.03.2017
comment
Большое спасибо. К сожалению, я не смог заставить его работать. mylib.so — это ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, not stripped. Я применил objcopy, как вы предложили, но mylib.so по-прежнему загружает ioctl из libc. Есть ли способ проверить, сработало ли переопределение символа? Я пытался проверить objdump -T mylib.so | grep ioctl и readelf --syms mylib.so | grep ioctl, и все они по-прежнему ссылаются на glibc. В обоих звонках также нет информации о my_ioctl. - person bonanza; 24.03.2017
comment
@bonanza: Ой! binutils не поддерживает переопределение символов таким образом. Я отредактировал свой ответ, чтобы отразить ситуацию. Быстрый тест показывает, что неприятный обходной путь - переименование динамической библиотеки ioctl() в iqct1() (или подобное) с использованием вызова sed - здесь работает для меня; не могли бы вы проверить этот подход? - person Nominal Animal; 24.03.2017
comment
Еще раз спасибо за вашу помощь и усилия! Я сделал то, что вы предложили (часть sed). Однако, когда я затем хочу dlopen "исправленную" mylib, dlopen выдает ошибку: "Функция не реализована" (при первой попытке) и "Неверный аргумент" при второй и всех последующих попытках. - person bonanza; 24.03.2017
comment
Возможно ли, что первая версия (код, написанный в моем посте) не работает, потому что mylib выполняет dlopen и dlsym для получения ioctl, что обходит мою реализацию ioctl? Если да, то есть ли способ (и разумный) также обернуть dlsym? В таком случае, как я могу загрузить настоящую dlsym из libc, если у меня есть собственная реализация dlsym в моем коде? - person bonanza; 24.03.2017
comment
@bonanza: вам может понадобиться добавить информацию о версии к символу iqct1 (поскольку ваша библиотека C обычно экспортирует определенную ее версию; GLIBC_2.2.5 на моем - помните, что для mylib.so ваша iqct1() должна выглядеть точно так же, как библиотека C ioctl()). Убедились ли вы, что iqct1() имеет динамическую связь (т. е. что ваша реализация экспортируется правильно)? Относится ли mylib.so к динамическому символу с именем dlsym? - person Nominal Animal; 24.03.2017
comment
В очередной раз благодарим за помощь! Я попытался добавить управление версиями символов с помощью упомянутой вами команды, но компиляция не удалась (см. Мое редактирование в исходной публикации). Я также добавил туда некоторый readelf вывод из обеих библиотек. Надеюсь, я вас правильно понял: Что касается экспорта, я добавил __attribute__((visibility("default"))) вместо iqct1. - person bonanza; 24.03.2017
comment
@bonanza: Нет, атрибута видимости недостаточно; см. здесь подробности о том, как вам нужно обеспечить динамическую компоновку iqct1. (TL;DR: при компиляции используйте -rdynamic или -Wl,-dynamic-list,symbols.txt с iqct1 в symbols.txt.) - person Nominal Animal; 24.03.2017
comment
Еще раз спасибо, я очень ценю это! Наконец-то я экспортировал символ и добавил версию (см. мое редактирование) в начальный пост. Тем не менее, я все еще получаю сообщение об ошибке dlopen о том, что mylib не может найти iqct1. - person bonanza; 24.03.2017
comment
@bonanza: Черт; это точно указывает, что mylib.so использует dlopen()/dlsym() для получения указателя функции на ioctl(). Авг; вам нужно будет вставить dlsym() также в окончательный двоичный файл. Не могли бы вы опубликовать (или отправить мне по электронной почте, мой адрес указан на моей домашней странице, ссылка на которую есть в моем профиле) полный список символов GLIBC от mylib.so, т.е. objdump -T mylib.so | | awk '/GLIBC_/ { printf "%s@%s\n", $NF, $(NF-1) }' | sort? - person Nominal Animal; 25.03.2017
comment
@bonanza: сотрите предыдущий запрос символов; Я добавил практический пример и дополнительное описание того, как заменить ioctl() и dlsym() в mylib.so на iqct1() и d15ym() соответственно. Очень важно, чтобы iqct1() и d15ym() были связаны в конечном двоичном файле, так как в противном случае нам нужно было бы каким-то образом обеспечить загрузку динамических библиотек, чтобы гарантии iqct1() и d15ym() были предоставлены достаточно рано. Это становится слишком грязным, на мой взгляд. - person Nominal Animal; 25.03.2017
comment
Теперь все работает, большое спасибо! Последний вопрос: насколько безопасно «исправление» с помощью sed? То есть, насколько вероятно, что действительно все вызовы на ioctl "перенаправляются" на iqct1? - person bonanza; 27.03.2017
comment
@bonanza: мы переименовываем символ, чтобы он повлиял на все явные ссылки. Если не существует какого-либо другого способа (кроме обычного динамического связывания), с помощью которого библиотека может получить указатель на функцию, это в значительной степени надежно. Риск действительно заключается в том, что команда sed исправляет слишком много, например строку "ioctl". Я рекомендую вам убедиться, что команда sed изменяет только один экземпляр ioctl на iqct1 и один экземпляр dlsym на d15ym (используя, например, cmp -b mylib.so libmylib.so); если это так, вы должны быть полностью покрыты. - person Nominal Animal; 27.03.2017