绕过eBPF安全监控
2022年10月11日 - 作者:Lorenzo Stella
当今有许多安全解决方案依赖Linux内核的扩展伯克利包过滤器(eBPF)功能来监控内核函数。这种最新监控技术的范式转变由多种原因驱动,其中一些是出于日益云主导世界中的性能需求等。Linux内核一直具有内核跟踪能力,如kprobes(2.6.9)、ftrace(2.6.27及以后)、perf(2.6.31)或uprobes(3.5),但借助BPF,最终可以在事件上运行内核级程序,从而修改系统状态,而无需编写内核模块。这对任何试图入侵系统并保持隐蔽的攻击者具有深远影响,开辟了新的研究和应用领域。如今,基于eBPF的程序用于DDoS缓解、入侵检测、容器安全和一般可观测性。
2021年,Teleport引入了一项名为增强会话记录的新功能,以弥补Teleport审计能力中的一些监控空白。所有报告的问题均已按照其2021年第四季度公开报告中的描述迅速修复、缓解或记录。以下展示了我们如何绕过基于eBPF的控制,以及红队或恶意行为者如何规避这些新入侵检测机制的一些思路。这些技术通常可应用于其他目标,以尝试绕过任何基于eBPF的安全监控解决方案:
关于eBPF工作原理的简要说明
扩展BPF程序使用高级语言编写,并通过工具链编译为eBPF字节码。用户模式应用程序使用bpf()系统调用将字节码加载到内核中,eBPF验证器将执行多项检查以确保程序在内核中运行“安全”。此验证步骤至关重要——eBPF为无特权用户提供了在ring 0执行的路径。由于允许无特权用户在内核中运行代码是一个成熟的攻击面,过去的一些研究专注于本地权限提升(LPE),但本文不会涵盖这些内容。
程序加载后,用户模式应用程序将程序附加到一个钩子点,当特定钩子点(事件)被触发(发生)时,将执行程序。在某些情况下,程序还可以JIT编译为本机汇编指令。用户模式应用程序可以使用eBPF映射和eBPF辅助函数与运行在内核中的eBPF程序交互并获取数据。
常见缺陷及潜在绕过方法(此处有风险)
1. 理解捕获哪些事件
虽然eBPF速度快(比auditd快得多),但由于性能原因,许多有趣区域无法合理使用BPF进行检测。根据安全监控解决方案最想保护的内容(例如,网络通信 vs 执行 vs 文件系统操作),可能存在过度探测会导致性能开销的区域,迫使开发团队忽略它们。这取决于终端代理的设计和实现方式,因此仔细审计eBPF程序的代码安全性至关重要。
1.1 执行绕过
例如,一个简单的监控解决方案可能决定仅钩住execve系统调用。与普遍看法相反,多个基于ELF的类Unix内核不需要磁盘上的文件来加载和运行代码,即使它们通常需要一个。实现此目的的一种方法是使用称为反射加载的技术。反射加载是一种重要的后渗透技术,通常用于避免检测并在锁定环境中执行更复杂的工具。execve()的手册页指出:“execve()执行由文件名指向的程序……”,并继续说“调用进程的文本、数据、bss和堆栈被加载的程序覆盖”。这种覆盖并不一定是Linux内核必须垄断的,与文件系统访问或许多其他事情不同。因此,execve()系统调用可以在用户态中以最小难度模拟。创建新进程映像因此是一个简单的事情:
- 清理地址空间;
- 检查并加载动态链接器;
- 加载二进制文件;
- 初始化堆栈;
- 确定入口点;
- 转移执行控制。
通过遵循这六个步骤,可以创建并运行一个新的进程映像。自2004年首次报告此技术以来,该过程如今已被OTS后渗透工具开创和简化。如预期,钩住execve的eBPF程序将无法捕获此操作,因为此自定义用户态exec将有效地用新映像替换当前地址空间中的现有进程映像。在此,用户态exec模拟了系统调用execve()的行为。然而,由于它在用户态操作,描述进程映像的内核进程结构保持不变。
其他系统调用可能未被监控,从而降低监控解决方案的检测能力。其中一些是clone、fork、vfork、creat或execveat。
如果BPF程序天真地信任execve系统调用参数引用正在执行文件的完整路径,则可能存在另一个潜在绕过。攻击者可以在不同位置创建Unix二进制文件的符号链接并执行它们——从而篡改日志。
1.2 网络绕过
不钩住所有与网络相关的系统调用可能带来一系列问题。一些监控解决方案可能只想钩住出口流量,而攻击者仍然可以通过滥用其他与网络敏感的操作(参见linux/security/apparmor/include/audit.h:78处的aa_ops) related to入口流量将数据发送到非允许主机:
- OP_BIND,bind()函数应为由描述符socket标识的未分配本地套接字地址的套接字分配本地套接字地址。
- OP_LISTEN,listen()函数应将由socket参数指定的连接模式套接字标记为接受连接。
- OP_ACCEPT,accept()函数应提取待处理连接队列上的第一个连接,创建一个具有与指定套接字相同套接字类型协议和地址族的新套接字,并为该套接字分配新文件描述符。
- OP_RECVMSG,recvmsg()函数应从连接模式或无连接模式套接字接收消息。
- OP_SETSOCKOPT,setsockopt()函数应设置由option_name参数指定的选项,在由level参数指定的协议级别,为与由socket参数指定的文件描述符关联的套接字设置由option_value参数指向的值。攻击者感兴趣的有趣选项是SO_BROADCAST、SO_REUSEADDR、SO_DONTROUTE。
通常,网络监控应类似于AppArmor查看所有基于套接字的操作。
如果同一本地用户混合了受监控和未监控的控制台会话,攻击者可能在受监控会话中利用打开的文件描述符和套接字将数据发送到受限主机。2020年,一些Linux内核版本引入了一个名为pidfd_getfd的新系统调用来实现此目的。少数操作系统(如Ubuntu)实现了Yama内核模块,该模块将文件描述符访问限制为仅子父进程。使用此函数的PoC代码可在Github上找到(TheZ3ro/fdstealer)。
2. 延迟执行
如果仅监控活动控制台会话,eBPF程序可能仅存在于会话的时间跨度内。通过延迟命令执行(通过sleep、timeout、sar、vmstat、inotifywait、at、cron……)并退出会话,可能完全规避解决方案。
3. 规避基于cgroup的范围事件监控
安全监控解决方案可能仅对审计特定用户或cgroup(如在远程控制台会话的上下文中)感兴趣。以Teleport为例,它通过将每个事件与具有控制组(特别是cgroupv2)的会话相关联来实现此目的。控制分组是Linux内核功能,用于限制对一组进程的资源访问。它在许多容器化技术中使用(Docker在幕后为容器创建一组命名空间和控制组),其特殊性在于所有子进程将保留父进程的ID。当Teleport启动SSH会话时,它首先重新启动自身并将其置于cgroup中。这不仅允许该进程,而且允许Teleport启动的所有未来进程使用唯一ID进行跟踪。Teleport运行的BPF程序已更新,以 also 发出执行它们的程序的cgroup ID。BPF脚本检查bpf_get_current_cgroup_id()返回的值,并仅关心重要的会话cgroup。对此审计策略的最简单规避是更改您的cgroup ID,但攻击者需要是root才能实现此目的。干预cgroupv2伪文件系统或滥用PAM配置也是影响cgroup/会话关联的潜在机会。
另一种技术涉及由init重新分配。在Teleport的情况下,当会话生成的bash进程死亡时,其子进程成为孤儿,Teleport进程终止其执行。当子进程成为孤儿时,在某些条件下(没有tty,是进程组领导者,加入新进程会话),操作系统可以将其分配给不同的cgroup。这允许攻击者绕过现有的限制。以下PoC是此设计绕过的示例:
- 打开一个新的eBPF监控会话
- 通过执行tmux命令启动tmux
- 按CTRL+B然后D从tmux分离
- 杀死作为tmux父级的bash进程
- 通过执行tmux attach重新附加到tmux进程。进程树现在将如下所示:
作为另一种攻击途径,利用机器上不同本地用户/cgroupv2运行的进程(滥用其他守护进程,委托systemd)也可以帮助攻击者规避此问题。此方面显然取决于托管监控解决方案的系统。防止这种情况很棘手,因为即使设置PR_SET_CHILD_SUBREAPER以确保后代不能将自己重新父级到init,如果祖先收割者死亡或被杀死(DoS),则该服务中的进程可以逃脱其cgroup“容器”。对此特权服务进程的任何妥协(或不当行为)允许它杀死其层次结构管理器进程并逃脱所有控制。
4. 内存限制和事件丢失
BPF程序有许多约束。仅为eBPF程序保留512字节的堆栈空间。变量将在执行开始时被提升和实例化,如果脚本尝试转储系统调用参数或pt-regs,它将很快耗尽堆栈空间。如果未设置指令限制的解决方法,则可能推动脚本检索某些太大而无法适应堆栈的内容,当执行变得复杂时很快失去可见性。但即使使用解决方法(例如,当使用多个探针跟踪相同事件但捕获不同数据,或将代码拆分为多个使用程序映射相互调用的程序时),仍然可能滥用它。BPF程序并非旨在永远运行,但它们必须在某个点停止。例如,如果监控解决方案在CentOS 7上运行并尝试捕获进程参数及其环境变量,则发出的事件可能具有太多的argv和太多的envp。即使在这种情况下,您也可能错过一些,因为循环更早停止。在这些情况下,事件数据将被截断。需要注意的是,这些限制基于运行BPF的内核以及终端代理的编写方式而不同。
eBPF的另一个特点是,如果无法足够快地消耗事件,它们将丢弃事件,而不是拖累整个系统的性能。攻击者可以通过生成足够数量的事件来填充perf环形缓冲区并在代理读取之前覆盖数据来滥用此点。
5. 从不信任用户空间
内核空间对pid的理解与用户空间对pid的理解不同。如果eBPF脚本尝试识别文件,正确的方法是获取inode编号和设备编号,而文件描述符则不那么有用。即使在这种情况下,探针也可能受到TOCTOU问题的影响,因为它们将向可以轻松更改的用户模式发送数据。如果脚本改为直接跟踪系统调用(使用tracepoint或kprobe),它可能卡在文件描述符上,并且可能通过玩弄当前工作目录和文件描述符来混淆执行(例如,通过结合fchdir、openat和execveat)。
6. 滥用seccomp-bpf缺失和内核差异
基于eBPF的监控解决方案应通过使用seccomp-BPF在生成控制台会话之前永久丢弃进行bpf()系统调用的能力来保护自己。否则,攻击者将有能力进行bpf()系统调用来卸载用于跟踪执行的eBPF程序。Seccomp-BPF使用BPF程序过滤任意系统调用及其参数(仅常量,无指针解引用)。
在处理内核时,要记住的另一件事是接口不保证一致和稳定。如果eBPF程序未在验证的内核版本上运行,攻击者可能滥用它们。通常,对这些程序的不同架构的条件编译非常复杂,您可能会发现针对特定内核的变体未正确目标。使用seccomp-BPF的一个常见陷阱是在系统调用编号上过滤而不检查seccomp_data->arch BPF程序参数。这是因为在任何支持多个系统调用调用约定的架构上,系统调用编号可能基于特定调用而变化。如果不同调用约定中的编号重叠,则过滤器中的检查可能被滥用。因此,重要的是确保seccomp-BPF过滤规则考虑了每个新支持架构的bpf()调用的差异。
7. 干扰代理
类似于(6),可能以不同方式干扰eBPF程序加载,例如针对eBPF编译器库(BCC的libbcc.so)或调整其他共享库预加载方法以篡改解决方案的合法二进制文件的行为,最终执行有害操作。如果攻击者成功更改解决方案的主机环境,他们可以在LD_LIBRARY_PATH前面添加一个目录,其中保存了具有相同libbcc.so名称并导出所有使用符号的恶意库(以避免运行时链接错误)。当解决方案启动时,它链接到恶意库而不是合法的bcc库。对此的防御可能包括使用静态链接程序、使用完整路径链接库或将程序运行到受控环境中。
非常感谢整个Teleport安全团队、@FridayOrtiz、@Th3Zer0和@alessandrogario在撰写此博客文章时的灵感和反馈。