Всем привет!
В 2010 году, shoumikhin написал замечательную статью Перенаправление функций в разделяемых ELF-библиотеках. Та статья очень грамотно написана, полная, но она описывает более харкордный способ замещения функций. В этой статье, мы будем использовать стандартную возможность динамического линкера — переменную окружения LD_PRELOAD, которая может загрузить вашу библиотеку до загрузки остальных.
Да очень просто — линкер загружает вашу библиотеку с вашими «стандартными» функциями первой, а кто первый — того и тапки. А вы из своей библиотеки можете загрузить уже реальную, и «проксировать» вызовы, попутно делая что вам угодно.
Мне очень нравится браузер Opera. А еще я использую KDE. Opera не очень уважает приоритеты приложений KDE, и, зачастую, так и норовит открыть скачанный ZIP-архив в mcomix, PDF в imgur-uploader, в общем, вы уловили суть. Однако, если ей запретить читать файл mimeinfo.cache, то она все будет открывать через «kioclient exec», а он-то уж лучше знает, в чем я хочу открыть тот или иной файл.
Чем может приложение открывать файл? На ум приходят две функции: fopen и open. В моем случае, opera использовала 64-битный аналог fopen — fopen64. Определить это можно, воспользовавшись утилитой ltrace, или просто посмотрев таблицу импорта утилитой objdump.
Что нам нужно для написания библиотеки? Первым делом, нужно составить прототип оригиналь��ой функции.
Судя по man fopen, прототип у этой функции следующий:
Как видите, все просто: объявляем функцию fopen64, загружаем «следующую» (оригинальную), по отношению к нашей, функцию, и проверяем, не открываем ли мы файл «mimeinfo.cache». Компилируем ее следующей командой:
И запускаем opera:
Успех!
Есть у меня проприетарное приложение, которое использует прямой доступ к принтеру (файл устройства /dev/usb/lp0). Захотел я написать для него свой сервер в целях отладки. Что возвращает open()? Файловый дескриптор. Что возвращает socket()? Такой же файловый дескриптор, на котором совершенно так же работают read() и write(). Приступаем:
С C++ все немного иначе. Скажем, есть у нас класс:
Но функции не будут называться «Testclass::getvar» и «Testclass::setvar» в результирующем файле. Чтобы узнать названия функций, достаточно посмотреть таблицу экспорта:
Тут есть два выхода: либо сделать библиотеку-перехватчик на C++, описав класс так же, каким он был в оригинале, но, в этом случае, у вас, с большой вероятностью, будут проблемы с доступом к конкретному инстансу класса, либо же сделать библиотеку на C, назвав функцию так, как она экспортируется, в таком случае, первым параметром вам передастся указатель на инстанс:
Вот, собственно, и все, о чем хотелось рассказать. Надеюсь, это будет кому-то полезно.
В 2010 году, shoumikhin написал замечательную статью Перенаправление функций в разделяемых ELF-библиотеках. Та статья очень грамотно написана, полная, но она описывает более харкордный способ замещения функций. В этой статье, мы будем использовать стандартную возможность динамического линкера — переменную окружения LD_PRELOAD, которая может загрузить вашу библиотеку до загрузки остальных.
Как это работает?
Да очень просто — линкер загружает вашу библиотеку с вашими «стандартными» функциями первой, а кто первый — того и тапки. А вы из своей библиотеки можете загрузить уже реальную, и «проксировать» вызовы, попутно делая что вам угодно.
Реальный Use-Case #1: Блокируем mimeinfo.cache в Opera
Мне очень нравится браузер Opera. А еще я использую KDE. Opera не очень уважает приоритеты приложений KDE, и, зачастую, так и норовит открыть скачанный ZIP-архив в mcomix, PDF в imgur-uploader, в общем, вы уловили суть. Однако, если ей запретить читать файл mimeinfo.cache, то она все будет открывать через «kioclient exec», а он-то уж лучше знает, в чем я хочу открыть тот или иной файл.
Чем может приложение открывать файл? На ум приходят две функции: fopen и open. В моем случае, opera использовала 64-битный аналог fopen — fopen64. Определить это можно, воспользовавшись утилитой ltrace, или просто посмотрев таблицу импорта утилитой objdump.
Что нам нужно для написания библиотеки? Первым делом, нужно составить прототип оригиналь��ой функции.
Судя по man fopen, прототип у этой функции следующий:
FILE *fopen(const char *path, const char *mode)И возвращает она указатель на FILE, либо NULL, если файл невозможно открыть. Отлично, пишем код:#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
static FILE* (*fopen64_orig)(const char * path, const char * mode) = NULL;
FILE* fopen64(const char * path, const char * mode) {
if (fopen64_orig == NULL)
fopen64_orig = dlsym(RTLD_NEXT, "fopen64");
if (strstr(path, "mimeinfo.cache") != NULL) {
printf("Blocking mimeinfo.cache read\n");
return NULL;
}
return fopen64_orig(path, mode);
}Как видите, все просто: объявляем функцию fopen64, загружаем «следующую» (оригинальную), по отношению к нашей, функцию, и проверяем, не открываем ли мы файл «mimeinfo.cache». Компилируем ее следующей командой:
gcc -shared -fPIC -ldl -O2 -o opera-block-mime.so opera-block-mime.cИ запускаем opera:
LD_PRELOAD=./opera-block-mime.so operaИ видим:Blocking mimeinfo.cache read
Blocking mimeinfo.cache read
Blocking mimeinfo.cache read
Blocking mimeinfo.cache readУспех!
Реальный Use-Case #2: Превращаем файл в сокет
Есть у меня проприетарное приложение, которое использует прямой доступ к принтеру (файл устройства /dev/usb/lp0). Захотел я написать для него свой сервер в целях отладки. Что возвращает open()? Файловый дескриптор. Что возвращает socket()? Такой же файловый дескриптор, на котором совершенно так же работают read() и write(). Приступаем:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
static int (*orig_open)(char * filename, int flags) = NULL;
int open(char * filename, int flags) {
if (orig_open == NULL)
orig_open = dlsym(RTLD_NEXT, "open");
if (strcmp(filename, "/dev/usb/lp0") == 0) {
//opening tcp socket
struct sockaddr_in servaddr, cliaddr;
int socketfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr=inet_addr("127.0.0.1"); // addr
servaddr.sin_port=htons(32000); // port
if (connect(socketfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == 0)
printf("[Open] TCP Connected\n");
else
printf("[Open] TCP Connection failed!\n");
return socketfd;
}
return orig_open(filename, flags);
}Не совсем реальный Use-Case #3: Перехват C++-методов
С C++ все немного иначе. Скажем, есть у нас класс:
class Testclass {
int var = 0;
public:
int setvar(int val);
int getvar();
}; #include <stdio.h>
#include "class.h"
void Testclass() {
int var = 0;
}
int Testclass::setvar(int val) {
printf("setvar!\n");
this->var = val;
return 0;
}
int Testclass::getvar() {
printf("getvar! %d\n", this->var);
return this->var;
}Но функции не будут называться «Testclass::getvar» и «Testclass::setvar» в результирующем файле. Чтобы узнать названия функций, достаточно посмотреть таблицу экспорта:
nm -D libclass.so
…
0000000000000770 T _Z9Testclassv
00000000000007b0 T _ZN9Testclass6getvarEv
0000000000000780 T _ZN9Testclass6setvarEiЭто называется name mangling.Тут есть два выхода: либо сделать библиотеку-перехватчик на C++, описав класс так же, каким он был в оригинале, но, в этом случае, у вас, с большой вероятностью, будут проблемы с доступом к конкретному инстансу класса, либо же сделать библиотеку на C, назвав функцию так, как она экспортируется, в таком случае, первым параметром вам передастся указатель на инстанс:
#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
typedef int (*orig_getvar_type)(void* instance);
int _ZN9Testclass6getvarEv(void* instance) {
printf("Wrapped getvar! %d\n", instance);
orig_getvar_type orig_getvar;
orig_getvar = (orig_getvar_type)dlsym(RTLD_NEXT, "_ZN9Testclass6getvarEv");
printf("orig getvar %d\n", orig_getvar(instance));
return 0;
}Вот, собственно, и все, о чем хотелось рассказать. Надеюсь, это будет кому-то полезно.
