В одном моем кроссплатформенном проекте мне понадобилась возможность проверять цифровые подписи плагинов перед загрузкой. Ни один из вариантов с созданием файла не является безопасным, так как можно подменить файл между проверкой подписи и его загрузкой, так же нельзя проверить подпись после загрузки, так как уже выполнились статические конструкторы. Поэтому необходимо загружать плагин, не создавая файла.
Перехватить функции 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;
}