eBPF—捕获进程启动/退出事件

上一次,我们尝试了用户空间自定义函数探测,里边会用到进程ID参数,那么实际使用场景下,我们可能需要能检测到进程的拉起和退出,今天就一起来实现下这个小功能。

一、概述

eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具,它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。

本文主要尝试通过探测内核的sys_enter_execve、sched_process_exit事件,捕获进程的拉起和退出事件。并通过ring buffer输出到用户空间程序中。

事实上我们还可以捕获一些关于新进程的有趣信息,例如二进制文件的文件名、测量进程的生命周期、消耗的资源量等。这是深入了解内核内部并观察事物如何运作的良好起点。

二、探测实现

1.定义事件信息结构体

我们新建一个exec.h用于定义存储事件的结构体,以方便内和空间和用户空间都可以方便的使用它。

#ifndef __EXEC_H
#define __EXEC_H

#define EXEC_CMD_LEN 128

struct event {
    int pid;
    int ppid;
    int uid;
    int retval;
    bool is_exit;
    char cmd[EXEC_CMD_LEN];
    unsigned long long ns;
};

#endif /* __EXEC_H */

2.ebpf探测程序实现

定义ebpf程序exec.bpf.c,用于探测进程的启动和退出事件。

/**
 * 捕获 Linux 内核中进程执行的事件
*/
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#include "exec.h"

// 定义ring buffer Map
struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);    // 256 KB
} rb SEC(".maps");


// 捕获进程执行事件,使用 ring buffer 向用户态打印输出
SEC("tracepoint/syscalls/sys_enter_execve")
int snoop_process_start(struct trace_event_raw_sys_enter* ctx)
{
    u64 id;
    pid_t pid;
    struct event *e;
    struct task_struct *task;

    // 获取当前进程的用户ID
    uid_t uid = (u32)bpf_get_current_uid_gid();
    // 获取当前进程ID
    id = bpf_get_current_pid_tgid();
    pid = id >> 32;
    // 获取当前进程的task_struct结构体
    task = (struct task_struct*)bpf_get_current_task();
    // 读取进程名称
    char *cmd = (char *) BPF_CORE_READ(ctx, args[0]);

    // 预订一个ringbuf样本空间
    e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
    if (!e)
        return 0;
    // 设置数据
    e->pid = pid;
    e->uid = uid;
    e->ppid = BPF_CORE_READ(task, real_parent, pid);
    bpf_probe_read_str(&e->cmd, EXEC_CMD_LEN, cmd);
    e->ns = bpf_ktime_get_ns();
    // 提交到ringbuf用户空间进行后处理
    bpf_ringbuf_submit(e, 0);

    // 使用bpf_printk函数在内核日志中打印 PID 和文件名
    // bpf_printk("TRACEPOINT EXEC pid = %d, uid = %d, cmd = %s\n", pid, uid, e->cmd);
    return 0;
}

// 监控进程退出事件,使用 ring buffer 向用户态打印输出
SEC("tp/sched/sched_process_exit")
int snoop_process_exit(struct trace_event_raw_sched_process_template* ctx)
{
    struct task_struct *task;
    struct event *e;
    pid_t pid, tid;
    u64 id, ts, *start_ts, start_time = 0;

    // 获取当前进程的用户ID
    uid_t uid = (u32)bpf_get_current_uid_gid();
    // 获取当前进程/线程ID
    id = bpf_get_current_pid_tgid();
    pid = id >> 32;
    tid = (u32)id;
    // 获取当前进程的task_struct结构体
    task = (struct task_struct *)bpf_get_current_task();
    start_time = BPF_CORE_READ(task, start_time);

    /* ignore thread exits */
    if (pid != tid)
        return 0;

    // 预订一个ringbuf样本空间
    e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
    if (!e)
        return 0;
    // 设置数据
    e->ns = bpf_ktime_get_ns() - start_time;
    e->pid = pid;
    e->uid = uid;
    e->ppid = BPF_CORE_READ(task, real_parent, tgid);
    e->is_exit = true;
    e->retval = (BPF_CORE_READ(task, exit_code) >> 8) & 0xff;
    bpf_get_current_comm(&e->cmd, sizeof(e->cmd));
    // 提交到ringbuf用户空间进行后处理
    bpf_ringbuf_submit(e, 0);

    // 使用bpf_printk函数在内核日志中打印 PID 和文件名
    // bpf_printk("TRACEPOINT EXIT pid = %d, uid = %d, cmd = %s\n", pid, uid, e->cmd);
    return 0;
}

char LICENSE[] SEC("license") = "GPL";

3.用户空间程序实现

编写用户空间程序exec.cc,加载ebpf程序到内核并读取ringbuffer中的探测数据。

/**
 * ebpf 用户空间程序(loader、read perf buffer)
*/
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "exec.skel.h"
#include "exec.h"

int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
	/* Ignore debug-level libbpf logs */
	if (level > LIBBPF_INFO)
		return 0;
	return vfprintf(stderr, format, args);
}

// Control-C process
static volatile bool exiting = false;
static void sig_handler(int sig)
{
	exiting = true;
}

// ring buffer data process
static int handle_event(void *ctx, void *data, size_t data_sz)
{
    const struct event *e = reinterpret_cast<struct event *>(data);
    struct tm *tm;
    char ts[32];
    time_t t;

    time(&t);
    tm = localtime(&t);
    strftime(ts, sizeof(ts), "%H:%M:%S", tm);

    if (e->is_exit) {
        printf("%s %-5s %d %d %s %d %llums\n", ts, "EXIT", e->pid, e->uid, e->cmd, e->retval, e->ns / 1000000);
    } else {
        printf("%s %-5s %d %d %s\n", ts, "EXEC", e->pid, e->uid, e->cmd);
    }

    return 0;
}

int main(int argc, char **argv)
{
    struct exec_bpf *skel;
    int err;
    struct ring_buffer *rb = NULL;

    /* 设置libbpf错误和调试信息回调 */
    libbpf_set_print(libbpf_print_fn);

    /* Control-C 停止信号 */
	signal(SIGINT, sig_handler);
	signal(SIGTERM, sig_handler);

    /* 加载并验证 exec.bpf.c 应用程序 */
    skel = exec_bpf__open_and_load();
    if (!skel) {
        fprintf(stderr, "Failed to open BPF skeleton\n");
        return 1;
    }

    /* 附加 exec.bpf.c 程序到跟踪点 */
    err = exec_bpf__attach(skel);
    if (err) {
        fprintf(stderr, "Failed to attach BPF skeleton\n");
        exec_bpf__destroy(skel);
        return -err;
    }
    // printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` to see output of the BPF programs.\n");

    /* 设置环形缓冲区轮询 */
    rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
    if (!rb) {
        err = -1;
        fprintf(stderr, "Failed to create ring buffer\n");
        exec_bpf__destroy(skel);
        return -err;
    }

    /* 处理收到的内核数据 */
    printf("%-8s %-8s %-7s %-7s %-16s %-8s %-8s\n", "TIME", "TYPE", "PID", "UID", "CMD", "RET", "DURATION");
    while (!exiting) {
        // 轮询内核数据
        err = ring_buffer__poll(rb, 100 /* timeout, ms */);
        if (err == -EINTR) {    /* Ctrl-C will cause -EINTR */
            err = 0;
            break;
        }
        if (err < 0) {
            printf("Error polling perf buffer: %d\n", err);
            break;
        }
    }
}

4.编译测试

cd build
cmake ..
make

sudo ./exec
    TIME     TYPE     PID     UID     CMD              RET      DURATION
    10:37:29 EXEC  459980 1000 /usr/bin/ls
    10:37:29 EXIT  459980 1000 ls 0 3ms

三、总结

本文主要尝试通过探测内核的sys_enter_execve、sched_process_exit事件,捕获进程的拉起和退出事件,并通过ring buffer输出到用户空间程序中供使用或分析。

好了,本文相对比较简单,大家有对哪些内容感兴趣希望我介绍的,可以留言告诉我~

 

yan 23.1.5

 

欢迎关注下方“非著名资深码农“公众号进行交流~

发表评论

邮箱地址不会被公开。