eBPF 运行时行为采集实战:主机异常检测配置

eBPF 运行时行为采集是把主机内核事件(进程执行、文件写入、网络连接)以极低开销导出为可检索日志的技术路径。相比 auditd 的规则匹配与采样,eBPF 直接挂载在内核钩子上,能拿到完整的进程上下文与调用参数,适合做入侵检测、异常外联发现与合规审计。本文给出工具链安装、四类核心采集脚本、systemd 持久化与性能评估的完整配置。

适用场景

  • Web 服务器异常命令执行排查:PHP-FPM 或 Nginx 进程派生 shell
  • 敏感文件写入监控:/etc/passwd、/root/.ssh、计划任务目录被改动
  • 后门外联发现:短生命周期进程发起的非常规端口连接
  • 容器与宿主机统一观测:在宿主侧采集容器内行为,无需入侵容器
  • 合规审计:要求留存进程与文件操作的原始事件记录

前置条件

  • Linux 内核 5.4 以上,建议 5.8 以上(支持 BTF 与 ring buffer,无需手动匹配头文件)
  • 具备 root 或 CAP_BPF / CAP_PERFMON 能力
  • 已挂载 bpffs:/sys/fs/bpf
  • 磁盘预留空间用于事件落盘,按每秒 200 事件的量级估算约 200 MB/天
  • 确认内核开启了 CONFIG_BPF_SYSCALL 与 CONFIG_DEBUG_INFO_BTF

原理说明

eBPF 程序在内核态沙箱中执行,由 verifier 做静态校验:不允许无界循环、不允许越界访问、不允许直接调用任意内核函数。加载后挂载到 kprobe、tracepoint、perf_event 等钩子点,事件通过 perf buffer 或 ring buffer 传回用户态。与 auditd 的关键差异有三点:

  • 上下文完整:eBPF 可直接读取内核结构体,取到调用参数、父进程、容器 cgroup,而 auditd 多依赖 syscall 号与部分参数。
  • 开销可控:内核态过滤先行,只有命中规则的事件才复制到用户态。
  • 粒度可编程:过滤逻辑写在脚本里,改动不需要重启审计服务。

代价是需要较新的内核,且 bpftrace 这类动态脚本工具在复杂脚本下可能被 verifier 拒绝,不适合当作唯一的审计留痕手段,建议与 journald 或 auditd 互补。

操作步骤

步骤 1:安装工具链并做能力检查

# Debian / Ubuntu
apt update && apt install -y bpftrace bpfcc-tools linux-headers-$(uname -r) jq

# RHEL / CentOS / 麒麟 / 欧拉
yum install -y bpftrace bcc-tools kernel-devel-$(uname -r) jq

# 能力检查
uname -r                                  # 建议 5.4 以上
mount | grep bpf                          # 确认 bpffs 已挂载
sysctl -w kernel.unprivileged_bpf_disabled=1   # 禁止非特权用户加载
bpftrace --info | head -20                # 查看内核版本与 BTF 支持
bpftrace -l 'tracepoint:syscalls:sys_enter_execve'   # 确认钩子可用

kernel.unprivileged_bpf_disabled=1 必须写入 /etc/sysctl.d/99-ebpf.conf 持久化,否则普通用户可加载 eBPF 程序,形成新的提权面。

步骤 2:采集进程执行事件

# /opt/ebpf/exec.bt —— 记录全部进程执行,含父进程与可执行文件路径
tracepoint:syscalls:sys_enter_execve
/args->filename != 0/
{
  printf("EVENT=exec ts=%s pid=%d ppid=%d comm=%s file=%s\n",
         strftime("%Y-%m-%dT%H:%M:%S", nsecs), pid, curtask->real_parent->pid,
         comm, str(args->filename));
}

运行:

bpftrace -q /opt/ebpf/exec.bt          # -q 抑制启动横幅

若内核不支持读取 real_parent(BTF 缺失),改用 pid 与 comm 即可,父进程信息可由日志平台关联补充。

步骤 3:聚焦 Web 进程派生的可疑命令

# /opt/ebpf/webexec.bt
tracepoint:syscalls:sys_enter_execve
/comm == "php-fpm" || comm == "nginx" || comm == "apache2" || comm == "www-data"/
{
  printf("EVENT=webexec ts=%s pid=%d comm=%s cmd=%s\n",
         strftime("%Y-%m-%dT%H:%M:%S", nsecs), pid, comm, str(args->filename));
}

这条规则的信噪比最高。Web 服务进程正常情况几乎不会主动执行外部命令,一旦出现 /bin/sh、curl、wget、nc 就要立刻跟进。若应用确实有合理的命令调用(如图片处理),把允许列表写进过滤条件:

tracepoint:syscalls:sys_enter_execve
/comm == "php-fpm" && str(args->filename) !~ /^\/(usr\/bin|usr\/local\/bin)\/(convert|ffmpeg)$/
{
  printf("EVENT=webexec-suspect ts=%s pid=%d cmd=%s\n",
         strftime("%Y-%m-%dT%H:%M:%S", nsecs), pid, str(args->filename));
}

步骤 4:监控敏感文件写入

# /opt/ebpf/filewrite.bt —— 只关心写打开且路径命中敏感目录
tracepoint:syscalls:sys_enter_openat
/(args->flags & 3) != 0/
/str(args->filename) ~ /^\/etc\/(passwd|shadow|sudoers|sudoers\.d|ssh\/)/
 || str(args->filename) ~ /^\/(root|home\/[^\/]+)\/\.ssh\//
 || str(args->filename) ~ /^\/etc\/cron\.(d|daily|hourly)/
 || str(args->filename) ~ /^\/var\/spool\/cron\//
 || str(args->filename) ~ /^\/etc\/(ld\.so\.preload|systemd\/system\/)/
{
  printf("EVENT=filewrite ts=%s pid=%d comm=%s file=%s\n",
         strftime("%Y-%m-%dT%H:%M:%S", nsecs), pid, comm, str(args->filename));
}

(args->flags & 3) != 0 用于筛出 O_WRONLY 与 O_RDWR,避免只读访问产生噪声。上面覆盖了账号文件、SSH 授权、计划任务与动态链接器劫持点位,这几处是持久化最常落脚的目录。

步骤 5:监控异常外联

# /opt/ebpf/connect.bt —— 记录新建的对外连接
tracepoint:sock:inet_sock_set_state
/args->oldstate == 2 && args->newstate == 1/
{
  printf("EVENT=connect ts=%s pid=%d comm=%s %s:%d -> %s:%d\n",
         strftime("%Y-%m-%dT%H:%M:%S", nsecs), pid, comm,
         ntop(args->family, args->saddr), args->sport,
         ntop(args->family, args->daddr), args->dport);
}

结合 comm 过滤可快速定位后门:/bin/sh、bash、python3 发起的新连接风险明显高于 nginx。若内核缺少 sock:inet_sock_set_state,可退回 kprobe:tcp_connect 并自行解析 sock 结构。

步骤 6:持久化与日志接入

# /etc/systemd/system/ebpf-audit.service
[Unit]
Description=eBPF runtime audit collectors
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/bpftrace -q /opt/ebpf/exec.bt
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now ebpf-audit
journalctl -u ebpf-audit -f

# 若需落盘到独立文件,用 -o 参数替代 journal
# bpftrace -q -o /var/log/ebpf/exec.log /opt/ebpf/exec.bt

生产环境建议每个采集脚本单独一个 unit,便于按需启停与评估开销。

配置验证

# 1. 观察已加载的 eBPF 程序
bpftool prog list | grep -E 'name|tag'

# 2. 触发一次进程执行,确认事件被采集
/usr/bin/logger "ebpf baseline"
journalctl -u ebpf-audit -n 5 --no-pager

# 3. 触发一次敏感文件写,确认告警命中
touch /etc/cron.d/ebpf-selftest
# 终端应出现 EVENT=filewrite ... file=/etc/cron.d/ebpf-selftest
rm -f /etc/cron.d/ebpf-selftest

# 4. 核对丢事件情况(内核态缓冲区不足会打印 Lost N events)
journalctl -u ebpf-audit | grep -i 'lost'

若出现 Lost events,优先用 -B 增大缓冲区(如 bpftrace -B 8m),并检查过滤条件是否过于宽松导致事件量超出处理能力。

常见问题

FAQ 1:提示 Could not resolve symbol 或 BTF 相关错误

说明内核缺少调试信息。依次检查:ls /sys/kernel/btf/vmlinux 是否存在;bpftrace --info 输出的 BTF 行是否为 yes。若缺失,可升级内核,或改动脚本避免访问结构体字段(只使用 tracepoint 提供的参数与 comm、pid 这类通用值)。部分发行版需要额外安装 kernel-debuginfo 或 linux-image-*-dbgsym 才能启用 kprobe 场景。

FAQ 2:开启后业务延迟升高,开销如何评估

开销主要来自事件量与用户态输出,不来自钩子本身。评估方法:先用单条规则灰度,用 stress-ng --cpu 4 --timeout 60s 做基线对比,记录开启前后的 CPU steal 与 /usr/bin/time 结果。控制手段包括:把过滤条件尽量前移(内核态过滤优于用户态 grep)、避免对每个事件都做字符串拼接、把 printf 频率降为按事件聚合,以及在非高峰时段才开启文件写入监控。经验区间是命中率低且规则简单时开销在 1% 以内,全量 execve 加字符串处理在 5% 上下。

FAQ 3:为什么 bpftrace 采到的事件比 auditd 少

两者用途不同。auditd 在 syscall 入口与退出两个点都记录,且保留完整审计上下文与结果码;bpftrace 脚本通常只在单一钩子上取参,且受内核态缓冲区与用户态处理速度限制,高并发场景会出现丢事件。正确做法是分工:bpftrace 负责高信噪比的针对性检测(Web 派生进程、敏感文件写、异常外联),auditd 或 journald 负责基础留痕;两者输出汇入同一日志平台,用 pid 与时间戳做关联。

总结

  • eBPF 的核心优势是内核态过滤加完整进程上下文,事件质量高于纯日志匹配。
  • 四条高性价比规则:全部 execve、Web 进程派生命令、敏感路径写入、新建对外连接。
  • 部署前必须持久化 kernel.unprivileged_bpf_disabled=1,并确认 bpffs 已挂载。
  • 用 systemd 托管脚本,输出进 journald,配合 bpftool prog list 与丢事件统计做运维核查。
  • 不要把 eBPF 当作唯一审计手段,与 auditd、journald 形成互补才是稳妥方案。