上一篇讲了"凌晨 3 点拦下勒索病毒"的事情。 很多人问:这玩意到底靠不靠谱?是真能落地,还是 PPT 架构? 这套方案不是万能的,有部分限制。 今天把原理拆开,从内核到 Rust,讲清楚它为什么可行、值不值得做。
靠谱。而且已经是个成熟的技术路线,不是实验室玩具。
eBPF 做主机安全,Meta、Netflix、Google 都在生产环境大规模用。防勒索只是其中一个相对简单的应用场景。
但"靠谱"不等于"随便写写就行"。它靠谱,是因为底层有几条硬邦邦的原理在撑着。下面逐条拆。
要拦勒索病毒,第一步是"看见"它在干什么。
传统监控在用户空间,能看到的是操作系统"愿意让你看的"。恶意程序提个权、走个非常规路径,你就看不见了。
eBPF 不一样,它直接跑在内核里,挂在系统调用的入口。
任何进程想读文件、写文件、删文件、改名,都得经过 syscall。而 eBPF 就蹲在 syscall 的门口——不管你是 root 还是普通用户,不管你提权还是没提权,只要你动文件,我就看得见。
而且内核敢让你跑代码,是因为 eBPF 有个叫 Verifier 的"守门员":
Verifier 在加载时就把关,不通过就不让你加载。所以 eBPF 程序不会搞崩内核。 这也是它能进入生产环境的根本前提。
具体到"监控文件读写",我们挂这几类钩子(以 syscall tracepoint 为例):
sys_enter_openat —— 任何打开/创建文件sys_enter_write —— 任何写操作sys_enter_rename —— 任何重命名(勒索改后缀的关键动作)sys_enter_unlink —— 任何删除在每个钩子里,我们能拿到的信息包括:当前进程 PID、进程名(comm)、被操作文件的路径、本次写入的字节数。
关键问题来了:这些信息怎么从内核传到用户态的监控程序?
答案是 Map。Map 是 eBPF 里内核态和用户态通信的桥梁,本质是一块内核管理的共享内存。我们用 PerfEventArray 或 RingBuf 这种 Map,把每一条文件事件"投递"到用户空间。
// 内核 eBPF 侧:把事件丢进 Map
#[map]
static mut EVENTS: PerfEventArray<FileEvent> =
PerfEventArray::with_max_entries(1024, 0);
#[tracepoint(name = "sys_enter_write")]
pub fn on_write(ctx: TracePointContext) -> u32 {
let pid = unsafe { bpf_get_current_pid_tgid() >> 32 };
let event = FileEvent { pid, op: OP_WRITE, bytes, .. };
unsafe { EVENTS.output(&ctx, &event, 0); }
0
}// 用户态 Rust 侧:从 Map 里消费事件
let perf = AsyncPerfEventArray::try_from(bpf.map("EVENTS"))?;
let mut buf = perf.open_async()?;
while let Some(event) = buf.next().await {
// 送进检测引擎
}内核只负责"看见 + 投递",判断逻辑全在用户态 Rust 里跑。职责分离,各干各的擅长的事。
这是整个方案可行性的核心。
很多人觉得:病毒会变种啊,你今天能拦,明天它换个加密算法不就绕过了?
但关键在于:加密这个动作本身,是物理上无法隐藏的。
不管你用什么加密算法、怎么加壳、怎么混淆,要加密一个文件,你都必须:
.locked 之类这三步,每一步都是 syscall。读、写、删、改名,全部暴露在 eBPF 面前。
更致命的是第二步的规模特征:正常进程偶尔写几个文件,勒索病毒要在几秒内加密成百上千个文件。这种"短时间内海量文件操作"的统计异常,不需要匹配任何特征库就能识别。
所以结论很清晰:勒索病毒无论怎么改算法、怎么免杀,只要它要在你的文件系统上动文件,就一定会被 eBPF 看见,而且一定会表现出异常的行为模式。
这就是"行为检测"比"特征检测"更抗变种的原因。
如果只做到"看见 + 报警",还不够。报警了,人没反应过来,文件已经没了。
真正有价值的,是"拦住"。
内核 5.7 之后,eBPF 支持挂到 LSM(Linux Security Module) 钩子上。LSM 是内核的安全决策点——每个敏感操作在执行前,都会问一遍 LSM:"准不准?"
我们挂一个 LSM BPF 程序,逻辑是:
#[lsm(hook = "path_rename")]
pub fn on_rename(ctx: *const c_void, old: *const path, new: *const path) -> i32 {
let pid = current_pid();
if is_flagged(pid) { // 这个进程已被判定为勒索风险
return -EPERM; // 直接拒绝改名,加密无法进行
}
0 // 放行
}返回 -EPERM,内核就直接拒绝这次 rename。病毒想把 a.docx 改成 a.docx.locked?不行,内核不让。
这是预防性阻断,不是事后报警。 病毒在真正加密之前就被按住了。
(注:LSM BPF 需要较新内核,5.7+。老内核上可以退化为"检测 + 用户态 kill 进程"的方案,效果稍弱但也够用。)
最后说为什么整个组件用 Rust 写。
内核侧用 Aya。 Aya 是纯 Rust 的 eBPF 工具链,不用写一行 C,不用装 LLVM/clang 混合环境,直接 cargo build 出 eBPF 字节码。工程体验比传统 C + libbpf 清爽太多。
用户态更离不开 Rust。
一是安全软件自身不能成为攻击面。你的监控 agent 跑在被保护的主机上,它自己要是能被攻破,那等于给攻击者开了后门。Rust 没有内存安全漏洞(use-after-free、缓冲区溢出这类),意味着恶意程序很难通过 agent 反攻。
二是性能。事件吞吐可能每秒上万条,用户态要实时聚合、滑动窗口统计、规则匹配。Rust 零成本抽象 + 无 GC 停顿,能保证稳定低延迟。
三是生态。Aya、libbpf-rs、tokio 异步运行时,工具链齐备,落地没有坑。
回到标题的问题。把价值拆成四层:
第一层:不可绕过。 监控点在内核 syscall 入口,比攻击点更底层。攻击者只要还在用系统调用,就逃不掉。这是所有价值的前提。
第二层:实时。 syscall 级别捕获,毫秒级响应。从第一次异常写入到进程被杀,200ms 内完成。病毒只来得及动十几个文件,而非几千个。
第三层:低开销。 内核态全量监控,不采样、不丢事件,CPU 占用 < 3%。对比用户态 HIDS 为了性能只能抽样,eBPF 是"全量且廉价"。
第四层:可演进。 检测逻辑是行为规则,不是特征库。新变种出来,更新规则即可,不依赖厂商出签名。你对自己的防御有完全的控制权。
写技术文章,最怕的是把方案吹成万能的。的确本文介绍的方案也不是万能的。
诚实的说几个边界:
但作为"最后一道、最接近文件系统的防线",它的性价比极高。
勒索病毒最可怕的地方,不是它技术多高深,而是它粗暴且有效——批量加密,然后要钱。
而 eBPF 恰好克制它的粗暴:你批量,我就看得到批量;你改名,我就看得到改名;你要写,我就拦在写之前。
技术实现上,这是一条成熟、可落地、有生态支撑的路线。不是 PPT 架构,是 Meta、Netflix、Google 都在用的东西,只不过我们把它用在了防勒索这个具体场景上。
看见底层,才能守住底层。