ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SysComb:用于攻击面缩减的细粒度透明系统调用过滤

SysComb:用于攻击面缩减的细粒度透明系统调用过滤

大家读完觉得有帮助记得关注和点赞!!!

摘要
限制应用程序可用的系统调用可以缩小内核的攻击面,并极大地减轻受损程序的影响。近期研究展示了生成系统调用过滤器的技术,然而,所有现有解决方案都需要修改内核或应用程序才能在运行时激活它们。这是侵入性的、容易出错,且通常不切实际,尤其是当代码由外部方维护时。本文提出了SysComb,一种基于eBPF的新颖解决方案,用于根据应用程序状态强制执行时间专门化的系统调用过滤器,无需修改应用程序或内核代码,从而解决了上述局限性。此外,SysComb让开发者可以在两种不同的强制策略之间选择:类似seccomp的策略,确保在状态转换后不会获得新权限;以及最小权限策略,对每个状态应用最具限制性的过滤器。我们使用广泛使用的软件评估了SysComb,展示了准确的状态感知系统调用过滤,以及可与内置内核解决方案相比的开销,证明了我们方法的实用性。

关键词:系统调用过滤,时间专门化,攻击面缩减,eBPF,Seccomp

1 引言
限制内核与用户态之间的接口对于维护操作系统完整性至关重要。这种方法有双重目的:它通过拒绝访问潜在易受攻击的代码路径来减少内核的攻击面[21],包括更容易出现漏洞的新内核函数[37],并且它限制了受损应用程序的能力[46]。这种纵深防御实践的重要性体现在其在关键应用(如Web浏览器、服务管理器和容器运行时)中的广泛采用,这些应用在执行前限制应用程序可用的功能(例如,[30, 44, 18])。

在Linux系统中,对可用系统调用(或syscall)的限制是通过seccomp[41]实现的,它通过指定过滤器来定义允许的系统调用集。此解决方案的有效性严格取决于过滤器的粒度[23]。理想情况下,应用程序应仅被授予其当前(和未来)操作所需的系统调用。这需要在可行时转换到更具限制性的过滤器,甚至将应用程序功能分区到不同的线程,每个线程由专用的、严格的过滤器限制。然而,在实践中,大多数生产应用程序在启动时采用过于宽松的过滤器,并且很少在之后修改它[6]。这是由于多种原因,例如:1. 对被调用的系统调用了解有限,2. 重构应用程序以支持多个过滤器所需的工作量,以及3. 将快速开发和部署置于安全性之上。

认识到这些挑战,大量研究工作致力于使用静态和动态分析技术生成与seccomp兼容的过滤器[25, 24, 26, 9, 54, 47, 16]。一些有影响力的工作[36, 25, 47]更进一步,为同一服务器应用程序生成两个系统调用过滤器,一个用于初始化阶段,另一个用于服务阶段,表明服务器应用程序的安全性可以大大受益于不同系统调用集的强制执行。虽然这些工作为系统调用过滤器的时间专门化概念铺平了道路,但在运行时有效且高效地激活这些过滤器仍然构成重大挑战。实际上,现有解决方案表现出以下一个或多个局限性:1. 它们需要应用程序修改以显式触发过滤器转换,2. 它们需要维护具有特殊功能的内核的替代补丁版本,或3. 它们将过滤器的强制委托给可信的用户空间组件,这代表了额外的攻击面。

在本文中,我们提出了一种新颖的解决方案,以自动识别并透明地激活基于应用程序执行状态的专门化系统调用过滤器。我们还细化了时间专门化的概念,使得能够强制执行每线程系统调用过滤器。据我们所知,这是首次在不需应用程序更改、内核补丁和委托给用户空间组件的情况下实现此结果的提议。我们的解决方案通过利用eBPF子系统在内核边界内操作。这使我们能够使用用户空间探针在运行时自动检测应用程序状态转换,观察所有系统调用事件,并强制执行安全决策。所提出的技术是灵活的,并支持两种策略:1. 类似seccomp,将应用程序线程限制为渐进式更具限制性的过滤器,以及2. 最小权限,将每个应用程序状态与可能的最具限制性过滤器关联。此外,SysComb在运行时自动验证应用程序状态转换,使得能够早期检测和阻止意外的执行模式。我们在广泛使用的应用程序上评估SysComb。从安全角度来看,我们证明我们的方法识别出细粒度的、状态感知的过滤器,更有效地减轻内核漏洞利用,并且与基于单一过滤器的解决方案相比,阻止了更多的shellcode。从性能角度来看,我们表明SysComb引入的开销与seccomp相当。

2 背景
本节概述了使用seccomp进行系统调用过滤,并提供了关于eBPF的基本背景,eBPF是SysComb使用的基础技术。

Seccomp
Seccomp BPF[41],或简称为seccomp,是一种Linux内核安全机制,允许进程定义系统调用过滤器。该过滤器是一个无状态程序,由内核在系统调用调用时评估,主要用于实现允许列表,仅当系统调用标识符(偶尔还有其原始参数)与指定规则匹配时才允许其执行。关键的是,一旦设置了过滤器,它将被所有子线程和进程继承,并且只能被收紧,不能被放松。要安装过滤器,应用程序进程必须配置它,然后将其加载到内核中。过滤器设置有两种选择:1. 使用一系列Berkeley Packet Filter规则直接编写它,然后调用seccomp系统调用来激活它,或2. 使用像libseccomp这样的平台无关高级库。无论哪种情况,都需要修改应用程序,可能需要重大的架构重构[11]。为减少开发人员的工作量,一些解决方案允许在应用程序启动前仅显式声明一次其可用的系统调用(例如,[30, 18, 53])。尽管实用,但这种方法不可避免地降低了有效性,因为复杂软件需要许多系统调用,当所有这些调用对每个内部组件都可用时,存在具体的安全风险。

图1:eBPF架构概述。
图描述:显示用户空间组件(如eBPF感知应用和库)通过bpf系统调用与内核中的eBPF虚拟机交互。内核包含eBPF验证器、JIT编译器和eBPF程序及映射。程序附加到各种钩子点(如kprobes、uprobes、tracepoints)。

eBPF。eBPF[15]是一种旨在安全且动态扩展操作系统内核功能的技术,无需修改源代码或加载新的内核模块。从根本上说,它允许用户定义的eBPF程序在内核内的抽象虚拟机中执行。当执行流到达指定的钩子点时,这些程序被触发,钩子点包括预定义位置,如网络套接字和内核跟踪点,以及当使用kprobes和uprobes附加时的任意内核和用户空间函数,这些在主流Linux发行版中默认启用。此设计允许非侵入式检查参数和返回值,并且对于某些钩子点,它还允许程序覆盖内核对事件的反应方式。为跨调用持久管理状态,程序使用称为eBPF映射(例如,哈希表、数组)的共享数据结构。重要的是,为降低内核不稳定的风险,每个程序在加载前都经过严格的验证过程,确保基本安全属性,如保证终止、防止越界内存访问以及遵守系统定义的资源限制。图1展示了eBPF架构,突出显示了负责加载eBPF程序和映射并与它们交互的用户空间组件。如图所示,支持eBPF的应用程序依赖用户空间库来抽象低级bpf系统调用的复杂性,并使eBPF程序能够可靠地针对各种内核版本,而无需重新编译。

3 需求与现有解决方案
在本节中,我们简要阐述目标、澄清威胁模型,并讨论现有研究工作,强调其优势和局限性。

3.1 目标
O1细粒度时间过滤器。我们旨在引入针对每个应用程序线程的特定执行状态定制的系统调用过滤器,从而在潜在危险情况下(如处理第三方数据或代码时)限制可用操作。
O2无需应用程序更改。时间策略不得要求开发人员重构应用程序源代码来激活系统调用过滤器。这便于采用,特别是当需要限制第三方代码时。
O3无需内核更改。除非补丁被内核社区接受并合并到主线,否则每当有内核更新时,都必须重新应用(并可能需要调整)它们,引入相当大的维护开销。此外,此代码中的错误可能导致系统不稳定。
O4无需用户空间委托。将安全决策委托给外部监视器通常会因频繁的上下文切换而增加显著延迟。此外,它总是依赖可信的用户空间组件,这扩展了攻击面。
O5完全支持依赖关系。我们寻求设计一个完全支持动态库和外部二进制文件执行的解决方案。详细地说,我们希望(a)避免过度近似,并从过滤器中排除所有虽然存在于动态库中但应用程序实际上并未使用的系统调用,以及(b)即使在动态库和外部二进制文件内部也保持管理时间过滤器的能力。
O6同步强制。强制必须有效地在系统调用调用和执行之间进行干预,在危险操作影响系统之前阻止禁止的系统调用。
3.2 威胁模型
我们认为内核固有可信但可能受漏洞影响,而用户空间应用程序(或其执行的特定关键状态)可能是不可信的和/或可能容易受到代码执行攻击。示例包括处理用户请求的Web服务器,或执行用户提供查询的DBMS。这些应用程序状态的识别取决于特定的应用程序逻辑,因此是由开发人员提供的信息。

与seccomp等效,SysComb的目标是通过严格限制应用程序可用的允许系统调用,来限制应用程序-内核接口,有效降低潜在受损软件的能力,并最小化内核的攻击面[37, 41]。我们强调,系统调用过滤解决方案的有效性取决于其在应用程序和内核之间进行干预以阻止禁止系统调用的能力。在SysComb中,安全检查是同步的且不可绕过的。

3.3 现有解决方案
我们系统地分析了现有的系统调用过滤解决方案,指出它们为何不能满足我们所有的要求。表1总结了分析结果。

用户空间系统调用过滤
有几种方法可用于在用户空间过滤系统调用。例如,LD_PRELOAD[42]是一个环境变量,用于指定在任何其他库之前加载的自定义共享库,然后可以实现其自己的函数和系统调用包装器版本。ptrace[43]系统调用允许父进程观察和控制另一个进程的执行。Syscall User Dispatch (SUD)[34]允许应用程序捕获其自身的系统调用。然而,所有这些方法都有缺点:LD_PRELOAD的范围仅限于动态库,ptrace由于频繁的上下文切换而引入高开销[57],而SUD并非旨在作为安全机制,因为它可以通过在系统调用调用前跳转到非受限内存区域来轻松绕过。

另一研究方向是二进制重写技术。例如,zpoline[56]重写每个syscall和sysenter指令,以调用用户定义的代码;bpftime[58]使用相同技术。尽管开销最小,但此方法无法捕获所有系统调用,因为它不支持动态库。为克服这些局限性,lazypoline[32]提出使用SUD捕获应用程序发出的所有系统调用,然后利用zpoline重写遇到的任何系统调用指令。然而,由于基于SUD,这种方法不适合实现安全覆盖。

基于Seccomp的解决方案
几项研究工作侧重于生成准确的系统调用过滤器,然后依靠seccomp通过使用应用程序包装器或注入二进制代码来强制执行。

许多提议为整个应用程序生成单个seccomp过滤器,然后在启动时应用它。例如,sysfilter[16]使用静态分析从二进制文件生成函数调用图,然后使用所有可达系统调用派生出单个过滤器;然而,在处理动态库时它会过度近似,降低准确性。为改进此方面,Chestnut[9]采用两步方法:首先,静态派生出初步系统调用列表,然后通过动态分析进行细化。为将方法扩展到容器,Confine[24]应用动态分析来发现运行的二进制文件列表,然后对每个文件执行静态分析以派生出系统调用过滤器。最后,C2C[26]提出结合应用程序的配置选项静态分析其源代码,以识别死代码并禁用所有不可达的系统调用:此方法主要适用于应用程序去臃肿化,依赖于配置,且与其它方法类似,在处理动态代码时准确性较低。由于其构建方式,此类别中的所有提议都生成一个宽松的单一seccomp过滤器,包含应用程序所需的每一个可能的系统调用,因此无法支持时间策略。

另一方面,有趣的研究描述了基于应用程序状态生成更严格过滤器的新方法。Ghavamnia等人[25](TSP)使用对其源代码的静态分析为服务器应用引入时间系统调用过滤器。然而,此方法仅支持两个执行阶段(初始化和服务),并且需要修补的编译器来重新编译所有应用程序代码,包括第三方库。SysPart[47]受此工作启发,通过还考虑动态模块,大幅改进了二进制应用程序过滤器的生成。尽管有改进,这两项工作仍然依赖seccomp来安装过滤器,因此继承了其所有局限性(见第2节)。

表1:基于目标的近期系统调用过滤提议比较。
O1:时间过滤器,O2:无需应用程序更改,O3:无需内核更改,
O4:无需委托,O5:依赖支持,O6:同步

提议O1O2O3O4O5O6
LD_PRELOAD [42]✗✓✓✗✗✗
ptrace [43]✗✓✓✗✓✓
Syscall User Dispatch [34]✗✓✓✗✓✗
zpoline [56]✗✓✓✗✗✗
lazypoline [56]✗✓✓✗✓✗
bpftime [58]✗✓✓✗✗✗
sysfilter [16]✗✓✓✓✗✓
TSP [25]✓✗✓✓✗✓
Chestnut [9]✗✓✓✓✗✓
C2C [26]✗✓✓✓✗✓
SysPart [47]✓✗✓✓✓✓
Confine [24]✗✓✓✓✗✓
SysXCHG [20]✓✓✗✓✓✓
Jia et al. [33]✓✓✗✓✓✓
B-Side [54]✓✓✓✓✓✗
∗ Optimus [55]✗✓✓✓✓✓
KubeRosy [31]✗✓✓✓✓✓
μPolicyCraft [8]✓✓✓✗✓✗
GoLeash [10]✓✓✓✗✓✗
Tetragon [12], KubeArmor [5]✗✓✓✓✓✓
Tracee [7], Falco [51], SysDig [52]✗✓✓✗✓✗
我们的解决方案✓✓✓✓✓✓
✗ 不支持, ◐ 部分支持, ✓ 支持, ∗ 实现依赖

需要内核更改的解决方案
为克服seccomp的局限性,几种方法建议更改内核实现。SysXCHG[20]允许程序在每次执行execve时更改其seccomp过滤器,符合最小权限原则。然而,它需要广泛修改seccomp继承策略,并且必须将过滤器嵌入ELF文件。Jia等人[33]提出修改seccomp和eBPF子系统以实现可编程系统调用过滤,从而启用时间策略的强制。然而,所提出的补丁未能在Linux内核社区获得足够共识,最终未被纳入主线内核。最后,B-Side[54]专注于二进制文件的静态分析,以相对较高的准确性检测状态和相关过滤器,但不支持过滤器强制,建议要么更改内核,要么引入用户空间监视器。

尽管内核修改开启了大量机会,但也引入了显著缺点。未合并的更改需要在每次内核更新时持续应用和修订补丁,导致相当大的维护成本负担。

基于eBPF的解决方案
一条新的研究路线关注容器化应用程序,并使用eBPF改进系统调用过滤[55, 31, 8, 10, 48, 1]。Optimus[55]检测应用程序所需的系统调用列表,而KubeRosy[31]持续强制执行开发者提供的seccomp过滤器。然而,两者都不支持时间策略。μPolicyCraft[8]超越了这一限制,允许通过用户空间监视器生成和强制时间策略。然而,由于安全决策被推迟到监视器处理事件,强制仅在潜在危险操作已执行后才发生。专注于Go程序,GoLeash[10]生成并强制执行每包系统调用过滤器。尽管它支持时间专门化,但它将强制委托给异步用户空间组件。

如今,许多云原生解决方案如Falco[51]、KubeArmor[5]、Sysdig[52]、Tetragon[12]和Tracee[7]使用eBPF来监控和限制工作负载。虽然KubeArmor和Tetragon提供内核内强制能力,但其它关注事件检测,将强制留给用户空间模块。它们都为整个容器配置单一过滤器,不支持时间专门化,并采用阻止列表策略,这固有地不如允许列表方法稳健。例如,预先存在的过滤器不会阻止新引入的系统调用。

4 SysComb架构
我们首先在第4.1节通过突出其特性来概述我们的解决方案,然后在专门章节中详细描述每个组件。

4.1 功能概述
在本文中,我们通过将每个应用程序线程(或进程)与一个执行状态关联来细化时间专门化概念,该状态可以在运行时演变(O1)。确实,我们确保当线程达到开发者指定的代码位置(我们称之为转换点)之一时,转换到新状态。为检测它们,我们在应用程序二进制文件(包括外部依赖项,O5)中设置eBPF用户探针,以触发eBPF程序的执行,无需对应用程序源代码或二进制文件进行任何修改(O2)。这允许透明地跟踪每个应用程序线程的当前状态,并在达到转换点时评估状态管理逻辑。

图2:状态图
图描述:一个简单的有向图,显示状态之间的转换。节点为状态0、状态1、状态2、状态3,边表示从状态0到状态1、状态1到状态2、状态2到状态3的转换。

为了捕获系统调用事件并强制执行安全决策,我们配置了系统调用处理程序上的eBPF内核探针和程序。在过滤识别期间,在跟踪模式下,此设置记录应用程序线程发出的所有系统调用,并将它们与当前应用程序状态关联。在强制模式下,它检测被当前过滤器拒绝的系统调用并触发强制动作(即,记录事件、返回权限拒绝错误或终止目标线程)。所有这些操作在系统调用执行之前在内核空间无缝执行(O4,O6)。不需要对内核或eBPF子系统进行修改;因此,我们的解决方案可随时部署在任何运行内核v6.6或更高版本的Linux系统上(O3)。

图3a:最小权限


图3b:反向传播


图3c:类似seccomp
图描述:三个维恩图或集合图,显示每个状态允许的系统调用。图3a显示每个状态有自己独立的集合。图3b显示通过反向传播,状态0的集合变为状态0∪状态1∪状态2∪状态3的并集。图3c显示状态0的集合最大,状态1是子集,状态2是子集,状态3是子集。黄色区域和虚线箭头突出显示反向传播的效果。

SysComb支持两种强制策略:1.类似seccomp通过始终转换到更具限制性的过滤器来模拟seccomp安全模型,但透明地进行,以及2.最小权限将每个应用程序状态绑定到最小的系统调用集,产生更细粒度的过滤器。在跟踪模式下,前者利用我们称为基于流的系统调用反向传播的后处理过程,确保由于状态转换不会获得新权限,而后者则不会。然而,两种策略都依赖状态管理逻辑来在有向图中记录应用程序的状态转换。这对于确保劫持程序控制流的攻击者不能滥用状态转换故意获取权限至关重要。图2和图3展示了一个简单的应用程序状态图及其相关的系统调用过滤器,突出了基于流的系统调用反向传播的效果。

4.2 应用程序状态管理
应用程序跟踪
为简化描述,我们将监控组件称为跟踪器,将任何被跟踪的应用程序线程或进程称为被跟踪者。跟踪器提供了一个接口,用于开始跟踪已在主机上运行的被跟踪者以及新进程。现有线程使用要跟踪的线程组(即用户、进程或线程ID)来识别,而新进程通过fork-exec模式和在应用程序二进制文件的ELF入口点附加的eBPF程序来跟踪。

为区分被跟踪者和其他任务,跟踪器将每个被跟踪者的身份和执行状态注释到名为tracee map的eBPF映射中。在跟踪器的生命周期内,此映射通过新的插入和删除来更新。使用clone和fork系统调用创建的新被跟踪者继承父任务相同的属性,而终止的被跟踪者被移除。为此,跟踪器将eBPF程序分别附加到与sched_process_fork和sched_process_exit事件关联的内核原始跟踪点。

转换检测
为实现对多状态的支持,我们首先期望开发者指定转换点。这些可以位于应用程序及其依赖项中,并且可以是1. 符号,即函数名称,2. 符号的偏移量,或3. 绝对地址。跟踪器依赖blazesym库[38]从ELF二进制文件的.symtab和.dynsym部分以及系统上可单独安装的调试符号包解析符号。然后,跟踪器将名为application state map的eBPF映射加载到内核中,该映射存储每个执行状态的系统调用过滤器和有效转换。此映射在跟踪模式下填充,在强制模式下为只读。每当被跟踪者达到转换点时,都会对此eBPF映射进行写入和读取。为实现此目的,跟踪器将uprobe eBPF程序附加到所有应用程序转换点。由于转换点可能很多,内核提供了创建可附加到多个uprobe的单个eBPF程序的方法,以及为每个附加点指定称为cookie的整数的方法,允许我们明确检测转换点,从而知道它们通向的应用程序状态。这使得能够保持tracee map中的执行状态最新。此外,通过跟踪状态和转换,跟踪器能够在跟踪模式下构建应用程序状态图(图4),并在强制模式下验证转换(图5)。确实,在强制期间,任何与应用程序状态图不匹配的转换都被视为无效,并报告为流完整性错误。这允许检测和防止那些通过改变易受攻击应用程序的控制流,以到达比从当前状态可达的过滤器集合更宽松的过滤器集合的攻击。

图4:跟踪模式下的应用程序状态管理。SysComb通过在转换点附加eBPF uprobes来跟踪线程状态,并构建应用程序状态图。
图描述:显示跟踪器进程、eBPF uprobes、应用程序线程和状态图。


图5:强制模式下的应用程序状态管理。在运行时,SysComb根据跟踪期间构建的状态图验证转换。
图描述:显示跟踪器、eBPF uprobes、应用程序线程和状态图验证。无效转换被阻止。


图6:跟踪每个应用程序状态使用的系统调用。
图描述:显示eBPF程序在系统调用进入时触发,从tracee map获取状态,并更新application state map中的系统调用位图。

分离
当所有被跟踪者终止时,跟踪器可以收集记录的信息(如果在跟踪模式下),然后卸载加载的eBPF映射和程序。为实现此功能,跟踪器维护一个表示存活被跟踪者任务数量的原子计数器。每当创建新被跟踪者时计数器递增,每当一个终止时递减。除了自然终止,跟踪器的早期终止也受支持,可以通过向跟踪器进程发送SIGINT信号(即Ctrl+C),或仅在跟踪情况下,通过最大持续时间计时器到期。基于被跟踪者的类型(即,使用入口点启动 vs 先前运行的进程),跟踪器的早期终止导致不同结果。在指定了应用程序入口点的情况下,跟踪器通过发送SIGTERM信号停止所有关联的被跟踪者,然后等待它们终止。这确保所有被跟踪者任务被正确终止,并且它们终止过程中调用的所有系统调用都被捕获。另一方面,当跟踪器监控已在主机上运行的进程时,跟踪器停止跟踪它们而不中断其执行。

4.3 系统调用过滤器的识别
系统调用跟踪
系统调用事件通过将eBPF程序附加到sys_enter内核原始跟踪点来捕获。如跟踪点格式所示,这允许eBPF程序接收有关系统调用号及其参数的信息。因此,对于被跟踪者任务,eBPF程序从tracee map检索当前状态,然后将其用作application state map的索引以检索关联的系统调用位图,最后基于系统调用号设置相应的位(见图6)。目前,跟踪器仅记录系统调用标识符。然而,此方法可以扩展为也跟踪原始类型参数,类似于seccomp(即,不进行指针解引用以避免TOCTTOU问题)。

过滤器集的后处理
跟踪终止后,跟踪器开始后处理记录的信息以生成过滤器集。首先,检测未使用的转换点并提示开发者,因为它们在定义过滤器集时没有用。然后,如果开发者选择了类似seccomp的策略,跟踪器执行额外步骤,即基于流的系统调用反向传播。此过程确保在遵循有效的转换序列后,应用程序与逐渐更具限制性的过滤器关联(即,永远不会获得新权限)。实现通过以反向拓扑顺序检索强连通分量,合并属于同一分量的所有系统调用集,并将系统调用反向传播到源节点,来考虑应用程序状态图中可能存在的循环(见附录0.A中的算法1)。随后通过修剪冗余状态来简化图,这些状态具有与每个前置节点相同的系统调用集。这简化了应用程序状态图,从而减小了application state map的大小和附加到应用程序二进制文件的uprobes数量。最后,跟踪器将结果导出到JSON策略文件(示例见代码清单),报告有关1. 应用程序和入口点(如果可用),2. 策略(类似seccomp vs 最小权限),以及3. 应用程序状态列表,包括状态标识符、转换点(即,路径、符号名称、偏移量或绝对地址)、人类可读格式的允许系统调用以及可达状态的信息。

值得注意的是,当采用最小权限策略时,不执行基于流的系统调用反向传播,因此识别出的系统调用过滤器可以显著更具限制性。然而,有效的状态转换可能导致激活更宽松的过滤器。虽然这可能被视为风险,但我们提醒,过滤器不是由应用程序本身设置的,而是由可信的内核内eBPF程序根据安全策略设置的。此外,即使受到控制流劫持攻击的应用程序也无法逃脱系统调用过滤器。确实,实现的控制流完整性检查确保只能基于可达状态的序列来行使系统调用。

4.4 系统调用过滤器的强制
策略加载
在强制模式下,跟踪器期望输入声明过滤器的JSON策略文件(例如,)。在解析和验证文件格式后,其内容被加载到application state map中。注意,虽然SysComb在跟踪模式下操作支持生成此类系统调用过滤器,但它们也可以由其他工具生成甚至手动编写。

强制动作
基于选定的配置,跟踪器可以以三种不同方式对策略违反做出反应:1. 记录事件,2. 注入并返回错误代码,或3. 终止被跟踪者。这些是现有系统调用过滤解决方案(如seccomp[41])实现的常见操作,可以适应多种应用程序需求。例如,通过日志记录可以跟踪违反事件而不改变应用程序流程,这在原型设计或微调过滤器时是有用的功能。另一方面,返回错误代码允许强制执行安全限制,同时让应用程序从错误中恢复并继续执行。这对于需要高可用性的关键应用程序很方便。最后,终止被跟踪者确保在检测到安全违反后终止。

基于动作的附加
基于开发者请求的动作,负责执行安全强制的eBPF程序附加到不同的钩子点。当请求的动作是日志记录时,eBPF程序直接附加到系统调用进入内核跟踪点,即sys_enter。当选择的动作是返回错误代码或终止时,过程更复杂,需要更多解释。

出于安全原因,eBPF程序可以改变正常内核执行流的钩子点存在限制,而跟踪点(与日志记录动作一起使用)受这些限制。幸运的是,Linux内核允许注入错误并修改所有实现每个系统调用入口的函数的返回值。然后可以通过检查内核源代码,特别是检查系统调用函数声明中ALLOW_ERROR_INJECTION宏的使用来评估系统调用的完整覆盖(代码清单1和2)。¹ 虽然这种方法确保所有系统调用都被覆盖,但缺乏所有系统调用的单一钩子点意味着跟踪器需要处理大量钩子点(目前约1000个不同函数)。幸运的是,内核允许通过创建单个eBPF程序以可扩展方式执行此操作,该程序附加到多个kprobes,再次使用整数cookie来区分钩子点,在这种情况下,该cookie以系统调用号为参数。为检索此号,跟踪器基于1. 内核支持的错误注入函数列表,以及2. 实现系统调用的函数与其系统调用号之间的关系构建映射。前者由内核通过sysfs提供,sysfs是一个导出关于各种内核子系统信息的伪文件系统。因此,用户空间进程可以在/sys/kernel/debug/tracing/events/error_injection读取完整的错误注入函数列表。后者在内核编译期间生成,并且可以在Linux头文件中获得,或者可以从内核代码仓库下载。

独立于基于动作的附加,跟踪器能够拦截所有系统调用。然而,并非所有附加方法性能相同,因为当使用原始跟踪点时,跟踪器加载eBPF跟踪程序,这是kprobes和tracepoints的更现代替代方案,它依赖eBPF trampolines来减少延迟。

图7:时间专门化系统调用过滤器的强制。被状态图拒绝的任何系统调用都会导致eBPF程序执行强制动作(例如,当调用write时返回错误)。
[图描述:显示应用程序线程调用系统调用,eBPF程序检查状态图,如果允许则继续执行,如果拒绝则注入错误。]

强制
当调用系统调用时,负责强制的eBPF程序被触发。首先,它确定发出系统调用的任务是否是被跟踪者。如果是,它从tracee map读取当前任务的应用程序状态,然后检索编码与其关联的过滤器的位图。最后,它使用系统调用标识符(作为参数提供给eBPF程序,或从bpf_get_attach_cookie辅助函数的调用中获取)作为索引来测试系统调用位图。如果过滤器允许系统调用,eBPF程序返回,内核继续其正常执行路径。否则,执行开发者选择的强制动作。该过程的简化可视化如图7所示。

注意,当向应用程序发送终止信号时,内核继续执行系统调用,直到达到信号处理安全点。另一方面,当注入错误时,系统调用入口函数被覆盖,其代码根本不执行。此行为具有安全影响。确实,虽然终止被跟踪者可能看起来有效,但它并不阻止系统调用代码至少部分执行,这可能导致不良副作用。因此,在实现中,我们选择在向被跟踪者发送终止信号时始终注入错误代码。这确保系统调用代码永远不会被执行,并防止副作用。

4.5 局限性
绿色线程
一些编程语言实现绿色线程,即由运行时库或虚拟机管理的轻量级线程。绿色线程在上下文切换期间通常比操作系统线程产生更低开销,因为操作不涉及内核。确实,绿色线程通常多路复用到单个操作系统线程中。目前,我们的提议为每个操作系统线程分配一个状态,因此不能直接应用于绿色线程。然而,可以通过将uprobes附加到负责其创建和多路复用的运行时库或虚拟机函数来支持绿色线程。

解释型编程语言
并非所有编程语言都在构建时编译为本地代码,因为一些(例如,Java、JavaScript、Python)要么被解释,要么即时编译,并在运行时内执行。SysComb与这些语言不直接兼容,因为它依赖将用户探针附加到应用程序二进制文件以透明地转换执行状态。然而,由于运行时本身始终以本地代码编写,可以将用户探针附加到运行时函数,并基于其参数执行状态转换。附加点的识别以及运行时与应用程序操作之间关系的评估不幸需要运行时内部知识。

特权跟踪器
与允许非特权用户收紧其过滤器的seccomp相反,SysComb的一部分需要超级用户权限来加载eBPF程序和映射。重要的是要注意,这仅适用于跟踪器,并且在eBPF对象加载后,不再需要超级用户权限,并在应用程序执行前自动释放。

5 讨论
我们讨论了SysComb实现的潜在攻击和对策。

篡改eBPF对象
与seccomp(保证系统调用过滤器持久直到进程终止)相反,我们方法的正确操作取决于eBPF程序和映射的完整性。攻击者可以尝试通过以下方式篡改过滤器:1. 通过bpf系统调用直接更改这些eBPF对象,或2. 通过强制终止持有其引用的跟踪器进程来间接触发其移除。² 为解释可能的防御,我们区分非特权攻击者和特权攻击者。非特权攻击者不能直接使用eBPF子系统,因此只能进行间接尝试。为阻止其尝试,SysComb将所有eBPF对象固定到eBPF文件系统(bpffs)[19]。相反,为防止特权攻击者的直接更改,我们通过Linux安全模块(LSM)配置额外的安全机制。确实,LSM适用于非特权和特权进程,并可以通过强制访问控制策略限制对内核资源(包括eBPF对象)的访问。目前,除了常见的LSM如SELinux[49]和AppArmor[40],几项近期工作[5, 3, 2, 4]利用eBPF程序[22]来实现临时强制访问控制解决方案。

控制流劫持攻击
虽然seccomp和SysComb都通过限制允许的系统调用来减少受损应用程序的影响,但两者都不能防止利用其控制流的用户空间漏洞。在SysComb中,这可能导致跳转到任意转换点,意图安装更宽松的过滤器。为规避此威胁,在强制模式下,我们仅允许在应用程序状态图中定义的状态转换。因此,攻击者无法获得从当前状态不可达的系统调用。此外,虽然不是我们提议的主要目标,但它启用了控制流完整性违反的粗粒度检测,并触发强制动作。

6 有效性评估
为评估我们解决方案的有效性,我们:1. 验证了跟踪应用程序调用的所有系统调用的能力,2. 评估了服务器应用程序生命周期中过滤器大小如何演变,最后3. 通过分析阻止近期内核漏洞的能力展示了安全优势。

6.1 准确的系统调用检测
我们通过使用strace[50]和SysComb跟踪常见服务器应用程序的执行来评估系统调用检测的准确性。具体来说,我们通过运行各自的测试套件测试了Apache Httpd、Lighttpd、Nginx、Memcached、MongoDB、Redis、MySQL、PostgreSQL和Bind。³ 然后我们测量了过滤器大小,定义为允许的系统调用数量。结果如图8所示。乍一看,strace和SysComb无专门化(SC_mono)之间的比较可能表明SysComb遗漏了strace捕获的一些系统调用。然而,进一步分析显示,这种差异源于strace的特定操作行为,它在调用启动被监控应用程序所需的execve之前开始跟踪,从而捕获了涉及加载和初始化二进制文件的所有系统调用,包括libc在__libc_start_main内部调用的那些。即使应用程序逻辑不需要这些系统调用,它们也会被记录,导致过于宽松的过滤器。相比之下,SysComb从应用程序入口点开始监控。这产生了更严格的系统调用过滤器,并与现有沙箱工具(如Minijail[29])一致,后者在执行__libc_start_main之后激活沙箱[28]。为验证这一点,我们在Minijail沙箱中使用SysComb识别的过滤器重新运行了应用程序。如预期,所有应用程序都成功完成了测试套件而无错误。

图8a:Apache Httpd


图8b:Lighttpd


图8c:Nginx


图8d:Memcached


图8e:MongoDB


图8f:Redis


图8g:MySQL


图8h:PostgreSQL


图8i:Bind
图8描述:九个条形图,每个应用程序一个。显示strace、SysComb无专门化(SC_mono)、类似seccomp(SC_sec)和最小权限(SC_lp)策略下每个状态(Startup、Listen、Serve、Cleanup)允许的系统调用数量。SC_sec和SC_lp的条形图通常低于SC_mono。

6.2 细粒度专门化过滤器
为评估状态感知过滤的优势,我们比较了SysComb在有和没有时间专门化的情况下生成的系统调用过滤器。我们的目标是确定纳入执行上下文是否会产生更具限制性的策略。对于第6.1节提到的每个应用程序,我们识别了三个关键转换点,将任务执行划分为对应于主要服务器执行阶段的四个状态:启动、监听、服务和清理。这些状态捕获(i)接受请求前的初始化,(ii)等待客户端连接请求,(iii)服务特定请求,以及(iv)终止。图8总结了结果。总体而言,状态感知过滤始终产生更具限制性的策略。确实,虽然类似seccomp策略(SC_sec)最初应用的过滤器与无专门化情况(SC_mono)相同,但它逐步转换到更严格的过滤器,在所有应用程序中,监听阶段平均减少11.82%的允许系统调用,服务阶段减少20.77%,清理阶段减少76.34%。最小权限策略(SC_lp)相比SC_mono,在启动阶段(不需要包含后续状态所需的系统调用)进一步减少35.22%的系统调用,监听阶段减少47.97%,服务阶段减少22.05%,清理阶段再次减少76.34%。通过比较图8中的SC_sec和SC_lp也可以看出,不同状态(除最后一个清理阶段外)依赖不同的系统调用子集,突出了最小过滤器的优势。

表2:阻止的安全敏感系统调用。

应用程序acceptaccept4bindchmodcloneconnectexecveexecveatforklistenmprotectptracerecvfromsetgidsetreuidsetuidsocket
Apache Httpd✓✓✓✓✓✓✓✓✓✓
Lighttpd✓✓✓✓✓✓✓✓✓✓✓✓
Nginx✓✓✓✓✓✓✓✓✓✓✓
Memcached✓✓✓✓✓✓✓✓✓✓✓✓✓
MongoDB✓✓✓✓✓✓✓✓✓✓✓✓✓✓
Redis✓✓✓✓
MySQL✓✓✓✓✓✓✓✓✓✓
PostgreSQL✓✓✓✓✓✓✓✓✓✓✓
Bind✓✓✓✓
被 ✓✓ 有专门化过滤, ✓ 无专门化过滤, 未被任何方法过滤

图9:在总共535个shellcode中被阻止的shellcode数量。
图描述:条形图,显示每个应用程序在无专门化和有专门化(类似seccomp)情况下被阻止的shellcode数量。有专门化时阻止的shellcode始终多于无专门化。

6.3 攻击面缩减
为展示攻击面缩减,我们专注于阻止内核漏洞的能力。为此,我们首先选择了45个近期内核CVE样本(如附录0.B表5所示),这些漏洞通过特定系统调用被利用。然后,我们重新检查了第6.1节讨论的应用程序,并计算了在以下情况下有多少应用程序受到保护:1. 使用单一过滤器,由seccomp或SysComb无专门化设置,以及2. 为服务状态使用专门化过滤器。数据显示,专门化过滤器在45种情况中有17种(37.8%的CVE)导致更好的保护。这一结果源于更有效过滤安全敏感系统调用的能力,在[25]中定义为shellcode中经常利用的那些。确实,在9个测试应用程序中,表2报告的17个安全敏感系统调用在55.9%的情况下被SysComb无专门化阻止,而当使用专门化时被阻止的比例为72.8%(+16.9%)。参考先前研究[25, 47]识别的535个shellcode集,这转化为每个应用程序平均多阻止98个shellcode(+27.8%),如图9所示。

我们提醒,虽然SysComb提供了识别和强制专门化过滤器的独特解决方案,但它也可以强制由其他方法[25, 47, 16]生成的过滤器。此外,它消除了为激活而注入seccomp调用的需要,它可以与两个以上状态一起工作,并且支持最小权限过滤器。不需要用户空间监视器和内核修改。

7 性能评估
为评估SysComb的性能,我们采用了1. 微基准测试,以测量绝对系统调用拦截开销,以及2. 宏基准测试,以评估对常见软件的影响。测试在配备Linux内核6.8.0、Threadripper 2950X CPU、128 GiB RAM和1 TiB SSD的服务器上运行。

微基准测试
我们比较了不同的系统调用拦截方法,测量了getpid系统调用的执行时间,该系统调用以其最小的内核执行路径而闻名,因此是隔离开销的理想选择。为提高准确性,我们采用了高分辨率计时器,执行了100万次连续的getpid调用,最后计算了平均值。测试针对ptrace[43]、LD_PRELOAD[13]、基于中断的插桩(int3)、SUD[34]、zpoline[56]、seccomp[41]和SysComb重复进行。结果如表3所示。

表3:使用不同拦截解决方案在100万次调用上getpid的平均执行时间。

拦截机制跟踪 (ns)强制 (ns)
ptrace1193112019
LD_PRELOAD200201
int328803157
SUD26582731
zpoline198199
seccomp5167217
SysComb(原始跟踪点)297303
SysComb(Kprobes)n.a.487
原生执行(无拦截)的平均执行时间为166 ns

如预期,所有拦截技术都比原生执行引入了一些开销,原生执行平均耗时166 ns。需要上下文切换到用户空间监视器的解决方案(ptrace、基于中断的插桩、SUD和用户空间通知模式下的seccomp)引入了最高的开销,每个系统调用的平均执行时间从2658 ns到12019 ns不等(减慢一到两个数量级)。相反,完全在用户空间(LD_PRELOAD、zpoline)或完全在内核空间(强制模式下的seccomp、SysComb)操作的技术增加了较低的开销,范围从198 ns到487 ns。尽管性能优越,但用户空间解决方案容易被绕过(见第3.3节),而基于内核的解决方案提供了强大的保护。总体而言,SysComb表现出有竞争力的性能,在跟踪模式下平均执行时间为297 ns,使用原始跟踪点的强制模式下为303 ns,使用kprobes的强制模式下为487 ns。

宏基准测试
在真实场景中,应用程序不会将大部分生命周期花在调用系统调用上,因此拦截开销被用户空间和内核空间执行时间摊销。为评估这一点,我们运行了几个宏基准测试,测量了原生执行时间(基线)以及seccomp和SysComb拦截下的执行时间。测试的应用程序包括Apache Httpd、Lighttpd、Nginx、Memcached、MongoDB、Redis、MySQL、PostgreSQL和Bind。对于基准测试,我们使用了1. Wrk[27]发送HTTP请求,2. YCSB[14]提交键值读取和写入查询,3. SysBench[35]和pgbench[45]执行SQL查询,以及4. dnsperf[17]解析DNS名称。表4显示了请求延迟的第90百分位数。

总体而言,在跟踪模式下,与基线相比,SysComb增加了最少0.002毫秒(Memcached)和最多14.596毫秒(Bind),后者仅占Bind请求时间的4.46%。在强制模式下,与seccomp相比,SysComb的开销范围从0.000毫秒(使用原始跟踪点的MySQL)到2.233毫秒(使用kprobes的Bind),后者仅占Bind请求时间的0.68%。结果表明开销低,表明我们的方法在生产环境中的可行性。

最后,我们强调强制动作的影响。当注入错误并返回给应用程序时,开销稍高。这是由于使用kprobes而非原始跟踪点,需要不同的eBPF程序类型并进行专门的安全检查,以及检索系统调用号的额外步骤(例如,基于cookie的查找)。然而,影响仍然有限,测量的最大延迟为0.521毫秒(MySQL)。

表4:使用不同拦截技术的第90百分位响应时间。所有值以毫秒为单位。

应用程序基线seccompSysComb (跟踪)SysComb (强制)
原始T.Kprobes
Apache Httpd7.7317.7438.3178.3278.531
Lighttpd11.44212.57812.63112.79012.852
Nginx7.4077.4148.1388.1698.411
Memcached0.0750.0770.0770.0780.080
MongoDB0.1630.1660.1690.1700.169
Redis0.1040.1060.1080.1090.108
MySQL27.16528.67328.67328.67329.194
PostgreSQL5.0345.1015.1115.1415.107
Bind312.776326.209327.372328.116328.442

8 结论
我们提出了SysComb,一种支持激活专门化系统调用过滤器的新颖解决方案。与现有工作不同,SysComb不需要修改应用程序和内核代码,也不引入用户空间监视器。确实,通过利用eBPF,SysComb完全在内核内操作,以自动跟踪应用程序行为,并实现对任何应用程序状态的每线程系统调用过滤器的完全透明、同步、不可绕过的强制。我们在九个广泛部署的应用程序上实现并评估了SysComb,包括Web服务器、关系型和NoSQL DBMS以及DNS解析器。结果表明系统调用检测准确,过滤器选择性提高,从而增强了对内核漏洞和利用载荷的保护。标准基准测试表明性能与seccomp相当。总体而言,SysComb提供了一种有效、低开销的解决方案,减少了内核攻击面,并稳健地缓解了现实世界应用程序中的攻击。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进