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 中的任何变化。 (而且因为它是线程本地的,所以没有人会看到任何变化,除非你尝试像&errno 这样愚蠢的东西。)应该在没有注意到随机效应的其余过程的情况下运行的代码,最好确保它保持@ 987654348@这种方式不变!
wrerr() 函数本身是一个简单的函数,可以安全地将字符串写入标准错误。它是异步信号安全的(这意味着您可以在信号处理程序中使用它,这与 printf() 等人不同),并且除了保持不变的 errno 之外,它不会影响其余进程的状态反正。简单地说,这是一种将字符串输出到标准错误的安全方式。它也很简单,每个人都可以理解。
其次,并非所有进程都使用标准 C I/O。例如,在 Fortran 中编译的程序不会。因此,如果您尝试使用标准 C I/O,它可能会工作,也可能不会,甚至可能会混淆目标二进制文件。使用wrerr() 函数可以避免所有这些:它只会将字符串写入标准错误,而不会混淆过程的其余部分,无论它是用什么编程语言编写的——好吧,只要该语言的运行时不动或关闭标准错误文件描述符 (STDERR_FILENO == 2)。
要在正在运行的进程中动态加载该库,您需要先将 ptrace 附加到它,然后在下次进入系统调用 (PTRACE_SYSEMU) 之前将其停止,以确保您处于可以访问的位置安全地进行 dlopen 调用。
检查/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, ®s)) {
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 发行版允许进程仅 ptrace 以同一用户身份运行的子进程。由于 target 不是 agent 的子代,您要么需要拥有超级用户权限(运行 sudo ./agent $(ps -o pid= -C target) 0x400700),要么修改 target 使其明确允许 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 输出;它直接检查目标进程的实际二进制文件。 (事实上,你可以很容易地找到代码映射末尾有多少未使用的代码,并自动使用它。)
还有其他问题?