Практическое применение LD_PRELOAD или замещение функций в Linux

    Всем привет!
    В 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;
        
    }


    Вот, собственно, и все, о чем хотелось рассказать. Надеюсь, это будет кому-то полезно.
    Share post

    Comments 14

      +4
      первым параметром вам передастся указатель на инстанс
      int _ZN9Testclass6getvarEv(int instance) {
      int instance
      int
      указатель
        0
        В последнем примере instance же должен быть type_t, а не инт?
        +2
        Да, это очень хороший способ. Кстати если ядро поддерживает systemtap, то через него можно сделать тоже самое. Но иногда приходится и так извиваться:)
        Кстати буквально вчера коллега выложил запатченный jemalloc, и послав SIGUSR2 процессу вы сможете увидеть статистику аллокациям к памяти (просто вывод malloc_stats). Просто включить его в LD_PRELOAD и просто послать SIGUSR2;)
          +1
          Спасибо! Интересная штука, этот systemtap.
          +3
          printf(«Blocking mimeinfo.cache read\n»);
          Чепятать всё же лучше в stderr в подобном случае, а не в stdout.
            +4
            Вспоминается, как через libc с изменённой strcmp (или чем-то в этом роде) запускали закрытую бету Steam для Linux.
            0
            Пару лет назад ресерчил в эту сторону, написал даже либу которая все данные заворачивала в БД (и в память)

            github.com/rowdyroad/farwel

            Но так и не хватило времени ее довести до нормального состояния)

              +1
              Use case #4: fakeroot
                0
                Use case #5: TNAT64
                  0
                  Есть либа https://github.com/sickill/stderred которая красит красным stderr, работает через LD_PRELOAD. Работает она неплохо, только почему-то вывод программы echo она не перехватывает, хотя с другими программами работает. Я специально вставлял вывод в лог в момент загрузки либы, и результат моих исследований — когда запускаешь echo «abcde» то LD_PRELOAD не срабатывает — по понятно, что раз не подгузило либу, то и перехват не сработает. А если запускаешь cat abcde — то срабатывает и либа подгружается.
                  Как такое может быть?
                    0
                    echo — не программа, а команда shell. Если вам нужна программа, используйте /bin/echo
                      0
                      Да, действительно. Частично помогло — теперь хоть по логам вижу, что либа загружается. А то что не красит stderr — это видимо там они, забыли перехватить что-то. По крайней мере, теперь работа этой штуки не выглядит как магия. Спасибо.

                  Only users with full accounts can post comments. Log in, please.