超越可观察性:使用 eBPF 修改系统调用行为

在我之前的两篇文章中,我们探讨了 eBPF 及其在内核空间使用探针的功能。通过实验"打开文件警戒""套接字监视",我们仅仅触及了 eBPF 在不改变系统调用行为的情况下的可观察性功能的表面。现在,我们将进一步深入研究如何使用 eBPF 不仅观察系统调用行为,还能影响系统调用行为。

eBPF 超越了单纯的可观察性,使我们能够主动修改系统调用行为,从而打开了更具交互性和可控性的系统交互的大门。

eBPF 助手

eBPF 助手是内核提供的预定义函数,用于促进 eBPF 程序与内核之间的交互。它们提供对内核内部数据和函数的受控访问,使我们能够读取或修改与系统调用和其他内核操作相关的数据。

这些辅助程序有助于修改系统调用行为,使 eBPF 程序能够修改与系统调用相关的数据、更改其返回值,甚至重定向网络数据包。通过 eBPF 辅助程序,我们可以主动影响系统行为,将 eBPF 的功能从单纯的可观察性扩展到更具交互性和可控性的系统交互。

eBPF 程序使用这些辅助函数与系统或其工作环境进行交互。例如,它们可用于打印调试消息、获取系统启动以来的时间、与 eBPF 映射进行交互,或操作网络数据包。由于 eBPF 程序类型多种多样,并且它们不在同一个上下文中运行,因此每种程序类型只能调用其中一部分辅助函数。

man7.org/linux/man-p...

在今天的实验中,我们将深入研究两个不同但功能强大的 eBPF 助手的实际用途:bpf_override_returnbpf_send_signal

bpf_override_return辅助函数有助于修改系统调用的返回值,使我们能够根据自定义逻辑操控系统交互。另一方面,bpf_send_signal它提供了向当前任务进程发送信号的途径。这些辅助函数共同展示了 eBPF 提供的灵活性和控制力,也为深入探索修改实际应用中的系统调用行为奠定了基础。通过本次实验,我们旨在展示这些辅助函数对系统行为的深远影响,以及对 eBPF 驱动开发的更广泛意义。

特别注意bpf_override_return

此辅助程序存在安全隐患,因此受到限制。它仅在内核使用CONFIG_BPF_KPROBE_OVERRIDE 配置选项编译时可用,并且在这种情况下,它仅适用于内核代码中标记为ALLOW_ERROR_INJECTION

的函数。 来源( man7.org/linux/man-p...

实验:我的珍贵秘密文件

想象一下,编写一个程序来保护我们宝贵的秘密文件,充分利用 eBPF 的全部功能。我们的重点是拦截系统openat调用,这是之前探索过的一个熟悉的实体。每当有人试图通过该系统调用访问我们的秘密文件时,我们都会介入并改变其行为,确保我们的文件始终处于严密的保护之下。

我们将为我们的文件设计一个双层安全机制。在 security 级别下level 1,修改系统调用的行为将导致EACCES向调用者返回错误,表示访问被拒绝。升级到 securitylevel 2或更高级别后,措施将变得更加严格;系统会发出 SIGKILL 信号来终止试图访问我们宝贵文件的程序,从而确保提供严密的保护措施,防止未经授权的访问。

为了确定哪些文件需要作为我们的秘密文件进行保护,我们需要一个媒介将这些信息从我们的user-space服务器传递到服务器kernel-space。为此,我们将使用 eBPF 映射来完成此任务:

如果您读过我之前的文章,那么您已经熟悉 eBPF 映射的理想用例之一,即作为内核和用户空间之间数据交换的管道。

sql 复制代码
         Kernel Space                         User Space
        +------------------+                 +-------------------+
        |                  |                 |                   |
        |   eBPF Program   |                 |  User Application |
        |                  |   eBPF Maps     |                   |
        | +-------------+  |<--------------->| +---------------+ |
        | | Probe Logic |  |    Interface    | | Map Interface | |
        | +-------------+  |                 | +---------------+ |
        |                  |                 |                   |
        +------------------+                 +-------------------+

Enter fullscreen mode Exit fullscreen mode

通过此设置,我们可以让user-space应用程序管理秘密文件列表,并kernel-space通过映射将此列表与 共享。驻留在 中的 eBPF 程序kernel-space将仅检查尝试打开的文件是否在映射中,如果是,则相应地更改行为。此方案为 提供了一种简化的方式,使user-spacekernel-space能够基于共享数据进行交互并做出明智的决策。

内核空间

我们首先检查 内核空间应用程序的 C 代码 。

代码C展示简洁明了,每个部分都很简单,之前也解释过。然而,这次的焦点集中在 eBPF 映射上,展示了一种在 eBPF 程序中管理更复杂数据关系的方法。

arduino 复制代码
// Define a key structure to hold the file name
struct key_t {
  char fname[NAME_MAX];
};

// Map to store secret files and their associated security levels
BPF_HASH(secret_files, struct key_t, int);

Enter fullscreen mode Exit fullscreen mode

在上面的代码片段中,key_t我们用一个自定义结构体作为 eBPF 映射的键类型secret_files,从而能够以更有条理的方式处理复杂的键。该结构体保存文件名,并映射到一个整数,该整数表示映射中文件的安全级别secret_files。这种方法不仅增强了数据组织能力,还方便了 eBPF 程序中多维数据的处理,展示了一种使用 eBPF 映射管理复杂数据关系的方法。

c 复制代码
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>

// Define the SIGKILL signal, which instructs the system to terminate the process
#define SIGKILL 9

// Define a key structure to hold the file name
struct key_t {
  char fname[NAME_MAX];
};

// Map to store secret files and their associated security levels
BPF_HASH(secret_files, struct key_t, int);

int syscall__openat(struct pt_regs *ctx, int dfd, const char __user *filename, int flags) {
    struct key_t key = {};
    // Get current user ID and group ID
    u32 uid = bpf_get_current_uid_gid();

    // Read the file name from user space into the key structure
    bpf_probe_read_user_str(&key.fname, sizeof(key.fname), (void *)filename);
    // Look up the file name in the secret_files map to get its security level
    int *security_level = secret_files.lookup(&key);
    if (security_level != 0) {
        // Check if the user is root
        if (uid == 0) {
            bpf_trace_printk("Root user opening secret file %s \\n", key.fname);
            return 0;
        }

        bpf_trace_printk("Non-root user attempting to open secret file %s with security level %d \\n", key.fname, *security_level );
        if (*security_level == 1) {
            // Override the return value of the syscall to indicate permission denied
            bpf_override_return(ctx, -EACCES);
        } else if (*security_level > 1) {
            // If security level is gt than 1, send the SIGKILL signal to terminate the process
            bpf_send_signal(SIGKILL);
        }
    }

    return 0;
}

Enter fullscreen mode Exit fullscreen mode

除了地图之外,上述代码中值得注意的方面是bpf_override_return和的使用bpf_send_signal,我们之前已经讨论过。

向当前任务对应的线程发送bpf_send_signal(u32 sig)信号_sig_。该信号可以传递给此进程的任意线程。如果要将信号发送到当前任务对应的特定线程,请使用bpf_send_signal_thread(u32 sig)

用于bpf_override_return(struct pt_regs regs, u64 rc)错误注入,此助手覆盖探测函数的返回值(在本例中为我们的openatsyscall)。

用户空间

继续 Python 我们的应用程序的代码user-space,代码仍然很简单并且与我们之前的实验有相似之处。

ini 复制代码
from bcc import BPF
import ctypes as ct

# Helper function to add a secret file to the map
def add_secret_file(map, file):
    key = map.Key()
    key.fname = file[0].encode()
    value = ct.c_int(file[1])
    # Update the map with the new entry
    map[key] = value


def main():
    # Read BPF Program
    with open("ebpf_program.c") as f:
        bpf_program = f.read()

    # Load BPF program
    b = BPF(text=bpf_program)

    # Attach the kprobe defined in the eBPF program to the clone system call.
    fnname_openat = b.get_syscall_prefix().decode() + 'openat'
    b.attach_kprobe(event=fnname_openat, fn_name="syscall__openat")

    # Get thee map
    secret_files = b.get_table("secret_files")

    # Add the secret files to the map
    for file in [("/tmp/secret.txt", 1), ("/tmp/ultra_secret.txt", 2)]:
        add_secret_file(secret_files, file)

    try:
        print("Attaching kprobe to sys_openat... Press Ctrl+C to exit.")
        b.trace_print()
    except KeyboardInterrupt:
        pass


if __name__ == "__main__":
    main()

Enter fullscreen mode Exit fullscreen mode

在提供的脚本中,重点在于获取映射并向其中添加条目。加载 eBPF 程序后,脚本secret_files使用检索 eBPF 映射b.get_table("secret_files")。之后,循环遍历一个秘密文件列表,每个文件都表示为一个包含文件路径和相关安全级别的元组。该add_secret_file函数以映射和每个文件元组作为参数调用,其中在映射中创建一个新条目,以文件名为键,安全级别为值。

这封装了使用秘密文件条目动态更新 eBPF 映射的过程,说明了一种从用户空间与 eBPF 映射交互和修改 eBPF 映射的直接机制。

结果

为了让我们的新程序运行起来,让我们在目录中创建两个秘密文件tmp

bash 复制代码
echo "www.kungfudev.com" > /tmp/secret.txt
echo "www.kungfudev.com" > /tmp/ultra_secret.txt

Enter fullscreen mode Exit fullscreen mode

使用命令运行程序sudo python3 app.py并尝试打开最近创建的文件后,我们应该观察到一些输出。请记住,secret.txt的安全级别为 1,而ultra_secret.txt的安全级别为 2。因此,我们应该看到以下输出:

bash 复制代码
$ cat /tmp/secret.txt 
cat: /tmp/secret.txt: Permission denied

$ cat /tmp/ultra_secret.txt 
Killed

# Since we added a root user validation in our eBPF program, we can open the file as root.
$ sudo cat /tmp/ultra_secret.txt 
www.kungfudev.com

Enter fullscreen mode Exit fullscreen mode

所有代码都可以在我的 存储库中找到。

总结

通过这个简单的例子,我们说明了 eBPF 超越了可观察性,展示了它改变系统调用行为的能力,从而突出了它的多功能性和对系统级交互的强大影响。

感谢您的阅读。这篇博客是我学习之旅的一部分,非常重视您的反馈。关于 eBPF,还有更多值得探索和分享的内容,敬请期待后续文章。欢迎您分享您的见解和经验,我们将在这个领域共同学习和成长。 祝您编程愉快!

相关推荐
冰暮流星23 分钟前
flask之app.py讲解
后端·python·flask
程序员契奇1 小时前
Tools工具使用
人工智能·后端
IT_陈寒1 小时前
SpringBoot自动配置没生效?你可能漏了这个注解
前端·人工智能·后端
长明1 小时前
C#项目组织与概念梳理
后端·c#
xn71331 小时前
个人网站站外分发怎么做归因?我给 XBSTACK 补了一套 UTM 追踪规则
后端·低代码
用户2330713074791 小时前
JUC 并发容器与工具
后端
冰暮流星2 小时前
flask之模版渲染
后端·python·flask
威武的花瓣2 小时前
细说ASP.NET的各种异步操作
后端·asp.net·php
漂亮的摩托2 小时前
如何编写一个SpringBoot项目告警推送的Starter
java·spring boot·后端
任性的芝麻2 小时前
ASP.NET MVC 中的异步方式
后端·asp.net·mvc