跨进程读取在调试器、进程监控和安全审计中非常常见。Linux内核提供的process_vm_readv系统调用允许一个进程直接读取另一个进程的地址空间,而无需使用ptrace附加。借助eBPF的kprobe挂载点与bpf_get_current_task辅助函数,可以在不修改内核的前提下捕获每一次跨进程读取操作。接下来将深入分析该系统调用的参数结构、eBPF追踪程序的编写方法以及监控数据的实际应用。

process_vm_readv的参数与内核执行流程
process_vm_readv的系统调用原型为ssize_t process_vm_readv(pid_t pid, const struct iovec *local_iov, unsigned long liovcnt, const struct iovec *remote_iov, unsigned long riovcnt, unsigned long flags)。其中pid是目标进程的进程号,local_iov和remote_iov分别指向本地和目标进程的I/O向量数组,liovcnt与riovcnt表示数组长度。flags目前没有实际用途,通常传0。
当用户态调用process_vm_readv时,内核会通过get_task_struct查找pid对应的task_struct,并获取其内存描述符mm_struct。接着内核遍历remote_iov中的每个iovec结构,将目标进程虚拟地址iov_base处的数据读取出来,复制到本地进程的local_iov对应缓冲区中。由于整个过程由内核完成,调用进程不需要与目标进程建立ptrace关系,也不需要目标进程主动配合。
从安全角度看,这种读取能力受ptrace权限模型约束。调用进程必须拥有对目标进程的ptrace访问权限,否则系统调用会返回EPERM或ESRCH。因此追踪process_vm_readv不仅能看到合法的调试行为,也能发现某些恶意软件尝试跨进程窃取敏感信息的尝试。
为什么用bpf_get_current_task捕获上下文
bpf_get_current_task是eBPF提供的一个辅助函数,它返回指向当前进程task_struct的指针。task_struct是内核描述进程的核心结构,包含进程名、PID、内存描述符、父进程关系等大量信息。然而eBPF程序运行在内核上下文,直接解引用task_struct字段会被验证器拒绝,必须使用bpf_probe_read_kernel或bpf_core_read系列函数来安全读取内核内存。
在本场景中,我们不仅需要当前进程的task_struct指针,还需要通过该指针获得发起者的PID。例如读取task->tgid可以得到线程组ID,也就是通常意义上的进程PID。虽然bpf_get_current_pid_tgid辅助函数也能直接返回PID,但使用bpf_get_current_task可以展示如何从task_struct中提取其他需要的信息,比如后续想要获取进程的可执行文件路径、uid、父进程等信息时,可以基于该指针继续读取。
在kprobe挂载点中,当前task就是发起process_vm_readv调用的进程。通过PT_REGS_PARM1或BPF_KPROBE宏可以直接获取系统调用的第一个参数pid,也就是目标进程号。x86_64架构下,系统调用参数依次放在rdi、rsi、rdx、r10、r8、r9寄存器中,BPF_KPROBE宏已经屏蔽了这些底层差异。
完整eBPF程序编写与加载
下面给出一个完整的eBPF追踪程序示例,它同时挂载了kprobe和kretprobe。kprobe部分获取发起进程的PID、目标PID,并读取remote_iov数组中第一个元素的长度;kretprobe部分打印系统调用的返回值。
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
char LICENSE[] SEC("license") = "GPL";
SEC("kprobe/sys_process_vm_readv")
int BPF_KPROBE(trace_process_vm_readv, pid_t pid, const struct iovec *local_iov,
unsigned long liovcnt, const struct iovec *remote_iov,
unsigned long riovcnt, unsigned long flags)
{
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
pid_t current_pid = 0;
struct iovec riov;
if (!task || !remote_iov)
return 0;
bpf_probe_read_kernel(¤t_pid, sizeof(current_pid), &task->tgid);
bpf_probe_read_user(&riov, sizeof(riov), remote_iov);
bpf_printk("vm_read pid=%d target=%d len=%lu",
current_pid, pid, riov.iov_len);
return 0;
}
SEC("kretprobe/sys_process_vm_readv")
int BPF_KRETPROBE(trace_process_vm_readv_ret, ssize_t ret)
{
pid_t current_pid = bpf_get_current_pid_tgid() >> 32;
bpf_printk("vm_read_ret pid=%d ret=%ld", current_pid, ret);
return 0;
}
代码中BPF_KPROBE宏来自bpf_tracing.h,它会自动展开成符合kprobe要求的函数签名。remote_iov是用户态指针,因此必须使用bpf_probe_read_user来读取iovec结构体。由于iovec数组可能非常大,这里只读取了第一个元素,避免验证器因为无界循环而拒绝加载。bpf_printk会将格式化信息输出到trace_pipe,便于调试。
编译时需要使用vmlinux.h获得完整的内核类型定义。如果使用BCC或传统libbpf,也可以手动定义struct iovec。加载成功后,通过cat /sys/kernel/debug/tracing/trace_pipe可以实时看到类似vm_read pid=1234 target=5678 len=4096的输出。返回值则通过kretprobe单独输出,形成完整的一次调用记录。
输出分析与安全监控实践
追踪输出能反映出跨进程读取的几个关键维度:谁在读、读谁、读多少、结果如何。发起进程PID用于定位工具或可疑程序;目标PID指向被读取的进程;len表示一次请求尝试读取的字节数,结合返回值可以判断读取成功与否。如果某个进程在短时间内对多个不同目标发起大量读取,或者读取对象是sshd、systemd等关键服务,就值得安全团队关注。
在安全监控场景中,可以将process_vm_readv的调用频率、读取长度和目标进程敏感性作为检测指标。例如数据库进程被其他非信任进程读取时,可能意味着凭据窃取。eBPF的优势在于这种监控几乎不产生额外开销,并且可以动态加载卸载,非常适合生产环境。
需要提醒的是,bpf_printk输出数量过大时会影响性能,生产环境建议改用BPF ring buffer或perf event array将事件传递给用户态程序进行过滤和聚合。同时kprobe挂载点依赖内核符号名,不同内核版本可能略有差异,使用前需要确认/proc/kallsyms中存在sys_process_vm_readv符号。
eBPFprocess_vm_readvbpf_get_current_task修改时间:2026-09-20 06:07:54