1.ebpf入门

1. 环境

1.1. 基础环境

虚拟机为 Ubuntu 22.04 64位,内核版本6.8.0-57-generic

1.2. eunomia-bpf

eunomia-bpf 是一个开源的 eBPF 动态加载运行时和开发工具链,是为了简化 eBPF 程序的开发、构建、分发、运行而设计的,基于 libbpf 的 CO-RE 轻量级开发框架。

下载编译器工具链,用于将 eBPF C 代码(内核代码)编译为各种输出格式,如 BPF 字节码、ELF 对象文件和配置文件

wget https://github.com/eunomia-bpf/eunomia-bpf/releases/latest/download/ecc && chmod +x ./ecc
mv ./ecc /usr/bin/ecc && chmod +x /usr/bin/ecc && ecc -h

 下载 ecli 工具,用于运行 eBPF 程序,可以加载和执行先前由 ecc 编译的 eBPF 代码

wget https://aka.pw/bpf-ecli -O ecli && chmod +x ./ecli
mv ./ecli /usr/bin/ecli && chmod +x /usr/bin/ecli && ecli -h

安装必要的包

sudo apt install clang llvm

设置环境变量

vim ~/.bashrc
    # 设置环境变量 BTF_FILE_PATH,将下行添加到文件的末尾。正在运行的内核的 vmlinux 文件,通常位于 /boot 目录下。
    # 比如我的是 /boot/vmlinuz-6.8.0-57-generic
    export BTF_FILE_PATH=/boot/vmlinuz-6.8.0-57-generic
source ~/.bashrc

1.3. bpftool

sudo apt install linux-tools-generic
whereis bpftool

2. ebpf入门

2.1. BPF/eBPF/Seccomp-BPF

2.1.1. BPF

是一种灵活的、低开销的虚拟机,最初用于网络数据包过滤。

2.1.2. eBPF:

进一步扩展了 BPF,为其提供了更多功能和灵活性,通常需要 4.4 内核版本以上。

eBPF起源于Linux内核,可以在操作系统的内核中运行沙盒程序。它被用来安全和有效地扩展内核的功能,而不需要改变内核的源代码或加载内核模块。eBPF通过允许在操作系统内运行沙盒程序,应用程序开发人员可以在运行时,可编程地向操作系统动态添加额外的功能。

云原生容器防护力eBPF,它提供了一种便利的可基于系统或程序事件高效安全执行特定代码的通用能力,并且它可检测的事件覆盖了系统的各个方面。

2.1.3. Seccomp-BPF

Seccomp-BPF: 用于沙箱进程,限制其系统调用,是 Seccomp 的一个扩展。

3. 事件

3.1. tp事件

3.1.1. 检测并打印从指定PID进入write系统调用的事件

使用 eunomia-bpf 进行编译运行。
minimal.bpf.c

/* 定义许可证,通常使用 "Dual BSD/GPL" */
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */

/* 禁用全局数据 */
#define BPF_NO_GLOBAL_DATA

/* eBPF的核心定义 */
#include <linux/bpf.h>

/* BPF辅助函数和跟踪功能 */
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

typedef unsigned int u32;
typedef int pid_t;
const pid_t pid_filter = 0;
char LICENSE[] SEC("license") = "Dual BSD/GPL";

/* 定义一个 handle_tp 函数并使用 SEC 宏把它附加到 sys_enter_write tracepoint(即在进入 write 系统调用时执行)。*/
SEC("tp/syscalls/sys_enter_write")
int handle_tp(void* ctx)
{
        /* 获取当前进程的PID,eBPF中没有直接获取PID的方法,可以使用下方函数获取当前线程的PID和TID(线程ID),右移32位可以获得PID*/
        pid_t pid = bpf_get_current_pid_tgid() >> 32;
        if (pid_filter && pid != pid_filter)
                return 0;
        /* 打印从特定PID进入 sys_enter_write 的消息。*/
        bpf_printk("BPF triggered sys_enter_write from PID %d.\n", pid);
        return 0;
Note

这段程序是一个基于eBPF的跟踪程序,它捕获并记录从指定进程ID(PID)执行的 write 系统调用。这个程序被注册为 sys_enter_write 的一个跟踪点,当系统中的某个进程执行 write 系统调用时,它将被触发,并输出该进程的 PID。

# 使用 ecc 编译程序
ecc minimal.bpf.c
# 使用 ecli 运行编译后的程序
sudo ecli run package.json  #一定要加sudo
# 运行上面命令后,可以通过查看 /sys/kernel/debug/tracing/trace_pipe 文件来查看 eBPF 程序的输出
cat /sys/kernel/debug/tracing/trace_pipe | grep "BPF triggered sys_enter_write"

PixPin_2025-04-20_10-31-44

Warning

如果正在使用的 Linux 发行版(例如 Ubuntu )默认情况下没有启用跟踪子系统可能看不到任何输出,使用以下指令打开这个功能,这样子我们就能通过 cat /sys/kernel/debug/tracing/trace_pipe 看到内核态的日志输出了。

sudo su
echo 1 > /sys/kernel/debug/tracing/tracing_on

3.2. tracepoint 事件

3.2.1. 捕获进程打开文件的系统调用

当进程打开一个文件时,它会向内核发出 sys_openat 系统调用,并传递相关参数(例如文件路径、打开模式等)。内核会处理这个请求,并返回一个文件描述符(file descriptor),这个描述符将在后续的文件操作中用作引用。通过捕获 sys_openat 系统调用,我们可以了解进程在什么时候以及如何打开文件。

 下面程序将捕获指定进程(或所有进程)的 sys_openat 系统调用,并在用户空间输出相关信息。

 

root@ubuntu22:~/ebpf# cat opensnoop.bpf.c
/* 禁用全局数据 */
#define BPF_NO_GLOBAL_DATA

/* 包含了内核数据结构的定义 */
#include <vmlinux.h>

/* BPF辅助函数 */
#include <bpf/bpf_helpers.h>

/* 用于过滤指定进程 ID。这里设为 0 表示捕获所有进程的 sys_openat 调用 */
const volatile int pid_target = 0;

/* 使用 SEC 宏定义一个 eBPF 程序,关联到 tracepoint "tracepoint/syscalls/sys_enter_openat"。这个 tracepoint 会在进程发起 sys_openat 系统调用时触发。*/
SEC( "tracepoint/syscalls/sys_enter_openat" )
int tracepoint__syscalls__sys_enter_openat( struct trace_event_raw_sys_enter* ctx ) /* trace_event_raw_sys_enter 包含了关于系统调用的信息*/
{
    /* 获取当前进程的PID,eBPF中没有直接获取PID的方法,可以使用下方函数获取当前线程的PID和TID(线程ID),右移32位可以获得PID*/
        u64     id      = bpf_get_current_pid_tgid();
        u32     pid     = id >> 32;

        if ( pid_target && pid_target != pid )
                return(false);
        bpf_printk( "Process ID: %d enter sys openat\n", pid );
        return(0);
}

/* 将程序许可证设置为 "GPL",这是运行 eBPF 程序的必要条件 */
char LICENSE[] SEC( "license" ) = "GPL";
ecc opensnoop.bpf.c
sudo ecli run package.json

# 通过查看 /sys/kernel/debug/tracing/trace_pipe 文件来查看 eBPF 程序的输出
cat /sys/kernel/debug/tracing/trace_pipe

# 输出
           <...>-165144  [000] .... 14738.287516: 0: Process ID: 165144 enter sys 
openat
           <...>-165145  [000] .... 14738.288076: 0: Process ID: 165145 enter sys 
openat
           <...>-165145  [000] .... 14738.288155: 0: Process ID: 165145 enter sys 
openat
           <...>-165145  [000] .... 14738.288357: 0: Process ID: 165145 enter sys 
openat
           <...>-165145  [000] .... 14738.288478: 0: Process ID: 165145 enter sys 
openat
           <...>-165145  [000] .... 14738.288564: 0: Process ID: 165145 enter sys 
openat
           <...>-165145  [000] .... 14738.288615: 0: Process ID: 165145 enter sys 
openat
           <...>-165145  [000] .... 14738.288668: 0: Process ID: 165145 enter sys 
openat
           <...>-165146  [000] .... 14738.289536: 0: Process ID: 165146 enter sys 
openat
           <...>-165146  [000] .... 14738.289623: 0: Process ID: 165146 enter sys 
openat

PixPin_2025-04-20_10-50-59

3.3. LSM(Linux Security Modules)事件

LSM(Linux Security Module) 可译为内核安全模块。截止到内核 5.7 版本,共有 9 个 LSM 安全模块实现,
Pasted image 20250420123605

3.3.1. 背景

Linux 5.7 引入在 LSM 中提供了对于 BPF 的支持 (简称 LSM BPF)。

在 LSM BPF 出现之前,能够实现实施安全策略目标的方式有两种选择:配置现有的 LSM 模块(如AppArmor、SELinux),或编写自定义内核模块。LSM BPF 则提供了第三种实现的方案 ,灵活且安全,具有可编程性。

使用 LSM BPF,开发人员能够在无需配置或加载内核模块的情况下编写精细策略,LSM BPF 程序会在加载时进行验证,然后在调用路径中到达 LSM hook 时执行。

需要注意的是,编写 LSM BPF 程序之前,请确保:
1. 内核版本至少为 5.7
2. LSM BPF 已启用

3.3.2. 确认 BPF LSM 是否可用

# 确认内核版本高于 5.7
$ cat /boot/config-$(uname -r) | grep BPF_LSM
CONFIG_BPF_LSM=y

# 内核是否支持 BPF LSM
$ cat /sys/kernel/security/lsm
lockdown,capability,yama,apparmor,bpfroot

# 如果上条命令结果不包含 bpf 选项,可以补充GRUB_CMDLINE_LINUX的值
$ vim /etc/default/grub
GRUB_CMDLINE_LINUX="lsm=ndlock,lockdown,yama,integrity,apparmor,bpf"

# 更新 grub 配置并重启
$ update-grub2
$ shutdown -r now

3.3.3. 限制通过 socket connect 函数对特定 IPv4 地址进行访问

# 窗口1
ecc lsm-connect.bpf.c
# 这里必须加 sudo,即便你是 root
sudo ecli run package.json

# 窗口2
ping 1.1.1.1
root@ubuntu22:~/ebpf# cat lsm-connect.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

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

#define EPERM 1
#define AF_INET 2

const __u32 blockme = 16843009; // 1.1.1.1 -> int

// 通过 BPF_PROG 宏定义函数,并通过 SEC 宏指定挂载点,在socket连接时执行此程序。
SEC("lsm/socket_connect")
int BPF_PROG(restrict_connect, struct socket *sock, struct sockaddr *address, int addrlen, int ret)
{
    // 如果连接返回值不为0(代表连接失败),则直接返回连接错误代码。
    if (ret != 0)
    {
        return ret;
    }

    // 本例中,如果连接的地址族不是IPv4,直接返回0,允许连接。
    if (address->sa_family != AF_INET)
    {
        return 0;
    }

    // 将地址转换为IPv4套接字地址。
    struct sockaddr_in *addr = (struct sockaddr_in *)address;

    // 提取连接的目标IP地址。
    __u32 dest = addr->sin_addr.s_addr;
    bpf_printk("lsm: found connect to %d", dest);

    // 如果连接的目标IP等于预定义的1.1.1.1,则返回权限错误。
    if (dest == blockme)
    {
        bpf_printk("lsm: blocking %d", dest);
        return -EPERM;
    }
    return 0;

Pasted image 20250420110351

看一下内核日志

cat /sys/kernel/debug/tracing/trace_pipe

Pasted image 20250420111456
运用: 如指定集群里面的特定地址无法访问
其中 168430091.1.1.1 的整形格式
Pasted image 20250420111604
可以看到 1.1.1.1 被阻止了