Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Перехват функций разделяемых библиотек

Оригинал: Let's Hook a Library Function
Автор: Dibyendu Roy
Дата публикации: 1 Августа 2011 г.
Перевод: А.Панин
Дата публикации перевода: 19 октября 2012 г.

В том случае, если вы являетесь разработчиком и хотите изменить принцип работы функции из разделяемой библиотеки, вы можете использовать информацию из этой статьи для того, чтобы понять с чего начать работу по перехвату функции библиотеки и провести ряд экспериментов с функциями вашей собственной разделяемой библиотеки. Исходные коды программ и библиотек разработаны на языке C, программы собраны при помощи компилятора GCC и протестированы в ОС Linux.

В соответствии с информацией из Wikipedia: "под термином перехват в программировании понимается ряд техник, используемых для изменения поведения или расширения возможностей операционных систем, приложений или других программных компонентов путем подмены вызовов функций, сообщений или событий, передаваемых между программными компонентами. Код, обрабатывающий перехваченные вызовы функций, события и сообщения, называется кодом перехвата".

Перехват функций разделяемых библиотек и использование кода для замещения также называется "функциональным посредничеством" (Function Interposition).

Перехват функций имеет ряд преимуществ:
  • Вам не нужно искать объявление функции в коде библиотеки, такой, как libc (glibc (GNU C Library) - это реализация библиотеки языка C от проекта GNU, при этом кодовая база libc составляет едва ли не половину glibc) и изменять его. Серьезно, это очень неприятная техническая задача (во всяком случае, для меня!).
  • Вам не нужно перекомпилировать библиотеку.

Библиотечные функции и системные вызовы

На рисунках 1 и 2 показан процесс перехвата библиотечной функции. Пожалуйста, рассмотрите их.

Вызов библиотечной функции
Рисунок 1: Вызов библиотечной функции

Вызов библиотечной функции с перехватом
Рисунок 2: Вызов библиотечной функции с перехватом

Теперь давайте рассмотрим перехват функции разделяемой библиотеки. Простейшая программа prog1.c, исходный код которой приведен ниже, сначала резервирует 10 байт из кучи, а затем освобождает их:
#include<stdio.h>
#include<malloc.h>
#include<stdlib.h>
int main(void)
{
        int *p;
        printf("calling from main...\n");
        p=(int *)malloc(10);
        if(!p)
        {
                printf("Got allocation error...\n");
                exit(1);
        }
        printf("returning to main...\n");
        free(p);                           /* освобождение зарезервированной памяти */
        printf("freeing memory...\n");
        return 0;
}
Мы увидим следующий вывод после компиляции и запуска программы:
[root@workbenchsvr malloc_hook]# gcc -o prog1 prog1.c
[root@workbenchsvr malloc_hook]# ./prog1
calling from main...
returning to main...
freeing memory...
[root@workbenchsvr malloc_hook]#
Следующая программа, исходный код которой находится в файле prog2.c, содержит простейшую функцию перехвата для функции malloc():
#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <dlfcn.h>                               /* заголовочный файл необходим для вызва dlsym() */

/* lcheck() - функция, необходимая для проверки на утечки памяти; ее код не приведен в данном листинге*/
void lcheck(void);
void* malloc(size_t size)
{
        static void* (*my_malloc)(size_t) = NULL;
        printf("inside shared object...\n");
        if (!my_malloc)
        my_malloc = dlsym(RTLD_NEXT, "malloc");  /* возвращает указатель на функцию malloc */
        void *p = my_malloc(size);               /* вызов malloc() с использованием указателя my_malloc */  
        printf("malloc(%d) = %p\n", size, p);
        lcheck();                                /* вызов функции do_your_stuff */
        printf("returning from shared object...\n");
        return p;
}
void lcheck(void)
{
        printf("displaying memory leaks...\n");
        /* выполнение необходимых действий */
}
Ход компиляции в выполнения выглядит следующим образом:
[root@workbenchsvr malloc_hook]# gcc -shared -ldl -fPIC prog2.c -o libprog2.so
[root@workbenchsvr malloc_hook]# LD_PRELOAD=/home/dibyendu/malloc_hook/libprog2.so ./prog1
calling from main...
inside shared object...
malloc(10) = 0x8191008
displaying memory leaks...
returning from shared object...
returning to main...
freeing memory...
[root@workbenchsvr malloc_hook]#

Теперь мы можем подробнее рассмотреть наш первый пример перехвата функции разделяемой библиотеки. Функция dlsym() принимает два параметра: первый параметр - это идентификатор, возвращенный функцией dlopen(). В нашем случае мы должны использовать параметр RTLD_NEXT для перехвата функции.

Этот параметр сообщает динамическому компоновщику о том, что необходимо найти следующую ссылку на заданную функцию, а не ту, которая находится при вызове dlsym(). Вторым параметром является имя функции (в нашем случае malloc) в форме строки. Функция dlsym() возвращает адрес функции, указанной в качестве второго параметра. Во время компиляции параметр fPIC используется для создания независимого от позиции объектного кода (position-independent object).

Переменная окружения LD_PRELOAD передает загрузчику динамических библиотек список библиотек, которые должны быть загружены в первую очередь. В нашем примере при помощи этой переменной мы подгружаем библиотеку libprog2.so и динамически связываем ее с программой prog1. Не забудьте, что при передаче параметра переменной окружения LD_PRELOAD, необходимо приводить абсолютный путь до файла разделяемой библиотеки. И конечно же, не забудьте объявление #define _GNU_SOURCE, если хотите использовать соответствующие расширения во время работы с GNU C Library, поскольку некоторые расширения могут быть недоступны на операционных системах, не имеющих отношения к проекту GNU, и добавление этого объявления улучшит переносимость на другие системы.

Можем ли мы перехватить любую функцию?

Приведенный выше метод перехвата функций разделяемых библиотек не будет корректно работать в том случае, если вы захотите перехватить саму функцию dlsym() или функции, содержащие вызов dlsym(). Есть ли возможность обойти это ограничение? Конечно, но вы не сможете использовать метод, разобранный ранее - проверьте сами. Во-первых, создайте разделяемую библиотеку из файлов file1.с и file2.c (приведенных ниже). После этого скомпилируйте программу при помощи команды gcc -rdynamic -o dl_prog1 dl_prog1.c -ldl. Да, вывод очевиден:
/* file1.c */
void file1(int *i)
{
        *i=100;
}
/* file2.c  */
void file2(int *i)
{
        *i=200;
}
Простейшая программа dl_prog1.c демонстрирует возможности функций dlopen() и dlsym(). Функции file1() и file2() описаны в файлах file1.c и file2.c.
#include<stdio.h>
#include<dlfcn.h>
#include<stdlib.h>

void file1(int *i);
void file2(int *i);
int main(void)
{
	void *handler;
	int (*fn) (int *);
	int x;
	char *error;
	handler = dlopen("/home/dibyendu/dlsym_hook/libfile.so", RTLD_LAZY);
	if (!handler)
	{
		 fprintf(stderr,"%s\n", dlerror());
		exit(1);
	}
	fn = dlsym(handler,"file1");     /* получение идентификатора при помощи dlsym() */
	if ((error = dlerror()) != NULL) /* проверка на наличие ошибок с помощью dlerror() */
	{
		 fprintf(stderr,"%s\n", error);
		exit(1);
	}
	(*fn)(&x);                            /* вызов функции file1() для получения значения x */
	printf("The value of x is %d\n", x);
	dlclose(handler);                 /* освобождение идентификатора */
	return 0;
}
Теперь попробуйте перехватить функцию dlsym(), в результате чего вы получите ошибку сегментирования из-за рекурсивных вызовов (функция dlsym() будет вызывать сама себя). Следующая программа dl_prog2.c является примером рекурсивного вызова dlsym(), который приводит к переполнению стека и ошибке сегментирования:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <dlfcn.h>

void *dlsym(void *handle, const char *name)
{
        void *(*dlsym_fn)(void *, const char *)=NULL;
        printf("inside shared object::before dlsym()...\n");
        dlsym_fn=dlsym(RTLD_NEXT, "dlsym");                     /* функция будет вызывать себя снова и снова */
        printf("inside shared object::after dlsym()...\n");
        return (*dlsym_fn)(handle, name);
}
Вывод:
[root@workbenchsvr dlsym_hook]# gcc -shared -ldl -fPIC dl_prog2.c -o libdl_prog2.so
[root@workbenchsvr dlsym_hook]# LD_PRELOAD=/home/dibyendu/dlsym_hook/libdl_prog2.so ./dl_prog1
inside shared object::before dlsym()...
..................................................................
inside shared object::before dlsym()...
Segmentation fault
[root@workbenchsvr dlsym_hook]#
А программа dl_prog3.c является примером успешного перехвата функции dlsym():
#define __USE_GNU
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

extern void *__libc_dlsym (void *, const char *);
void *dlsym(void *handle, const char *symbol)
{
        printf("Ha Ha...dlsym() Hooked\n");
        void* result = __libc_dlsym(handle, symbol); /* теперь библиотечная функция dlsym() будет корректно вызвана */
        return result;
}
Вывод:
[root@workbenchsvr dlsym_hook]# gcc -shared -ldl -fPIC dl_prog3.c -o libdl_prog3.so
[root@workbenchsvr dlsym_hook]# LD_PRELOAD=/home/dibyendu/dlsym_hook/libdl_prog3.so ./dl_prog1
Ha Ha...dlsym() Hooked
The value of x is 100
[root@workbenchsvr dlsym_hook]#

Что еще можно сделать?

Мне удалось перехватить такие функции, как: getaddrinfo(), open() и другие, поэтому и вы можете перехватывать функции, которые пожелаете. При этом существует несколько ограничений:
  • Остерегайтесь функций, которые вызывают dlsym(), когда вам необходимо вызывать __libc_dlsym (идентификатор, символ) в коде перехвата.
  • Удостоверьтесь в том, что бит SUID не установлен, так как в этом случае использование переменной окружения LD_PRELOAD невозможно.
  • Вызовы внутренних функций в рамках библиотеки формируются на этапе статической компоновки - например, если какая-нибудь функция в библиотеке libc вызывает getaddrinfo() или malloc(), то код перехвата из другой библиотеки не будет исполнен ни при каких обстоятельствах.