技术博客_eBPF内核可编程采集

把过滤推进内核:eBPF 为什么正在取代 auditd

摘要:本文是「命令审计」系列的收官篇,从 auditd 在容器时代的两大结构性痛点(无容器元数据、过滤在用户态)切入,系统讲解 eBPF 是什么、过滤位置为何决定性能数量级、五件套工作原理(程序类型 / Map / Verifier / BTF 与 CO-RE / 用户态框架),以及它为何天生适合容器。随后梳理 Falco、Tetragon 等成熟生态与内核态阻断能力,并逐条拆解内核版本门槛、Verifier 拒绝、事件静默丢弃、eBPF 自身攻击面四类坑。最后给出根本边界------内存马不产生 execve,四层采集全部失效,需靠网络层、Web 日志、内存检测补位,并附选型决策表与一分钟自查清单。
------命令审计系列之四(收官篇):当采集本身变成可编程的
系列前作:

  • 之一《TTY 为空的命令,为什么最危险?》------ 能被伪造的字段只能做线索
  • 之二《实时 ≠ 可靠:为什么 PROMPT_COMMAND 挡不住一行 unset》------ 能被一条命令关掉的采集源只能做线索
  • 之三《内核里的黑匣子:auditd 如何看见 root 也改不掉的事》------ 分层让线索升级为证据

这篇是收官。前三篇从字段、采集源、层级三个角度逼近同一个结论,这一篇给出它的工程终局:如果内核层才是正确答案,那为什么不干脆把判断逻辑也放进内核?


一个消失在容器里的进程

告警:某 K8s 节点上有个容器在对外挖矿。

值班同学立刻去查宿主机的 auditd 日志,很快找到了那条命令:

复制代码
type=SYSCALL ... pid=28471 ppid=28450 uid=0 comm="xmrig" exe="/tmp/.x/xmrig"
type=CWD  ... cwd="/app"

PID 有了,命令有了,路径有了。然后卡住了两个问题:

第一个问题:这个 PID 属于哪个 Pod?

auditd 在宿主机视角工作,日志里只有宿主机 PID。要反查得手工去 /proc/28471/cgroup 里抠容器 ID,再拿容器 ID 去问 containerd,再拿 container ID 去问 K8s 查 Pod 名和命名空间。

等他走完这套流程,容器已经被销毁重建了。 攻击者用的是短生命周期的 Pod,跑完就删,PID 28471 早就不存在,/proc/28471/ 打不开,链路断死。

第二个问题:这个集群有一千个节点,为什么只在出事之后才查?

因为 auditd 的日志量扛不住------全集群开 execve 全量审计,日均几十亿条,日志平台先崩。所以实际只开了关键节点。而这次出事的,恰好不在"关键节点"名单里。

这个困境是 auditd 在容器时代的结构性痛点,不是配置问题:

  1. 没有容器元数据 ------ 它不知道 Pod 是谁、镜像是哪个、命名空间是什么
  2. 过滤在用户态 ------ 内核全量吐,扛不住就只能在覆盖面上妥协

而这两点,恰恰是 eBPF 的全部优势所在。


一、eBPF 是什么:内核里的可编程沙箱

eBPF = extended Berkeley Packet Filter。 名字里还留着 "Packet Filter",但它早就和网络抓包没什么关系了。

一句话定义:

eBPF 让你在不改内核源码、不加载内核模块的前提下,把一段自己写的程序安全地送进内核去执行。

这个能力有多颠覆?在它之前,想在内核里加点自己的逻辑只有两条路:

老路子 问题
改内核源码重新编译 不可能,每家公司的内核都不一样
写内核模块(LKM) 写错一行直接 kernel panic,整个系统崩给你看

eBPF 解决了这个死结------它给内核加了一个沙箱虚拟机 :你的程序进去之前,先由一个叫 Verifier(验证器) 的组件做静态分析,确认它绝对不会死循环、绝对不会越界访问、绝对不会把内核搞崩。通不过就拒绝加载。

所以 eBPF 的本质是:把"内核可编程"从高危操作变成了安全操作。

这也解释了它在安全领域的爆发------安全恰恰是最需要在内核里做观测、又最不敢动内核的领域。


二、核心差异:过滤在哪一层

回到系列第三篇留下的那个结论:auditd 的阿喀琉斯之踵是过滤在用户态。

对比一下数据流向:

auditd 的路径:

复制代码
系统调用发生
  → 内核埋点,收集全部上下文(UID/PID/路径/参数...)
  → 格式化成文本
  → netlink 传到用户态
  → auditd 收到,再按规则过滤
  → 写盘

代价发生在前三步。 哪怕这条事件最后被规则丢弃了,内核也已经为它付出了采集、格式化、传输的全部开销。

eBPF 的路径:

复制代码
系统调用发生
  → 内核埋点,执行你的 eBPF 程序
  → 【在内核里就判断:是不是监控 agent 自己?是不是系统服务?要不要?】
  → 不要 → 直接丢弃,零后续开销
  → 要 → 只把需要的字段放进 map / ringbuf
  → 用户态读取

差别就在那个中括号。 判断发生在内核态,不想要的事件连格式化和传输的成本都不会产生。

这是性能差一个数量级的根本原因,也是整个系列的工程终局。


三、工作原理:五件套

理解 eBPF 落地,需要认识五个东西:

① 程序类型(挂在哪)

类型 挂载点 特点
kprobe / kretprobe 任意内核函数的入口/返回 最灵活,但依赖函数名,内核升级可能失效
tracepoint 内核预定义的静态埋点 稳定,推荐
raw_tracepoint tracepoint 的裸参数版 开销更小
fentry / fexit (5.5+) 基于 BTF 的函数入口/出口 性能最好,现代内核首选
LSM hook (5.7+) security_* 安全钩子 安全审计的理想挂载点

做命令审计,常用的三个埋点:

复制代码
sys_enter_execve     ← 系统调用进入时(拿不到返回值)
sched_process_exec   ← 进程成功 exec 后触发(信息最全,推荐)
security_bprm_check  ← LSM 钩子,权限检查点

生产上首推 sched_process_exec------它在 exec 成功后触发,能拿到完整的进程信息和参数,且是稳定 tracepoint,不依赖具体函数名。

② Map(数据怎么出来)

eBPF 程序不能直接往用户态写,必须通过一个叫 map 的内核态数据结构交互:

map 类型 用途
BPF_MAP_TYPE_HASH 存状态(比如进程树、黑名单)
BPF_MAP_TYPE_PERF_EVENT_ARRAY 事件流(老方案,每 CPU 一个缓冲,可能乱序)
BPF_MAP_TYPE_RINGBUF (5.8+) 事件流(新方案,全局共享,保序,推荐)

ringbuf 是现在的主流选择,它解决了 perf buffer 的两个老问题:每 CPU 独立缓冲导致的乱序、以及内存浪费。

③ Verifier(为什么它不会搞崩内核)

加载前做静态分析,检查:

  • 有没有不可达指令
  • 会不会越界访问内存
  • 循环是否有明确上界(5.3+ 支持 bounded loop)
  • 栈使用是否超限(512 字节)
  • 指令数是否超限(早期 4096,新内核已大幅放宽)

这是 eBPF 敢用在生产内核上的全部底气。

④ BTF 与 CO-RE(解决"换个内核就跑不了")

这是 eBPF 从"玩具"变成"生产可用"的关键一步。

老问题:eBPF 程序要读内核结构体(比如 task_struct),但不同内核版本的结构体字段偏移不一样。用 kprobe 的老方案,换个内核版本就得重新编译,甚至直接失效。

BTF(BPF Type Format) 把内核的类型信息直接编译进内核镜像,就像一个自带的内核头文件。CO-RE(Compile Once, Run Everywhere) 基于 BTF 做重定位------一次编译,到处能跑。

要求内核 5.2+ 带 BTF,现在主流发行版基本都满足。

⑤ 用户态框架

框架 语言 特点
BCC Python + C 上手快,自带大量工具,但运行时编译,部署重
bpftrace 自有 DSL 一行命令搞定简单观测,排查神器
libbpf / Cilium ebpf C / Go 生产级,CO-RE,无运行时依赖
cilium/ebpf (Go) Go 安全 Agent 的主流选择

生产环境的安全 Agent 一般用 cilium/ebpf(Go),因为可以编译成单个静态二进制,不依赖目标机器的编译器和头文件。


四、为什么它天生适合容器

这是 eBPF 在云原生时代碾压 auditd 的地方------它能在内核里直接拿到容器的上下文。

auditd 的困境

auditd 日志里给你一个宿主机 PID,然后你得自己找:

bash 复制代码
cat /proc/28471/cgroup
# → 12:pids:/kubepods/burstable/pod3f2a.../a1b2c3d4...
# 然后拿 a1b2c3d4 去 crictl 查,再拿 container id 去 kubectl 查 Pod

而且这个反查是"事后"的 ------容器一旦销毁,/proc/<pid>/ 就没了,链路断死(就是开篇那个场景)。

eBPF 的做法

在内核里,通过 bpf_get_current_task() 拿到当前进程的 task_struct,可以直接读出:

  • cgroup ID → 反查容器
  • PID namespace → 判断在哪个 namespace 里
  • 配合用户态程序预加载的容器元数据(通过 informer 监听 K8s API 拿 Pod 列表,写进 map)

于是事件产生时,Pod 名、命名空间、容器名、镜像名就已经富化在事件里了。

复制代码
{
  "event": "execve",
  "cmd": "/tmp/.x/xmrig",
  "pid": 28471,
  "pod": "payment-api-7d9f8",
  "namespace": "prod",
  "container": "app",
  "image": "registry.internal/payment-api:v2.1.3",
  "node": "node-07"
}

注意最后一行的价值 :这已经不是一条"日志",而是一条带完整上下文的可行动告警。分析师不需要做任何关联查询,直接就能判断"这是 prod 环境的支付服务在跑挖矿"。

从"记录"到"可行动",这一步富化是关键。 这正是系列第一篇强调的"字段上限决定分析上限"的终极体现。


五、生态:不要从零写

除非你是做安全产品,否则不要自己写 eBPF 探针,直接用成熟项目:

项目 出品方 特点
Falco Sysdig → CNCF(已毕业) 事实标准,规则语言强大,容器元数据原生支持
Tetragon Isovalent / Cilium 基于 BTF,性能极好,K8s 场景设计,可做到内核态阻断
Tracee Aqua Security 事件覆盖全面,取证友好
Pixie New Relic 偏可观测性

Falco 规则长什么样

yaml 复制代码
- rule: 容器中执行 shell
  desc: 检测容器内启动了交互式 shell
  condition: >
    spawned_process and container and
    proc.name in (bash, sh, zsh, dash) and
    not proc.pname in (entrypoint, run.sh)
  output: >
    容器内启动 shell (user=%user.name pod=%k8s.pod.name
    ns=%k8s.ns.name image=%container.image.repository cmd=%proc.cmdline)
  priority: WARNING

注意 output 里那些字段 :%k8s.pod.name、%k8s.ns.name、%container.image.repository------这些是 Falco 从内核拿到 cgroup 后,自动关联 K8s API 富化出来的。

这整段配置,auditd 一句话都做不到。

Tetragon 的独门能力:内核态阻断

Falco 主要是检测 ,Tetragon 能把 LSM hook 用起来做内核态阻断:

yaml 复制代码
# 直接阻止某个进程执行,而不是"执行完再告警"
- call: "security_bprm_check"
  args:
    - index: 0
      type: "linux_binprm"
  selectors:
    - matchBinaries:
        - operator: "In"
          values: ["/usr/bin/nc", "/usr/bin/ncat"]
      matchActions:
        - action: "Sigkill"     # ← 内核里直接杀掉

这是前几篇所有方案都做不到的:在命令真正执行之前,在内核里就把它拦下来。

这呼应了系列第二篇那个痛点------PROMPT_COMMAND 在"命令执行完"才记录,trap DEBUG 虽然能在执行前记录但拦不住。而 Tetragon 是在内核里执行前拦截。


六、限制与坑:别把它当银弹

eBPF 很强,但有几个必须知道的边界。

坑一:内核版本门槛

能力 最低内核
基础 eBPF 4.x
BTF / CO-RE 5.2+
ringbuf 5.8+
fentry/fexit 5.5+
LSM hook 5.7+

CentOS 7 的 3.10 内核基本用不了------这正是 auditd 至今无可替代的现实原因。

所以选型结论很朴素:新机器(Rocky 9、Ubuntu 22.04+)上 eBPF,老机器上 auditd,两者并存不是妥协,是现实。

坑二:Verifier 会拒绝你的程序

写复杂逻辑时最常遇到的挫败:

复制代码
libbpf: prog 'my_prog': BPF program load failed: Permission denied
...
R2 invalid mem access 'scalar'

原因通常是:栈超限(512 字节)、循环无上界、指针未校验就解引用。

解法:逻辑拆成多个程序、用 map 存中间状态、把复杂判断下放到用户态。

坑三:事件也会丢

这是最危险的坑,和 auditd 的 backlog 溢出是同一类问题。

ringbuf / perf buffer 满了会丢事件。而且默认不告警------你以为在审计,其实在漏。

必须监控丢弃计数,Tetragon 和 Falco 都暴露了相关 metrics:

promql 复制代码
# 看丢事件速率
rate(tetragon_events_total{type="lost"}[5m]) > 0

这跟 auditd 的 -b 缓冲区是同一个道理:任何采集系统,丢事件都是静默的,必须主动监控。

坑四:eBPF 自己也是攻击面

这一条最少有人讲,但很重要。

既然 eBPF 能在内核里执行代码,那攻击者当然也能用它:

  • 拿到 CAP_BPF 或 CAP_SYS_ADMIN 后,加载恶意 eBPF 程序
  • 用来隐藏进程、隐藏网络连接、劫持系统调用------也就是 eBPF rootkit
  • 现实中已经出现利用 eBPF 的恶意软件

更现实的威胁是:攻击者可以直接卸载你的探针。

应对:

bash 复制代码
# 1. 定期盘点内核里加载了哪些 eBPF 程序
bpftool prog list
bpftool prog show

# 2. 异常的程序(非本 Agent 加载的)要能发现
# 建议做成基线:正常状态有 N 个程序,多出来就告警

# 3. 开启内核 lockdown
# /sys/kernel/security/lockdown  → 阻止未签名程序加载

# 4. 收紧权限:不要给业务容器 CAP_BPF / CAP_SYS_ADMIN
#    K8s 里就是 securityContext.capabilities.drop: [ALL]

这恰恰说明了一个贯穿系列的道理 :没有哪一层是绝对安全的。内核层比应用层可靠,但内核层自己也有攻击面。所以永远要多源对账。


七、根本边界:内存马不产生 execve

这是整个系列的根本边界,必须在收官篇讲清楚。

前面四篇讲的所有采集源------bash_history、PROMPT_COMMAND、auditd、eBPF------盯的都是 execve 这个动作。

那么问题来了:

如果攻击根本不创建新进程呢?

Java 内存马就是典型:攻击者利用反序列化或注入漏洞,在已运行的 JVM 进程内 注册一个恶意 Filter 或 Controller。此后所有攻击行为都在这个进程内完成,一次 execve 都不产生。

采集源 能看见内存马吗
bash_history ❌
PROMPT_COMMAND ❌
auditd (execve) ❌
eBPF (execve) ❌

四层采集,全部失效。

这不是某一层的失败,是这条技术路线的边界------只要你的检测建立在"进程创建"这个信号上,绕过方式就是"不创建进程"。

所以命令审计永远不能是唯一的检测手段,必须配:

  • 网络层:内存马再隐蔽也要通信,网络流量镜像 / NetFlow / DNS 日志
  • Web 访问日志:内存马必然体现在异常的 URL 访问模式上
  • 文件完整性:如果它落了文件
  • 内存检测:JVM 内的 Filter/类扫描(比如专门的 Java 内存马检测工具)
  • K8s API Server 审计日志:独立证据源,攻击者拿单个节点权限改不了

回到系列第三篇那句话:任何一个单一数据源都可能有盲区,交叉验证才有韧性。

这句话在收官篇得到了最终验证------连最强的内核层采集都有盲区。


八、选型决策表

什么时候用什么,一张表说清楚:

场景 选择 理由
内核 5.8+、容器化环境 eBPF(Tetragon/Falco) 性能好、容器元数据原生、可阻断
内核 3.10(CentOS 7) auditd eBPF 跑不了
等保测评要求 auditd(可并存) 测评师认它
轻量临时排查 bpftrace 一行命令,不用部署
eBPF 装不了又想实时 PROMPT_COMMAND 降级兜底(见第二篇)
老系统实在没招 bash_history 有胜于无

生产推荐组合:

复制代码
内核层(eBPF 或 auditd)为主
  + PROMPT_COMMAND 走 syslog 独立通道做交叉验证(见第二篇)
  + 全部日志秒级外送到 WORM

注意第二行的价值 :它不提供新信息,但它提供独立通道。攻击者要同时干掉两条路径才能让日志干净,这显著提高了成本和暴露概率。


九、eBPF内核可编程采集一句话总括

"eBPF 的价值分两块。

第一是性能:它把过滤推进了内核。auditd 是内核全采、用户态再筛,不想要的事件也已经付过采集和传输的开销了;eBPF 在内核里就判断要不要,不要的直接丢弃。这是差一个数量级的根本原因。

第二是容器元数据 :它能在内核里通过 task_struct 直接拿到 cgroup 和 PID namespace,事件产生时就富化出 Pod 名、命名空间、镜像名,不用像 auditd 那样事后反查------而容器销毁后反查就断链了。

生产上我不自己写探针,用 Falco 或 Tetragon。Tetragon 还能挂 LSM hook 做内核态阻断,这个能力是应用层方案完全做不到的。

但它有边界:一是内核门槛,CentOS 7 的 3.10 用不了,所以 auditd 还得留着;二是eBPF 本身也是攻击面 ,拿到 CAP_BPF 就能加载恶意程序,所以要定期 bpftool prog list 做基线盘点;三是最根本的边界------内存马不产生 execve,四层采集全部失效,必须靠网络层、Web 日志、内存检测补位。"


总结:四篇收官,三句话

  1. 四层采集的演进,本质是"记录者离攻击者越来越远"。 bash_history 在同一个进程里、PROMPT_COMMAND 在同一个 shell 里、auditd 在内核里、eBPF 在内核里且可编程。离得越远,越难被篡改;可编程,才能把判断逻辑也搬进去。

  2. 性能优势的根源是过滤位置,不是埋点位置。 auditd 和 eBPF 都埋在内核,但一个在用户态筛、一个在内核态筛------这一个位置的差别,决定了数量级的差距。

  3. 没有银弹,连内核层都有盲区。 内存马不产生 execve,四层采集全军覆没。所以命令审计的定位是纵深感的一部分,不是全景。

系列四篇的完整主线:

  • 之一:能被伪造的字段,只能做线索(TTY)
  • 之二:能被一条命令关掉的采集源,只能做线索(PROMPT_COMMAND)
  • 之三:分层让线索升级为证据(auditd / auid)
  • 之四:把判断逻辑本身也推进内核,但依然要承认边界(eBPF)

四句话合起来,是 Linux 命令审计完整的设计哲学:

从"记录发生了什么",到"让记录的过程独立于被审计者的意志",再到"接受自己看不见的部分,用别的手段去覆盖它"。

最后这半句才是成熟------知道自己看不见什么,比看见什么更重要。


附录:一分钟自查清单

选型

  • 内核版本是多少?5.8+ 走 eBPF,3.10 走 auditd
  • 有没有等保要求?(有就 auditd 得留着)
  • 是否容器化?是则强烈倾向 eBPF

部署

  • 用的是成熟项目(Falco/Tetragon/Tracee)还是自研?(建议前者)
  • 探针是不是 DaemonSet 部署在每个节点?
  • 有没有开启 BTF/CO-RE?(避免内核升级后失效)

可靠性

  • 有没有监控丢事件计数?(ringbuf 满了会静默丢弃)
  • 日志有没有秒级外送到 WORM?
  • 有没有心跳断流检测?(Agent 被杀了要能发现)

自身安全

  • 业务容器有没有 drop 掉 CAP_BPF / CAP_SYS_ADMIN?
  • 有没有做 bpftool prog list 基线盘点?(发现非预期的 eBPF 程序)
  • 有没有考虑内核 lockdown?

边界认知

  • 有没有意识到内存马不产生 execve?
  • 有没有网络层、Web 访问日志、K8s apiserver 审计日志做补位?
  • 有没有在授权靶场真打一遍? 包括:普通反弹 shell、pty.spawn 升级、unset PROMPT_COMMAND、auditctl -D、Java 内存马------看看哪些能抓到、哪些是盲区

最后这条最实在 :把这张清单里的攻击手法,在授权环境里全部打一遍,然后列一张表------哪些告警了、哪些没告警、没告警的靠什么补。这张表就是你这套体系的真实能力地图。

相关推荐
Zkaisen1 天前
技术博客_PROMPT_COMMAND实时审计的幻觉
入侵检测·prompt_command·命令审计
爱好曙光4 天前
生产延迟尖刺排查:用 bpftrace 抓系统调用与调度延迟
linux·ebpf·延迟·性能排查·bpftrace
金色熊族7 天前
ebpf程序开发(一)--ubuntu20.04
linux·ebpf
mounter6252 个月前
从内核安全到可编程基础设施:Meta 的 eBPF 实践与形式化验证探索
linux·ebpf·linux kernel·kernel
mounter6252 个月前
eBPF 驱动的云原生网络变革:Cilium 的前世今生与全机套接字迭代技术演进
linux·ebpf·linux kernel·kernel
mounter6253 个月前
突破单点限制:BPF 多跟踪点高效附加机制与新 ftrace 架构解析
linux·ebpf·linux kernel·kernel·ftrace
mounter6253 个月前
深度解析 eBPF LSM:如何利用 eBPF 构建安全的 Linux 内核纵深防御体系
linux·安全·ebpf·linux kernel·kernel
mounter6253 个月前
认识 Tetragon:基于 eBPF 的安全监控与强制执行工具
linux·ebpf·cve·kernel
mounter6253 个月前
高性能网络技术演进与创新探索:RDMA、eBPF/XDP 深度解析及 LSF/MM/BPF 2023 专题演讲
linux·ebpf·linux kernel·kernel·rdma·xdp