eBPF实战:用户级文件访问审计与报告生成
1. eBPF 简介
2. 审计目标
3. eBPF 程序设计
3.1 登录监控程序
3.2 文件访问监控程序
4. 用户空间程序
5. 针对特定用户或用户组进行审计
6. 生成审计报告
7. 总结与展望
在Linux系统中,对用户的文件访问行为进行审计对于安全监控和合规性检查至关重要。传统的审计方法通常依赖于Auditd等工具,但这些工具可能会引入较大的性能开销。eBPF(扩展伯克利包过滤器)提供了一种更高效、更灵活的方式来实现用户级的文件访问审计。本文将介绍如何使用eBPF程序来监控特定用户或用户组的文件访问行为,并生成审计报告。
1. eBPF 简介
eBPF 是一种内核技术,允许用户在内核中安全地运行自定义代码,而无需修改内核源代码或加载内核模块。eBPF 程序可以附加到各种内核事件,例如系统调用、函数入口/出口、网络事件等。这使得 eBPF 成为性能监控、安全分析、网络调试等领域的强大工具。
eBPF 的主要优势包括:
- 安全性:eBPF 程序在加载到内核之前会经过验证器的严格检查,确保程序的安全性,防止程序崩溃或恶意操作。
- 高性能:eBPF 程序通常以 JIT(即时编译)方式编译成本地机器码,执行效率很高。
- 灵活性:eBPF 程序可以根据需要自定义,满足各种不同的监控和分析需求。
2. 审计目标
我们的目标是使用 eBPF 程序来监控以下信息:
- 用户登录时间:记录用户登录系统的时间。
- 访问的文件名:记录用户访问的文件名。
- 访问类型:记录用户对文件的访问类型(读、写、执行)。
- 用户ID/组ID: 记录发起访问的用户ID和组ID
我们还需要能够针对特定的用户或用户组进行审计,并生成易于阅读的审计报告。
3. eBPF 程序设计
为了实现我们的审计目标,我们需要编写以下 eBPF 程序:
- 登录监控程序:附加到
security_socket_unix_stream_connect
钩子,记录用户登录时间。 - 文件访问监控程序:附加到
vfs_read
、vfs_write
、vfs_execute
等 VFS(虚拟文件系统)函数,记录文件访问信息。
3.1 登录监控程序
以下是一个简单的登录监控程序的示例:
#include <linux/kconfig.h> #include <linux/ptrace.h> #include <linux/version.h> #if (LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0)) #include <uapi/linux/bpf.h> #else #include <linux/bpf.h> #endif #include "bpf_helpers.h" struct data_t { u32 pid; u32 uid; char comm[64]; u64 ts; }; BPF_PERF_OUTPUT(events); int kprobe__security_socket_unix_stream_connect(struct pt_regs *ctx, struct socket *sock, struct sockaddr_un *address) { struct data_t data = {}; data.pid = bpf_get_current_pid_tgid(); data.uid = bpf_get_current_uid_gid(); data.ts = bpf_ktime_get_ns(); bpf_get_current_comm(&data.comm, sizeof(data.comm)); events.perf_submit(ctx, &data, sizeof(data)); return 0; } LICENSE("GPL");
这个程序使用 kprobe
附加到 security_socket_unix_stream_connect
函数。当用户通过 SSH 或其他方式登录时,该函数会被调用。程序会记录进程ID(PID)、用户ID(UID)、进程名(comm)和时间戳(ts),并将这些信息发送到用户空间的 perf ring buffer。
3.2 文件访问监控程序
以下是一个简单的文件访问监控程序的示例:
#include <linux/kconfig.h> #include <linux/ptrace.h> #include <linux/version.h> #include <linux/fs.h> #if (LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0)) #include <uapi/linux/bpf.h> #else #include <linux/bpf.h> #endif #include "bpf_helpers.h" struct data_t { u32 pid; u32 uid; u32 gid; char comm[64]; char filename[256]; int type; u64 ts; }; BPF_PERF_OUTPUT(events); static int probe_file_operation(struct pt_regs *ctx, struct file *file, int type) { struct data_t data = {}; data.pid = bpf_get_current_pid_tgid(); data.uid = bpf_get_current_uid_gid(); data.gid = ((struct task_struct *)bpf_get_current_task())->cred->gid; data.ts = bpf_ktime_get_ns(); data.type = type; bpf_get_current_comm(&data.comm, sizeof(data.comm)); // Get filename struct dentry *dentry = file->f_path.dentry; struct qstr d_name = dentry->d_name; bpf_probe_read_str(data.filename, sizeof(data.filename), d_name.name); events.perf_submit(ctx, &data, sizeof(data)); return 0; } int kprobe__vfs_read(struct pt_regs *ctx, struct file *file, char __user *buf, size_t count, loff_t *pos) { return probe_file_operation(ctx, file, 1); // 1 for read } int kprobe__vfs_write(struct pt_regs *ctx, struct file *file, const char __user *buf, size_t count, loff_t *pos) { return probe_file_operation(ctx, file, 2); // 2 for write } int kprobe__vfs_open(struct pt_regs *ctx, struct file *file) { return probe_file_operation(ctx, file, 3); // 3 for open } LICENSE("GPL");
这个程序使用 kprobe
附加到 vfs_read
、vfs_write
和 vfs_open
函数。当用户读取、写入或打开文件时,相应的函数会被调用。程序会记录进程ID(PID)、用户ID(UID)、进程名(comm)、文件名、访问类型(type)和时间戳(ts),并将这些信息发送到用户空间的 perf ring buffer。
注意:
- 为了获取文件名,我们使用了
bpf_probe_read_str
函数从内核空间读取字符串。这个函数需要小心使用,以避免安全问题。 - 访问类型使用整数表示,例如 1 表示读,2 表示写,3 表示打开。
- 代码使用了 task_struct 结构体来获取gid,在不同的内核版本中,获取gid的方式可能不同,需要根据实际情况修改。
4. 用户空间程序
用户空间程序负责加载 eBPF 程序、从 perf ring buffer 读取数据,并将数据生成审计报告。
以下是一个简单的用户空间程序的示例(使用 libbpf):
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <signal.h> #include <time.h> #include <bpf/libbpf.h> #include <bpf/bpf.h> // 假设 eBPF 程序编译后的文件名为 audit.o #define BPF_OBJECT "audit.o" static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) { return vfprintf(stderr, format, args); } static volatile bool exiting = false; static void sig_handler(int sig) { exiting = true; } int main(int argc, char **argv) { struct bpf_object *obj = NULL; int err = 0; // 设置 libbpf 打印函数 libbpf_set_print(libbpf_print_fn); // 加载 eBPF 对象 obj = bpf_object__open_file(BPF_OBJECT, NULL); if (!obj) { fprintf(stderr, "Failed to open BPF object: %s\n", strerror(errno)); return 1; } err = bpf_object__load(obj); if (err) { fprintf(stderr, "Failed to load BPF object: %s\n", strerror(errno)); bpf_object__close(obj); return 1; } // 获取 perf event map 的 fd int events_fd = bpf_object__find_map_fd_by_name(obj, "events"); if (events_fd < 0) { fprintf(stderr, "Failed to find events map: %s\n", strerror(errno)); bpf_object__close(obj); return 1; } // 设置信号处理函数 signal(SIGINT, sig_handler); signal(SIGTERM, sig_handler); // 循环读取 perf event struct perf_buffer *pb = perf_buffer__new(events_fd, 8, NULL, NULL, NULL); if (!pb) { fprintf(stderr, "Failed to create perf buffer: %s\n", strerror(errno)); bpf_object__close(obj); return 1; } struct data_t { u32 pid; u32 uid; u32 gid; char comm[64]; char filename[256]; int type; u64 ts; } data; while (!exiting) { err = perf_buffer__poll(pb, 100); // 100ms 超时 if (err < 0 && err != -EAGAIN) { fprintf(stderr, "Error polling perf buffer: %s\n", strerror(errno)); break; } // 从 perf buffer 读取数据 while (true) { struct perf_event_header *hdr = perf_buffer__read(pb, &data, sizeof(data)); if (!hdr) break; // No more events // 处理数据 time_t t = data.ts / 1000000000; // Convert ns to seconds struct tm *tm_info = localtime(&t); char time_str[30]; strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info); const char *type_str; switch (data.type) { case 1: type_str = "READ"; break; case 2: type_str = "WRITE"; break; case 3: type_str = "OPEN"; break; default: type_str = "UNKNOWN"; break; } printf("%s PID: %d UID: %d GID: %d COMM: %s TYPE: %s FILE: %s\n", time_str, data.pid, data.uid, data.gid, data.comm, type_str, data.filename); perf_buffer__consume(pb, hdr->size); } } perf_buffer__free(pb); bpf_object__close(obj); return 0; }
这个程序使用 libbpf 库来加载 eBPF 程序、从 perf ring buffer 读取数据,并将数据打印到控制台。你可以根据需要修改这个程序,将数据写入文件或数据库,并生成更复杂的审计报告。
编译用户空间程序:
首先确保安装了libbpf, 并且安装了开发包, 例如在ubuntu下: apt install libbpf-dev
然后使用gcc编译: gcc -Wall -g user_space.c -o user_space -lbpf
注意:
- 需要根据实际情况修改
BPF_OBJECT
宏,指定 eBPF 程序编译后的文件路径。 - 需要在编译时链接 libbpf 库。
- 为了使程序能够读取 perf ring buffer,需要以 root 权限运行。
5. 针对特定用户或用户组进行审计
为了针对特定用户或用户组进行审计,我们需要修改 eBPF 程序,添加过滤条件。例如,以下代码只记录 UID 为 1000 的用户的文件访问行为:
int kprobe__vfs_read(struct pt_regs *ctx, struct file *file, char __user *buf, size_t count, loff_t *pos) { u32 uid = bpf_get_current_uid_gid(); if (uid != 1000) { return 0; // Skip if not user 1000 } return probe_file_operation(ctx, file, 1); // 1 for read }
类似地,我们可以使用 bpf_get_current_gid
函数获取当前进程的 GID,并添加 GID 过滤条件。
6. 生成审计报告
用户空间程序可以对收集到的数据进行分析,并生成各种类型的审计报告。例如,可以生成以下报告:
- 用户登录历史:记录用户的登录时间、登录IP地址等信息。
- 文件访问统计:统计用户访问的文件数量、访问类型分布等信息。
- 异常访问报告:检测异常的文件访问行为,例如访问敏感文件、频繁访问同一文件等。
报告可以使用各种格式生成,例如文本、CSV、JSON等。可以使用各种工具来分析和可视化报告,例如 Elasticsearch、Kibana、Grafana等。
7. 总结与展望
eBPF 提供了一种高效、灵活的方式来实现用户级的文件访问审计。通过编写 eBPF 程序,我们可以监控各种系统事件,并收集我们需要的信息。通过分析收集到的数据,我们可以生成各种类型的审计报告,并提升系统安全性。
随着 eBPF 技术的不断发展,我们可以期待 eBPF 在安全领域的应用会越来越广泛。例如,可以使用 eBPF 来实现入侵检测、恶意软件分析、漏洞挖掘等功能。
希望本文能够帮助你了解如何使用 eBPF 进行文件访问行为审计。