WEBKT

使用 eBPF 实时监控内核模块行为:原理、实践与案例分析

81 0 0 0

引言

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 的工作流程如下:

  1. 编写 eBPF 程序:使用 C 或其他高级语言编写 eBPF 程序,程序需要包含一个或多个 eBPF 函数,这些函数将在内核事件发生时被调用。
  2. 编译 eBPF 程序:使用 LLVM 编译器将 eBPF 程序编译成 eBPF 字节码。
  3. 加载 eBPF 程序:使用 bpf() 系统调用将 eBPF 字节码加载到内核中。加载时,eBPF 验证器会对程序进行安全检查。
  4. 附加 eBPF 程序:将 eBPF 程序附加到指定的内核事件,例如函数调用、系统调用等。当事件发生时,eBPF 程序将被调用。
  5. 数据收集和分析:eBPF 程序可以将数据存储在 eBPF maps 中,或者通过 ring buffer 或 perf event 等机制将数据传递到用户态。用户态程序可以对这些数据进行分析和处理。

使用 eBPF 监控内核模块

要使用 eBPF 监控内核模块的行为,我们需要编写 eBPF 程序来捕获内核模块相关的事件,例如模块加载、卸载、函数调用等。下面我们将介绍如何使用 eBPF 监控这些事件。

监控模块加载和卸载

内核模块的加载和卸载是通过 init_modulecleanup_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_modulesys_exit_init_modulesys_enter_delete_modulesys_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

现在,当你加载或卸载内核模块时,你将在内核日志中看到相应的消息。

监控模块函数调用

要监控内核模块的函数调用,我们可以使用 kprobeuprobekprobe 用于监控内核函数的调用,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 函数被调用时,你将在用户态收到相应的事件。

监控模块数据访问

要监控内核模块的数据访问,我们可以使用 kprobetracepointkprobe 可以用于监控内核函数的调用,从而间接监控数据访问。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_PARM1PT_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 的强大功能,并将其应用到实际的内核监控任务中。

Linux探索者 eBPF内核模块监控Linux安全

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/7369