📜 ⬆️ ⬇️

Loading a dynamic library from memory in Linux

In one of my cross-platform projects, I needed the ability to verify the digital signatures of plug-ins before downloading. None of the options for creating a file is safe, since you can replace the file between the signature verification and its loading; you also cannot verify the signature after the download, because the static constructors have already been executed. Therefore it is necessary to download the plugin without creating a file.

It is impossible to intercept the open, mmap and other functions, since ld.so is linked to the library statically, the executable files loaded by its own loader are “inferior” (even with the interception of functions in libdl): they are not registered in the list of loaded libraries and / or their characters are not visible through dlsym. Consequently, there remains only the interception of system calls.

Entry point to bootloader:
void *dlopen_memory(void *base, size_t size, void *(*custom_dlopen)(const char *filename, void *arg), void *arg); 

Options:

The return value is the same as the dlopen: library handle or NULL and the error description via dlerror ().

It works as follows:
  1. A pseudo-random library name is generated. I needed exactly this, if you need readable - pass extra. filename parameter.
  2. An empty SIGQUIT signal handler is installed, the old one is saved. This signal is used later when the download is complete.
  3. A child process is created that will actually perform the download.
  4. Waiting to set the ready flag of the child process.
  5. Library is being loaded via dlopen or custom_dlopen.
  6. SIGQUIT is sent to its own process.
  7. Expecting completion of the child process.
  8. The SIGQUIT handler is restored.

The child process performs the following actions:
  1. Starts tracing its parent. This leads to some interesting consequences, for example, he becomes the parent of his parent.
  2. Sets the ready flag in the parent process.
  3. While the parent process is running and the stop has not been requested:
    1. The parent process is executed prior to entering the system call. If the system call is not the mmap of the pseudo file, then the process is also performed before the call is terminated.
    2. Retrieves child process registers.
    3. If this call is a SIGQUIT call to the parent process, then the trace is terminated.
    4. If this call is the opening of a library pseudo file, then the output descriptor is replaced with a special descriptor, which is used to identify pseudo file operations.
    5. If this call is a read from a pseudo file, then it reads from the library image into the buffer of the parent process and the call succeeds.
    6. If this call is the receipt of file attributes, then information about the pseudo-file is formed and placed in the buffer of the parent process (only the size makes sense in it).
    7. If this call is file closing, then the call succeeds.
    8. If this call is a pseudo file mapping to memory, then the following actions are performed:
      1. The MAP_ANONYMOUS flag is set (a region of memory that is not mapped to the file is requested).
      2. Call mmap.
      3. The requested part of the pseudo file is copied to the created region in the parent process.
  4. The process is disconnected from the parent and completes its work.

This code is not portable and will work only on Linux and only on processors of the IA-32 architecture in 32-bit mode. For systems of another architecture, it is necessary (by wrapping in # if / # end) to implement system call emulation, for systems with a different word length, it is also necessary to change the peekstring procedure. If the call to ptrace or waitpid fails, then the work of the parent and child processes will be terminated. If you need a different behavior - rewrite the handler for the label fail.
')
The code is available for free use without any restrictions.
 #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; } 

Source: https://habr.com/ru/post/131725/


All Articles