使用 eBPF 实时监控内核模块行为:原理、实践与案例分析
引言
内核模块是 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 的强大功能,并将其应用到实际的内核监控任务中。