Mac OS X Lion 的 Sandbox 技术初探

时间:2022-05-30 16:44:33

Mac OS X Lion 的 Sandbox 是一项了不起的创新。当然,我不反对有人批评目前的 entitlement 可选项不够完备,还需要扩展。在假设今后可能加入新选项的前提下,现有的概念和实现已是巨大的进步。

操作系统局限于 discretionary access control 和 mandatory access control 两种安全模型已经太久了!后者概念复杂,除了涉密极高的部门,连电信银行等大型企业都几乎无人采用,完全没可能进入个人计算领域。前者又过于简单:资源(通常是文件)本身拥有一个访问限制列表 (access control list) [1];进程被赋予一个系统帐号身份,为下列情况之一:

  • 用户的登录帐号,如果是从普通的系统图形化工具如 OS X Finder 或者 Windows 的「start」菜单启动,这是最常见的情况;
  • Supervisor 用户,如果是用 sudo 或者 Windows 的 lauch as administrator 功能启动;
  • 其它帐号,如果是通过 login 等工具设定 shell 进程的帐号再从 shell 启动。

操作系统根据进程的帐号身份和资源的 ACL 决定满足还是拒绝进程对资源的访问请求。用户可能接触到两种程序和两种数据:

  • 非可信程序:其访问和处理数据的行为不明;
  • 非可信数据:可能通过 buffer overflow 等手段向程序注入恶意代码;所以,从系统安全的角度,非可信的数据和程序被等同视为恶意代码的潜在载体,下文统称「恶意代码」。
  • 可信程序:其访问和处理数据的行为经过验证(比如通过低权限帐号长时间运行而没有在系统的 audit logs 中发现其可疑行为被拒绝的纪录;或者由声誉良好的组织发布;或者是操作系统的自带工具)。系统安全的目的之一是避免可信程序被恶意代码篡改。如果可信程序遭到篡改,那么系统中就出现了名义上可信而实质上有恶意的代码,安全防线即被攻破。
  • 可信数据:用户自己创建的文件,或者通过其它方式确保不含有恶意成分的数据(比如通过低权限帐号运行的进程打开,而未在系统的 audit logs 中发现其可疑行为被拒绝的纪录)。系统安全的目的之一是避免可信数据被恶意代码篡改,以及避免敏感的可信数据恶意代码读取和泄漏。可信数据遭到篡改的后果和可信程序被篡改的危害相同,因为可信数据本身可以是控制其它程序行为的配置数据,也可以被修改为夹带恶意注入代码。

在可能接触到恶意代码的环境中,要通过 DAC 保护可信程序和用户的可信数据需要在使用中自觉遵守下列行为规范:

  • 每个用户要有多个系统帐号,对应不同的权限级别。
  • 可信数据的 ACL 只对足够高级别的系统帐号身份开放操作,对可信数据写操作的权限要求的帐号级别比读权限更高。
  • 不可信数据的 ACL 要先设置为对低级别帐号开放。
  • 用户访问不可信数据要以低权限级别的系统帐号启动应用程序。这样即使不可信数据对程序注入了恶意代码,也不会破坏或者泄漏可信数据。
  • 一定要用低权限级别的系统帐号启动不可信的程序。不可信程序只能暂时处理实验性质的数据(其 ACL 对低权限帐号开放,例如非敏感数据的实验拷贝)。
  • 当数据足够可信时,要修改它的 ACL 相应的提高允许访问的级别。
  • 当程序足够可信时才用足够高权限的帐号启动它,以便处理可信数据。

DAC 的问题

DAC 的概念虽然比 MAC 容易理解,但需要用户自觉遵守繁琐的行为规范 [2]。对于专门的服务器维护人员来说这不算什么。但是对个人用户来说,即便我这样的程序员也很难严格遵循所有规范(最多不用 supervisor 用户登录,避免系统文件被破坏,对用户可信数据的保护明显不够)。DAC 的主要问题在于静态性。资源的 ACL 是静态的。理论上说帐号在整个进程 session 中是静态的 [3] 。实际中更糟糕,由于缺乏工具(多为 sudo, login 之类的命令行工具而少有图形工具,多为提权 (previlege elevate) 而非限权操作),用户会在整个 login session 使用同一帐号启动所有程序(对,我也是这种人)。但是数据和程序的可信度是动态的,可信度的变化的对于不同的访问操作(读或写)也不相同。要求普通用户根据情况不断手工微调 ACL 和采用不同帐号的策略是不现实的。

人们采取过一些方法来弥补 DAC 的静态缺陷。一是权限隔离 (privilege separation) 。让程序中可能访问不可信数据的代码从主进程分离出去,运行在低权限的进程中 [4] 。这种方案的局限在于它只能弥补用户身份的静态缺陷而不能解决 ACL 的静态缺陷,不过它能满足一定适用范围的需求 —— 通常可信程序和数据的 ACL 会把读访问开放给权限较低的帐号,把写访问限制于权限较高的帐号,权限隔离可以防止可信数据和程序遭到篡改。但是对于可信数据泄漏等方面,仍然存在静态 ACL 固有的问题。

另一种弥补 DAC 静态缺陷的方法是动态 ACL 扩展:在操作系统本身为资源设定的静态 ACL 基础上,通过某种扩展为资源再强加一个更容易动态修改的 ACL。从基本概念来看,这是个改进,但是我一直对其很反感,因为从来没有一个真正可靠的动态 ACL 扩展实现:

  • 几乎所有动态 ACL 扩展都不是内核的有机组成部分,而是通过一些 daemon/service 加 kernel-hook 实现的半吊子监视器。除去各种具体的 bug 不一一列举,这种半吊子监视器甚至被暴露出普遍在多核系统上存在 race condition,会 grant 错误的,甚至是任意权限给恶意代码。
  • 由于不是操作系统的内建组成部分,难以保证所有应用程序都遵守动态 ACL。
  • 尽管比修改静态 ACL 方便,动态 ACL 的修改仍然需要过多的用户手工操作。
  • 要实现比较细致的安全保护,动态 ACL 需要动态的进程帐号来配合,而后者超出了一般动态 ACL 扩展的控制范围。

DAC 模型始终让用户和安全专家充满挫折感。它的困难在于缺乏有效的工具同时动态调整两个静态因素 —— 帐号和 ACL。通过半个多月的阅读文档,讨论,以及编程测试,我发现 OS X Lion sandbox 完全解决了上述问题。它是 kernel,系统服务和 UI framework 的完整组成部分,提供动态权限的模式脱离了基于资源的静态 ACL,基本不需要用户额外的手工干预。

基本 Sandbox —— Container 目录

首先回顾一下现有的 DAC 机制如何决定一次访问是否被允许。

Mac OS X Lion 的 Sandbox 技术初探

如上图所示,DAC 机制下一次资源访问的具体步骤是:

  1. 应用程序向 kernel 提出访问资源的请求。我们日常讨论的时候经常把整个过程简化描述为「应用程序读文件」。但是从技术细节上来说,应用程序不可能绕过 kernel 直接访问资源。
  2. Kernel 检查资源的 ACL,确认当前程序的帐号是否被允许此次访问。
  3. 如果允许,kernel 将相应的信息(如文件内容,或者允许后续文件操作的文件引用)返回给应用程序。

Sandbox 下的一次访问如下图所示。操作系统 kernel 会为每个 sandbox-ed 应用定义一个 sandbox 范围。在文件访问方面,最严格的也是每个进程 session 初始状态的 sandbox 范围是分配给应用程序一个只供它使用的目录,称为 container 目录。Sandbox-ed 应用只能在这个目录里读取和创建文件 [5]。由于可访问的资源限制在 container 目录之内,这些资源的 ACL 没有实质的作用。

Mac OS X Lion 的 Sandbox 技术初探

这和 iOS 的 sandbox 机制类似。但是对于桌面计算环境来说,大多数应用程序产生和读取文件的操作必需能在任意目录下进行。OS X Lion sandbox 的最大优势是提供安全而且友好的方式扩大 sandbox 范围。

必要的噪音

下图显示了一个 sandbox-ed 应用如何扩展自己的 sandbox 范围。在 OS X Lion 系统中有一个 Powerbox 服务进程。Sandbox-ed 应用程序向 Powerbox 提出请求扩展 sandbox 范围。Powerbox 执行步骤 2-6 ,通过 DAC 机制决定是否可以加入新文件到 sandbox 范围。如果得到允许,通知 kernel 扩展该应用的 sandbox 范围。

Mac OS X Lion 的 Sandbox 技术初探

问题的关键是 Powerbox 基于什么原则来扩展 sandbox 范围。这是一个危险的敏感操作。通常安全系统的设计惯例要求这种操作释放出用户能感受到的「噪音」,即图中第二步的「necessary noise」。用户在接收到噪音之后判断程序的提权操作是否可疑。如果可疑就中止整个过程。

这种噪音的呈现形式既不能弱到让用户无所察觉地扩展 sandbox 范围,也不能强到过于干扰用户。Windows UAC 就是这方面一个著名的失败设计 —— 强迫用户过于频繁地回答「是」或「否」的问题让用户养成不假思索点击 OK 的习惯动作。(而 UAC 还只是一个简单的动态修改进程帐号的功能,并非 sandbox 这样完整的动态授权方案。)

OS X Lion sandbox 机制的聪明之处在于把这种提示做得不着痕迹又不可能被忽略。当应用程序请求 Powerbox 为其扩大 sandbox 范围时后者会弹出 Open/Save File Panel 让用户指定读取或者保存/创建的文件。这是把原本属于应用程序职责的两个操作 —— 提示用户打开和保存文件(注意是提示而不是打开/保存的操作本身,后者依然属于应用程序)—— 移交给 Powerbox 作为扩展 sandbox 范围的「噪音」形式。噪音从唐突的「是」或「否」问题形式,变成了用户早已熟悉而且一定会谨慎执行的操作。在 OS X 原有的 DAC 安全系统基础上,整个方案涉及多个模块改进和协作:kernel 提供系统调用 (system call) 允许服务进程  Powerbox 修改 sandbox-ed 应用的 sandbox 范围;Powerbox 提供 Open/Save File Panel,并且保证它们和应用本身 UI 的无缝集成(包括在正确的位置显示 panel,绘制 custom accessory sub-panel 以及在 Powerbox 和应用之间正确双向接力 accessory sub-panel 的动作);Cocoa 要保证 sandbox-ed 应用在 API 调用方面和非 sandbox 应用程序保持源代码一致。这是任何操作系统之外的第三方产品都无法做到的,也要求操作系统本身具有高度的整体性。

Sandbox 实测

由于 OS X Lion 发布时间不长,关于 sandbox 的文档和讨论并不多。我写了一个程序来验证上述概念。

Mac OS X Lion 的 Sandbox 技术初探

验证程序的 UI 如上所示。在文本框中输入文件的路径。点击 Test 按钮,程序就会在 console 中打印出对这个文件是否有读操作的权限。UI 上还有两个 checkbox。原本打算在「Open Panel」选中的时候,让 Test 操作忽略文本框中的路径,(通过 Powerbox)开启 Open File Panel 让用户指定被测试文件。但在测试中发现了一个惊喜:Cocoa 的 NSTextField 支持从 Finder 拖放 (drag-and-drop) 文件到文本框。其中 drop 的过程已经被 Powerbox 接管,Powerbox 会把被拖放的文件加入目标程序的 sandbox 范围然后继续原来的 NSTextField 行为(显示文件路径)。整个过程等同于用 Open File Panel 打开文件,因此「Open Panel」的 checkbox 没有采用。

Test 按钮的行为由下面的代码实现:

Mac OS X Lion 的 Sandbox 技术初探

首先只关心 24-30 行。如果文本框的内容是通过拖放从 Finder 中得到(也就是 Powerbox 已经扩展了 sandbox 范围),那么 console 的输出为「Data is available: YES」。如果是键盘输入,那么输出结果为「Data is available: NO」。

接下来关注刚才忽略的忽略 16-22 行与 32-25 行。这是为了验证一个在讨论中困惑了我和同事很久的问题:一个进程的 sandbox 范围在刚刚启动时总是只包括 container 目录,其后每次扩大范围都要释放「噪音」,这让某些操作非常不便。假设用户在上一次程序运行 session 中通过 Powerbox 打开过一个文件,那么在下一 session 中是否可以在某些情况下认为这个文件是无需用户确认即可被访问的?如果按照基本 sandbox 机制,就不可能再无「噪音」的实现目前大多数程序提供的 Open Recent 菜单。特别是 OS X Lion 提出了 automatic termination 和 resume 的概念,程序对用户呈现的运行状态和进程 session 不再一一对应,这就需要 sandbox 机制允许在同一个程序的先后两个进程 session 间无「噪音」地继承部分 sandbox 范围。

Sandbox 确实提供了这种机制。在 NSWindow 中提供了 setStorable: 和setRestorationClass: 等方法。不过这些属于特别为 document-based 多窗口应用设计的高级接口。在验证程序里检验了更基本的方式。对于一个已在当前 session 的 sandbox 范围内的文件,调用NSDocumentController 的 noteNewRecentDocumentURL: 方法会把它加入到 recent document 列表中。这个列表由 Powerbox 为每个 sandbox-ed 应用程序维护。在该应用下次运行时,只要调用 recentDocumentURLs 方法,Powerbox 就会把列表中的文件无「噪音」地加入 sandbox 范围。用验证程序检验这个行为的步骤是:

  1. 点中「Add to Recent」checkbox,
  2. 从 Finder 拖放某文件到文本框,
  3. 按 Test 按钮,
  4. 退出并且重新启动验证程序,
  5. 然后手工输入刚才的文件路径,
  6. 按 Test 按钮,程序会输出「Data is available: YES」。

Cocoa 实现的界面会自动反映调用这些方法的效果。noteNewRecentDocumentURL: 会向 Open Recent 菜单增加相应的条目。recentDocumentURLs 会在用户打开 Open Recent 菜单的时候被自动调用(因此在只实现 Open Recent 菜单的时候无需程序员显式调用该方法)。删掉 Open Recent 菜单不会影响这些方法对 sandbox 范围所起的作用。这个方法可以用于非 document-based 的简单应用。目前看来这是跨 session 继承 sandbox 范围的机制中 Cocoa 和 Powerbox 最直接打交道的部分,其它方法或多或少基于此实现。

结论

OS X Lion sandbox 是个人计算安全模型的一个大发展。它是对 DAC 模式的一个易于理解的扩展。通过 kernel,Powerbox 服务和 Cocoa 框架的无缝集成,给开发者引入的负担和给用户带来的干扰比权限隔离 [6] 、动态 ACL、UAC 等方案要小得多。最后,利用 recent document 列表跨进程 session 继承 sandbox 范围的机制进一步避免了向用户发出不必要的噪音。尽管对 recent document 列表中的文件保护有所削弱,但是和提供的功能相比风险代价是合理的。需要普通用户注意的是,System Preferences 中 Appearance 下的 Number of recent items 也成为了一个关系到安全风险的选项。

脚注:

  1. 在 OS X 这样既提供 BSD Unix 实现又加入了任意 ACL 的系统中,狭义的 ACL 特指文件访问权限属性 (user, group, others) 以外的扩展部分。此处的 ACL 为广义定义,包括 Unix 的文件访问属性。
  2. DAC 本身概念的简单性正是以行为规范的复杂为代价的。
  3. 在一个进程 session 中改变帐号是可能的,但是需要发出「噪音」(见下文解释)。
  4. 有一种「相反的」权限隔离的方案是主进程运行于较低权限,分离的进程运行于较高权限(当然需要释放「噪音」提权)。这种方案是用于那些需要偶尔修改系统文件的程序,降低系统文件受攻击的风险。和本文讨论的保护用户可信数据的场景不同。
  5. 当然,除此之外,必不可少的读取和调用系统库文件肯定是允许的。
  6. 但在其它适当的场合采用权限隔离仍然是被鼓励的,只是说权限隔离不再是弥补静态 ACL 缺陷的必要方法。