Как стать автором
Обновить

Загрузка динамической библиотеки из памяти в Linux

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

Перехватить функции open, mmap и прочие невозможно, так как ld.so слинкован с библиотекой си статически, исполняемые файлы, загруженные же своим загрузчиком, «неполноценны» (даже с перехватом функций в libdl): они не регистрируются в списке загруженных библиотек и/или их символы не видны через dlsym. Следовательно, остается только перехват системных вызовов.

Точка входа в загрузчик:
void *dlopen_memory(void *base, size_t size, void *(*custom_dlopen)(const char *filename, void *arg), void *arg);

Параметры:
  • base — базовый адрес, по которому загружен образ. После завершения этой функции больше не нужен и может быть освобожден.
  • size — размер образа в байтах.
  • custom_dlopen — пользовательская обертка над dlopen. Может быть использована, если вы хотите загрузить библиотеку из памяти с помощью какой-нибудь библиотечной функции, которая вызывает dlopen. Если не нужна — передайте NULL.
  • arg — дополнительный параметр для custom_dlopen.

Возвращаемое значение такое же, как dlopen: хендл библиотеки или NULL и описание ошибки через dlerror().

Работает она следующим образом:
  1. Генерируется псевдослучайное имя библиотеки. Мне нужно было именно такое, если вам необходимо читаемое — передайте доп. параметр filename.
  2. Устанавливается пустой обработчик сигнала SIGQUIT, старый сохраняется. Этот сигнал используется позже при завершении загрузки.
  3. Создается дочерний процесс, который собственно будет выполнять загрузку.
  4. Ожидается установка флага готовности дочернего процесса.
  5. Выполняется загрузка библиотеки через dlopen или custom_dlopen.
  6. Собственному процессу посылается SIGQUIT.
  7. Ожидается завершение дочернего процесса.
  8. Восстанавливается обработчик SIGQUIT.

Дочерний процесс выполняет следующие действия:
  1. Начинает трассировать своего родителя. Это приводит к некоторым интересным последствиям, например, он становится родителем своего родителя.
  2. Устанавливает флаг готовности в родительском процессе.
  3. Пока родительский процесс выполняется и не была запрошена остановка:
    1. Родительский процесс выполняется до входа в системный вызов. Если системный вызов — не mmap псевдофайла, то процесс выполняется еще и до выхода из вызова.
    2. Извлекаются регистры дочернего процесса.
    3. Если этот вызов — посылка вызова SIGQUIT родительскому процессу, то трассировка прекращается.
    4. Если этот вызов — открытие псевдофайла библиотеки, то выходной описатель заменяется специальным описателем, который используется для идентификации операций с псевдофайлом.
    5. Если этот вызов — чтение из псевдофайла, то выполняется чтение из образа библиотеки в буфер родительского процесса и вызов успешно завершается.
    6. Если этот вызов — получение атрибутов файла, то формируется и помещается в буфер родительского процесса информация о псевдофайле (в ней имеет смысл только размер).
    7. Если этот вызов — закрытие файла, то вызов успешно завершается.
    8. Если этот вызов — отображение псевдофайла на память, то выполняются следующие действия:
      1. Ставится флаг MAP_ANONYMOUS (запрашивается регион памяти, не отображенный на файл).
      2. Выполняется вызов mmap.
      3. В созданный регион в родительском процессе копируется запрошенная часть псевдофайла.
  4. Процесс отсоединяется от родителя и завершает свою работу.

Данный код не портабелен и будет работать только на Linux и только на процессорах архитектуры IA-32 в 32-битном режиме. Для систем другой архитектуры необходимо (обернув в #if/#end) реализовать эмуляцию системных вызовов, для систем с другой длиной слова также необходимо изменить процедуру peekstring. Если вызов ptrace или waitpid завершится неуспешно, то работа и родительского, и дочернего процесса будет прекращена. Если вам необходимо другое поведение — перепишите обработчик за меткой fail.

Код доступен для свободного использования без каких-либо ограничений.


#include <sys/mman.h>
#include <sys/ptrace.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <time.h>
#include <stdio.h>
#include <sched.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <dlfcn.h>
#include <errno.h>
#include <limits.h>
#include <fcntl.h>
#include <sys/stat.h>

#define min(a, b) ((a) < (b) ? (a) : (b))

static void generate_name(char *name, size_t length) {
        assert(length > 5);
        strcpy(name + length - 4, ".so");

        for(unsigned int i = 1; i < length - 4; i++)
                name[i] = rand() % ('Z' - 'A' + 1) + 'A';

        name[0] = '/';
}

static void quit_handler(int sig) {
        (void) sig;
}

static int peekstring(pid_t pid, void *base, char *dest, size_t length) {
        unsigned int word;
        unsigned int offset = 0;

        do {
                word = ptrace(PTRACE_PEEKDATA, pid, base + offset, NULL);

                memcpy(dest + offset, &word, sizeof(unsigned int));

                offset += sizeof(unsigned int);

        } while((word & 0xFF) && (word & 0xFF00) &&
                (word & 0xFF0000) && (word & 0xFF000000) && offset < length);

        dest[length - 1] = 0;

        return 0;
}

static int pokedata(pid_t pid, void *address, const void *data, size_t length) {
        length = (length + sizeof(unsigned int) - 1) & ~(sizeof(unsigned int) - 1);
        const unsigned int *src = data;

        for(unsigned int offset = 0; offset < length; offset += sizeof(unsigned int)) {
                if(ptrace(PTRACE_POKEDATA, pid, address + offset, (void *) *src++) == -1)
                        return -1;
        }

        return 0;
}

void *dlopen_memory(void *base, size_t size, void *(*custom_dlopen)(const char *filename, void *arg), void *arg) {
        char fakename[16];
        int ready = 0;
        unsigned int offset = 0;

        generate_name(fakename, sizeof(fakename));

        struct sigaction old_handler, new_handler = {
                .sa_handler = quit_handler,
                .sa_flags = 0,
                .sa_restorer = NULL
        };

        sigfillset(&new_handler.sa_mask);

        if(sigaction(SIGQUIT, &new_handler, &old_handler) == -1)
                return NULL;

        pid_t child = fork();

        if(child == -1) {
                sigaction(SIGQUIT, &old_handler, NULL);

                return NULL;
        } else if(child == 0) {
                pid_t parent = getppid();

                if(ptrace(PTRACE_ATTACH, parent, NULL, NULL) == -1) {
                        kill(parent, SIGKILL);

                        _exit(1);
                }

                ready = 1;
                if(ptrace(PTRACE_POKEDATA, parent, &ready, (void *)ready) == -1)
                        goto fail;

                int status;
                char path[PATH_MAX];
                int handle = getdtablesize();

                do {
                        struct user_regs_struct regs;

                        for(int i = 0; i < 2; i++) {
                                if(ptrace(PTRACE_SYSCALL, parent, NULL, NULL) == -1)
                                        goto fail;

                                if(waitpid(parent, &status, 0) == -1)
                                        goto fail;

                                if(!WIFSTOPPED(status))
                                        goto outer_break;

                                if(ptrace(PTRACE_GETREGS, parent, NULL, &regs) == -1)
                                        goto fail;

                                if(regs.orig_eax == SYS_mmap2 && regs.edi == handle)
                                        break;
                        }


                        if(regs.orig_eax == SYS_kill && regs.ebx == parent && regs.ecx == SIGQUIT) {
                                break;
                        } else if(regs.orig_eax == SYS_open) {
                                if(peekstring(parent, (void *) regs.ebx, path, PATH_MAX) == -1)
                                        goto fail;

                                if(strcmp(path, fakename) != 0)
                                        continue;

                                regs.eax = handle;
                                offset = 0;
                        } else if(regs.orig_eax == SYS_read && regs.ebx == handle) {
                                unsigned int bytes = min((unsigned int) regs.edx, size - offset);

                                if(pokedata(parent, (void *) regs.ecx, base + offset, bytes) == -1)
                                        goto fail;

                                offset += bytes;
                                regs.eax = bytes;

                        } else if(regs.orig_eax == SYS_close && regs.ebx == handle) {
                                regs.eax = 0;
                        } else if(regs.orig_eax == SYS_fstat64 && regs.ebx == handle) {
                                struct stat statbuf = {
                                        .st_dev = 0,
                                        .st_ino = 1,
                                        .st_mode = 0444,
                                        .st_nlink = 1,
                                        .st_uid = 0,
                                        .st_gid = 0,
                                        .st_rdev = 0,
                                        .st_size = size,
                                        .st_blksize = 512,
                                        .st_blocks = (size + 511) & ~511,
                                        .st_atim = { 0, 0 },
                                        .st_mtim = { 0, 0 },
                                        .st_ctim = { 0, 0 }
                                };

                                if(pokedata(parent, (void *) regs.ecx, &statbuf, sizeof(struct stat)) == -1)
                                        goto fail;

                                regs.eax = 0;
                        } else if(regs.orig_eax == SYS_mmap2 && regs.edi == handle) {
                                regs.esi |= MAP_ANONYMOUS;

                                if(ptrace(PTRACE_SETREGS, parent, NULL, &regs) == -1)
                                        goto fail;

                                if(ptrace(PTRACE_SYSCALL, parent, NULL, NULL) == -1)
                                        goto fail;

                                if(waitpid(parent, &status, 0) == -1)
                                        goto fail;

                                if(!WIFSTOPPED(status))
                                        break;

                                if(ptrace(PTRACE_GETREGS, parent, NULL, &regs) == -1)
                                        goto fail;

                                unsigned int offset = regs.ebp * 4096;
                                unsigned int bytes = min(size - offset, (unsigned int) regs.ecx);

                                if(pokedata(parent, (void *) regs.eax, base + offset, bytes) < 0) {
                                        regs.eax = -errno;

                                        if(ptrace(PTRACE_SETREGS, parent, NULL, &regs) == -1)
                                                goto fail;
                                }

                                continue;
                        } else if(regs.orig_eax == -1)
                                break;

                        if(ptrace(PTRACE_SETREGS, parent, NULL, &regs) == -1)
                                goto fail;

                } while(1);
outer_break:
                ptrace(PTRACE_DETACH, parent, NULL, NULL);

                _exit(0);

fail:
                ptrace(PTRACE_KILL, parent, NULL, NULL);
                _exit(1);
        }


        while(ready == 0)
                sched_yield();

        void *handle;

        if(custom_dlopen)
                handle = custom_dlopen(fakename, arg);
        else
                handle = dlopen(fakename, RTLD_NOW);

        kill(getpid(), SIGQUIT);
        waitpid(child, NULL, 0);
        sigaction(SIGQUIT, &old_handler, NULL);

        return handle;
}
Теги:dlopenlinuxизвращения
Хабы: Ненормальное программирование
Всего голосов 45: ↑41 и ↓4+37
Просмотры4.8K

Похожие публикации

Лучшие публикации за сутки