钩子简介
eBPF
程序是事件驱动,当内核或用户态程序经过一定的钩子点时运行。预定义的钩子包括系统调用、函数入口/出口、内核跟踪点、网络事件等。
如果特殊场景不存在预定义钩子,ebpf
程序基本上可以通过kprobe
挂钩到内核任何地址或uprobe
挂钩到用户态程序任何地址。
eBPF
程序如何编写
在很多场景下,eBPF
并不是直接使用,而是通过一些项目如Cilium
,bcc
,bpftrace
来间接使用。这些项目在eBPF
之上进行抽象,使得不需要直接编写eBPF
程序,而是提供一些基于目标定义的能力,然后由eBPF
实现这些定义。
如果不存在上层抽象,eBPF
就需要直接编写。Linux
内核要求eBPF
程序以一种字节码的方式加载。这将是非常痛苦。如果确实是需要直接写eBPF
字节码,通常的开发实践是利用像LLVM
这样的编译器把伪C
代码编译成eBPF
字节码。
加载和校验架构
当需要的钩子已经选好了,eBPF
程序可以通过系统调用bpf
加载到Linux
内核。这一般是通过某个可用的eBPF
库来实现。下一节介绍一些可用的开发工具链。
当从eBPF
程序加载到Linux
内核到附加在指定的钩子点前,会经过下面两个步骤:
校验
eBPF
安全运行。它检验程序符合某些条件,如:
-
加载 eBPF
程序到内核的进程拥有相应的权限。如果关闭非特权eBPF
开关,只有特权进程才允许加载eBPF
程序 -
eBPF
程序不能弄崩或破坏系统 -
eBPF
程序必须能够运行结束,意味着程序不能存在不终结的循环。
JIT
编译
JIT
编译步骤把eBPF
字节编译成机器码来优化运行速度,使得eBPF
程序运行效率和内核代码或内核模块代码一样。
映射
eBPF
程序的一个重要特性是共享收集的信息和存储状态的能力。为此,eBPF
程序可以利用eBPF
映射的概念来存储和检索各种数据结构中的数据。eBPF
映射可以由eBPF
程序访问,也可以由用户态程序通过系统调用访问。
为了了解映射所支持数据结构的多样性,下面是一个不完整的列表,每种映射类型都适用于CPU
内或CPU
之间共享
-
hash
表,数组 -
LRU
列表 -
环形缓存 -
栈 -
LPM
-
...
帮助函数
eBPF
程序不可以调用任意内核函数。因为这种方式会使得它和特定内核版本绑定,从而增加兼容性的复杂度。相反,eBPF
程序通过调用由内核提供通用稳定的帮助函数来实现这样能力。
可用的帮助函数系列一直在演变,相应的例子有:
-
生成随机数 -
获取当前时间和日期 -
访问 eBPF
映射 -
获取进程或 cgroup
的上下文 -
操作网络包和转发逻辑 -
接龙和调用
eBPF
程序可以通过接龙和函数调用来组合。
-
函数调用允许在一个 eBPF
程序内定义和调用函数 -
接龙允许一个 eBPF
程序调用另外一个eBPF
程序,并替换当前执行上下文,类似execve
系统调用
eBPF
安全性
《功夫》里阿鬼说的“能力越大,责任越大”
eBPF
是一种难以置信的强大技术,并且是运行在很多关键基础组件的核心。在eBPF
的演变中,当引入它到Linux
内核时,安全性是最关键的考虑因素。eBPF
安全通过某些层次来确保:
必要的特权
在关闭非特权eBPF
开关情况下,所有要把eBPF
程序加载到Linux
内核的进程必须要运行在特权模式(root
)或者要有CAP_BPF
能力。这意味着不可信程序不能加载eBPF
程序。
如果非特权eBPF
开关开启,非特权进程可以加载部分功能受限制的eBPF
程序,且只能有限访问内核
检验器
即使一个进程允许加载eBPF
程序,所有eBPF
程序还要经过检验器的检查。一个eBPF
检验器确保程序本身的安全性。
这意味着:
-
程序必须是会运行结束,不允许阻塞或无限循环。程序里可以包含循环,但必须要有一个必然会出现的退出条件。 -
程序不能使用未初始化变量或越界访问内存 -
程序大小必须要满足系统要求,不允许有任意大小的 eBPF
程序 -
程序的复杂性必须有限。检验器在配置复杂度阈值内能够评估所有执行路径和完成分析
加固
校验结束后,无论eBPF
是由特权进程或非特权进程加载,都需要经过一个加固流程。步骤包括:
-
程序运行保护:存放 eBPF
程序的内核内存必须是保护和可读。在任何情况,无论是内核缺陷还是恶意操作来修改这块内存,都会引起内核崩溃,而不是继续执行。 -
缓解指令幽灵:在指令预测情况下,CPU往往会错误预测分支,并且留下可通过旁路方式获取的可见副作用。比如: eBPF
程序会屏蔽内存访问,以便将临时指令访问重定向到受控区域,检验器也会跟踪那些只会在指令预测情况下的分支,并且在接龙调用无法转换成直接调用情况下,JIT
编译器会发现Retpolines
(一种避免指令幽灵漏洞的方法) -
屏蔽常量:代码中的所有常量都是不可见的,以防止JIT喷射攻击。这可以防止攻击者将可执行代码作为常量插入,在出现另一个内核缺陷时,可能允许攻击者跳入 eBPF
程序的内存区来执行代码。
抽象运行时上下文
eBPF
程序无法直接访问任意内核内存。访问位于程序上下文之外的数据和对象,需要通过eBPF
帮助函数访问。这保证了数据一致性访问,也遵循了eBPF
的程序权限,比如,如果数据修改保证是安全的,一个eBPF
程序是允许去修改的。一个eBPF
程序是不允许任意修改内核的数据。
暗号:56015
原文始发于微信公众号(奶牛安全):eBPF的介绍
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论