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 形成互补才是稳妥方案。