Внедрить разделяемую библиотеку в процесс

Я только начал изучать методы внедрения в Linux и хочу написать простую программу для внедрения общей библиотеки в работающий процесс. (библиотека просто напечатает строку.) Однако, после нескольких часов исследований, я не смог найти ни одного полного примера. Что ж, я понял, что мне, вероятно, нужно использовать ptrace(), чтобы приостановить процесс и внедрить содержимое, но не уверен, как загрузить библиотеку в область памяти целевого процесса и переместить содержимое в C-код. Кто-нибудь знает какие-нибудь хорошие ресурсы или рабочие примеры для внедрения в общую библиотеку? (Конечно, я знаю, что могут быть некоторые существующие библиотеки, такие как hotpatch, которые я могу использовать, чтобы сделать инъекцию намного проще, но это не то, что мне нужно)

И если кто-то может написать какой-нибудь псевдокод или привести пример, я буду признателен за это. Благодарю.

PS: я не спрашиваю о трюке LD_PRELOAD.

1 ответ

"Трюк LD_PRELOAD" Андре Пуель, упомянутый в комментарии к первоначальному вопросу, на самом деле не трюк. Это стандартный метод добавления функциональности - или, чаще, вставки существующей функциональности - в динамически связанный процесс. Это стандартная функциональность, предоставляемая ld.so динамический компоновщик Linux.

Динамический компоновщик Linux управляется переменными среды (и файлами конфигурации); LD_PRELOAD это просто переменная среды, которая предоставляет список динамических библиотек, которые должны быть связаны с каждым процессом. (Вы также можете добавить библиотеку в /etc/ld.so.preload, в этом случае он автоматически загружается для каждого двоичного файла, независимо от LD_PRELOAD переменная окружения.)

Вот пример, example.c:

#include <unistd.h>
#include <errno.h>

static void init(void) __attribute__((constructor));

static void wrerr(const char *p)
{
    const char *q;
    int        saved_errno;

    if (!p)
        return;

    q = p;
    while (*q)
        q++;

    if (q == p)
        return;

    saved_errno = errno;

    while (p < q) {
        ssize_t n = write(STDERR_FILENO, p, (size_t)(q - p));
        if (n > 0)
            p += n;
        else
        if (n != (ssize_t)-1 || errno != EINTR)
            break;
    }

    errno = saved_errno;
}

static void init(void)
{
    wrerr("I am loaded and running.\n");
}

Скомпилируйте это в libexample.so с помощью

gcc -Wall -O2 -fPIC -shared example.c -ldl -Wl,-soname,libexample.so -o libexample.so

Если вы затем запустите любой (динамически связанный) двоичный файл с полным путем к libexample.so перечислены в LD_PREALOD Переменная окружения, двоичный файл будет выводить "Я загружен и работаю" на стандартный вывод до его нормального вывода. Например,

LD_PRELOAD=$PWD/libexample.so date

будет выводить что-то вроде

I am loaded and running.
Mon Jun 23 21:30:00 UTC 2014

Обратите внимание, что init() Функция в библиотеке примеров выполняется автоматически, потому что она помечена __attribute__((constructor)); этот атрибут означает, что функция будет выполнена до main(),

Моя библиотека примеров может показаться вам смешной - нет printf() и так далее, wrerr() возиться с errno -, но есть очень веские причины, по которым я написал это так.

Первый, errno является локальной переменной потока Если вы запускаете некоторый код, изначально сохраняя оригинал errno значение и восстановление этого значения непосредственно перед возвратом, прерванный поток не увидит никаких изменений в errno, (И поскольку он является локальным для потока, никто больше не увидит никаких изменений, если вы не попробуете что-то глупое, как &errno.) Код, который должен выполняться без остальной части процесса, замечающего случайные эффекты, лучше убедиться, что он сохраняет errno без изменений таким образом!

wrerr() сама функция - это простая функция, которая безопасно записывает строку в стандартную ошибку. Это асинхронный сигнал-безопасный (то есть вы можете использовать его в обработчиках сигналов, в отличие от printf() и др.), и кроме errno который остается неизменным, это никак не влияет на состояние остальной части процесса. Проще говоря, это безопасный способ вывода строк со стандартной ошибкой. Это также достаточно просто для понимания.

Во-вторых, не все процессы используют стандартный C I/O. Например, программы, скомпилированные в Fortran, этого не делают. Таким образом, если вы попытаетесь использовать стандартный C I/O, он может сработать, может не сработать или даже может сбить с толку целевой двоичный файл. С использованием wrerr() Функция избегает всего этого: она просто записывает строку в стандартную ошибку, не путая остальную часть процесса, независимо от того, на каком языке программирования она была написана - ну, пока среда выполнения этого языка не перемещает и не закрывает стандартную ошибку дескриптор файла (STDERR_FILENO == 2).


Чтобы динамически загрузить эту библиотеку в работающем процессе, вам нужно сначала присоединить ptrace затем остановите его перед следующим входом в системный вызов (PTRACE_SYSEMU), чтобы убедиться, что вы где-то, вы можете безопасно сделать дулен.

Проверьте /proc/PID/maps чтобы убедиться, что вы находитесь в собственном коде процесса, а не в коде общей библиотеки. Ты можешь сделать PTRACE_SYSCALL или же PTRACE_SYSEMU перейти к следующему пункту остановки кандидата. Кроме того, не забудьте wait() чтобы ребенок фактически остановился после прикрепления к нему, и что вы прикрепляете ко всем потокам.

Пока остановлен, используйте PTRACE_GETREGS чтобы получить состояние регистра, и PTRACE_PEEKTEXT скопировать достаточно кода, чтобы вы могли заменить его PTRACE_POKETEXT к позиционно-независимой последовательности, которая вызывает dlopen("/path/to/libexample.so", RTLD_NOW), RTLD_NOW быть целочисленной константой, определенной для вашей архитектуры в /usr/include/.../dlfcn.h, как правило, 2. Поскольку путь является константной строкой, вы можете (временно) сохранить его в коде; В конце концов, вызов функции принимает указатель на него.

Получите эту независимую от позиции последовательность, которую вы использовали для перезаписи части существующего кода, заканчивая системным вызовом, чтобы вы могли запустить вставленный с помощью PTRACE_SYSCALL (в цикле, пока он не закончится на этом вставленном системном вызове) без единого шага. Тогда вы используете PTRACE_POKETEXT вернуть код в исходное состояние и, наконец, PTRACE_SETREGS вернуть состояние программы в исходное состояние.


Рассмотрим эту тривиальную программу, составленную как сказать target:

#include <stdio.h>
int main(void)
{
    int c;
    while (EOF != (c = getc(stdin)))
        putc(c, stdout);
    return 0;
}

Допустим, мы уже запускаем это (pid $(ps -o pid= -C target)), и мы хотим добавить код, который печатает "Hello, world!" к стандартной ошибке.

На x86-64 системные вызовы ядра выполняются с использованием syscall инструкция (0F 05 в двоичном формате; это двухбайтовая инструкция). Таким образом, чтобы выполнить любой системный вызов от имени целевого процесса, вам нужно заменить два байта. (На x86-64 PTRACE_POKETEXT фактически передает 64-битное слово, предпочтительно выровненное по 64-битной границе.)

Рассмотрим следующую программу, составленную так: agent:

#define  _GNU_SOURCE
#include <sys/ptrace.h>
#include <sys/user.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/syscall.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
    struct user_regs_struct oldregs, regs;
    unsigned long  pid, addr, save[2];
    siginfo_t      info;
    char           dummy;

    if (argc != 3 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s PID ADDRESS\n", argv[0]);
        fprintf(stderr, "\n");
        return 1;
    }

    if (sscanf(argv[1], " %lu %c", &pid, &dummy) != 1 || pid < 1UL) {
        fprintf(stderr, "%s: Invalid process ID.\n", argv[1]);
        return 1;
    }

    if (sscanf(argv[2], " %lx %c", &addr, &dummy) != 1) {
        fprintf(stderr, "%s: Invalid address.\n", argv[2]);
        return 1;
    }
    if (addr & 7) {
        fprintf(stderr, "%s: Address is not a multiple of 8.\n", argv[2]);
        return 1;
    }

    /* Attach to the target process. */
    if (ptrace(PTRACE_ATTACH, (pid_t)pid, NULL, NULL)) {
        fprintf(stderr, "Cannot attach to process %lu: %s.\n", pid, strerror(errno));
        return 1;
    }

    /* Wait for attaching to complete. */
    waitid(P_PID, (pid_t)pid, &info, WSTOPPED);

    /* Get target process (main thread) register state. */
    if (ptrace(PTRACE_GETREGS, (pid_t)pid, NULL, &oldregs)) {
        fprintf(stderr, "Cannot get register state from process %lu: %s.\n", pid, strerror(errno));
        ptrace(PTRACE_DETACH, (pid_t)pid, NULL, NULL);
        return 1;
    }

    /* Save the 16 bytes at the specified address in the target process. */
    save[0] = ptrace(PTRACE_PEEKTEXT, (pid_t)pid, (void *)(addr + 0UL), NULL);
    save[1] = ptrace(PTRACE_PEEKTEXT, (pid_t)pid, (void *)(addr + 8UL), NULL);

    /* Replace the 16 bytes with 'syscall' (0F 05), followed by the message string. */
    if (ptrace(PTRACE_POKETEXT, (pid_t)pid, (void *)(addr + 0UL), (void *)0x2c6f6c6c6548050fULL) ||
        ptrace(PTRACE_POKETEXT, (pid_t)pid, (void *)(addr + 8UL), (void *)0x0a21646c726f7720ULL)) {
        fprintf(stderr, "Cannot modify process %lu code: %s.\n", pid, strerror(errno));
        ptrace(PTRACE_DETACH, (pid_t)pid, NULL, NULL);
        return 1;
    }

    /* Modify process registers, to execute the just inserted code. */
    regs = oldregs;
    regs.rip = addr;
    regs.rax = SYS_write;
    regs.rdi = STDERR_FILENO;
    regs.rsi = addr + 2UL;
    regs.rdx = 14; /* 14 bytes of message, no '\0' at end needed. */
    if (ptrace(PTRACE_SETREGS, (pid_t)pid, NULL, &regs)) {
        fprintf(stderr, "Cannot set register state from process %lu: %s.\n", pid, strerror(errno));
        ptrace(PTRACE_DETACH, (pid_t)pid, NULL, NULL);
        return 1;
    }

    /* Do the syscall. */
    if (ptrace(PTRACE_SINGLESTEP, (pid_t)pid, NULL, NULL)) {
        fprintf(stderr, "Cannot execute injected code to process %lu: %s.\n", pid, strerror(errno));
        ptrace(PTRACE_DETACH, (pid_t)pid, NULL, NULL);
        return 1;
    }

    /* Wait for the client to execute the syscall, and stop. */
    waitid(P_PID, (pid_t)pid, &info, WSTOPPED);

    /* Revert the 16 bytes we modified. */
    if (ptrace(PTRACE_POKETEXT, (pid_t)pid, (void *)(addr + 0UL), (void *)save[0]) ||
        ptrace(PTRACE_POKETEXT, (pid_t)pid, (void *)(addr + 8UL), (void *)save[1])) {
        fprintf(stderr, "Cannot revert process %lu code modifications: %s.\n", pid, strerror(errno));
        ptrace(PTRACE_DETACH, (pid_t)pid, NULL, NULL);
        return 1;
    }

    /* Revert the registers, too, to the old state. */
    if (ptrace(PTRACE_SETREGS, (pid_t)pid, NULL, &oldregs)) {
        fprintf(stderr, "Cannot reset register state from process %lu: %s.\n", pid, strerror(errno));
        ptrace(PTRACE_DETACH, (pid_t)pid, NULL, NULL);
        return 1;
    }

    /* Detach. */
    if (ptrace(PTRACE_DETACH, (pid_t)pid, NULL, NULL)) {
        fprintf(stderr, "Cannot detach from process %lu: %s.\n", pid, strerror(errno));
        return 1;
    }

    fprintf(stderr, "Done.\n");
    return 0;
}

Он принимает два параметра: pid целевого процесса и адрес, который нужно использовать для замены введенным исполняемым кодом.

Две магические константы, 0x2c6f6c6c6548050fULL а также 0x0a21646c726f7720ULL просто родное представление на x86-64 для 16 байтов

0F 05 "Hello, world!\n"

без завершающего строки байта NUL. Обратите внимание, что длина строки составляет 14 символов и начинается через два байта после исходного адреса.

На моей машине работает cat /proc/$(ps -o pid= -C target)/maps - который показывает полное сопоставление адресов для цели - показывает, что код цели находится в 0x400000 .. 0x401000. objdump -d ./target показывает, что нет кода после 0x4006ef или около того. Поэтому адреса от 0x400700 до 0x401000 зарезервированы для исполняемого кода, но не содержат его. Адрес 0x400700 - на моей машине; вполне может отличаться от вашего! - поэтому очень хороший адрес для внедрения кода в цель во время ее работы.

Бег ./agent $(ps -o pid= -C target) 0x400700 внедряет необходимый код и строку системного вызова в целевой двоичный файл в 0x400700, выполняет внедренный код и заменяет введенный код оригинальным кодом. По сути, он выполняет желаемую задачу: для цели вывести "Hello, world!" к стандартной ошибке.

Обратите внимание, что в настоящее время Ubuntu и некоторые другие дистрибутивы Linux позволяют процессу отслеживать только свои дочерние процессы, работающие под тем же пользователем. Поскольку target не является дочерним элементом агента, вам также необходимо иметь привилегии суперпользователя (запустите sudo ./agent $(ps -o pid= -C target) 0x400700), или измените цель так, чтобы она явно разрешала трассировку (например, добавляя prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY); ближе к началу программы). Смотрите man ptrace и man prctl для подробностей.

Как я уже объяснил выше, для более длинного или более сложного кода используйте ptrace, чтобы сначала выполнить цель mmap(NULL, page_aligned_length, PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0), который выделяет исполняемую память для нового кода. Таким образом, на x86-64 вам нужно найти только одно 64-битное слово, которое вы можете безопасно заменить, а затем вы можете PTRACE_POKETEXT новый код для выполнения цели. Хотя в моем примере используется системный вызов write(), это небольшое изменение, если использовать вместо него системный вызов mmap() или mmap2().

(На x86-64 в Linux номер системного вызова указан в rax, а параметры в rdi, rsi, rdx, r10, r8 и r9 читаются слева направо соответственно; возвращаемое значение также в rax.)

анализ /proc/PID/maps очень полезно - смотрите /proc/PID/maps под man 5 proc. Он предоставляет всю необходимую информацию о адресном пространстве целевого процесса. Чтобы выяснить, есть ли полезные неиспользуемые области кода, проанализируйте objdump -wh /proc/$(ps -o pid= -C target)/exe выход; он непосредственно проверяет фактический двоичный файл целевого процесса. (На самом деле, вы можете легко найти, сколько неиспользуемого кода имеется в конце кода, и использовать его автоматически.)

Дальнейшие вопросы?

Другие вопросы по тегам