在Linux操作系统中,设备文件的创建是一个关键的内核行为,通常由用户空间的程序通过mknodat系统调用触发。为了深入监控和分析这一行为,我们可以利用eBPF技术,特别是bpf_get_current_task辅助函数,来获取触发该系统调用的进程上下文信息。这种方法不仅开销极低,而且能在不修改内核源码的情况下,实现对设备文件创建过程的动态追踪和深度分析。

mknodat系统调用与设备文件创建机制
mknodat是Linux系统中用于创建文件节点(包括普通文件、设备文件、命名管道等)的系统调用。与mknod相比,它允许调用者通过文件描述符指定相对路径,从而在多线程环境下提供更安全的路径解析。当创建字符设备或块设备文件时,mknodat接收的mode参数中必须包含S_IFCHR或S_IFBLK标志,同时dev参数需要包含主设备号和次设备号。
在内核态,mknodat的执行会进入VFS(虚拟文件系统)层,最终调用具体文件系统的mknod方法。这个过程涉及到权限检查、inode分配、dentry创建等步骤。对于设备文件而言,内核还会将其与对应的设备驱动模型关联起来。理解这一流程对于后续编写eBPF程序至关重要,因为我们需要在合适的挂载点捕获这些参数和上下文状态。
为了有效追踪设备文件的创建,我们通常关注sys_enter_mknodat或sys_exit_mknodat这两个tracepoint。在进入系统调用时,用户空间的参数(如路径名、模式和设备号)会被拷贝到内核空间,此时我们可以安全地读取这些数据。而在系统调用退出时,我们可以获取到执行结果,判断设备文件是否成功创建。
bpf_get_current_task辅助函数原理剖析
bpf_get_current_task是eBPF提供的一个强大辅助函数,它允许eBPF程序获取当前进程的task_struct结构体指针。task_struct是Linux内核中用于描述进程或线程的核心数据结构,包含了进程标识符(PID)、进程名称、凭证信息、调度信息等几乎所有与进程相关的状态数据。通过访问这个结构体,eBPF程序能够提取出触发mknodat调用的详细进程上下文。
需要注意的是,直接在eBPF程序中解引用task_struct指针存在安全风险,因为不同内核版本中task_struct的成员布局可能发生变化。为了解决这个问题,现代eBPF程序通常使用BPF_CORE_READ宏或者BTF(BPF Type Format)支持的CO-RE机制。这种机制使得eBPF程序能够在不依赖特定内核版本的情况下,安全地读取task_struct中的成员,如获取进程的PID、TGID以及真实用户ID等信息。
在分析设备文件创建时,结合mknodat传入的参数和bpf_get_current_task获取的上下文,我们可以构建出完整的审计日志。例如,当某个未知进程尝试创建一个指向敏感设备号的节点时,我们可以立即捕获到该进程的PID、可执行文件路径以及启动时间,从而为安全响应提供关键线索。
编写eBPF程序追踪mknodat调用
下面我们将实际编写一个eBPF程序,演示如何结合tracepoint和bpf_get_current_task来追踪mknodat调用。我们将使用BCC(BPF Compiler Collection)框架,因为它提供了便捷的Python前端和C后端接口。我们的目标是捕获尝试创建设备文件的进程信息,包括PID、进程名以及传入的设备号。
在C代码部分,我们定义一个结构体来保存事件数据。然后编写eBPF处理函数,挂载到syscalls:sys_enter_mknodat tracepoint上。在函数内部,首先通过bpf_get_current_task获取当前任务指针,然后使用BPF_CORE_READ读取进程信息,同时从tracepoint上下文中提取mknodat的参数。
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
#include <linux/fs.h>
// 定义事件数据结构
struct event_data {
u32 pid;
char comm[16];
u32 mode;
u32 dev;
};
// 挂载到sys_enter_mknodat tracepoint
int trace_mknodat(struct pt_regs *ctx) {
struct event_data data = {};
// 获取当前进程的task_struct
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
// 使用BPF_CORE_READ安全读取数据
data.pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&data.comm, sizeof(data.comm));
// 从tracepoint参数中读取mode和dev (简化示例)
// 实际应用中需根据tracepoint格式结构体提取
// 这里假设通过ctx获取
// ...
// 提交事件到用户空间
// bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &data, sizeof(data));
return 0;
}
上述代码展示了基本的框架。在实际应用中,sys_enter_mknodat的参数并不是直接通过pt_regs获取,而是通过特定的tracepoint格式结构体。我们需要查阅内核源码中trace/events/syscalls.h文件,找到sys_enter_mknodat对应的参数结构,通常包含filename指针、mode和dev。通过bpf_probe_read_user_str函数,我们可以安全地将用户空间的文件路径拷贝到eBPF映射中,与进程信息一起发送到用户态进行进一步分析和展示。
数据解析与安全审计应用
当eBPF程序将采集到的事件数据发送到用户空间后,我们需要一个用户态程序(如Python脚本)来接收和解析这些数据。通过BCC的perf_buffer接口,我们可以高效地接收内核态发来的事件。对于每一个接收到的mknodat事件,我们解析出PID、进程名、创建的文件路径以及设备号。
在安全审计场景中,这些数据具有极高的价值。正常情况下,设备文件的创建通常发生在系统初始化阶段,由udev或devtmpfs等工具完成。如果在系统正常运行期间,突然有未知的进程尝试通过mknodat创建设备文件,这极有可能是恶意软件试图建立与恶意驱动的通信通道,或者试图绕过正常权限控制访问硬件资源。
通过建立基线行为模型,我们可以设置报警规则。例如,只允许特定PID(如systemd-udevd)执行mknodat创建特定主设备号的节点。任何偏离基线的mknodat调用都将触发实时告警,甚至可以通过eBPF的返回值修改机制直接阻断该系统调用,从而在内核层实现零信任的设备文件创建防护。这种基于eBPF的监控方案,不仅性能损耗极小,而且具有极高的灵活性和准确性。
bpf_get_current_taskmknodateBPF修改时间:2026-08-26 12:09:32