WEBKT

基于eBPF的容器运行时安全:系统调用追踪与实时告警实践

17 0 0 0

1. eBPF简介

2. 容器内部系统调用追踪

2.1 编写eBPF程序

2.2 部署eBPF程序

2.3 数据收集

3. 实时告警系统构建

3.1 告警规则定义

3.2 告警触发机制

3.3 告警通知发送

4. 关键指标和可视化

5. 总结

容器技术在现代应用开发和部署中扮演着至关重要的角色。然而,容器的普及也带来了新的安全挑战。由于容器共享主机内核,容器内的恶意行为可能会影响整个系统。为了增强容器安全性,我们需要一种能够实时监控和分析容器内部行为的机制。eBPF(扩展伯克利包过滤器)作为一种强大的内核技术,为我们提供了一种高效、灵活的方式来追踪容器内部进程的系统调用行为,并构建实时告警系统。

1. eBPF简介

eBPF 是一种革命性的内核技术,它允许用户在内核空间安全地运行自定义代码,而无需修改内核源代码或加载内核模块。eBPF 程序运行在受限的沙箱环境中,可以访问内核数据,并执行各种操作,例如过滤、修改和追踪。eBPF 最初设计用于网络数据包过滤,但现在已被广泛应用于性能分析、安全监控和流量控制等领域。

在安全领域,eBPF 可以用于:

  • 系统调用追踪: 监控进程的系统调用行为,检测潜在的恶意活动。
  • 网络流量分析: 检查网络数据包,识别恶意流量。
  • 安全策略执行: 根据安全策略,阻止或允许特定的操作。

2. 容器内部系统调用追踪

系统调用是用户空间程序与内核交互的唯一方式。通过追踪系统调用,我们可以了解进程的行为,并检测潜在的恶意活动。例如,如果一个进程尝试打开一个不应该访问的文件,或者执行一个危险的系统调用,我们可以立即发出警报。

2.1 编写eBPF程序

要使用 eBPF 追踪容器内部的系统调用,我们需要编写一个 eBPF 程序。以下是一个简单的 eBPF 程序示例,用于追踪 execve 系统调用:

#include <linux/kconfig.h>
#include <linux/ptrace.h>
#include <linux/version.h>
#if (LINUX_VERSION_CODE >= KERNEL_VERSION(4,11,0))
#include <linux/btf.h>
#endif
struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
char filename[256];
};
BPF_PERF_OUTPUT(events);
int kprobe__do_sys_openat2(struct pt_regs *ctx, int dirfd, const char *pathname, struct open_how *how)
{
struct data_t data = {};
data.pid = bpf_get_current_pid_tgid();
data.ts = bpf_ktime_get_ns();
bpf_get_current_comm(&data.comm, sizeof(data.comm));
// Copy the filename from kernel space to eBPF program's memory
bpf_probe_read_user_str(data.filename, sizeof(data.filename), (void *)pathname);
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}

这个程序使用 kprobe 机制来附加到 do_sys_openat2 函数的入口点。当 do_sys_openat2 函数被调用时,eBPF 程序会被执行。程序会获取进程 ID、时间戳、进程名和文件名,并将这些数据发送到用户空间。

解释:

  • #include <linux/kconfig.h>#include <linux/ptrace.h>#include <linux/version.h>: 引入必要的 Linux 内核头文件,提供 eBPF 程序所需的内核数据结构和函数。
  • #if (LINUX_VERSION_CODE >= KERNEL_VERSION(4,11,0)) ... #endif: 这是一个条件编译块,用于检查 Linux 内核版本。如果内核版本大于或等于 4.11.0,则包含 <linux/btf.h> 头文件。BTF (BPF Type Format) 是一种描述内核数据类型的元数据格式,用于 eBPF 程序在运行时访问内核数据结构。
  • struct data_t: 定义一个数据结构,用于存储从内核空间收集到的数据。这个结构体包含以下字段:
    • u32 pid: 进程 ID (PID)。
    • u64 ts: 时间戳 (timestamp),表示事件发生的时间。
    • char comm[TASK_COMM_LEN]: 进程名 (command name),TASK_COMM_LEN 是内核定义的进程名最大长度。
    • char filename[256]: 文件名,用于存储 openat2 系统调用打开的文件名。
  • BPF_PERF_OUTPUT(events): 定义一个 perf 事件输出队列,用于将 eBPF 程序收集到的数据发送到用户空间。events 是队列的名称。
  • int kprobe__do_sys_openat2(struct pt_regs *ctx, int dirfd, const char *pathname, struct open_how *how): 定义一个 kprobe 处理函数,用于在 do_sys_openat2 函数被调用时执行。kprobe__ 前缀是必需的,用于告诉 bcc 工具这是 kprobe 处理函数。do_sys_openat2 是内核函数名,对应于 openat2 系统调用的内核实现。
    • struct pt_regs *ctx: 指向寄存器状态的指针,包含了函数调用时的寄存器值。
    • int dirfd: 目录文件描述符,用于指定打开文件的目录。
    • const char *pathname: 指向要打开的文件名的指针。
    • struct open_how *how: 指向 open_how 结构的指针,包含了打开文件的标志和模式。
  • struct data_t data = {};: 创建一个 data_t 结构体实例,并将其初始化为零。
  • data.pid = bpf_get_current_pid_tgid();: 获取当前进程的 PID,并将其存储到 data.pid 字段中。bpf_get_current_pid_tgid() 是一个 eBPF 辅助函数,用于获取 PID 和线程组 ID (TGID)。
  • data.ts = bpf_ktime_get_ns();: 获取当前时间戳(纳秒),并将其存储到 data.ts 字段中。bpf_ktime_get_ns() 是一个 eBPF 辅助函数,用于获取内核时间。
  • bpf_get_current_comm(&data.comm, sizeof(data.comm));: 获取当前进程名,并将其存储到 data.comm 字段中。bpf_get_current_comm() 是一个 eBPF 辅助函数,用于获取进程名。
  • bpf_probe_read_user_str(data.filename, sizeof(data.filename), (void *)pathname);: 从用户空间读取文件名,并将其存储到 data.filename 字段中。bpf_probe_read_user_str() 是一个 eBPF 辅助函数,用于从用户空间读取字符串。由于 eBPF 程序运行在内核空间,不能直接访问用户空间内存,需要使用此辅助函数。
  • events.perf_submit(ctx, &data, sizeof(data));: 将收集到的数据提交到 perf 事件输出队列,以便用户空间程序可以读取。perf_submit()BPF_PERF_OUTPUT 宏定义的函数,用于提交数据。
  • return 0;: kprobe 处理函数必须返回一个整数值。0 表示成功。

2.2 部署eBPF程序

可以使用 BCC (BPF Compiler Collection) 工具来编译和部署 eBPF 程序。BCC 提供了一组 Python 工具,可以简化 eBPF 程序的开发和部署过程。

首先,需要安装 BCC 工具:

sudo apt-get update
sudo apt-get install -y bpfcc-tools linux-headers-$(uname -r)

然后,可以使用以下命令来编译和运行 eBPF 程序:

from bcc import BPF
# 加载 eBPF 程序
b = BPF(src_file="trace_open.c")
b.attach_kprobe(event="do_sys_openat2", fn_name="kprobe__do_sys_openat2")
# 打印输出
def print_event(cpu, data, size):
event = b["events"].event(data)
print("{} {}: Filename = {}".format(event.pid, event.comm.decode(), event.filename.decode()))
# 循环读取 perf 输出
b["events"].open_perf_buffer(print_event)
while True:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()

解释:

  • from bcc import BPF: 导入 BCC 库的 BPF 类,用于加载和管理 eBPF 程序。
  • b = BPF(src_file="trace_open.c"): 创建一个 BPF 对象,并从 trace_open.c 文件加载 eBPF 程序。trace_open.c 文件包含了上面定义的 C 代码。
  • b.attach_kprobe(event="do_sys_openat2", fn_name="kprobe__do_sys_openat2"): 将 eBPF 程序附加到 do_sys_openat2 函数的 kprobe。event 参数指定要附加的内核函数名,fn_name 参数指定 eBPF 程序中的处理函数名。
  • def print_event(cpu, data, size): ...: 定义一个回调函数,用于处理从 eBPF 程序接收到的数据。cpu 参数指定 CPU 核心,data 参数指向接收到的数据,size 参数指定数据的大小。
  • event = b["events"].event(data): 从接收到的数据中提取事件信息。b["events"] 访问 eBPF 程序中定义的 events perf 事件输出队列,event(data) 方法将接收到的数据转换为 Python 对象。
  • print("{} {}: Filename = {}".format(event.pid, event.comm.decode(), event.filename.decode())): 打印事件信息,包括进程 ID、进程名和文件名。.decode() 方法用于将字节串转换为字符串。
  • b["events"].open_perf_buffer(print_event): 打开一个 perf 缓冲区,并将 print_event 回调函数注册到该缓冲区。当 eBPF 程序向 events 队列发送数据时,print_event 函数会被调用。
  • while True: ...: 进入一个无限循环,不断轮询 perf 缓冲区,以便接收来自 eBPF 程序的数据。
  • b.perf_buffer_poll(): 轮询 perf 缓冲区,检查是否有新的数据到达。
  • except KeyboardInterrupt: exit(): 捕获键盘中断信号 (Ctrl+C),并退出程序。

2.3 数据收集

eBPF 程序会将收集到的系统调用数据发送到用户空间。可以使用 BCC 提供的 Python 工具来读取这些数据,并将其存储到日志文件或数据库中。

3. 实时告警系统构建

有了系统调用数据,我们就可以构建一个实时告警系统。告警系统需要具备以下功能:

  • 告警规则定义: 定义告警规则,例如基于系统调用频率、系统调用序列或系统调用参数。
  • 告警触发机制: 实现告警触发机制,例如使用滑动窗口或状态机。
  • 告警通知发送: 将告警通知发送到安全团队,例如通过邮件、Slack或PagerDuty。

3.1 告警规则定义

告警规则可以使用 YAML 或 JSON 格式定义。以下是一个 YAML 格式的告警规则示例:

name: Suspicious file access
description: Detects processes attempting to access sensitive files.
condition:
syscall: openat
filename: /etc/shadow
severity: high

这个规则表示,如果一个进程尝试打开 /etc/shadow 文件,就会触发一个高危告警。

3.2 告警触发机制

可以使用滑动窗口或状态机来实现告警触发机制。

  • 滑动窗口: 滑动窗口是一种时间窗口,用于统计特定事件的发生次数。例如,可以设置一个 5 分钟的滑动窗口,如果一个进程在 5 分钟内执行了 10 次 execve 系统调用,就触发一个告警。
  • 状态机: 状态机是一种有限状态自动机,用于跟踪进程的状态。例如,可以设置一个状态机,如果一个进程先执行了 connect 系统调用,然后执行了 send 系统调用,就触发一个告警。

3.3 告警通知发送

可以使用各种方式将告警通知发送到安全团队,例如:

  • 邮件: 将告警信息发送到指定的邮箱地址。
  • Slack: 将告警信息发送到 Slack 频道。
  • PagerDuty: 将告警信息发送到 PagerDuty,以便安全团队可以及时响应。

4. 关键指标和可视化

为了快速定位和响应安全事件,告警系统需要具备以下关键指标和可视化功能:

  • 告警数量: 统计告警数量,以便了解系统的安全状况。
  • 告警类型: 对告警进行分类,以便了解主要的威胁类型。
  • 告警严重程度: 对告警进行分级,以便优先处理高危告警。
  • 告警趋势图: 绘制告警趋势图,以便了解告警数量随时间的变化趋势。
  • 告警地理位置分布图: 绘制告警地理位置分布图,以便了解攻击的来源地。
  • 告警关联图: 绘制告警关联图,以便了解告警之间的关联关系。

可以使用各种工具来实现可视化功能,例如:

  • Grafana: Grafana 是一种流行的开源数据可视化工具,可以用于创建各种仪表盘和图表。
  • Kibana: Kibana 是 Elasticsearch 的官方可视化工具,可以用于搜索、分析和可视化 Elasticsearch 中的数据。

5. 总结

eBPF 是一种强大的内核技术,可以用于追踪容器内部进程的系统调用行为,并构建实时告警系统。通过使用 eBPF,我们可以增强容器安全性,及时发现和响应安全事件。本文介绍了如何使用 eBPF 来追踪容器内部的系统调用,以及如何构建一个实时告警系统。希望本文能够帮助读者更好地理解 eBPF 技术,并将其应用于实际的安全场景中。

容器安全喵 eBPF容器安全系统调用追踪

评论点评

打赏赞助
sponsor

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

分享

QRcode

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