将共享库注入进程

use*_*119 5 c linux

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

如果有人可以写一些伪代码或给我一个例子,我会很感激.谢谢.

PS:我不是在问LD_PRELOAD技巧.

Nom*_*mal 22

AndréPuel在对原始问题的评论中提到的"LD_PRELOAD技巧",真的不是绝招.它是在动态链接过程中添加功能(或更常见地,插入现有功能)的标准方法.它是ld.soLinux动态链接器提供的标准功能.

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");
}
Run Code Online (Sandbox Code Playgroud)

将其编译为libexample.so使用

gcc -Wall -O2 -fPIC -shared example.c -ldl -Wl,-soname,libexample.so -o libexample.so
Run Code Online (Sandbox Code Playgroud)

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

LD_PRELOAD=$PWD/libexample.so date
Run Code Online (Sandbox Code Playgroud)

会输出类似的东西

I am loaded and running.
Mon Jun 23 21:30:00 UTC 2014
Run Code Online (Sandbox Code Playgroud)

请注意,init()示例库中的函数会自动执行,因为它已标记__attribute__((constructor)); 该属性意味着该函数将在之前执行main().

我的示例库对你来说可能看起来很有趣 - 没有printf()等等,wrerr()搞乱了errno- 但是我有很好的理由这样写它.

首先,errno是一个线程局部变量.如果您运行一些代码,最初保存原始errno值,并在返回之前恢复该值,则中断的线程将不会看到任何更改errno.(并且因为它是线程本地的,否则没有人会看到任何变化,除非你尝试愚蠢的东西&errno.)应该在没有其他过程注意随机效果的情况下运行的代码,更好地确保它errno以这种方式保持不变!

wrerr()函数本身是一个简单的功能安全标准错误将一个字符串.它是异步信号安全的(意思是你可以在信号处理程序中使用它printf(),与其他人不同),errno除此之外保持不变,它不会以任何方式影响进程其余部分的状态.简单地说,这是一种将字符串输出到标准错误的安全方法.它也很容易让人们理解.

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


要在正在运行的进程中动态加载该库,您需要首先附加ptrace到它,然后在下次进入syscall(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;
}
Run Code Online (Sandbox Code Playgroud)

假设我们已经在运行那个(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;
}
Run Code Online (Sandbox Code Playgroud)

它需要两个参数:目标进程的pid,以及用于替换注入的可执行代码的地址.

这两个魔术常量,0x2c6f6c6c6548050fULL并且0x0a21646c726f7720ULL,仅仅对X86-64本地表示为16个字节

0F 05 "Hello, world!\n"
Run Code Online (Sandbox Code Playgroud)

没有字符串终止的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)或修改目标,以便明确允许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输出; 它直接检查目标进程的实际二进制.(事实上​​,你可以很容易地找到代码映射结束时有多少未使用的代码,并自动使用它.)

进一步的问题?

  • 非常好的答案顺便说一下. (4认同)
  • 抱歉,LD_PREALOD 方式不是我想要的。我在问一种方法来做注射的东西。在这种情况下,我必须使用 ptrace()。你有什么关于如何将库正确加载到内存空间并重新分配函数的好例子吗?谢谢。 (2认同)
  • @user1726119:我添加了一个经过测试的 x86-64 工作示例,它将 `write(STDERR, "Hello, world!\n", 14)` 注入目标进程,临时重写指定目标地址处的 16 个字节(必须在其自己的文本部分中)。 (2认同)