В одном моем кроссплатформенном проекте мне понадобилась возможность проверять цифровые подписи плагинов перед загрузкой. Ни один из вариантов с созданием файла не является безопасным, так как можно подменить файл между проверкой подписи и его загрузкой, так же нельзя проверить подпись после загрузки, так как уже выполнились статические конструкторы. Поэтому необходимо загружать плагин, не создавая файла.
Перехватить функции open, mmap и прочие невозможно, так как ld.so слинкован с библиотекой си статически, исполняемые файлы, загруженные же своим загрузчиком, «неполноценны» (даже с перехватом функций в libdl): они не регистрируются в списке загруженных библиотек и/или их символы не видны через dlsym. Следовательно, остается только перехват системных вызовов.
Точка входа в загрузчик:
Параметры:
Возвращаемое значение такое же, как dlopen: хендл библиотеки или NULL и описание ошибки через dlerror().
Работает она следующим образом:
Дочерний процесс выполняет следующие действия:
Данный код не портабелен и будет работать только на Linux и только на процессорах архитектуры IA-32 в 32-битном режиме. Для систем другой архитектуры необходимо (обернув в #if/#end) реализовать эмуляцию системных вызовов, для систем с другой длиной слова также необходимо изменить процедуру peekstring. Если вызов ptrace или waitpid завершится неуспешно, то работа и родительского, и дочернего процесса будет прекращена. Если вам необходимо другое поведение — перепишите обработчик за меткой fail.
Код доступен для свободного использования без каких-либо ограничений.
Перехватить функции 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().
Работает она следующим образом:
- Генерируется псевдослучайное имя библиотеки. Мне нужно было именно такое, если вам необходимо читаемое — передайте доп. параметр filename.
- Устанавливается пустой обработчик сигнала SIGQUIT, старый сохраняется. Этот сигнал используется позже при завершении загрузки.
- Создается дочерний процесс, который собственно будет выполнять загрузку.
- Ожидается установка флага готовности дочернего процесса.
- Выполняется загрузка библиотеки через dlopen или custom_dlopen.
- Собственному процессу посылается SIGQUIT.
- Ожидается завершение дочернего процесса.
- Восстанавливается обработчик SIGQUIT.
Дочерний процесс выполняет следующие действия:
- Начинает трассировать своего родителя. Это приводит к некоторым интересным последствиям, например, он становится родителем своего родителя.
- Устанавливает флаг готовности в родительском процессе.
- Пока родительский процесс выполняется и не была запрошена остановка:
- Родительский процесс выполняется до входа в системный вызов. Если системный вызов — не mmap псевдофайла, то процесс выполняется еще и до выхода из вызова.
- Извлекаются регистры дочернего процесса.
- Если этот вызов — посылка вызова SIGQUIT родительскому процессу, то трассировка прекращается.
- Если этот вызов — открытие псевдофайла библиотеки, то выходной описатель заменяется специальным описателем, который используется для идентификации операций с псевдофайлом.
- Если этот вызов — чтение из псе��дофайла, то выполняется чтение из образа библиотеки в буфер родительского процесса и вызов успешно завершается.
- Если этот вызов — получение атрибутов файла, то формируется и помещается в буфер родительского процесса информация о псевдофайле (в ней имеет смысл только размер).
- Если этот вызов — закрытие файла, то вызов успешно завершается.
- Если этот вызов — отображение псевдофайла на память, то выполняются следующие действия:
- Ставится флаг MAP_ANONYMOUS (запрашивается регион памяти, не отображенный на файл).
- Выполняется вызов mmap.
- В созданный регион в родительском процессе копируется запрошенная часть псевдофайла.
- Процесс отсоединяется от родителя и завершает свою работу.
Данный код не портабелен и будет работать только на 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, ®s) == -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, ®s) == -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, ®s) == -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, ®s) == -1) goto fail; } continue; } else if(regs.orig_eax == -1) break; if(ptrace(PTRACE_SETREGS, parent, NULL, ®s) == -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; }
