WEBKT

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

252 0 0 0

在 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

评论点评