WEBKT

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

39 0 0 0

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

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

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

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

进阶应用:权限提升检测

eBPF 的局限性与挑战

总结与展望

容器安全:用 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容器安全系统调用监控

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/9704