WEBKT

利用 eBPF 实现特定进程的系统调用监控:实践指南

22 0 0 0

1. eBPF 简介

2. 监控系统调用的 eBPF 程序结构

3. 过滤无关的系统调用

4. 解析系统调用的参数和返回值

5. 用户空间程序

6. 总结

在 Linux 系统中,系统调用是用户空间程序与内核交互的唯一途径。监控特定进程的系统调用对于理解其行为、调试问题以及进行安全分析至关重要。eBPF(扩展的伯克利包过滤器)作为一种强大的内核技术,允许我们在内核中安全地运行自定义代码,而无需修改内核源代码或加载内核模块。本文将深入探讨如何使用 eBPF 实现对特定进程的系统调用监控,包括过滤无关系统调用以及解析系统调用的参数和返回值。

1. eBPF 简介

eBPF 最初设计用于网络数据包过滤,但现在已扩展到许多其他领域,包括性能分析、安全监控和跟踪。eBPF 程序在内核上下文中运行,但受到严格的验证和安全限制,以防止崩溃或恶意行为。eBPF 程序通常由用户空间程序加载和控制,并通过共享内存(例如 BPF 映射)与用户空间通信。

2. 监控系统调用的 eBPF 程序结构

要监控系统调用,我们需要编写一个 eBPF 程序,该程序将在每次系统调用发生时被触发。这通常通过将 eBPF 程序附加到 tracepointkprobe 来实现。

  • tracepoint: tracepoint 是内核中预定义的钩子,允许我们安全地跟踪特定事件。syscalls:sys_enter_*syscalls:sys_exit_* tracepoint 分别在系统调用进入和退出时触发。
  • kprobe: kprobe 允许我们在内核函数的任意位置插入钩子。虽然更灵活,但也更危险,因为内核函数的签名和实现可能会在内核版本之间发生变化。不小心使用 kprobe 可能会导致系统不稳定。

对于系统调用监控,建议使用 tracepoint,因为它更稳定且更安全。以下是一个基本的 eBPF 程序结构,用于监控系统调用:

#include <linux/bpf.h>
#include <bpf_helpers.h>
#include <linux/sched.h>
// 定义 BPF 映射,用于存储数据
BPF_PERF_OUTPUT(events);
// 系统调用进入时的处理函数
int syscall__enter(struct trace_event_raw_sys_enter* ctx) {
// 获取进程 ID
pid_t pid = bpf_get_current_pid_tgid() >> 32;
// 获取系统调用号
long syscall_nr = ctx->id;
// 在这里添加你的过滤和处理逻辑
return 0;
}
// 系统调用退出时的处理函数
int syscall__exit(struct trace_event_raw_sys_exit* ctx) {
// 获取进程 ID
pid_t pid = bpf_get_current_pid_tgid() >> 32;
// 获取系统调用号
long syscall_nr = ctx->id;
// 获取返回值
long ret = ctx->ret;
// 在这里添加你的过滤和处理逻辑
return 0;
}
char LICENSE[] SEC("license") = "GPL";

3. 过滤无关的系统调用

监控所有系统调用会产生大量数据,因此我们需要过滤掉与我们不相关的系统调用。以下是一些常见的过滤方法:

  • 基于进程 ID (PID) 过滤: 只监控特定进程的系统调用。
  • 基于系统调用号过滤: 只监控特定类型的系统调用(例如 open, read, write)。

以下是如何在 eBPF 程序中实现这些过滤器的示例:

#include <linux/bpf.h>
#include <bpf_helpers.h>
#include <linux/sched.h>
// 定义要监控的 PID
#define TARGET_PID 1234
// 定义要监控的系统调用号 (例如 open 系统调用号是 2)
#define TARGET_SYSCALL_NR 2
BPF_PERF_OUTPUT(events);
int syscall__enter(struct trace_event_raw_sys_enter* ctx) {
pid_t pid = bpf_get_current_pid_tgid() >> 32;
long syscall_nr = ctx->id;
// 基于 PID 过滤
if (pid != TARGET_PID) {
return 0;
}
// 基于系统调用号过滤
if (syscall_nr != TARGET_SYSCALL_NR) {
return 0;
}
// 在这里添加你的处理逻辑
struct event_data {
pid_t pid;
long syscall_nr;
} data = {pid, syscall_nr};
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
char LICENSE[] SEC("license") = "GPL";

在这个例子中,我们定义了 TARGET_PIDTARGET_SYSCALL_NR 宏,用于指定要监控的进程 ID 和系统调用号。eBPF 程序首先检查当前进程的 PID 和系统调用号是否与这些值匹配。如果不匹配,程序将立即返回,从而过滤掉无关的系统调用。

4. 解析系统调用的参数和返回值

仅仅知道系统调用被调用是不够的,我们通常需要知道系统调用的参数和返回值,以便更好地理解其行为。访问系统调用的参数和返回值取决于我们使用 tracepoint 还是 kprobe

  • 使用 tracepoint: tracepoint 提供了一个结构化的方式来访问系统调用的参数和返回值。例如,syscalls:sys_enter_open tracepoint 提供了一个 filename 字段,用于访问 open 系统调用的文件名参数。
  • 使用 kprobe: 使用 kprobe 需要我们了解内核函数的参数布局。这需要查看内核源代码,并可能因内核版本而异。

以下是如何使用 tracepoint 解析 open 系统调用的文件名参数的示例:

#include <linux/bpf.h>
#include <bpf_helpers.h>
#include <linux/sched.h>
#include <linux/string.h>
// 定义要监控的 PID
#define TARGET_PID 1234
BPF_PERF_OUTPUT(events);
struct event_data {
pid_t pid;
long syscall_nr;
char filename[256];
};
int syscall__enter_open(struct trace_event_raw_sys_enter* ctx) {
pid_t pid = bpf_get_current_pid_tgid() >> 32;
// 基于 PID 过滤
if (pid != TARGET_PID) {
return 0;
}
struct event_data data = {0};
data.pid = pid;
data.syscall_nr = ctx->id;
// 获取文件名参数
const char* filename = (const char*)ctx->args[0];
bpf_probe_read_user_str(data.filename, sizeof(data.filename), filename);
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
char LICENSE[] SEC("license") = "GPL";

在这个例子中,我们使用 syscalls:sys_enter_open tracepoint 来监控 open 系统调用。ctx->args[0] 包含了文件名参数的地址。我们使用 bpf_probe_read_user_str 函数从用户空间读取文件名,并将其存储在 event_data 结构中。

注意: bpf_probe_read_user_str 函数用于从用户空间读取字符串。由于用户空间地址可能无效,因此使用此函数时需要小心。确保提供足够的缓冲区大小,并处理可能的错误。

要解析系统调用的返回值,我们可以使用 syscalls:sys_exit_* tracepointctx->ret 字段包含系统调用的返回值。

5. 用户空间程序

eBPF 程序需要在用户空间加载和控制。我们可以使用 libbpf 库来简化这个过程。以下是一个简单的用户空间程序,用于加载 eBPF 程序并从 BPF 映射中读取数据:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#define BPF_OBJECT "syscall_monitor.o" // eBPF 程序的编译输出文件
static int stop = 0;
static void sig_handler(int sig) {
stop = 1;
}
int main(int argc, char **argv)
{
struct bpf_object *obj;
int events_map_fd;
// 注册信号处理程序
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
// 加载 eBPF 程序
obj = bpf_object__open_file(BPF_OBJECT, NULL);
if (!obj) {
fprintf(stderr, "Failed to open BPF object file: %s\n", BPF_OBJECT);
return 1;
}
// 加载所有程序到内核
int err = bpf_object__load(obj);
if (err) {
fprintf(stderr, "Failed to load BPF object: %s\n", strerror(-err));
bpf_object__close(obj);
return 1;
}
// 获取 events 映射的文件描述符
events_map_fd = bpf_object__find_map_fd_by_name(obj, "events");
if (events_map_fd < 0) {
fprintf(stderr, "Failed to find events map\n");
bpf_object__close(obj);
return 1;
}
// 使用 perf buffer 读取数据
struct perf_buffer_opts pb_opts = {};
struct perf_buffer *pb = perf_buffer__new(events_map_fd, 8, &pb_opts);
if (!pb) {
fprintf(stderr, "Failed to create perf buffer: %s\n", strerror(errno));
bpf_object__close(obj);
return 1;
}
// 定义处理 perf buffer 事件的回调函数
perf_buffer__set_sample_cb(pb, (perf_buffer_sample_fn)print_event);
perf_buffer__set_lost_cb(pb, lost_cb);
// 循环读取 perf buffer 中的数据
while (!stop) {
err = perf_buffer__poll(pb, 100); // 100ms 超时
if (err < 0 && err != -EINTR) {
fprintf(stderr, "Error polling perf buffer: %s\n", strerror(-err));
break;
}
}
// 清理资源
perf_buffer__free(pb);
bpf_object__close(obj);
return 0;
}
// 处理 perf buffer 事件的回调函数 (需要根据你的事件结构定义)
static void print_event(void *ctx, int cpu, void *data, __u32 data_sz)
{
struct event_data *event = (struct event_data *)data;
printf("PID: %d, syscall: %ld, filename: %s\n", event->pid, event->syscall_nr, event->filename);
}
// 处理丢失事件的回调函数
static void lost_cb(void *ctx, int cpu, __u64 lost_cnt)
{
fprintf(stderr, "Lost %llu events on CPU %d\n", lost_cnt, cpu);
}

这个程序首先加载 eBPF 程序,然后创建一个 perf buffer 用于从内核接收数据。print_event 函数用于处理从 perf buffer 接收到的事件数据,并将其打印到控制台。lost_cb 函数用于处理由于 perf buffer 溢出而丢失的事件。

编译 eBPF 程序和用户空间程序

  1. 安装必要的工具: 确保你已经安装了 libbpfclangllvm

  2. 编译 eBPF 程序: 使用 clang 编译 eBPF 程序。例如:

    clang -O2 -target bpf -D__TARGET_ARCH_x86_64 -I/usr/include/ -c syscall_monitor.c -o syscall_monitor.o
    

    确保包含正确的头文件路径 (-I/usr/include/)。-D__TARGET_ARCH_x86_64 用于指定目标架构。

  3. 编译用户空间程序: 使用 gcc 编译用户空间程序。例如:

    gcc -Wall -g userspace.c -o userspace -lbpf
    

    -lbpf 用于链接 libbpf 库。

6. 总结

本文介绍了如何使用 eBPF 实现对特定进程的系统调用监控。我们讨论了 eBPF 程序的结构、如何过滤无关的系统调用以及如何解析系统调用的参数和返回值。我们还提供了一个简单的用户空间程序,用于加载 eBPF 程序并从 BPF 映射中读取数据。

eBPF 是一种强大的工具,可以用于许多不同的目的,包括系统监控、性能分析和安全分析。通过理解 eBPF 的基本原理和技术,我们可以构建强大的工具来解决各种实际问题。

进一步学习:

免责声明:

本文提供的代码示例仅供参考。在生产环境中使用 eBPF 程序时,请务必进行充分的测试和验证,以确保其安全性和稳定性。不正确的 eBPF 程序可能会导致系统不稳定或崩溃。

BPF探索者 eBPF系统调用监控Linux

评论点评

打赏赞助
sponsor

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

分享

QRcode

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