将共享库注入进程

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

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

PS:我不是在问LD_PRELOAD技巧。

“LD_PRELOAD技巧”AndréPuel在对原始问题的评论中提到,真的没有把戏。 这是在动态链接过程中添加功能的标准方法,或者更常见的是插入现有功能。 这是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环境变量中列出了LD_PREALOD的完整路径,则二进制文件将在其正常输出之前输出“I已加载并正在运行”标准输出。 例如,

 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 wrerr() ,但是我写这个的原因有很多。

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

wrerr()函数本身是一个简单的函数,可以安全地将字符串写入标准错误。 它是异步信号安全的(意味着你可以在信号处理程序中使用它,与printf()等不同),而不是保持不变的errno ,它不会以任何方式影响过程的其余部分的状态。 简而言之,将字符串输出到标准错误是一种安全的方法。 对于任何人来说,这也很简单。

其次,并不是所有的流程都使用标准的CI / O。 例如,在Fortran中编译的程序不会。 所以,如果你尝试使用标准的CI / 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指令完成内核系统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显示objdump -d ./target之后没有代码。 因此,地址0x400700到0x401000是为可执行代码保留的,但不包含任何代码。 地址0x400700 – 在我的机器上; 可能会有很大不同你的! – 因此在运行时将代码注入目标是非常好的地址。

运行./agent $(ps -o pid= -C target) 0x400700将必要的系统调用代码和字符串注入到目标二进制文件0x400700,执行注入的代码,并用原始代码替换注入的代码。 本质上,它完成了所需的任务:为目标输出“你好,世界!” 到标准错误。

请注意,Ubuntu和其他一些Linux发行版现在允许一个进程只跟踪他们作为同一用户运行的子进程。 由于目标不是代理程序的子代,所以您需要拥有超级用户权限(运行sudo ./agent $(ps -o pid= -C target) 0x400700 ),或修改目标,以便明确允许ptracing(例如,通过添加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()系统调用代替它是一个非常小的改变。

(在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输出; 它直接检查目标进程的实际二进制。 (实际上,你可以很容易地找到在代码映射结束时有多少未使用的代码,并自动使用它。)

进一步的问题?