eBPF的介绍
2022-12-5 08:2:37 Author: 奶牛安全(查看原文) 阅读量:13 收藏


钩子简介

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检验器确保程序本身的安全性。

这意味着:

  1. 程序必须是会运行结束,不允许阻塞或无限循环。程序里可以包含循环,但必须要有一个必然会出现的退出条件。
  2. 程序不能使用未初始化变量或越界访问内存
  3. 程序大小必须要满足系统要求,不允许有任意大小的eBPF程序
  4. 程序的复杂性必须有限。检验器在配置复杂度阈值内能够评估所有执行路径和完成分析

加固

校验结束后,无论eBPF是由特权进程或非特权进程加载,都需要经过一个加固流程。步骤包括:

  1. 程序运行保护:存放eBPF程序的内核内存必须是保护和可读。在任何情况,无论是内核缺陷还是恶意操作来修改这块内存,都会引起内核崩溃,而不是继续执行。
  2. 缓解指令幽灵:在指令预测情况下,CPU往往会错误预测分支,并且留下可通过旁路方式获取的可见副作用。比如:eBPF程序会屏蔽内存访问,以便将临时指令访问重定向到受控区域,检验器也会跟踪那些只会在指令预测情况下的分支,并且在接龙调用无法转换成直接调用情况下,JIT编译器会发现Retpolines(一种避免指令幽灵漏洞的方法)
  3. 屏蔽常量:代码中的所有常量都是不可见的,以防止JIT喷射攻击。这可以防止攻击者将可执行代码作为常量插入,在出现另一个内核缺陷时,可能允许攻击者跳入eBPF程序的内存区来执行代码。

抽象运行时上下文

eBPF程序无法直接访问任意内核内存。访问位于程序上下文之外的数据和对象,需要通过eBPF帮助函数访问。这保证了数据一致性访问,也遵循了eBPF的程序权限,比如,如果数据修改保证是安全的,一个eBPF程序是允许去修改的。一个eBPF程序是不允许任意修改内核的数据。

暗号:56015


文章来源: http://mp.weixin.qq.com/s?__biz=MzU4NjY0NTExNA==&mid=2247487138&idx=1&sn=5a78a10595df3bd8a60ef1200c69d646&chksm=fdf965b7ca8eeca14b2a647d098b81dca8ea969ca78291ef9677a867fdf723914bd5dd495da2#rd
如有侵权请联系:admin#unsafe.sh