WEBKT

巧用 eBPF 容器安全利器?揪出 setuid 这类高危操作!

108 0 0 0

容器安全:用 eBPF 揪出容器里的“内鬼”?

各位安全大佬、运维老鸟,今天咱们聊点硬核的,容器安全!容器跑得欢,安全隐患也得防。别以为容器隔离就万事大吉,权限提升、恶意代码,照样能把你的系统搞瘫痪。所以,如何实时监控容器内部行为,及时发现并阻止潜在威胁,就成了咱们的头等大事。

今天的主角是 eBPF (Extended Berkeley Packet Filter),这可不是传统的包过滤工具,它已经进化成 Linux 内核中一个强大的可编程引擎。 简单来说,你可以用它来干很多事情,包括 安全监控!

为什么选择 eBPF?容器安全监控的痛点

在深入 eBPF 之前,我们先来看看传统的容器安全监控方案有哪些不足?

  • 入侵性强: 很多监控工具需要在容器内部署 Agent,这无疑增加了容器的攻击面,一旦 Agent 被攻破,整个容器就暴露了。
  • 性能损耗大: 传统的系统调用审计(例如 auditd)会产生大量的日志,对系统性能有一定的影响,在高并发场景下尤为明显。
  • 滞后性: 很多安全事件的检测依赖于事后分析日志,无法做到实时响应,错失最佳防御时机。

而 eBPF 的出现,完美地解决了这些痛点!

  • 非侵入式: eBPF 程序运行在内核态,无需修改容器内部署任何 Agent,避免了引入新的安全风险。
  • 高性能: eBPF 程序经过内核校验和 JIT 编译,执行效率非常高,对系统性能的影响极小。
  • 实时性: eBPF 可以实时监控系统调用,一旦发现可疑行为,立即采取行动,实现零延迟防御。

eBPF 如何“揪出内鬼”?原理剖析

eBPF 的核心思想是:在内核中插入“探针”,实时监控内核事件,并根据预定义的规则进行分析和处理。 是不是有点像电影里的特工?只不过我们的特工是代码,监控的是系统调用。

具体来说,我们可以利用 eBPF 追踪容器内部进程的系统调用,例如 execvesetuidsetcap 等。 一旦发现有进程尝试执行这些敏感操作,eBPF 程序就会立即捕获相关信息,并触发告警或采取阻止措施。

关键步骤:

  1. 选择合适的 eBPF 探针: 根据需要监控的系统调用类型,选择合适的探针。常见的探针类型包括 kprobe、tracepoint、perf_event 等。
  2. 编写 eBPF 程序: 使用 C 语言编写 eBPF 程序,定义监控规则和处理逻辑。 例如,我们可以编写一个 eBPF 程序,用于检测容器内进程是否尝试调用 setuid 系统调用。
  3. 加载 eBPF 程序: 使用 bpftool 或其他 eBPF 工具,将 eBPF 程序加载到内核中。
  4. 收集和分析数据: eBPF 程序会将捕获到的数据发送到用户态程序,进行进一步的分析和处理。 例如,我们可以将数据发送到 Elasticsearch 中进行存储和分析,或者使用 Grafana 进行可视化展示。

实战演练:用 eBPF 监控 setuid 系统调用

说了这么多理论,咱们来点实际的。 下面,我将演示如何使用 eBPF 监控容器内进程的 setuid 系统调用。

环境准备:

  • Linux 内核版本 >= 4.14 (推荐 5.x)
  • 安装 bpftool 工具
  • 安装 libbpf
  • Docker 环境

步骤:

  1. 编写 eBPF 程序 (setuid_monitor.c):
#include <linux/kconfig.h>
#include <linux/version.h>
#include <uapi/linux/bpf.h>
#include <linux/sched.h>

#define BPF_PROG_NAME(name) __attribute__((section(".text." #name))) name

#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 15, 0)
#define BPF_RINGBUF_OUTPUT 1
#else
#define bpf_ringbuf_output(map, rec, size, flags) ({ bpf_perf_event_output(NULL, map, BPF_PERF_OUTPUT_RINGBUF, rec, size); 0; })
#endif


struct data_t {
    u32 pid;
    u32 uid;
    u32 gid;
    char comm[TASK_COMM_LEN];
};


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


SEC("kprobe/sys_setuid")
int BPF_PROG_NAME(kp_sys_setuid)(struct pt_regs *regs)
{
    struct data_t data = {};
    data.pid = bpf_get_current_pid_tgid() >> 32;
    data.uid = bpf_get_current_uid_gid();
    data.gid = bpf_get_current_uid_gid() >> 32;
    bpf_get_current_comm(&data.comm, sizeof(data.comm));

    struct data_t *record = bpf_ringbuf_reserve(&rb, sizeof(data), 0);
    if (!record) {
        return 0;
    }

    *record = data;
    bpf_ringbuf_submit(record, 0);

    return 0;
}

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

代码解释:

  • 定义了一个 data_t 结构体,用于存储进程的 PID、UID、GID 和进程名。
  • 定义了一个 BPF_MAP_TYPE_RINGBUF 类型的 Ring Buffer,用于将数据从内核态传递到用户态。
  • 使用 kprobe/sys_setuid 探针,监控 sys_setuid 系统调用。
  • kp_sys_setuid 函数中,获取当前进程的 PID、UID、GID 和进程名,并将数据写入 Ring Buffer。
  1. 编译 eBPF 程序:
clang -Wall -target bpf -O2 -g -c setuid_monitor.c -o setuid_monitor.o
  1. 编写用户态程序 (main.go):
package main

import (
    "bytes"
    "encoding/binary"
    "fmt"
    "log"
    "os"
    "os/signal"
    "syscall"

    "github.com/cilium/ebpf/link"
    "github.com/cilium/ebpf/ringbuf"
    "github.com/cilium/ebpf/rlimit"
)

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -cflags "-Wall" setuidMonitor setuid_monitor.c -- -I./headers

type dataT struct {
    Pid uint32
    Uid uint32
    Gid uint32
    Comm [16]byte
}

func main() {

    err := rlimit.RemoveMemlock()
    if err != nil {
        log.Fatalf("Failed to remove memory lock: %s", err)
    }

    // 加载 eBPF 对象
    var objs setuidMonitorObjects
    err = loadSetuidMonitorObjects(&objs, nil)
    if err != nil {
        log.Fatalf("Failed to load eBPF objects: %s", err)
    }
    defer objs.Close()


    // 附加 kprobe
    l, err := link.AttachRawTracepoint(link.RawTracepointOptions{Name: "sys_enter_setuid", Program: objs.KpSysSetuid})
    if err != nil {
        log.Fatalf("Failed to attach kprobe: %s", err)
    }
    defer l.Close()


    // 设置 Ring Buffer 读取器
    rb, err := ringbuf.NewReader(objs.Rb)
    if err != nil {
        log.Fatalf("Failed to create ring buffer reader: %s", err)
    }
    defer rb.Close()


    // 监听 INT 和 TERM 信号
    sigs := make(chan os.Signal, 1)
    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)


    // 启动 goroutine 处理 Ring Buffer 数据
    go func() {
        var event dataT

        for {
            record, err := rb.Read()
            if err != nil {
                if errors.Is(err, ringbuf.ErrClosed) {
                    return
                }
                log.Printf("Error reading from ring buffer: %s", err)
                continue
            }

            // 解析 Ring Buffer 数据到 event 结构体
            err = binary.Read(bytes.NewReader(record.RawSample), binary.LittleEndian, &event)
            if err != nil {
                log.Printf("Error parsing ring buffer data: %s", err)
                continue
            }

            // 打印事件信息
            fmt.Printf("PID: %d, UID: %d, GID: %d, COMM: %s\n", event.Pid, event.Uid, event.Gid, string(bytes.TrimRight(event.Comm[:], "\x00")))
        }
    }()


    fmt.Println("Waiting for events...")


    // 等待信号
    <-sigs


    fmt.Println("Exiting...")
}

代码解释:

  • 使用 github.com/cilium/ebpf 库加载 eBPF 对象。
  • 使用 link.AttachRawTracepoint 函数,将 eBPF 程序附加到 sys_enter_setuid 探针。
  • 使用 ringbuf.NewReader 函数,创建一个 Ring Buffer 读取器。
  • 启动一个 goroutine,用于从 Ring Buffer 中读取数据,并将数据解析为 dataT 结构体。
  • 打印事件信息,包括进程的 PID、UID、GID 和进程名。
  1. 编译用户态程序:
go mod init setuid_monitor
go get github.com/cilium/ebpf
go get github.com/cilium/ebpf/link
go get github.com/cilium/ebpf/ringbuf
go get github.com/cilium/ebpf/rlimit
go run . 
  1. 运行:

先运行用户态程序

sudo go run main.go

然后,在另一个终端中,运行一个 Docker 容器,并在容器内执行 setuid 命令:

docker run -it ubuntu bash
root@container:/# setuid 1000

此时,你应该能在用户态程序的终端中看到类似下面的输出:

PID: 2876, UID: 0, GID: 0, COMM: bash

这表明 eBPF 程序成功地捕获到了 setuid 系统调用,并输出了相关信息。

进阶应用:权限提升检测

仅仅监控 setuid 系统调用还不够,我们还需要检测其他可能导致权限提升的行为,例如:

  • setcap:用于给文件设置 Capabilities,可能导致普通用户获得 root 权限。
  • execve:如果执行的程序具有 SUID 或 SGID 权限,也可能导致权限提升。

我们可以通过编写更复杂的 eBPF 程序,同时监控多个系统调用,并结合进程的 Capabilities 信息,更准确地判断是否存在权限提升行为。

eBPF 的局限性与挑战

虽然 eBPF 功能强大,但也存在一些局限性:

  • 学习曲线陡峭: eBPF 编程需要一定的 C 语言基础和内核知识,学习曲线比较陡峭。
  • 内核版本兼容性: 不同的内核版本可能支持不同的 eBPF 功能,需要针对不同的内核版本编写不同的 eBPF 程序。
  • 安全性: 虽然 eBPF 程序经过内核校验,但仍然存在一定的安全风险,例如程序漏洞可能导致内核崩溃。

总结与展望

eBPF 作为一种新兴的内核技术,为容器安全监控带来了革命性的变革。 它具有非侵入式、高性能、实时性等优点,可以有效地检测和阻止容器内部的恶意行为。

当然,eBPF 仍然处于快速发展阶段,未来还有很多值得探索的方向,例如:

  • 更强大的安全策略: 利用 eBPF 实现更复杂的安全策略,例如基于行为的异常检测、基于规则的访问控制等。
  • 更广泛的应用场景: 将 eBPF 应用于更多的安全场景,例如网络安全、主机安全等。
  • 更易用的开发工具: 开发更易用的 eBPF 开发工具,降低 eBPF 的学习门槛。

希望通过本文的介绍,能够帮助大家更好地了解 eBPF 在容器安全领域的应用,并将其应用到实际工作中,提升容器安全防护能力。

容器安全吹哨人 eBPF容器安全系统调用监控

评论点评