使用 eBPF 实时监控内核模块行为:原理、实践与案例分析
引言
eBPF 简介
eBPF 的优势
eBPF 的工作原理
使用 eBPF 监控内核模块
监控模块加载和卸载
监控模块函数调用
监控模块数据访问
案例分析
案例 1:检测恶意模块加载
案例 2:监控内核模块的 rootkit 行为
总结
引言
内核模块是 Linux 内核的重要组成部分,它们允许在不重新编译内核的情况下动态地添加或删除功能。然而,内核模块也可能成为安全漏洞的来源,恶意模块可能被用来隐藏恶意行为或破坏系统安全。因此,实时监控内核模块的行为对于维护系统安全至关重要。
传统的内核监控方法,如使用 kprobes 或 tracepoints,需要编写内核模块或使用内核调试工具,这可能会引入额外的安全风险或影响系统性能。eBPF(extended Berkeley Packet Filter)提供了一种更安全、更高效的内核监控方法。eBPF 允许用户在内核中安全地运行用户态代码,而无需修改内核源代码或编写内核模块。
本文将深入探讨如何使用 eBPF 实时监控内核模块的行为。我们将介绍 eBPF 的基本原理、eBPF 程序的编写和部署,以及如何使用 eBPF 监控内核模块的加载、卸载、函数调用等行为。此外,我们还将提供一些实际的案例分析,展示 eBPF 在内核模块监控中的应用。
eBPF 简介
eBPF 最初是为网络数据包过滤而设计的,后来被扩展到支持更广泛的内核跟踪和监控任务。eBPF 程序运行在内核的虚拟机中,可以访问内核数据结构和函数,但受到严格的安全限制,以防止恶意代码破坏系统。
eBPF 的优势
- 安全性:eBPF 程序在内核中运行,但受到 eBPF 验证器的严格检查,确保程序不会崩溃或破坏系统。eBPF 验证器会检查程序的控制流、内存访问和函数调用,以确保程序是安全的。
- 高性能:eBPF 程序可以使用 JIT(Just-In-Time)编译器编译成机器码,从而获得接近原生代码的性能。此外,eBPF 程序可以使用 ring buffer 或 perf event 等机制高效地将数据传递到用户态。
- 灵活性:eBPF 程序可以使用 C 或其他高级语言编写,然后编译成 eBPF 字节码。eBPF 程序可以附加到各种内核事件,如函数调用、系统调用、网络事件等,从而实现灵活的内核监控。
eBPF 的工作原理
eBPF 的工作流程如下:
- 编写 eBPF 程序:使用 C 或其他高级语言编写 eBPF 程序,程序需要包含一个或多个 eBPF 函数,这些函数将在内核事件发生时被调用。
- 编译 eBPF 程序:使用 LLVM 编译器将 eBPF 程序编译成 eBPF 字节码。
- 加载 eBPF 程序:使用
bpf()
系统调用将 eBPF 字节码加载到内核中。加载时,eBPF 验证器会对程序进行安全检查。 - 附加 eBPF 程序:将 eBPF 程序附加到指定的内核事件,例如函数调用、系统调用等。当事件发生时,eBPF 程序将被调用。
- 数据收集和分析:eBPF 程序可以将数据存储在 eBPF maps 中,或者通过 ring buffer 或 perf event 等机制将数据传递到用户态。用户态程序可以对这些数据进行分析和处理。
使用 eBPF 监控内核模块
要使用 eBPF 监控内核模块的行为,我们需要编写 eBPF 程序来捕获内核模块相关的事件,例如模块加载、卸载、函数调用等。下面我们将介绍如何使用 eBPF 监控这些事件。
监控模块加载和卸载
内核模块的加载和卸载是通过 init_module
和 cleanup_module
系统调用完成的。我们可以使用 eBPF 监控这两个系统调用来捕获模块加载和卸载事件。以下是一个示例 eBPF 程序,用于监控模块加载和卸载:
#include <linux/kconfig.h> #include <linux/module.h> #include <linux/vermagic.h> #include <uapi/linux/bpf.h> #include <linux/ptrace.h> #include <linux/version.h> #include <linux/types.h> #include "bpf_helpers.h" SEC("tracepoint/syscalls/sys_enter_init_module") int sys_enter_init_module(void *ctx) { bpf_printk("Module loading\n"); return 0; } SEC("tracepoint/syscalls/sys_exit_init_module") int sys_exit_init_module(void *ctx) { bpf_printk("Module loaded\n"); return 0; } SEC("tracepoint/syscalls/sys_enter_delete_module") int sys_enter_delete_module(void *ctx) { bpf_printk("Module unloading\n"); return 0; } SEC("tracepoint/syscalls/sys_exit_delete_module") int sys_exit_delete_module(void *ctx) { bpf_printk("Module unloaded\n"); return 0; } char LICENSE[] SEC("license") = "GPL";
这个程序使用了 tracepoint
来捕获 sys_enter_init_module
、sys_exit_init_module
、sys_enter_delete_module
和 sys_exit_delete_module
事件。当这些事件发生时,eBPF 程序将分别打印 "Module loading"、"Module loaded"、"Module unloading" 和 "Module unloaded" 到内核日志中。
要编译和运行这个程序,你需要安装 LLVM 和 libbpf。然后,你可以使用以下命令编译程序:
clang -O2 -target bpf -c module_monitor.c -o module_monitor.o
接下来,你可以使用 bpftool
或其他 eBPF 工具加载和附加程序。例如,你可以使用以下命令加载程序:
bpftool prog load module_monitor.o /sys/fs/bpf/module_monitor
然后,你可以使用以下命令附加程序到 tracepoint:
bpftool prog attach trace module_monitor /sys/fs/bpf/module_monitor
现在,当你加载或卸载内核模块时,你将在内核日志中看到相应的消息。
监控模块函数调用
要监控内核模块的函数调用,我们可以使用 kprobe
或 uprobe
。kprobe
用于监控内核函数的调用,uprobe
用于监控用户态函数的调用。由于内核模块运行在内核空间,我们可以使用 kprobe
来监控模块的函数调用。
以下是一个示例 eBPF 程序,用于监控指定内核模块的函数调用:
#include <linux/kconfig.h> #include <linux/module.h> #include <linux/vermagic.h> #include <uapi/linux/bpf.h> #include <linux/ptrace.h> #include <linux/version.h> #include <linux/types.h> #include "bpf_helpers.h" struct data_t { u32 pid; u64 ts; char func[64]; char comm[64]; }; BPF_PERF_OUTPUT(events); SEC("kprobe/my_module_function") int kprobe_my_module_function(struct pt_regs *ctx) { struct data_t data = {}; data.pid = bpf_get_current_pid_tgid(); data.ts = bpf_ktime_get_ns(); bpf_get_current_comm(&data.comm, sizeof(data.comm)); strcpy(data.func, "my_module_function"); events.perf_submit(ctx, &data, sizeof(data)); return 0; } char LICENSE[] SEC("license") = "GPL";
这个程序使用了 kprobe
来监控名为 my_module_function
的内核函数的调用。当这个函数被调用时,eBPF 程序将收集进程 ID、时间戳、函数名和进程名等信息,并将这些信息通过 perf event 发送到用户态。
要编译和运行这个程序,你需要将 my_module_function
替换为你想要监控的内核函数的名称。然后,你可以使用以下命令编译程序:
clang -O2 -target bpf -c module_function_monitor.c -o module_function_monitor.o
接下来,你需要找到 my_module_function
函数的地址。你可以使用 nm
命令来查找函数的地址:
nm /path/to/my_module.ko | grep my_module_function
然后,你可以使用 bpftool
或其他 eBPF 工具加载和附加程序。例如,你可以使用以下命令加载程序:
bpftool prog load module_function_monitor.o /sys/fs/bpf/module_function_monitor
然后,你可以使用以下命令附加程序到 kprobe:
bpftool prog attach kprobe module_function_monitor:my_module_function /sys/fs/bpf/module_function_monitor
其中,my_module_function
是函数的地址。现在,当 my_module_function
函数被调用时,你将在用户态收到相应的事件。
监控模块数据访问
要监控内核模块的数据访问,我们可以使用 kprobe
或 tracepoint
。kprobe
可以用于监控内核函数的调用,从而间接监控数据访问。tracepoint
可以用于监控特定的数据访问事件,例如读写内存等。
以下是一个示例 eBPF 程序,用于监控指定内核模块的数据访问:
#include <linux/kconfig.h> #include <linux/module.h> #include <linux/vermagic.h> #include <uapi/linux/bpf.h> #include <linux/ptrace.h> #include <linux/version.h> #include <linux/types.h> #include "bpf_helpers.h" struct data_t { u32 pid; u64 ts; u64 addr; u64 val; char comm[64]; }; BPF_PERF_OUTPUT(events); SEC("kprobe/my_module_data_access") int kprobe_my_module_data_access(struct pt_regs *ctx) { struct data_t data = {}; data.pid = bpf_get_current_pid_tgid(); data.ts = bpf_ktime_get_ns(); bpf_get_current_comm(&data.comm, sizeof(data.comm)); data.addr = PT_REGS_PARM1(ctx); data.val = PT_REGS_PARM2(ctx); events.perf_submit(ctx, &data, sizeof(data)); return 0; } char LICENSE[] SEC("license") = "GPL";
这个程序使用了 kprobe
来监控名为 my_module_data_access
的内核函数的调用。当这个函数被调用时,eBPF 程序将收集进程 ID、时间戳、数据地址和数据值等信息,并将这些信息通过 perf event 发送到用户态。
要编译和运行这个程序,你需要将 my_module_data_access
替换为你想要监控的内核函数的名称。然后,你需要根据函数的参数列表修改 PT_REGS_PARM1
和 PT_REGS_PARM2
等宏,以获取正确的数据地址和数据值。最后,你可以使用 bpftool
或其他 eBPF 工具加载和附加程序,类似于监控模块函数调用的方法。
案例分析
案例 1:检测恶意模块加载
假设我们怀疑系统中存在恶意模块,该模块可能会隐藏恶意行为或破坏系统安全。我们可以使用 eBPF 监控模块加载事件,并检查模块的签名和权限,以检测恶意模块。
首先,我们可以编写一个 eBPF 程序,用于监控模块加载事件:
#include <linux/kconfig.h> #include <linux/module.h> #include <linux/vermagic.h> #include <uapi/linux/bpf.h> #include <linux/ptrace.h> #include <linux/version.h> #include <linux/types.h> #include "bpf_helpers.h" struct data_t { u32 pid; u64 ts; char module_name[64]; char comm[64]; }; BPF_PERF_OUTPUT(events); SEC("tracepoint/syscalls/sys_enter_init_module") int sys_enter_init_module(struct pt_regs *ctx) { struct data_t data = {}; data.pid = bpf_get_current_pid_tgid(); data.ts = bpf_ktime_get_ns(); bpf_get_current_comm(&data.comm, sizeof(data.comm)); // 获取模块名称 const char *module_path = (const char *)PT_REGS_PARM1(ctx); bpf_probe_read_user_str(data.module_name, sizeof(data.module_name), module_path); events.perf_submit(ctx, &data, sizeof(data)); return 0; } char LICENSE[] SEC("license") = "GPL";
这个程序使用了 tracepoint
来捕获 sys_enter_init_module
事件。当模块加载时,eBPF 程序将收集进程 ID、时间戳、模块名称和进程名等信息,并将这些信息通过 perf event 发送到用户态。
然后,我们可以编写一个用户态程序,用于接收 eBPF 程序发送的事件,并检查模块的签名和权限:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <linux/perf_event.h> #include <linux/hw_breakpoint.h> #include <errno.h> #include "bpf_insn.h" struct data_t { u32 pid; u64 ts; char module_name[64]; char comm[64]; }; int main() { // 打开 perf event int perf_fd = open("/sys/kernel/debug/tracing/events/syscalls/sys_enter_init_module/id", O_RDONLY); if (perf_fd < 0) { perror("open"); return 1; } char buf[64]; ssize_t len = read(perf_fd, buf, sizeof(buf) - 1); if (len <= 0) { perror("read"); close(perf_fd); return 1; } buf[len] = '\0'; close(perf_fd); long event_id = strtol(buf, NULL, 10); struct perf_event_attr attr = { .type = PERF_TYPE_TRACEPOINT, .size = sizeof(struct perf_event_attr), .config = event_id, .sample_type = PERF_SAMPLE_RAW, .sample_period = 1, .wakeup_events = 1, }; perf_fd = syscall(__NR_perf_event_open, &attr, -1, 0, -1, 0); if (perf_fd < 0) { perror("perf_event_open"); return 1; } // 映射 perf event buffer size_t page_size = sysconf(_SC_PAGE_SIZE); size_t mmap_size = page_size * (1 + 128); // 128 pages for data void *mmap_ptr = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, perf_fd, 0); if (mmap_ptr == MAP_FAILED) { perror("mmap"); close(perf_fd); return 1; } // 循环读取 perf event while (1) { struct perf_event_mmap_page *header = (struct perf_event_mmap_page *)mmap_ptr; char *data_ptr = (char *)mmap_ptr + page_size; // Check if there is new data if (header->data_head == header->data_tail) { usleep(1000); // Sleep for 1ms continue; } // Read the data struct data_t data; memcpy(&data, data_ptr + header->data_tail % mmap_size, sizeof(data)); // Update the tail pointer header->data_tail += sizeof(data); // 打印模块信息 printf("Module loaded: pid=%d, ts=%llu, module_name=%s, comm=%s\n", data.pid, data.ts, data.module_name, data.comm); // 检查模块签名和权限 // 这里可以调用系统命令或使用 libmodule 库来检查模块签名和权限 // 例如: // char cmd[256]; // snprintf(cmd, sizeof(cmd), "modinfo %s", data.module_name); // system(cmd); } // 取消映射和关闭 perf event munmap(mmap_ptr, mmap_size); close(perf_fd); return 0; }
这个程序使用了 perf_event_open
系统调用来打开 perf event,并使用 mmap
系统调用将 perf event buffer 映射到用户态。然后,程序循环读取 perf event,并打印模块信息。在打印模块信息之后,程序可以调用系统命令或使用 libmodule 库来检查模块签名和权限,以检测恶意模块。
案例 2:监控内核模块的 rootkit 行为
Rootkit 是一种恶意软件,它可以隐藏恶意行为或获取系统特权。内核模块 rootkit 运行在内核空间,可以修改内核数据结构或劫持系统调用,从而实现隐藏恶意行为的目的。我们可以使用 eBPF 监控内核模块的函数调用和数据访问,以检测 rootkit 行为。
例如,我们可以编写一个 eBPF 程序,用于监控内核模块是否修改了系统调用表:
#include <linux/kconfig.h> #include <linux/module.h> #include <linux/vermagic.h> #include <uapi/linux/bpf.h> #include <linux/ptrace.h> #include <linux/version.h> #include <linux/types.h> #include "bpf_helpers.h" struct data_t { u32 pid; u64 ts; u64 addr; u64 val; char comm[64]; }; BPF_PERF_OUTPUT(events); SEC("kprobe/sys_call_table") int kprobe_sys_call_table(struct pt_regs *ctx) { struct data_t data = {}; data.pid = bpf_get_current_pid_tgid(); data.ts = bpf_ktime_get_ns(); bpf_get_current_comm(&data.comm, sizeof(data.comm)); data.addr = PT_REGS_PARM1(ctx); data.val = PT_REGS_PARM2(ctx); events.perf_submit(ctx, &data, sizeof(data)); return 0; } char LICENSE[] SEC("license") = "GPL";
这个程序使用了 kprobe
来监控 sys_call_table
变量的访问。当内核模块修改了系统调用表时,eBPF 程序将收集进程 ID、时间戳、数据地址和数据值等信息,并将这些信息通过 perf event 发送到用户态。
然后,我们可以编写一个用户态程序,用于接收 eBPF 程序发送的事件,并检查数据地址是否在系统调用表的范围内,以检测 rootkit 行为。
总结
eBPF 提供了一种安全、高效和灵活的内核监控方法。我们可以使用 eBPF 实时监控内核模块的行为,例如模块加载、卸载、函数调用和数据访问。通过分析这些事件,我们可以检测恶意模块和 rootkit 行为,从而维护系统安全。
本文介绍了 eBPF 的基本原理、eBPF 程序的编写和部署,以及如何使用 eBPF 监控内核模块的行为。此外,我们还提供了一些实际的案例分析,展示 eBPF 在内核模块监控中的应用。
希望本文能够帮助读者了解 eBPF 的强大功能,并将其应用到实际的内核监控任务中。