利用 FreeBSD Capsicum 框架实现程序沙箱化

当今时代,数据泄露的平均成本高达四百余万美元,令人惊讶的是,只有 51% 的公司计划增加安全投资^1^。这一数字很可能归因于实施健全安全解决方案所需的高昂成本。

许多安全框架都非常复杂。Capsicum 的独特之处在于其简洁性。开发者能相对容易地将 Capsicum 集成到程序中,从而在保护应用的同时降低高昂的实施成本。

本文展示了 Capsicum 的功能,并撰写了将该框架集成到新旧程序中的完整指南。

Capsicum

Capsicum 是款轻量级的安全框架,提供了用于限制程序能力的原语。更具体地说,Capsicum 能让开发者将其程序隔离到安全沙箱中。该框架基于最小特权原则设计,程序仅能访问其运行所需的资源。

能力沙箱

在支持 Capsicum 的系统上,程序能使用 cap_enter(2) 进入沙箱:

#include <sys/capsicum.h>
#include <stdio.h>

int
main(void)
{
    /* 进入 Capsicum 的能力模式。 */
    cap_enter();
    printf("能力模式下的 Hello world\n");
    return (0);
}

注意

为简洁起见,本文档中的代码片段省略了错误处理。实际使用时,应在适当位置加入错误处理。

不用链接额外的库;对 Capsicum 的支持已内置于标准 C 库(libc)。

Capsicum 能让开发者通过能力模式将程序沙箱化。在程序调用 cap_enter(2) 进入能力模式后,程序将无法自行获取新的资源。例如,使用 open(2) 打开文件会触发能力违规,造成函数调用失败,并将 errno 设置为 ECAPMODE译者注:特定错误码):这在能力模式下不被允许。

一种对程序进行 Capsicum 化的方法是在进入能力模式之前就打开好全部资源。程序事先拥有的资源能在沙箱中继续使用。

若程序需要访问某子目录域中数量未知的资源,则可以使用 openat(2)mkdirat(2)bindat(2)*at() 系统调用。这些函数需要额外的描述符参数,用作相对引用点来打开新资源。

在这个例子中,dirfd 是在进入能力模式之前获取的。若程序进入能力模式,dirfd 就能作为相对参考点访问 home/jfree/foo。无法访问相对引用所提供子目录域之外的资源。

openat(2) 调用会失败,因为 ../beastie 路径指向的目录不在 dirfd 提供的目录层级之内。

进程间通信同样受到限制。进入能力模式的进程无法向其他进程发送信号,命名共享内存对象亦被禁止。某些系统接口完全不可用,如 reboot(2)kldload(2)。不过,这些限制都有相应的解决方法。

沙箱化的分区

预先打开资源对那些资源需求可预测的程序非常有效,但有些程序需要按需访问资源。在这种情况下,开发者可选择仅对程序的特定部分进行沙箱化。

如果无法在沙箱中运行完整的程序,那么可以将其隔离分区(compartmentalization)。隔离分区的做法是把一款程序拆分成多个分区,每个分区承担自身的基本任务。通过隔离分区架构,开发者可以将受信任的代码保留在沙箱之外,而将不安全或危险的代码隔离到沙箱中的分区。如果危险代码中发现了安全漏洞,将被隔离。

Capsicum 为程序的特定部分提供了直观的沙箱接口。程序能在任意时刻派生一个新的子进程,并在能力模式下执行危险代码。诸如管道和套接字这样的进程间通信原语可用于数据交换,而不会触发能力违规。

父进程可在沙箱之外运行,而子进程在隔离环境中执行危险代码。若程序已经实现了隔离分区,开发者可以从沙箱化每个分区开始。大多数分区可能需要针对能力模式进行一些重构,但开发者可以自行选择哪些部分需要沙箱化。若正确实施,相较于对整个程序进行沙箱化,这种方式工作量更少,但安全性有大幅提升。

使用 libcasper(3) 请求资源

有些程序在设计时并未考虑隔离分区。开发者可以重新架构这些软件,但这通常需要大量时间和资源。幸运的是,库 libcasper(3) 为那些隔离分区无效的复杂程序提供了帮助。开发者可以利用 libcasper(3) 提供的接口在能力沙箱中获取新资源。

使用 Casper 服务

在程序进入能力模式之前,可以与某 casper 服务建立通信通道。Casper 服务是运行在能力沙箱之外的进程,与调用进程并行运行。建立的通信通道可用于向 casper 服务请求新资源。

注意

必须在进入能力模式之前就打开 libcasper(3) 通道,否则 casper 进程会继承父进程的沙箱。

cap_net(3) 利用 libcasper(3) 提供了支持能力模式的 libc 网络函数,否则这些函数会因 ECAPMODE 而失败。通过 libcasper(3) 接口的函数通常以 cap_ 前缀命名,以表明它们能在能力模式下成功执行。类似于 cap_net(3) 的其他 casper 服务库也存在,并提供以 cap_ 为前缀的 libc 函数。可在 libcasper(3) 手册页中找到完整列表。

如果程序支持在没有 libcasper(3) 的情况下构建,开发者依然可以使用 cap_ 函数,而无需用 #ifdef WITH_CASPER 包装。大多数 casper 服务将其 cap_ 函数定义为宏,当 WITH_CASPER 未定义时会替换为非 cap_ 的形式。

创建 Casper 服务

Casper 服务库很好地隐藏了接口 libcasper(3),使程序开发者无需直接与其交互。但有时程序需要访问现有 casper 服务库未提供的资源,这种情况下开发者可以自行创建 libcasper(3) 服务。

所有 libcasper(3) 服务都基于 CREATE_SERVICE(3) 宏:

所有参数都很重要,其中 command_func 函数指针尤为值得关注,因为它是服务的主例程。当通过 cap_service_open(3) 打开服务时,服务会等待命令。待接收到命令,就会传递给 command_funccmd 参数。

大多数 command_func 会将传入的 cmd 字符串与一组字面量比较,若匹配则调用对应函数。Casper 服务运行在能力沙箱之外,因此在 command_func 内调用的所有函数都会以环境权限(ambient authority)执行,这意味着它们可以随意获取新资源。

可以使用函数在程序和 casper 服务之间 cap_xfer_nvlist(3) 交换资源。命令字符串和相关资源必须先封装进 nvlist(9),再传输给服务。Nvlist 可存储数字、字符串、二进制、描述符以及其他 nvlists。每个资源都必须配有一个标识名,用于检索。

cap_xfer_nvlist(3) 函数会将 nvl nvlist 发送到与 chan 绑定的 casper 服务。待 nvlist 到达 casper 服务,命令字符串会被提取并传入服务的 command_func

回顾 net_command() 中的片段:

cap_bind() 发送了 "bind" 命令字符串,因此该 strcmp() 条件成立并调用 net_bind()

bind(2) 成功时,套接字必须被传回能力沙箱。nvlist_move_descriptor(9) 函数会将套接字描述符移入 nvlout nvlist,并接管该描述符的所有权。

回顾 cap_bind() 中的片段:

cap_xfer_nvlist(9) 的返回值是 nvlout nvlist,它持有已绑定的套接字描述符。可以通过 nvlist_get_descriptor(nvl, "sockfd") 从返回的 nvlist 中获取该描述符。

限制 Casper 服务

Casper 服务提供的接口通常过于宽泛。在使用 cap_net(3) 时,你可能仅需要服务提供的部分函数。大多数服务库会定义自己的限制机制,使开发者可以禁用程序不需要的函数。

每个服务库都提供不同的限制机制。由于使用模式过多,建议开发者查阅各服务库的手册以获取详细信息和示例。

为 Casper 服务创建限制

开发者可以在 CREATE_SERVICE(3) 中指定 limit_func 函数指针以限制服务接口。当程序调用 cap_limit_set(3) 时,提供的 limits 会被重定向到 casper 服务的 limit_func 中,并相应应用。该机制的灵活性允许服务对其接口进行精细限制。

大多数服务库会为 cap_limit_set(3) 提供包装函数,并使用自定义的 _limit_t 类型。该类型由服务库定义,用于跟踪服务特定的限制。

随着限制的细化,实现可能很快变得复杂。像 cap_net(3) 这样的服务提供了对接口的广泛控制,因此其限制函数需要处理所有边界情况。快速查看 cap_net(3) 的限制函数可以发现,为不同的“限制模式”实现了单独的验证函数,以确保限制被正确应用和执行。

限制函数依赖于其所限制的服务,因此没有统一的编写模式。想要为 casper 服务创建限制的开发者应参考 FreeBSD 源码中 lib/libcasper/services 的系统服务库示例。

回顾:Casper 服务的组成部分

casper 服务由以下四个主要部分组成:

  • cap_ 为前缀的函数,通过 cap_xfer_nvlist(3) 向 casper 服务发送命令;

  • 在沙箱外执行命令相关代码并返回新资源的 command_func

  • 限制服务用途的 limit_func

  • 将服务拼接在一起的 CREATE_SERVICE(3) 宏。

虽然在技术上可以创建返回任意命名资源的 casper 服务,但这会破坏将程序隔离到沙箱的目的。设计良好的 casper 服务接口应当有限且受约束,避免被利用。

检测违规

当程序进入能力模式时,它是否遵循沙箱规则并不总是显而易见。尝试打开受限资源的函数会触发能力违规,并返回 errno = ECAPMODE。即便有适当的错误检查,排查违规也可能耗时。好在 ktrace(2) 内核跟踪工具能帮助发现违规。

通常,程序必须进入能力模式才会报告违规,但 ktrace(2) 能在程序 未进入 能力模式前记录违规。这意味着开发者能在不修改程序的情况下运行违规跟踪,查看违规发生的位置。由于程序未真正进入能力模式,它依然会获取资源并正常执行。

在程序开头添加以下两行即可启用违规跟踪:

这段代码创建了 ktrace(2) 的输出文件,并指定 KTRFAC_CAPFAIL 跟踪点以记录能力失败。

注意

ktrace(2) 手册页对系统调用的使用有详细说明。启用其他跟踪点(如 KTRFAC_NAMEI 记录文件名查找)有助于定位文件系统违规的来源。

下面的 cap_violate 例程试图触发 ktrace(2) 可捕获的所有违规。无需理解其具体做了什么,只需知道它能触发违规就可以了。

若进程被跟踪,跟踪数据会一直记录,直到进程退出或清除跟踪点。此时可以使用 kdump(2)ktrace(2) 的转储转换为可读格式。

cap_violate 程序中的每次违规都会在 kdump(1) 输出中生成一条 CAP 记录。开发者可利用这些输出定位并替换触发违规的代码。

大多数实际程序应尽量避免违规,而不是像 cap_violate 那样触发它们。下面的 kdump(1) 输出展示了在启用 KTRFAC_CAPFAILKTRFAC_NAMEI 跟踪点后,使用 unzip(1) 解压归档文件(未进行 Capsicum 化)时的结果。

这类输出更接近开发者在自己程序中会看到的情况。unzip(2) 正在重建 zip 文件中的目录结构。所有 open(2)fstat(2)mkdir(2) 调用都被 libc 隐式转换为带 AT_FDCWD*at() 变体。由于 AT_FDCWD 在能力模式下不可用,因此触发违规。这类问题可通过在进入能力模式前打开一个当前目录描述符(等价于 AT_FDCWD),并将其传递给 openat(2)fstatat(2)mkdirat(2) 来规避。

注意

在能力模式下违规总会返回错误,但在跟踪过程中不会被当作错误,因此程序行为可能不同。

违规跟踪是开发者工具箱中的一项实用工具。只需几秒钟即可用 ktrace(2) 运行程序,其结果几乎总能作为使用 Capsicum 沙箱化程序的良好开始。

能力 (Capabilities)

尽管名称相似,能力模式 (capability mode) 和能力 (capabilities) 是不同的内核原语。

  • 能力模式是 Capsicum 实现的安全沙箱。

  • 能力是拥有权限的文件描述符。

如果用户想要对某项能力描述符执行 read(2),则该描述符必须拥有 CAP_READ 权限。如果用户想要对某个套接字能力描述符执行 bind(2),则该描述符必须拥有 CAP_BIND 权限。几乎所有描述符操作都有细粒度的权限;完整列表见手册页 rights(4)

当使用 open(2)socket(2) 等创建文件描述符时,它会被赋予完整的能力集。可以使用 cap_rights_limit(2) 函数限制描述符的能力。

能力的设计原则是最小化权限。只要描述符的权限被限制,它就不应执行超出权限的操作。描述符的权限始终可以被限制,但不能被提升。

在对程序进行沙箱化过于严格的情况下,开发者可以选择限制能力描述符。能力提供了对允许操作的精细控制,但使用这种保护的程序易受到人为疏忽的影响。如果开发者忘记限制某项能力,就可能引入恶意代码滥用的风险。

想要最大化安全性的开发者可以在能力模式下结合使用能力。能力提供了能力模式本身不具备的细粒度功能。例如,仅允许描述符进行 read(2) 操作,这是能力可以做到的,但能力模式不能。

使用 Capsicum 提升安全性

能力模式能力 的设计初衷都是为了让程序更安全。能力模式通过将程序与系统其余部分隔离提供了确定的安全性。能力则提供了一种更灵活但不如前者严格的方式来限制程序权限。只要正确集成其中任意一种原语,开发者就能确信,他们的程序比在引入 Capsicum 前更加安全。

参考资料

  1. "Cost of a Data Breach Report 2023". IBM Corporation. 2023-07-24. Retrieved 2023-08-31.

相关资料

最后更新于

这有帮助吗?