【问题标题】:Inject shared library into a process将共享库注入进程
【发布时间】:2014-06-22 20:14:18
【问题描述】:

我刚开始学习 Linux 中的注入技术,想编写一个简单的程序将共享库注入到正在运行的进程中。 (该库将简单地打印一个字符串。)但是,经过几个小时的研究,我找不到任何完整的示例。好吧,我确实发现我可能需要使用 ptrace() 来暂停进程并注入内容,但不确定如何将库加载到目标进程的内存空间和 C 代码中的重定位内容。有谁知道共享库注入的任何好的资源或工作示例? (当然,我知道可能有一些现有的库,例如 hotpatch,我可以使用它来简化注入,但这不是我想要的)

如果有人能写一些伪代码或给我一个例子,我将不胜感激。谢谢。

PS:我不是在问 LD_PRELOAD 技巧。

【问题讨论】:

  • 你知道“LD_PRELOAD”技巧吗?也许它足以满足您的需求?
  • 如果您能够通过某种蹦床或溢出执行任意代码...那么您可以 dlopen/dlload 库函数...但您的漏洞利用取决于您所在的位置...否则您可以查看叠加图像...但这完全取决于您的环境。
  • 我仍然不确定为什么你不能只使用 dlopen() 和 dlsym()。
  • 我的错误。没关系。 dlopen() 正在工作。我只是想知道如何将它加载到进程的内存空间并重新分配库的函数,以便它们可以在恢复进程后执行。

标签: c linux


【解决方案1】:

André Puel 在对原始问题的评论中提到的“LD_PRELOAD 技巧”真的不是技巧。它是在动态链接过程中添加功能的标准方法——或者更常见的是插入现有功能。它是由 Linux 动态链接器 ld.so 提供的标准功能。

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

如果您随后使用LD_PREALOD 环境变量中列出的libexample.so 的完整路径运行任何(动态链接的)二进制文件,该二进制文件将输出“我已加载并运行” 到标准输出在其正常输出之前。例如,

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 中的任何变化。 (而且因为它是线程本地的,所以没有人会看到任何变化,除非你尝试像&amp;errno 这样愚蠢的东西。)应该在没有注意到随机效应的其余过程的情况下运行的代码,最好确保它保持@ 987654348@这种方式不变!

wrerr() 函数本身是一个简单的函数,可以安全地将字符串写入标准错误。它是异步信号安全的(这意味着您可以在信号处理程序中使用它,这与 printf() 等人不同),并且除了保持不变的 errno 之外,它不会影响其余进程的状态反正。简单地说,这是一种将字符串输出到标准错误的安全方式。它也很简单,每个人都可以理解。

其次,并非所有进程都使用标准 C I/O。例如,在 Fortran 中编译的程序不会。因此,如果您尝试使用标准 C I/O,它可能会工作,也可能不会,甚至可能会混淆目标二进制文件。使用wrerr() 函数可以避免所有这些:它只会将字符串写入标准错误,而不会混淆过程的其余部分,无论它是用什么编程语言编写的——好吧,只要该语言的运行时不动或关闭标准错误文件描述符 (STDERR_FILENO == 2)。


要在正在运行的进程中动态加载该库,您需要先将 ptrace 附加到它,然后在下次进入系统调用 (PTRACE_SYSEMU) 之前将其停止,以确保您处于可以访问的位置安全地进行 dlopen 调用。

检查/proc/PID/maps 以验证您在进程自己的代码中,而不是在共享库代码中。您可以通过PTRACE_SYSCALLPTRACE_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,以及用来替换注入的可执行代码的地址。

0x2c6f6c6c6548050fULL0x0a21646c726f7720ULL 这两个魔术常量只是 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 发行版允许进程仅 ptrace 以同一用户身份运行的子进程。由于 target 不是 agent 的子代,您要么需要拥有超级用户权限(运行 sudo ./agent $(ps -o pid= -C target) 0x400700),要么修改 target 使其明确允许 ptracing(例如,通过在程序开头附近添加 prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY);)。详情请参阅man ptraceman 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() 系统调用是一个非常小的变化。

(在Linux的x86-64上,系统调用号在rax中,参数在rdi、rsi、rdx、r10、r8和r9中,分别从左到右读取;返回值也在rax中。 )

解析/proc/PID/maps 非常有用——参见man 5 proc 下的/proc/PID/maps。它提供有关目标进程地址空间的所有相关信息。要找出是否有有用的未使用代码区域,请解析objdump -wh /proc/$(ps -o pid= -C target)/exe 输出;它直接检查目标进程的实际二进制文件。 (事实上​​,你可以很容易地找到代码映射末尾有多少未使用的代码,并自动使用它。)

还有其他问题?

【讨论】:

  • 对不起,LD_PREALOD 方式不是我想要的。我在问一种方法来做注射的东西。在这种情况下,我必须使用 ptrace()。您是否有任何关于如何将库正确加载到内存空间并重新分配函数的好例子?谢谢。
  • 到目前为止,我已经使用 PTRACE_ATTACH 附加了一个进程,然后使用 dlopen 加载库并存储其返回的引用,但我不确定如何映射到目标进程的内存空间以及如何使用该寄存器值来自 PTRACE_GETREGS。谢谢。
  • @user1726119:您确实意识到您需要在 ptraced 进程本身中执行 dlopen?在进行 ptrace 调用的进程中调用 dlopen() 对 ptraced 进程没有影响;两者不共享地址空间。您将不得不修改目标中的一些现有代码,以执行您注入的 dlopen 调用——或者至少足以让 mmap(0,page,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) 系统调用获得一个新的可执行页面来放入 dlopen 调用代码。但是您确实需要在所有情况下覆盖现有代码,AFAIK。
  • @user1726119:我为 x86-64 添加了一个经过测试的工作示例,它将write(STDERR, "Hello, world!\n", 14) 注入目标进程,临时重写指定目标地址的 16 个字节(必须在它自己的文本部分)。
  • 顺便说一句非常好的答案。
猜你喜欢
  • 2020-08-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-04-02
  • 2011-06-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多