且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

注射共享库到过程

更新时间:2022-11-05 21:47:11

在LD_ preLOAD绝招安德烈·普埃尔在原来的问题的评论中提到,没有绝招,真的。它是添加功能的标准方法 - 或者更常见的是,中介的现有功能 - 在一个动态链接过程。它是由 ld.so $ C $提供的标准功能C> ,Linux的动态连接器。

The "LD_PRELOAD trick" André Puel mentioned in a comment to the original question, is no trick, really. It is the standard method of adding functionality -- or more commonly, interposing existing functionality -- in a dynamically-linked process. It is standard functionality provided by ld.so, the Linux dynamic linker.

Linux的动态链接器是由环境变量(和配置文件)进行控制; LD_ preLOAD 仅仅是一个环境变量,提出了应针对每个过程链接动态库的列表。 (你也可以将库添加到 /etc/ld.so.$p$pload ,在这种情况下,它会自动加载每个二进制,无论 LD_ preLOAD 环境变量。)

The Linux dynamic linker is controlled by environment variables (and configuration files); LD_PRELOAD is simply an environment variable that provides a list of dynamic libraries that should be linked against each process. (You could also add the library to /etc/ld.so.preload, in which case it is automatically loaded for every binary, regardless of the LD_PRELOAD environment variable.)

下面是一个例子, 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 $上市C $ C>环境变量,二进制将输出的我加载并运行正常输出之前的标准输出。例如,

If you then run any (dynamically linked) binary with the full path to libexample.so listed in LD_PREALOD environment variable, the binary will output "I am loaded and running" to standard output before its normal output. For example,

LD_PRELOAD=$PWD/libexample.so date

将输出类似

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

请注意,在示例库中的的init()功能自动执行,因为它被标记__attribute__((constructor))$c$c>;该属性表示该功能将之前执行的main()

Note that the init() function in the example library is automatically executed, because it is marked __attribute__((constructor)); that attribute means the function will be executed prior to main().

我的例子库似乎好笑给你 - 没有的printf()等等, WRERR()搞乱与错误号 - ,但有很好的理由,我写像这样

My example library may seem funny to you -- no printf() et cetera, wrerr() messing with errno --, but there are very good reasons I wrote it like this.

首先,错误号是一个线程局部变量。如果你运行一些code,最初保存原始错误号价值,只是在返回前恢复该值时,被中断的线程不会看到错误号。 (由于它是线程本地的,没有人会看到任何改变或者,除非你尝试像&放一些愚蠢的;错误号)code,它应该运行没有注意到过程随机效应的休息,***确保它保持这种方式错误号不变!

First, errno is a thread-local variable. If you run some code, initially saving the original errno value, and restoring that value just before returning, the interrupted thread will not see any change in errno. (And because it is thread-local, nobody else will see any change either, unless you try something silly like &errno.) Code that is supposed to run without the rest of the process noticing random effects, better make sure it keeps errno unchanged in this manner!

WRERR()函数本身是一个简单的函数,安全标准错误将一个字符串。这是异步信号安全的(这意味着你可以在信号处理程序使用它,不像的printf()等),比错误号被保持不变,它不以任何方式影响所述过程的其余部分的状态。简单地说,它是一种安全的方式来输出字符串到标准错误。它也非常简单,你感到沉沦理解。

The wrerr() function itself is a simple function that writes a string safely to standard error. It is async-signal-safe (meaning you can use it in signal handlers, unlike printf() et al.), and other than errno which is kept unchanged, it does not affect the state of the rest of the process in any way. Simply put, it is a safe way to output strings to standard error. It is also simple enough for everbody to understand.

其次,并非所有的进程都使用标准C I / O。例如,在Fortran语言编译的程序没有。所以,如果您尝试使用标准C I / O,它可能工作,它可能没有,或者它甚至可能混淆挫折感目标二进制。使用 WRERR()函数避免一切:它只是写入字符串到标准错误,而不混淆过程的其余部分,不管是什么编程语言,它是写在 - 好吧,只要这种语言的运行时不移动或关闭标准错误文件描述符( STDERR_FILENO == 2

Second, not all processes use standard C I/O. For example, programs compiled in Fortran do not. So, if you try to use standard C I/O, it might work, it might not, or it might even confuse the heck out of the target binary. Using the wrerr() function avoids all that: it will just write the string to standard error, without confusing the rest of the process, no matter what programming language it was written in -- well, as long as that language's runtime does not move or close the standard error file descriptor (STDERR_FILENO == 2).

要动态加载该库在运行过程中,你需要先附着 ptrace的来,然后下入到一个系统调用( PTRACE_SYSEMU ),以确保你的地方,你可以放心地做的dlopen调用。

To load that library dynamically in a running process, you'll need to first attach ptrace to it, then stop it before next entry to a syscall (PTRACE_SYSEMU), to make sure you're somewhere you can safely do the dlopen call.

检查 / proc /进程/图来验证你的过程中自己的code之内,而不是在共享库code。你可以做 PTRACE_SYSCALL PTRACE_SYSEMU 继续下一个候选停止点。还有,记得等待()为孩子真正停止附着于后,并且您连接到的所有线程。

Check /proc/PID/maps to verify you are within the process' own code, not in shared library code. You can do PTRACE_SYSCALL or PTRACE_SYSEMU to continue to next candidate stopping point. Also, remember to wait() for the child to actually stop after attaching to it, and that you attach to all threads.

在停止状态下,使用 PTRACE_GETREGS 来得到寄存器的状态,而 PTRACE_PEEKTEXT 复制足够code,所以你可以用 PTRACE_POKETEXT 更换它来调用的dlopen一个独立的位置序列(/路径/要/ libexample.so,RTLD_NOW) RTLD_NOW 是在 / usr / include目录/.../ dlfcn.h中你的架构中定义一个整型常量code>,通常2,由于路径名是常量字符串,可以保存(暂时)在code;函数调用需要一个指向它,毕竟。

While stopped, use PTRACE_GETREGS to get the register state, and PTRACE_PEEKTEXT to copy enough code, so you can replace it with PTRACE_POKETEXT to a position-independent sequence that calls dlopen("/path/to/libexample.so", RTLD_NOW), RTLD_NOW being an integer constant defined for your architecture in /usr/include/.../dlfcn.h, typically 2. Since the pathname is constant string, you can save it (temporarily) over the code; the function call takes a pointer to it, after all.

你有没有用于重写现有的一些code端与系统调用的与位置无关的顺序,这样就可以运行使用插入 PTRACE_SYSCALL (以一个循环,直到它在该插入系统调用结束),而无需单步它。然后,可以使用 PTRACE_POKETEXT 来恢复的code到原来的状态,最后 PTRACE_SETREGS 来恢复程序的状态什么它的初始状态了。

Have that position-independent sequence you used to rewrite some of the existing code end with a syscall, so that you can run the inserted using PTRACE_SYSCALL (in a loop, until it ends up at that inserted syscall) without having to single-step it. Then you use PTRACE_POKETEXT to revert the code to its original state, and finally PTRACE_SETREGS to revert the program state to what its initial state was.

考虑这个重要的程序,编译成说目标

Consider this trivial program, compiled as say target:

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

比方说,我们已经在运行(PID $(ps的-o PID = -C目标)),我们希望注入code,打印的你好,世界!的标准错误。

Let's say we're already running that (pid $(ps -o pid= -C target)), and we wish to inject code that prints "Hello, world!" to standard error.

在x86-64的,内核系统调用使用系统调用指令(二进制 0F 05 完成的;它是一个两个字节的指令)。因此,要执行你想代表目标进程的系统调用任何你需要更换两个字节。 (在X86-64 PTRACE_POKETEXT实质上转移了64位字,$ P $ 64位边界上对齐pferably)

On x86-64, kernel syscalls are done using the syscall instruction (0F 05 in binary; it's a two-byte instruction). So, to execute any syscall you want on behalf of a target process, you need to replace two bytes. (On x86-64 PTRACE_POKETEXT actually transfers a 64-bit word, preferably aligned on a 64-bit boundary.)

考虑下面的程序,编译说

Consider the following program, compiled to say 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,地址使用与注入的可执行文件code来代替

It takes two parameters: the pid of the target process, and the address to use to replace with the injected executable code.

这两个魔术常量, 0x2c6f6c6c6548050fULL 0x0a21646c726f7720ULL ,只是本地重新$ P $ x86的上psentation 64为16字节

The two magic constants, 0x2c6f6c6c6548050fULL and 0x0a21646c726f7720ULL, are simply the native representation on x86-64 for the 16 bytes

0F 05 "Hello, world!\n"

有没有字符串终止NUL字节。请注意,字符串为14个字符长,和原来的地址后开始两个字节。

with no string-terminating NUL byte. Note that the string is 14 characters long, and starts two bytes after the original address.

在我的机器上,运行执行cat / proc / $(PS -o PID = -C目标)/图 - 它显示了目标的完整地址映射 - - 显示目标的code位于的0x400000 .. 0x401000。 objdump的-d ./target 显示有0x4006ef左右后无code。因此,地址0x400700到0x401000保留用于可执行code,但不包含任何。地址0x400700 - 我的机器上;可以在你的很好的不同! - 所以是一个很好的地址注入code到目标在运行时

On my machine, running cat /proc/$(ps -o pid= -C target)/maps -- which shows the complete address mapping for the target -- shows that target's code is located at 0x400000 .. 0x401000. objdump -d ./target shows that there is no code after 0x4006ef or so. Therefore, addresses 0x400700 to 0x401000 are reserved for executable code, but do not contain any. The address 0x400700 -- on my machine; may very well differ on yours! -- is therefore a very good address for injecting code into target while it is running.

运行 ./剂$(ps的-o PID = -C目标)0x400700 注入了必要的系统调用code和字符串目标二进制在0x400700,则执行注入code,并取代注入code。与原来的code。本质上,它完成所需的任务:你好,世界为目标,以输出的 的标准误差

Running ./agent $(ps -o pid= -C target) 0x400700 injects the necessary syscall code and string to the target binary at 0x400700, executes the injected code, and replaces the injected code with original code. Essentially, it accomplishes the desired task: for target to output "Hello, world!" to standard error.

需要注意的是Ubuntu和其他一些Linux发行时下允许进程ptrace的只有运行相同的用户自己的子进程。由于目标不是剂的孩子,你要么需要有超级用户权限(运行须藤./agent $(ps的-o PID = -C目标)0x400700 ),或修改目标,以便它明确地允许ptracing(例如,通过添加 使用prctl (PR_SET_PTRACER,PR_SET_PTRACER_ANY); 程序的开始附近)。请参见人ptrace的并的man使用prctl 了解详情。

Note that Ubuntu and some other Linux distributions nowadays allow a process to ptrace only their child processes running as the same user. Since target is not a child of agent, you either need to have superuser privileges (run sudo ./agent $(ps -o pid= -C target) 0x400700), or modify target so that it explicitly allows the ptracing (for example, by adding prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY); near the start of the program). See man ptrace and man prctl for details.

就像我已经解释了上面,更长或更复杂的code,使用ptrace函数来使目标首先执行 MMAP(NULL,page_aligned_length,PROT_READ | PROT_EXEC,MAP_PRIVATE | MAP_ANONYMOUS,-1 ,0),其中分配新的code可执行内存。因此,在X86-64,你只需要找到一个64位的字你可以安全地替换,然后你就可以PTRACE_POKETEXT新的code为目标来执行。虽然我的示例使用的write()系统调用,这是一个非常小的变化有它使用mmap()或mmap2()系统调用来代替。

Like I explained already above, for longer or more complicated code, use ptrace to cause the target to first execute mmap(NULL, page_aligned_length, PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0), which allocates executable memory for new code. So, on x86-64, you only need to locate one 64-bit word you can replace safely, and then you can PTRACE_POKETEXT the new code for the target to execute. While my example uses the write() syscall, it is a really small change to have it use mmap() or mmap2() syscall instead.

(在Linux中x86-64的,系统调用号码是在RAX,和在偏下,RSI,RDX,R10,R8和R9参数,从读出分别从左到右;和返回值也RAX。 )

(On x86-64 in Linux, the syscall number is in rax, and parameters in rdi, rsi, rdx, r10, r8, and r9, reading from left to right, respectively; and return value is also in rax.)

解析 / proc /进程/图是非常有用的 - 见下的man 5 PROC 。它提供了对目标进程地址空间中的所有相关信息。要了解是否有未使用的有用code区,解析 objdump的-WH的/ proc / $(PS -o PID = -C目标)/ EXE 输出;它直接检查目标进程的实际二进制代码。 (事实上​​,你可以很容易地找到多少闲置code存在于code映射的结束,使用自动)。

Parsing /proc/PID/maps is very useful -- see /proc/PID/maps under man 5 proc. It provides all the pertinent information on the target process address space. To find out whether there are useful unused code areas, parse objdump -wh /proc/$(ps -o pid= -C target)/exe output; it examines the actual binary of the target process directly. (In fact, you could easily find how much unused code there is at the end of the code mapping, and use that automatically.)

还有问题吗?