3. 内核级监控:Windows内核回调机制、Linux eBPF技术、内核钩子原理
聊到EDR最核心的监控能力,我个人的看法是——必须得进内核。
用户态那点事儿,说白了就是“看门大爷”,能拦住普通小偷。但真正的攻击者,尤其是APT组织,人家一上来就提权、注入驱动、直接在内核层动手脚。你EDR如果只蹲在用户态,那基本等于睁眼瞎。
这一章,我带你看看EDR在内核层是怎么“安插眼线”的。Windows和Linux两套体系,思路完全不同,但目标一致:拿到系统最高权限的“监听权”。
3.1 Windows内核回调机制
Windows系统从Vista开始,微软就提供了一套非常“文明”的监控接口——内核回调。为什么说文明?因为它不需要你写驱动去Hook系统服务表(SSDT),也不需要你Inline Hook。你只需要注册一个回调函数,系统在特定事件发生时,就会主动调用你。
嗯,这里要注意:回调机制是微软官方支持的,所以稳定性相对好很多。我在做EDR产品时,第一版就全用的回调,没敢上钩子。
3.1.1 进程与线程回调
最常用的就是 PsSetCreateProcessNotifyRoutineEx 和 PsSetCreateThreadNotifyRoutine。注册之后,只要有新进程创建或退出,你的回调函数就会被调用。
我曾经遇到过一个坑:回调函数里不能做太重的操作,比如等待锁、分配大内存。否则系统会蓝给你看。EDR在这里一般只做“记录+快速判断”,真正的深度分析扔到用户态去。
// 伪代码示例:注册进程回调
NTSTATUS status = PsSetCreateProcessNotifyRoutineEx(MyProcessCallback, FALSE);
if (!NT_SUCCESS(status)) {
// 注册失败,通常是因为驱动签名问题或系统版本不兼容
KdPrint(("注册进程回调失败,错误码: 0x%X\n", status));
}
3.1.2 注册表与对象回调
除了进程,注册表也是攻击者的重灾区。比如修改启动项、关掉UAC、注入自启动服务。用 CmRegisterCallbackEx 可以监控所有注册表操作。
对象回调(ObRegisterCallbacks)则更底层。它能监控句柄的打开和复制操作。举个例子:你想知道谁在偷偷打开 lsass.exe 的进程句柄(典型的凭据窃取行为),对象回调就能抓到。
核心要点: Windows回调机制是EDR的“合法监听器”。它不修改内核代码,只是“订阅”事件。缺点是事件粒度有限,有些底层操作(比如直接读写物理内存)它抓不到。
3.2 Linux eBPF技术
Linux这边,情况就完全不同了。早期做Linux EDR,基本靠LKM(可加载内核模块)加Hook,或者用SystemTap。但这些方案要么太重,要么对内核版本太敏感。
直到eBPF出现。说实话,我第一次接触eBPF时,觉得这玩意儿简直是给安全工程师量身定做的。它让你能安全地在内核里跑一段沙箱化的代码,而且性能开销极低。
3.2.1 eBPF的工作原理
eBPF程序不是直接编译成内核模块,而是被编译成字节码。然后通过 bpf() 系统调用加载到内核。内核的验证器(Verifier)会严格检查这段代码:有没有死循环?有没有越界访问?确认安全后,再通过JIT编译成机器码执行。
你想想看,这相当于给内核装了一个“安全插件系统”。EDR可以挂载到各种内核事件上:系统调用、网络包、函数入口/出口。
我的经验: 在写eBPF程序时,一定要留意栈大小限制。eBPF的栈只有512字节,稍微复杂点的逻辑就得用map来传数据。我曾经因为一个局部变量数组太大,被Verifier直接拒绝加载,排查了半天。
3.2.2 常用eBPF Hook点
| Hook点 | 监控能力 | 典型攻击场景 |
|---|---|---|
| tracepoint/syscalls/sys_enter_execve | 进程执行 | 反弹Shell、恶意脚本执行 |
| kprobe/do_sys_open | 文件打开 | 敏感文件读取(/etc/shadow) |
| kprobe/tcp_v4_connect | 网络连接 | C2外连、数据外泄 |
| tracepoint/syscalls/sys_enter_socket | Socket创建 | 端口监听、反向连接 |
我个人习惯把eBPF程序分成两类:一类是“事件采集器”,只负责把原始数据塞进Perf Event Buffer;另一类是“快速过滤器”,在内核里直接做黑白名单判断,减少用户态压力。
3.3 内核钩子原理
聊完官方机制,咱们得说说“硬核”手段——内核钩子。为什么EDR还需要用钩子?因为回调机制覆盖不了所有场景。比如你想监控驱动加载、想拦截特定的IOCTL调用,回调就无能为力了。
3.3.1 SSDT Hook
SSDT(System Service Dispatch Table)是Windows内核里的一张表,存着所有系统调用的入口地址。攻击者常通过修改这张表来劫持系统调用。EDR反过来也可以用它来做监控。
举个例子:我把 NtCreateProcess 的地址换成我自己的函数。在我的函数里,先记录调用者信息,再调用原始函数。这样就能监控到所有进程创建行为。
警告: SSDT Hook在Windows 10 64位系统上非常危险。PatchGuard(内核保护)会定期检查SSDT的完整性,一旦发现被修改,直接触发蓝屏。我建议只在早期Windows版本或测试环境中使用。
3.3.2 Inline Hook
Inline Hook更“暴力”。它直接修改目标函数的前几个字节,跳转到你的Hook函数。这种技术不依赖SSDT,所以可以Hook任意内核函数。
但问题也很明显:
- 并发问题: 修改指令时,如果正好有别的CPU核心在执行这个函数,会崩溃。
- 兼容性: 不同Windows版本,函数开头指令可能不同(比如有无热补丁支持)。
- 检测风险: 攻击者也能通过扫描内存中的跳转指令来发现你的Hook。
我记得有一次,我在做驱动层Hook时,没处理好原子操作。结果在8核机器上,有3个核心同时触发了Hook点,直接导致系统挂起。从那以后,我所有Inline Hook都加上了 InterlockedExchange 和内存屏障。
3.4 知识体系结构图
下面这张图,是我梳理的内核级监控的三种技术路线。你可以看到,Windows和Linux各有侧重,但最终都汇聚到“事件采集”和“行为阻断”这两个目标上。
3.5 避坑指南与个人经验
做内核级监控,说白了就是在刀尖上跳舞。我总结几条血泪教训:
- 别在内核回调里做重操作。 我曾经在内核回调里写了个日志落盘,结果高并发下直接导致系统IO阻塞,所有进程创建都卡住了。后来改成异步队列,才解决问题。
- eBPF的Verifier很严格。 你的代码必须“可证明终止”。别想着在内核里做循环遍历,除非你能让Verifier相信循环次数是有限的。
- Hook之前先备份。 无论你是做SSDT Hook还是Inline Hook,一定要先保存原始指令。否则卸载驱动时恢复不了,系统就废了。
- 考虑多核并发。 内核代码随时可能被多个核心同时执行。你的Hook函数必须是可重入的,或者用自旋锁保护关键区。
我的建议: 如果你是刚开始做EDR内核模块,优先用Windows回调 + Linux eBPF。这两个是官方支持的路径,兼容性好,社区资料也多。等产品稳定了,再考虑用钩子做补充监控。别一上来就全上钩子,否则你会被各种蓝屏和内核Panic折磨到怀疑人生。
嗯,内核级监控这块,水很深。但只要你理解了这三种技术的本质——回调是“订阅”,eBPF是“沙箱化执行”,钩子是“劫持”——你就能根据实际场景灵活组合。下一章我们会聊到更细粒度的文件系统监控,到时候你会看到这些内核技术是怎么落地到具体检测场景的。