非常棒的一些材料
待看
- 基于eBPF监控和排查云原生环境中的磁盘IO性能问题
- 深度解密基于 eBPF 的 Kubernetes 问题排查全景图
- eBPF 已无处不在,而你还在使用 iptables?
- 一行代码:开启 eBPF,代替 iptables,加速 Istio
- 初识 eBPF,你应该知道的知识
- KubeCon 2021|使用 eBPF 代替 iptables 优化服务网格数据面性能
- eBPF 基本架构及使用
- eBPF是什么?为什么对可观测性很重要?
- 性能提升40%:腾讯TKE用eBPF绕过conntrack优化Kubernetes Service
学习路径
- 来自极客时间 《 eBPF 核心技术与实战》
- 其还有一本好书 《 Linux 性能优化实战》

eBPF
为什么会出现
eBPF 的出现本质上是为了解决内核迭代速度慢和系统需求快速变化的矛盾,在 eBPF 领域常用的一个例子是 eBPF 相对于 Linux Kernel 类似于 Javascript 相对于 HTML,突出的是可编程性。一般来说可编程性的支持通常会带来一些新的问题,比如内核模块其实也是为了解决这个问题,但是他没有提供很好的边界,导致内核模块会影响内核本身的稳定性,在不同的内核版本需要做适配等。eBPF 采用以下策略,使得其成为一种安全高效地内核可编程技术:
一般来说,我们编写的应用程序需要通过系统调用接口,来请求内核帮它做一些事情。
而使用 eBPF ,我们就能在不更改内核代码的前提下,实时获取和修改操作系统的行为
- 安全
eBPF 程序必须被验证器校验通过后才能执行,且不能包含无法到达的指令;eBPF 程序不能随意调用内核函数,只能调用在 API 中定义的辅助函数;eBPF 程序栈空间最多只有 512 字节,想要更大的存储,就必须要借助映射存储。
- 高效
借助即时编译器(JIT),且因为 eBPF 指令依然运行在内核中,无需向用户态复制数据,大大提高了事件处理的效率。
- 标准
通过 BPF Helpers,BTF,PERF MAP 提供标准的接口和数据模型供开发者使用。
- 功能强大
eBPF 不仅扩展了寄存器的数量,引入了全新的 BPF 映射存储,还在 4.x 内核中将原本单一的数据包过滤事件逐步扩展到了内核态函数、用户态函数、跟踪点、性能事件(perf_events)以及安全控制等领域。
可以用在哪
网络优化
eBPF 兼具高性能和高可扩展特性,使得其成为网络方案中网络包处理的优选方案:
- 高性能
JIT 编译器提供近乎内核本地代码的执行效率。
- 高可扩展
在内核的上下文里,可以快速地增加协议解析和路由策略。
故障诊断
eBPF 通过 kprobe,tracepoints 跟踪机制兼具内核和用户的跟踪能力,这种端到端的跟踪能力可以快速进行故障诊断,与此同时 eBPF 支持以更加高效的方式透出 profiling 的统计数据,而不需要像传统系统需要将大量的采样数据透出,使得持续地实时 profiling 成为可能。
安全控制
eBPF 可以看到所有系统调用,所有网络数据包和 socket 网络操作,一体化结合进程上下文跟踪,网络操作级别过滤,系统调用过滤,可以更好地提供安全控制。
性能监控
相比于传统的系统监控组件比如 sar,只能提供静态的 counters 和 gauges,eBPF 支持可编程地动态收集和边缘计算聚合自定义的指标和事件,极大地提升了性能监控的效率和想象空间。
工作原理
理解方式1:
一个完整的 eBPF 程序,通常包含用户态和内核态两部分:用户态程序需要通过 BPF 系统调用跟内核进行交互,进而完成 eBPF 程序加载、事件挂载以及映射创建和更新等任务;而在内核态中,eBPF 程序也不能任意调用内核函数,而是需要通过 BPF 辅助函数完成所需的任务。尤其是在访问内存地址的时候,必须要借助 bpf_probe_read 系列函数读取内存数据,以确保内存的安全和高效访问。在 eBPF 程序需要大块存储时,我们还需要根据应用场景,引入特定类型的 BPF 映射,并借助它向用户空间的程序提供运行状态的数据。
理解方式2:
eBPF 程序是事件驱动的,并附加到代码路径上。代码路径包含特定的触发器(称为钩子),这些触发器在传递附加的 eBPF 程序时执行它们。钩子的一些例子包括网络事件、系统调用、函数项和内核追踪点。
当触发时,代码首先被编译为 BPF 字节码。然后,字节码在运行之前会被验证,以确保它不会创建循环。这个步骤可以防止程序无意或故意损害 Linux 内核。
在钩子上触发程序之后,它就会进行助手调用。这些助手调用是为 eBPF 配备许多用于访问内存的特性的函数。助手调用需要由内核预先定义,但是存在的函数列表在不断增长[3]。
eBPF 最初被用作过滤网络数据包时,提高可观察性和安全性的一种方法。然而,随着时间的推移,它成为了一种使用户提供的代码实现更安全、更方便和性能更好的方法。
理解方式3:
eBPF是Linux内核中软件实现的虚拟机。用户把eBPF程序编译为eBPF指令,然后通过bpf()系统调用将eBPF指令加载到内核的特定挂载点,由特定的事件来触发eBPF指令的执行。在挂载eBPF指令时内核会进行充分验证,避免eBPF代码影响内核的安全和稳定性。另外内核也会进行JIT编译,把eBPF指令翻译为本地指令,减少性能开销。
内核在网络处理路径上中预置了很多eBPF的挂载点,例如xdp、qdisc、tcp-bpf、socket等。eBPF程序可以加载到这些挂载点,并调用内核提供的特定API来修改和控制网络报文。eBPF程序可以通过map数据结构来保存和交换数据。
怎么使用
5 个步骤
1、使用 C 语言开发一个 eBPF 程序;
即插桩点触发事件时要调用的 eBPF 沙箱程序,该程序会在内核态运行。
2、借助 LLVM 把 eBPF 程序编译成 BPF 字节码;
eBPF 程序编译成 BPF 字节码,用于后续在 eBPF 虚拟机内验证并运行。
3、通过 bpf 系统调用,把 BPF 字节码提交给内核;
在用户态通过 bpf 系统,将 BPF 字节码加载到内核。
4、内核验证并运行 BPF 字节码,并把相应的状态保存到 BPF 映射中;
内核验证 BPF 字节码安全,并且确保对应事件发生时调用正确的 eBPF 程序,如果有状态需要保存,则写入对应 BPF 映射中,比如监控数据就可以写到 BPF 映射中。
5、用户程序通过 BPF 映射查询 BPF 字节码的运行状态。
用户态通过查询 BPF 映射的内容,获取字节码运行的状态,比如获取抓取到的监控数据。
Hook 点
回顾一下eBPF技术的hook点:

从图中可以看出,eBPF的hook点功能包括以下几部分:
- 可以在Storage、Network等与内核交互之间;
- 也可以在内核中的功能模块交互之间;
- 又可以在内核态与用户态交互之间;
- 更可以在用户态进程空间。
eBPF的功能覆盖XDP、TC、Probe、Socket等,每个功能点都能实现内核态的篡改行为,从而使得用户态完全致盲,哪怕是基于内核模块的HIDS,一样无法感知到这些行为。
基于eBPF的功能函数,从业务场景来看,网络、监控、观测类的功能促进了云原生领域的产品发展;跟踪/性能分析、安全类功能,加快了安全防御、审计类产品演进;而安全领域的恶意利用,也会成为黑客关注的方向。本文将与大家探讨一下新的威胁与防御思路。
从数据流所处阶段来看,本文划分为两部分,接下来一起来讨论恶意利用、风险危害与防御思路。
- Linux网络层恶意利用
- Linux系统运行时恶意利用
eBPF 运行时安全
对eBPF有了初步了解后,我们再来谈谈运行时安全,为什么运行时安全在云原生下拥有这么重要的地位,以致于我们说它是云原生安全的重要部分呢?这其实比较好理解,我们说云原生所依赖的基础是容器化,容器是一种应用层面上的抽象,隔离依赖于namespace+cgroup,相比传统VM模式,其隔离程度是较低的
而这种隔离的不彻底性,从安全的视角来看,有以下几个问题:
- 各个容器之间共享系统内核,这就造成了单个内核提权漏洞会对所有的容器环境造成影响
- 弱隔离性导致攻击在不同容器环境中的横向移动变得相对容易
- 容器自身及扩展应用引入了新的安全风险,如docker api、k8s api暴露的风险
- 弹性设计导致很多容器生命周期较短,攻击行为还未被及时捕捉,环境就被销毁,进而提高了攻击发现成本
- 容器权限管理较为混乱,实际环境中特权容器问题普遍存在,这导致主机与容器间隔离很容易被打破
我们常见的一些针对云原生环境的攻击手段,比如docker.sock逃逸、挂载目录逃逸、k8s Server API攻击等,都是利用了云原生环境下的这类问题。
利用eBPF,可以实现:
- 检测容器中用户操作及可疑的 shell 脚本的执行
- 检测容器运行时是否打开了新的监听端口或者建立意外连接的异常网络活动
- 检测容器运行时是否存在文件系统读取和写入的异常行为,例如在运行的容器中安装了新软件包或者更新配置
- 检测容器运行时是否创建其他进程
- 检测容器中是否运行了黑客工具,是否存在反弹shell行为
- 检测容器中是否执行了挂载系统目录操作
BCC 排查工具
-
想排查网络问题,在没有 eBPF 的时候,一般都是借助 tcpdump 了解网络层面。但等到排查网络丢包等类似问题,会发现 tcpdump 根本不够用,只知道网络上传输了哪些包,想再深一步,知道为什么这么传输,就无能为力了。
-
但是,有了 eBPF ,以及 BCC 这个 eBPF 工具集的助力,这个问题就变得非常容易解决了,处理效率也会大大提升。你可以看看这张图,感受下 BCC 的强大:
Killing 云原生可观测性项目。
Kindling架构设计中有一个很重要的理念:关注点分离(Separation of Concerns)。eBPF技术或者内核模块是一种内核技术,需要的背景知识是C语言和操作系统知识。而可观测开发者关注的是要输出什么样的指标,同时因为平时使用Go、Java这一类语言较多,对C也比较生疏,所以我们的设计是基于两层的分层领域。下层是eBPF的开发能力,主要是为事件透出服务;上层是可观测性需求开发,主要是为数据分析和指标产生服务,同时可以方便扩展可观测场景化需求。
另外一个重要的理念是:不重复造轮子,我们目标是把eBPF的能力以简单的方式透出给用户使用。所以kindling的设计是以falcosecurity-libs为基础的。目前这个开源项目承担的主要职责就是系统调用的事件透出,对于可观测方面的能力需要进一步扩展。但是它有一个优势是它会将原始内核数据和cgroup信息进行关联,方便后续将数据关联到k8s相关的resources, 同时falcosecurity-libs也对原始数据做了预处理,比如将网络数据进行更丰富的关联,让用户能够直接拿到某个对fd操作的网络事件属于哪个四元组的信息,所以我们复用了这部分能力。但falcosecurity-libs本身并不支持kprobe、uprobe等能力,kindling目前已经对其扩展了kprobe能力,后续也会持续不断地扩展uprobe等能力,同时还会集成其他开源工具的数据能力。
一般来说,eBPF探针主要由两部分程序组成:内核态程序用作采集数据以及用户态程序用作分析数据。但基于以上两个理念,我们的架构并不是传统的两部分。我们基于关注点分离理念,为了让cloud-native领域的开发者能够更方便地使用eBPF的能力,把原来falcosecurity-libs的C/C++用户态程序拆分成了一个Go程序和一个C/C++程序,让用户能更关注自己擅长的领域。
传统ebpf程序结构
Kindling探针整体包含三个部分:用户态Go程序、用户态C/C++程序和内核态drivers程序。用户态Go程序满足的是上层可观测需求的开发,其他两个部分实现的是内核需求的开发。这样不同领域的人可以用自己擅长的语言开发自己关注的内容,同时探针也有较好的松耦合特性。Kindling具体组件描述如下:
eBPF 安全项目 Tracee
Tracee
是一个用 于 Linux
的运行时安全和取证工具。它使用 Linux eBPF
技术在运行时跟踪系统和应用程序,并分析收集的事件以检测可疑的行为模式。Tracee
以 Docker
镜像的形式交付,监控操作系统并根据预定义的行为模式集检测可疑行为
Tracee
运行系统最低内核版本要求 >= 4.18,可以根据是否开启CO-RE
进行BPF
底层代码编译。运行Tracee
需要足够的权限才能运行,测试可以直接使用root
用户运行或者在Docker
模型下使用--privileged
模式运行。
Tracee
在最近的版本中增加了一个非常有意思的功能抓取--capture
,可以将读写文件、内存文件、网络数据包等进行抓取并保存,该功能主要是用于取证相关功能。
Tracee 与 Falco 的区别
看到 Tracee
这款基于 eBPF
技术的安全产品,很自然想到的对应产品是 Falco
,如果你对 Falco
不了解,那么可以参见这篇文章[7]。Tracee
与 Falco
还是有诸多类似的功能,只是从实现和架构上看, Tracee
更加直接和简单,也没有特别复杂的规则引擎,作者给出的与 Falco
定位不同如下,更加详细的可参见这里[8]:
Falco 是一个规则引擎,基于 sysdig 的开放源代码。它从 sysdig 获取原始事件,并与 yaml 文件中 falco 语言定义的规则相匹配。相比之下,Tracee 从 eBPF 中追踪事件,但不执行基于这些事件的规则。
我们编写 Tracee 时考虑到了以下几点:
- Tracee 从一开始就被设计成一个基于 eBPF 的轻量级事件追踪器。
- Tracee 建立在 bcc 的基础上,并没有重写低级别的 BPF 接口。
- Tracee 被设计成易于扩展,例如,在 tracee 中添加对新的系统调用的支持就像添加两行代码一样简单,在这里你可以描述系统调用的名称和参数类型。
- 其他事件也被支持,比如内部内核函数。我们现在已经支持 cap_capable,我们正在增加对 security_bprm_check lsm 钩子的支持。由于 lsm 安全钩子是安全的战略要点,我们计划在不久的将来增加更多这样的钩子。
其实,从使用的场景上来说 Tracee
与 Falco
不是非 A 即 B 的功能, Tracee
也可以与 FalcoSideKick
进行集成,作为一个事件输入源使用。
从下面两者架构图的对比,我们也可以略微熟悉一二, Tracee
更加直接和简洁,规则引擎的维护也不是重点,而且规则引擎恰恰是 Falco
的重点。
Falco
的架构图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v7clE3Gz-1658233367386)(https://mmbiz.qpic.cn/mmbiz/CrEZgiblEdJWAo8L2dK52INl22AEKmO8Lh8lmMkrFGUoQDP4z9ly68GxF9epicBKQxZUc4E0H8Bpsu371XWsYshQ/640?wx_fmt=other&wxfrom=5&wx_lazy=1&wx_co=1)]
而 Tracee
的架构图如下:
eBPF LSM热修复Linux内核漏洞
Linux Security Modules[2](LSM)是一个钩子的基于框架,用于在Linux内核中实现安全策略和强制访问控制。直到现在,能够实现实施安全策略目标的方式只有两种选择,配置现有的LSM模块(如AppArmor、SELinux),或编写自定义内核模块。
Linux Kernel 5.7[3]引入了第三种方式:LSM扩展伯克利包过滤器[4](eBPF)(简称BPF LSM)。LSM BPF允许开发人员编写自定义策略,而无需配置或加载内核模块。LSM BPF程序在加载时被验证,然后在调用路径中,到达LSM钩子时被执行。
提权原理
提权
是操作系统的常见攻击面。user获得权限的一种方法是通过unshare syscall[5]将其命名空间映射到root
空间,并指定CLONE_NEWUSER
标志。这会告诉unshare
创建一个具有完全权限的新用户命名空间,并将新用户和Group ID映射到以前的命名空间。即使用unshare(1)[6]程序将root映射到原始命名空间:
$ id
uid=1000(fred) gid=1000(fred) groups=1000(fred) …
$ unshare -rU
# id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)
# cat /proc/self/uid_map
0 1000 1
多数情况下,使用unshare
是没有风险的,都是以较低的权限运行。但是,已经被用于提权了,比如CVE-2022-0492[7],那么本文就重点以这个场景为例。
Syscalls clone
和clone3
也很值得研究,都有CLONE_NEWUSER
的功能。但在这篇文章中,我们将重点关注unshare
。
Debian用add sysctl to disallow unprivileged CLONE_NEWUSER by default[8]补丁解决了这个问题,但它没有被合并到源码mainline主线中。另一个类似的补丁sysctl: allow CLONE_NEWUSER to be disabled 尝试合并到mainline,但被拒绝了。理由是在某些特定应用中无法切换到该特性[9]。在Controlling access to user namespaces[10]一文中,作者写道:
… 目前的补丁似乎没有一条通往mainline主线的捷径。
如你所示,补丁最终没有包含到vanilla内核[11]中。
详细见 https://mp.weixin.qq.com/s/UJEC8nmfQbdsWdJMfju0ig
了解了
LSM BPF
是什么,如何使用unshare将user
映射到root
,以及如何通过在eBPF中实现程序来解决真实场景的问题。跟踪准确的钩子不是一件容易的事,需要有丰富的经验,以及丰富的内核经验。这些策略代码是用C语言编写的,所以我们可以因地制宜,不同的问题做不同的策略,代码轻微调整,就可以快速扩展,增加其他钩子点等。最后,我们对比了这个LSM程序的性能影响,性能与安全的权衡,是你需要考虑的问题。