简单来说,这项技术就像给正在跑步的人做心脏手术——不需要重启程序,不需要修改磁盘上的文件,就能让目标进程加载并执行你指定的共享库(.so 文件)。
想象一下这个场景:
这在安全研究、动态调试、热更新、甚至恶意软件领域都有应用。本质上,它利用了 Linux 操作系统提供的进程调试接口,把"调试能力"用到了极致。
在深入代码之前,先了解这项技术涉及的关键领域:
| 进程调试 | ptrace | |
| 内存管理 | mmapmprotect, munmap | |
| 进程间通信 | process_vm_writev | |
| 文件描述符传递 | pidfdmemfd_create, pidfd_getfd | |
| 动态链接 | dlopendlclose | |
| ELF 格式 | ||
| 调用约定 | ||
| 系统调用拦截 | PTRACE_SYSCALL |
整个注入过程可以类比为一次精密的手术操作,分为四个阶段:
首先要控制住目标进程。就像手术需要病人保持静止一样,我们不能让目标进程在注入过程中乱跑。
// 核心操作:PTRACE_SEIZE 比传统 ATTACH 更温和
ptrace(PTRACE_SEIZE, pid, NULL, PTRACE_O_TRACESYSGOOD);
ptrace(PTRACE_INTERRUPT, pid, NULL, NULL);
waitpid(pid, &status, __WALL); // 等待进程停下
这里用到了 PTRACE_SEIZE 而非传统的 PTRACE_ATTACH,区别在于:
SEIZE 不会立即停止进程,而是配合 INTERRUPT 使用PTRACE_O_TRACESYSGOOD),让系统调用停止时发送 SIGTRAP | 0x80 信号,方便识别这一步是拦截目标进程正在进行的系统调用。为什么选择系统调用拦截点?
因为系统调用是用户态和内核态的交界,进程在这里会"暂停"等待内核处理,给我们一个干净的执行环境。
ptrace(PTRACE_SYSCALL, pid, NULL, NULL); // 执行到下一个系统调用入口
waitpid(pid, &status, __WALL); // 等待停止
ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &iov); // 保存寄存器状态
关键技巧:取消当前系统调用:
orig_rax 设为 -1NT_ARM_SYSTEM_CALL 设置系统调用号为 -1这样内核会"跳过"这次系统调用,让进程回到用户态,但控制权在我们手中。
这是整个技术的核心创新点。我们需要解决三个问题:
目标进程不会自动给你内存,我们需要主动申请:
// 在目标进程中执行 mmap,申请可读写的内存
___regs_set_sys_args(®s, __NR_mmap,
0, // 让内核选地址
data_sz + exec_sz, // 申请两页:数据页 + 代码页
PROT_WRITE | PROT_READ,
MAP_ANONYMOUS | MAP_PRIVATE,
-1, 0);
申请到内存后,我们要写入两部分内容:
跳板代码的作用是让目标进程执行 dlopen:
# x86-64 版本
__inj_call:
call *%rax # 调用 rax 指向的函数(dlopen)
__inj_trap:
int3 # 执行完后触发断点,控制权回到注入器
# ARM64 版本
__inj_call:
blr x8 # 调用 x8 指向的函数
__inj_trap:
brk #0 # 断点指令
通过篡改寄存器实现:
// 设置函数参数(x86-64 调用约定:rdi, rsi, rdx, rcx, r8, r9)
regs.rdi = data_mmap_addr; // dlopen 的第一个参数:路径
regs.rsi = RTLD_LAZY; // dlopen 的第二个参数:加载标志
regs.rax = dlopen_tracee_addr; // 要调用的函数地址
regs.rip = exec_mmap_addr; // 指令指针指向我们的跳板代码
ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, &iov);
ptrace(PTRACE_CONT, pid, NULL, NULL); // 放行!
当目标进程恢复执行时,它会:
dlopen 加载共享库int3 断点指令,再次暂停手术做完了,需要恢复现场,让病人继续正常生活:
// 恢复原始寄存器状态(包括调整过的指令指针)
orig_regs.rip -= 2; // x86-64 系统调用指令是 2 字节,需要回退
ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, &iov);
ptrace(PTRACE_SYSCALL, pid, NULL, NULL); // 重新执行原始系统调用
ptrace(PTRACE_DETACH, pid, NULL, NULL); // 脱钩,让进程自由运行
传统注入需要把共享库写入磁盘,再让目标进程加载。但这样会在文件系统留下痕迹。
现代 Linux(内核 3.17+)提供了 memfd_create,可以创建内存中的匿名文件:
// 在目标进程中创建匿名文件
memfd_fd = memfd_create("shlib-inject", MFD_CLOEXEC);
// 通过 pidfd 机制,把目标进程的 fd 映射到当前进程
int local_fd = pidfd_getfd(pid_fd, remote_memfd_fd, 0);
// 直接往这个 fd 写入共享库内容
copy_file_to_fd("libinj.so", local_fd);
// 目标进程通过 /proc/self/fd/<fd> 路径加载
dlopen("/proc/self/fd/3", RTLD_LAZY);
整个过程不落磁盘,规避了文件监控。
代码中通过宏定义实现了双架构支持,关键差异:
raxrdi-r9(参数) | x8x0-x7(参数) | |
rax | x8 | |
int3 | brk #0 | |
NT_ARM_SYSTEM_CALL |
相比 ptrace 的逐字写入,process_vm_writev 可以批量传输数据,效率更高:
structioveclocal = {
.iov_base = (void *)local_src,
.iov_len = sz
};
structiovecremote = {
.iov_base = (void *)remote_dst,
.iov_len = sz
};
process_vm_writev(pid, &local, 1, &remote, 1, 0);
注意:这个系统调用尊重内存权限,只能写入可写区域。这也是为什么我们需要先 mmap 申请可写内存。
┌─────────────────┐
│ 启动注入器 │
│ (attach 到 PID) │
└────────┬────────┘
▼
┌─────────────────┐
│ PTRACE_SEIZE │◄──── 温和地"抓住"目标进程
│ PTRACE_INTERRUPT│
└────────┬────────┘
▼
┌─────────────────┐
│ 等待系统调用 │◄──── 在系统调用入口"截停"
│ (syscall-stop) │
└────────┬────────┘
▼
┌─────────────────┐
│ 保存原始寄存器 │◄──── 备份现场,方便恢复
│ GETREGSET │
└────────┬────────┘
▼
┌─────────────────┐
│ 取消当前系统调用 │◄──── 让内核跳过这次调用
│ (orig_rax = -1) │
└────────┬────────┘
▼
┌─────────────────┐ ┌─────────────────┐
│ 注入 mmap 调用 │────►│ 在目标进程中 │
│ (申请内存) │ │ 执行 mmap() │
└─────────────────┘ └────────┬────────┘
▼
┌─────────────────┐
│ 返回内存地址 │
│ (data + exec) │
└────────┬────────┘
▼
┌─────────────────┐ ┌─────────────────┐
│ 写入跳板代码 │◄────│ process_vm_ │
│ (__inj_call) │ │ writev() │
└────────┬────────┘ └─────────────────┘
▼
┌─────────────────┐
│ 注入 mprotect │◄──── 把代码页改成可执行
│ (改内存权限) │
└────────┬────────┘
▼
┌─────────────────┐ ┌─────────────────┐
│ 注入 memfd_ │────►│ 创建匿名文件 │
│ create() │ │ (内存中的"文件") │
└─────────────────┘ └────────┬────────┘
▼
┌─────────────────┐ ┌─────────────────┐
│ 通过 pidfd_ │◄────│ 把共享库内容 │
│ getfd 复制数据 │ │ 写入 memfd │
│ 到 memfd │ │ │
└────────┬────────┘ └─────────────────┘
▼
┌─────────────────┐
│ 篡改寄存器 │◄──── 设置 dlopen 参数
│ 指向跳板代码 │ (路径、标志位)
└────────┬────────┘
▼
┌─────────────────┐
│ PTRACE_CONT │◄──── 放行目标进程
│ (执行跳板) │
└────────┬────────┘
▼
┌─────────────────┐
│ 捕获 SIGTRAP │◄──── 跳板执行完毕,断点触发
│ (执行完成信号) │
└────────┬────────┘
▼
┌─────────────────┐
│ 恢复原始系统调用 │◄──── 让目标进程继续原来的工作
│ PTRACE_DETACH │ (仿佛什么都没发生)
└─────────────────┘
...
intmain(int argc, char *argv[])
{
...
if (argc != 2) {
printf("Usage: %s <PID>\n", argv[0]);
return1;
}
pid = atoi(argv[1]);
if (pid <= 0) {
printf("Invalid PID: %s\n", argv[1]);
return1;
}
printf("Target PID: %d\n", pid);
sa.sa_handler = signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction SIGINT");
return1;
}
if (sigaction(SIGTERM, &sa, NULL) == -1) {
perror("sigaction SIGTERM");
return1;
}
int pid_fd = syscall(SYS_pidfd_open, pid, 0);
if (pid_fd < 0) {
err = -errno;
fprintf(stderr, "pidfd_open(%d) failed: %d\n", pid, err);
return1;
}
long libc_self_end = 0;
long libc_self_base = find_libc_base(-1, &libc_self_end);
long libc_tracee_base = find_libc_base(pid, NULL);
if (libc_self_base == 0 || libc_tracee_base == 0)
return1;
char libc_path[512];
snprintf(libc_path, sizeof(libc_path), "/proc/self/map_files/%lx-%lx", libc_self_base, libc_self_end);
long dlopen_off = find_elf_sym_info(libc_path, "dlopen");
long dlclose_off = find_elf_sym_info(libc_path, "dlclose");
if (dlopen_off == 0 || dlclose_off == 0)
return1;
long dlopen_tracee_addr = libc_tracee_base + dlopen_off;
long dlclose_tracee_addr = libc_tracee_base + dlclose_off;
printf("Local libc base: 0x%lx (dlopen offset %lx, dlclose offset %lx)\n",
libc_self_base, dlopen_off, dlclose_off);
printf("Remote libc base: 0x%lx (dlopen @ 0x%lx, dlclose @ 0x%lx)\n",
libc_tracee_base, dlopen_tracee_addr, dlclose_tracee_addr);
structuser_regs_structorig_regs, regs;
printf("Intercepting tracee...\n");
if (ptrace_intercept(pid, &orig_regs, "tracee-intercept") < 0)
return1;
print_regs(&orig_regs, "ORIG REGS");
constlong page_size = sysconf(_SC_PAGESIZE);
constlong data_mmap_sz = page_size;
constlong exec_mmap_sz = page_size;
long data_mmap_addr = 0;
long exec_mmap_addr = 0;
printf("Executing mmap(data + exec)...\n");
/* void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); */
regs = orig_regs;
___regs_set_sys_args(®s, __NR_mmap,
0, data_mmap_sz + exec_mmap_sz,
PROT_WRITE | PROT_READ, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (ptrace_exec_syscall(pid, ®s, ®s, "mmap-data+exec") < 0)
return1;
data_mmap_addr = ___regs_result(®s);
if (data_mmap_addr <= 0) {
fprintf(stderr, "mmap() inside tracee failed: %ld, bailing!\n", data_mmap_addr);
return1;
}
exec_mmap_addr = data_mmap_addr + data_mmap_sz;
printf("mmap() returned 0x%lx (data @ %lx, exec @ %lx)\n",
data_mmap_addr, data_mmap_addr, exec_mmap_addr);
err = remote_vm_write(pid, (void *)exec_mmap_addr, __inj_call, __inj_call_sz);
if (err)
return1;
printf("Executing mprotect(r-x)...\n");
regs = orig_regs;
___regs_set_sys_args(®s, __NR_mprotect,
exec_mmap_addr, exec_mmap_sz, PROT_EXEC | PROT_READ);
long mprotect_ret;
if (ptrace_exec_syscall(pid, ®s, ®s, "mprotect-rx") < 0)
return1;
mprotect_ret = ___regs_result(®s);
if (mprotect_ret < 0) {
fprintf(stderr, "mprotect(r-x) inside tracee failed: %ld, bailing!\n", mprotect_ret);
return1;
}
printf("Executing memfd_create()...\n");
char memfd_name[] = "shlib-inject";
int memfd_remote_fd = -1;
err = remote_vm_write(pid, (void *)data_mmap_addr, memfd_name, sizeof(memfd_name));
if (err)
return1;
regs = orig_regs;
___regs_set_sys_args(®s, __NR_memfd_create, data_mmap_addr, MFD_CLOEXEC);
if (ptrace_exec_syscall(pid, ®s, ®s, "memfd_create") < 0)
return1;
memfd_remote_fd = ___regs_result(®s);
if (memfd_remote_fd < 0) {
fprintf(stderr, "memfd_create() inside tracee failed: %d, bailing!\n", memfd_remote_fd);
return1;
}
printf("memfd_create() result: %d\n", memfd_remote_fd);
int memfd_local_fd = syscall(SYS_pidfd_getfd, pid_fd, memfd_remote_fd, 0);
if (memfd_local_fd < 0) {
err = -errno;
fprintf(stderr, "pidfd_getfd(pid %d, remote_fd %d) failed: %d\n", pid, memfd_remote_fd, err);
return1;
}
err = copy_file_to_fd("libinj.so", memfd_local_fd);
if (err)
return1;
char memfd_path[64];
snprintf(memfd_path, sizeof(memfd_path), "/proc/self/fd/%d", memfd_remote_fd);
err = remote_vm_write(pid, (void *)data_mmap_addr, memfd_path, sizeof(memfd_path));
if (err)
return1;
printf("Executing dlopen() injection...\n");
long dlopen_handle;
regs = orig_regs;
___regs_set_func_args(®s, data_mmap_addr, RTLD_LAZY);
if (ptrace_exec_user_call(pid, exec_mmap_addr, dlopen_tracee_addr, ®s, &dlopen_handle, "dlopen") < 0)
return1;
printf("dlopen() result: %lx\n", dlopen_handle);
if (dlopen_handle == 0) {
fprintf(stderr, "Failed to dlopen() injection library, bailing...\n");
return1;
}
printf("Replaying original syscall and detaching tracee...\n");
if (ptrace_replay(pid, &orig_regs, "replay-syscall") < 0)
return1;
sleep(1);
printf("Re-intercepting for cleanup...\n");
if (ptrace_intercept(pid, &orig_regs, "tracee-reintercept") < 0)
return1;
print_regs(&orig_regs, "ORIG REGS (2)");
printf("Executing dlclose() injection...\n");
long dlclose_ret;
regs = orig_regs;
___regs_set_func_args(®s, dlopen_handle);
if (ptrace_exec_user_call(pid, exec_mmap_addr, dlclose_tracee_addr, ®s, &dlclose_ret, "dlclose") < 0)
return1;
printf("dlclose() result: %ld\n", dlclose_ret);
if (dlclose_ret != 0) {
fprintf(stderr, "Failed to dlclose() injection library, bailing...\n");
return1;
}
printf("Executing munmap(data + exec)...\n");
regs = orig_regs;
___regs_set_sys_args(®s, __NR_munmap, data_mmap_addr, data_mmap_sz + exec_mmap_sz);
if (ptrace_exec_syscall(pid, ®s, ®s, "munmap-data+exec") < 0)
return1;
long munmap_ret = ___regs_result(®s);
if (munmap_ret < 0) {
fprintf(stderr, "munmap() inside tracee failed: %ld, bailing!\n", munmap_ret);
return1;
}
printf("munmap() result: %ld\n", munmap_ret);
printf("Replaying original syscall and detaching tracee...\n");
if (ptrace_replay(pid, &orig_regs, "replay-syscall-final") < 0)
return1;
printf("Tracee detached and running...\n");
printf("Press Ctrl-C to exit...\n");
while (!should_exit) {
usleep(50000);
}
printf("Exited gracefully.\n");
return0;
}
代码运行测试:
打开第一个终端,执行官方指令:
./app
记住这个 PID(比如 1234),也可以用pidof app快速查:
pidof app # 输出数字就是app的PID
执行注入 打开第二个终端,执行官方注入指令:
# 直接用pidof获取app的PID
sudo ./inject `pidof app`
# 手动填PID
sudo ./inject 1234 # 替换成你的app PID
这项技术虽然是合法的调试/研究工具,但也可能被滥用。了解防御机制同样重要:
现代 Linux 发行版默认启用 Yama 安全模块,通过 /proc/sys/kernel/yama/ptrace_scope 控制:
0:允许任何进程 ptrace 任何同 UID 进程1:只允许 ptrace 子进程(默认)2:需要 CAP_SYS_PTRACE 能力(通常是 root)3:完全禁止 ptrace如果进程调用了 prctl(PR_SET_DUMPABLE, 0),或者执行了 setuid 程序,会变成不可转储状态,此时任何远程内存访问都会被拒绝。
严格沙箱可以限制进程能使用的系统调用,阻止 mmap 申请可执行内存等操作。
安全团队可以监控以下异常行为:
ptraceprocess_vm_writev 调用/proc/PID/maps 监控)memfd_create 使用这项技术展示了 Linux 操作系统进程间控制能力的极限。它巧妙地组合了多个内核机制:
这种技术的应用边界很广:合法场景下可以做应用监控、动态插桩;但也可能被滥用,因此操作系统也有对应的防护机制(比如 ptrace_scope 限制、SELinux、AppArmor 等)。理解它的原理,不仅能掌握一项实用技术,更能深入理解 Linux 进程运行的底层逻辑。