try ai
科普
编辑
分享
反馈
  • 保护域

保护域

SciencePedia玻尔百科
核心要点
  • 保护域是像进程一样的容器,用于隔离程序和资源,以防止故障在系统范围内扩散。
  • 访问权限由面向对象的访问控制列表 (ACL) 或由主体持有的能力 (Capability) 来管理,它们在共享和撤销权限方面提供了不同的权衡。
  • “困惑的代理人” (confused deputy) 漏洞发生在一个特权程序被诱骗滥用其权限时,基于能力的安​​全模型可以避免这种风险。
  • 内存管理单元 (MMU) 和输入/输出内存管理单元 (IOMMU) 等硬件特性对于强制执行操作系统定义的隔离边界至关重要。
  • 像 Spectre 这样的侧信道攻击利用共享的物理硬件,即使在定义最明确的保护域之间也能泄露信息。

引言

在现代计算中,无数进程同时运行,共享内存和 CPU 时间等公共资源。这种共享环境带来了一个根本性挑战:我们如何防止一个程序中不可避免的错误或恶意行为者导致整个系统发生灾难性故障?没有稳健的规则和边界,我们的数字世界将陷入混乱。本文通过深入探讨​​保护域​​——在计算中创建秩序和安全的基础概念——来解决这个问题。

我们将探索数字隔离的架构,从抽象到具体。本文的结构首先在​​“原理与机制”​​一章中建立坚实的概念基础,其中我们将定义什么是保护域,剖析访问矩阵模型,并比较两种强制执行的支柱:访问控制列表 (ACL) 和能力 (capability)。随后,​​“应用与跨学科联系”​​一章将揭示这些原理如何无处不在地应用,从操作系统和云基础设施的设计,到构成安全基石的硬件特性,甚至包括这些保护被颠覆的微妙方式。通过理解这些概念,您将洞悉那使得复杂、可靠的软件能够安全运行的无形架构。

原理与机制

想象一座繁华的城市。成千上万的人在忙着自己的事——有的在盖房子,有的在送信,有的在烤面包。现在,想象一下,没有墙,没有门,没有锁,也没有法律。面包师可以随意走进银行家的金库,邮递员可以开始重新布置你家的家具。那将是一片混乱。一个笨拙或恶意的人就可能让整个城市陷入停顿。

我们的计算机就像这座城市。在内部,成百上千的程序——在计算机科学的语言中称为“主体” (subject)——同时运行,共享着相同的资源:CPU、内存、磁盘。这些资源是我们数字世界中的“客体” (object)。为了防止混乱,操作系统必须扮演城市规划者和警察的角色,执行一套规则来管理谁能对谁做什么。这一宏大组织背后的核心概念就是​​保护域​​。

最孤独的数字:为什么进程不仅仅是一个程序

让我们从头说起。一个程序只是一系列指令。一个运行中的程序需要一个地方来记录它正在执行哪条指令(程序计数器,或 PCPCPC),以及一个用于计算的草稿板(寄存器和栈)。我们可以称之为​​线程​​——一个单一的执行流。

为什么不让所有线程在计算机内存中肆意运行呢?思考一个思想实验:一个“只有线程”的操作系统。在这个世界里,只有一个巨大的、共享的内存空间。每个应用程序的每个线程——你的网页浏览器、音乐播放器、文字处理器——都生活在一个大房间里。如果你的音乐播放器有一个小错误,不小心将数据写入了错误的内存地址,它可能会覆盖你网页浏览器的代码,导致其崩溃。或者更糟,它可能会悄无声息地损坏你工作了数小时的文档。在这个系统中,没有隔离。一个单一的故障就可能导致灾难性的、系统范围的失败。

这就是为什么操作系统发明了​​进程​​。一个进程远不止是一个正在运行的程序。它是一个容器,一个堡垒,一个为程序及其线程提供的私有宇宙。至关重要的是,每个进程都被赋予了自己的虚拟地址空间——它自己的私有内存映射。在其堡垒内部,一个进程认为它独占了整个计算机。操作系统在硬件的帮助下,在幕后不知疲倦地工作,以维持这种幻觉,将进程的私有地址转换成真实的物理内存位置。

进程是基本的​​保护域​​。它不仅包含正在运行的代码,还包含它拥有的资源(如打开的文件),最重要的是,它的身份。当一个进程请求打开一个文件时,操作系统不仅仅问“这是哪个程序?”它问,“这个进程代表谁行事?”这个身份是所有访问控制决策所依据的主体身份。没有进程,我们就没有有意义的方式来组织资源和身份,保护的概念也就土崩瓦解了。

宏伟的规则手册:访问矩阵

那么,我们有了主体(进程)和客体(文件、设备,甚至其他进程)。我们如何决定它们之间的游戏规则?我们可以想象一个巨大的、概念性的表格,称为​​访问矩阵​​。

这个矩阵的行是系统中的所有主体。列是所有的客体。矩阵中的一个条目,位于主体 sss 和客体 ooo 的交汇处,包含一组权限——比如 {读, 写} 或 {执行}——即主体 sss 对客体 ooo 拥有的权限。

这个矩阵是整个系统完美、理想化的“规则手册”。在任何主体尝试对任何客体进行任何操作之前,我们的操作系统,这个不知疲倦的引用监视器,概念上会查找访问矩阵中相应的单元格。如果权限存在,操作就被允许。如果不存在,则被拒绝。这是一个极其简单而强大的模型。

当然,一台拥有数百万文件和数千进程的真实计算机实际上无法存储这个庞大的矩阵。取而代之的是,系统使用两种巧妙的方式来组织这些信息,这对应于按列或按行查看矩阵。

守卫与钥匙:访问控制列表 vs. 能力

系统在实践中如何执行访问矩阵的规则?有两种经典方法,它们代表了安全设计中一种深刻而根本的二元性。

访问控制列表 (ACL):门口的保镖

实现矩阵的一种方法是为每个客体附加一个列表。这个​​访问控制列表 (ACL)​​ 指定了哪些主体被允许访问它,以及它们拥有什么权限。这就像逐列查看访问矩阵。把它想象成城市里每个俱乐部(客体)门口站着的保镖。当你(一个主体)试图进入时,保镖会核对你的名字是否在他们的名单上。

ACL 直观且常见。你电脑上的文件权限就是一种简单的 ACL。然而,这些规则有时会以意想不到的方式相互作用。想象一个文件系统,其权限是从父目录继承的。你可能将一个归档目录 A 设置为对开发人员只读。但如果其父目录 P 有一条规则授予开发人员写权限,并且该规则被设置为由所有子目录继承,那么在 A 中创建的新文件可能会意外地从 P 继承写权限。你精心构建的完整性目标就被破坏了!。这里的教训是,安全策略最好建立在​​默认拒绝和显式允许的原则​​之上:除非明确授予权限,否则应予以拒绝。最健壮的安全系统会阻止那些在本地级别没有被明确重新确认的继承权限。

能力:你手中的钥匙

另一种方法是给每个主体一个它们的权限列表。这个列表由​​能力 (capability)​​ 组成。一个能力就像一把不可伪造的钥匙。它是一个令牌,指明了一个客体以及你对它拥有的权限。拥有这把钥匙本身就是你访问权的证明。操作系统是锁匠,负责制造这些钥匙并确保它们不能被仿造。这就像逐行查看访问矩阵。

能力为我们提供了一种强大的方式来推理​​最小权限原则​​。考虑操作系统中常见的 fork-exec 模式,即一个进程创建一个子进程,然后子进程转变为一个新程序。父进程可能拥有很高的权限,持有许多强大的钥匙。当它 forks 时,子进程是一个完美的克隆,继承了整个钥匙串。但是,如果这个子进程即将 exec 一个简单的、不受信任的工具程序,让那个新程序持有父进程的所有钥匙将是鲁莽的。一个负责任的进程会首先“清理”子进程继承的能力,撤销每一项权限——关闭每一个不需要的文件,放弃每一个特殊许可——除了新程序运行所需的最基本权限之外。这就是域切换的实际应用:从一个更高权限的域创建一个新的、权限更低的域。

委托与撤销之舞

当考虑更改权限时,ACL 和能力之间真正的哲学差异就显现出来了。

在 ACL 的世界里,如果你想给朋友访问你文件的权限,你不能只是告诉他们有权限。你必须对该文件拥有特殊的管理权限,才能去编辑它的 ACL——告诉保镖把你朋友的名字添加到名单上。相反,撤销他们的访问权限很容易:你只需告诉保镖把他们的名字划掉。控制权集中在客体上。

在能力的世界里,委托是轻而易举的。你只需复制你的钥匙并把它给你的朋友。但这造成了臭名昭著的​​撤销问题​​。你的朋友可能已经复制了钥匙并给了他们的朋友。你如何收回所有这些钥匙?你不能。

那么,我们是否陷入了在轻松委托和轻松撤销之间做出选择的困境?完全不是。计算机科学通过一个​​间接层​​提供了一个优雅的解决方案。你不是直接分发城堡的钥匙,而是分发通往一个特殊门房的钥匙。门房则维护着一个 ACL——一份当前有效钥匙的列表。要撤销访问权限,你不用去追回所有被复制的钥匙。你只需告诉门房管理员不再承认某个特定的钥匙。瞬间,那把钥匙的所有副本都变得无用了。这种优美的模式,即一个​​撤销门​​,将能力的分散式共享与 ACL 的集中式控制相结合,让我们两全其美。

当程序假扮身份:困惑的代理人

我们经常需要程序临时获得权限来执行特定的敏感任务。在 Unix 系统上,当你更改密码时,一个普通用户程序需要写入一个受高度保护的系统文件。这是通过像 setuid 这样的机制实现的,密码更改程序会临时以系统管理员 (root) 的权限运行。进程的域从你的用户域切换到 root 域,这一事件称为​​权限提升​​。这非常强大,但也为一种最微妙和危险的漏洞打开了大门:​​困惑的代理人​​ (confused deputy)。

“代理人”是一个拥有高权限的程序,它代表权限较低的用户执行操作。当代理人被用户欺骗以滥用其权力时,它就变得“困惑”了。

想象一个系统服务——我们的代理人——它读取一个机密的配置文件,并向客户端指定的位置写入日志。一个客户端请求服务将一条日志消息写入名为 /tmp/log.txt 的文件。服务利用其高权限,打开该文件并写入。但如果一个恶意客户端提供了文件名 /etc/secrets 呢?这个困惑的代理人将顺从地使用其权力覆盖一个关键的秘密文件!

根本的错误在于客户端传递了一个名称(一个字符串),而代理人使用自己的​​环境权限​​来访问它。正确的、基于能力的设计完全避免了这个问题。客户端不传递名称。相反,客户端首先打开一个它已经被授权写入的文件。这个动作授予客户端一个能力——一把钥匙,或者在现代系统中,一个文件描述符。然后客户端将这个能力传递给服务。服务现在使用客户端委托给它的权限进行写入,而不是它自己的权限。它只能写入那把钥匙能打开的确切文件。它没有可以被混淆的环境权限。

同样的模式也出现在像容器这样的现代系统中。一个外部特权容器中的进程可能会被诱骗,将一个敏感的主机卷挂载到一个嵌套的、非特权容器中,从而提升了内部容器的权限。解决方案是相同的:约束代理人的权限。其挂载卷的能力必须被限定范围,防止它代表一个信任度较低的子域来行使该能力。

从进程的围墙到能力的钥匙,保护域是让我们的复杂数字城市得以运作的无形架构。它们不仅仅是为了阻止坏人;它们使我们能够构建复杂、可靠的系统,并仔细平衡相互竞争的目标,例如确保病人的医疗记录既防篡改又能在紧急情况下立即获取。理解这些原则揭示了支撑我们数字生活安全的背后隐藏的优雅和深邃思想。

应用与跨学科联系

如果你已经理解了保护域的原理,你可能会感觉自己有点像刚学会了语法规则的人。这当然很有趣,但真正的乐趣来自于看到它能创造出怎样的诗篇。保护域的诗意在哪里?它无处不在。它体现在驱动我们世界的操作系统的优雅设计中,体现在云中守护我们数据的无声、无形的墙壁里,甚至体现在萦绕于我们处理器最深处的微妙幻影中。那么,让我们来一次巡游,看看这个基本的分离工具包构建了什么。

数字围墙的日常世界

你可能认为保护域是内核黑客和芯片设计师的深奥关注点。完全不是!你每天与它们互动数百次。考虑一下简单的复制粘贴操作。当你复制一段敏感文本——一个密码,一个银行账号——它进入了一个共享空间:剪贴板。是什么阻止了一个在后台悄悄运行的恶意应用程序偷看它呢?

一个简单的规则可能是:只有前台应用程序才能看到剪贴板。这是一个基本的保护域,但它很脆弱。如果你为了查看一下而把一个游戏切换到前台,并不打算粘贴任何东西呢?这个游戏可能会窃取剪贴板的内容。一个更优雅的解决方案,也是现代操作系统正在发展的方向,是把粘贴的权利不看作一个常设的特权,而是一张临时的、一次性的票据,或者说一个能力。当你发起粘贴时,操作系统会给目标应用程序一个特殊的、不可伪造的令牌,该令牌仅对那个特定的内容有效,且有效期非常短。一个后台应用程序,从未收到这个令牌,就被锁在门外了。这个设计巧妙地将最小权限原则应用于一个日常的、普通的功能,防止了大量的隐私泄露。

这种在宽泛的静态规则和细粒度的临时权限之间的舞蹈是一个反复出现的主题。想一想在像 GitHub 这样的平台上进行的协作软件项目。main 分支是神圣的产物,是真理的来源。仓库所有者建立了一条静态规则,一个访问控制列表 (ACL),规定:“任何开发人员都不能直接向 main 推送更改。”这是一个保护域。但工作必须继续!新代码如何进入?开发人员在一个单独的 feature 分支上工作,这是一个他们确实有权推送的域。准备就绪后,他们创建一个拉取请求 (Pull Request, PR)。这个动作就像敲响 main 分支的门。门不会自动打开。相反,经过自动化检查和人工审查后,系统会铸造一个特殊的、权限缩减的能力——一个令牌,授予执行恰好一次 merge 操作的权利,仅此而已。它不授予 force_push 或重写历史的权利。这种混合模型,结合了静态 ACL 和动态的、单一用途的能力,既提供了稳健的完整性,又保证了灵活的协作。

操作系统:虚拟宇宙的建筑师

如果说应用程序使用保护域,那么操作系统就是提供它们的宏伟建筑师。操作系统历史上最基本的设计选择之一就围绕着这个概念展开。你是构建一个​​单体内核​​,让所有的核心服务——驱动程序、文件系统、网络栈——都共同生活在一个巨大的、特权的地址空间里吗?这就像一个开放式办公室:沟通很快,但如果有人把咖啡洒在关键服务器上,整个办公室可能都会关闭。单个驱动程序中的一个故障就可能导致整个系统崩溃。

或者,你构建一个​​微内核​​,只将绝对必要的服务放在特权核心中,而其他所有东西——驱动程序、文件系统——都推到独立的用户空间进程中?每个服务都生活在自己的保护域里,自己的小楼里。它们通过一个正式的、消息传递接口相互交谈。这更慢,就像在大楼之间发送备忘录而不是在房间里大喊大叫。但其美妙之处在于它的弹性。如果文件系统服务器崩溃,它不会带走网络栈或内核。你只需重启失败的服务器。这种卓越的故障隔离是操作系统组件之间强制执行强保护域的直接结果。

这种创建隔离世界的想法在虚拟化中达到了顶峰。当你听到“云”时,你实际上听到的是一个以工业规模制造保护域的巨型工厂。但并非所有域都是生而平等的。​​容器​​(如 Docker)是一种操作系统级虚拟化。它们就像一栋大楼里的公寓。每个容器都有自己的私有空间,但它们都共享相同的基础和管道——主操作系统的内核。如果在这个共享内核中发现漏洞,攻击者可能会突破他们的“公寓”并影响整栋大楼。

另一方面,​​虚拟机​​ (VM) 是一种强度高得多的隔离形式。一个 VM 就像一座完全独立的房子,建在自己的地基(自己的客户机内核)上,并有自己的管道系统。分隔这些房子的“土地”由一个名为虚拟机监控程序 (hypervisor) 的特殊软件管理。攻击面要小得多;攻击者需要找到虚拟机监控程序本身的缺陷,这比在通用操作系统内核中找到缺陷要困难得多。这就是为什么在运行真正不受信任的代码时,VM 通常被认为是更安全的选择。

硬件:隔离的基石

如果没有硅片中不屈不挠的逻辑,所有关于域和墙的讨论都将是纯粹的幻想。硬件必须提供强制执行的基本机制。现代 CPU 通过内存管理单元 (MMU) 来实现这一点,MMU 将程序使用的虚拟地址转换为 RAM 中的物理地址。指导这种转换的页表不仅用于寻址;它们也是存储保护信息的地方。

这方面一个绝佳的例子是一个名为用户空间保护密钥 (Protection Keys for Userspace, PKU) 的功能。CPU 在每个页表项 (PTE)——即映射内存页面的数据结构——中保留几位作为“密钥”编号。然后 CPU 维护一个寄存器,其中包含每个密钥的一组“锁”。一个线程只有在持有与页面锁匹配的密钥时才能访问该页面。这允许单个进程将其自身的内存划分为多达 16 个不同的、由硬件强制执行的域,并且几乎可以瞬间在它们之间切换。这是一种极其高效的方式,例如,用于在大型应用程序中实现一个沙箱化插件。

但 CPU 并不是计算机中唯一强大的角色。像网卡和 GPU 这样的设备可以使用一种称为直接内存访问 (DMA) 的机制直接写入内存,完全绕过 CPU 的保护检查。一个恶意的设备,或者虚拟机中一个被攻破的设备,可以使用 DMA 来涂写主机操作系统的内存,导致整个系统被接管。解决方案是另一块硬件:输入/输出内存管理单元 (IOMMU)。IOMMU 位于设备和主内存之间,扮演着边境守卫的角色。它为 I/O 维护自己的一套“页表”,确保传递给虚拟机的设备只能在该 VM 分配的内存内执行 DMA,而不能在其他任何地方。它将狂野的 I/O 西部置于其自己管理良好、有警察巡逻的保护域中。

拥有这些硬件工具是一回事;安全地使用它们是另一回事。操作系统必须提供管理它们的 API。一个天真的 API 可能会导致“困惑的代理人”问题,即一个特权组件(内核)被一个权限较低的组件(驱动程序)欺骗以滥用其权限。一个现代、安全的设计通过使用对象-能力模型来避免这种情况。驱动程序不是请求“请为我的设备映射这块物理内存”,而是必须出示两个不可伪造的能力:一个证明其对设备的权限,另一个证明其对内存的权限。内核的角色仅仅是验证这些能力,从不做出任何环境判断。将宽泛、危险的特权(如“管理员权限”)驯化为具体的、权限缩减的能力,是安全工程中最强大的思想之一,从设备驱动程序到容器网络,无处不适用。

机器中的幽灵:当域发生泄漏

我们已经建好了我们的墙。它们坚固,由硬件强制执行,并由聪明的软件管理。我们安全了吗?不尽然。因为有些幽灵可以穿墙而过。

保护域在逻辑上可能是分离的,但它们几乎总是共享物理硬件。想象一下,来自不同域的两个程序在同一个 CPU 核上运行。它们不共享内存,但它们共享 CPU 的缓存。如果程序 A 访问一块数据,该数据被拉入缓存。片刻之后,当程序 B 运行时,如果它试图访问相同的数据,它的访问会非常快(缓存命中)。如果它访问不同的数据,它的访问可能会很慢(缓存未命中),因为它可能需要先驱逐 A 的数据。因此,域 B 中的一个聪明的间谍程序可以通过测量自己内存访问的时间来了解域 A 中受害者的内存访问模式。这是一种​​时序侧信道​​。解决方案?在缓存内部建立墙壁,这是一种称为缓存分区的技术,我们将一定数量的缓存“路”专门分配给每个域。这加强了隔离,但它有代价:每个域现在拥有一个较小的有效缓存,这可能会损害其性能。

这个兔子洞还更深。现代 CPU 为了不懈地追求速度,会进行​​推测执行​​。它们猜测程序会走向哪个分支,并在甚至不知道这个分支是否正确之前就执行该路径上的指令。如果猜测错误,CPU 会丢弃结果,假装什么都没发生。但这次执行,虽然是瞬态的,却是真实的。它可能在微体系结构中留下了微弱、幽灵般的痕迹,就像雪地里的脚印。像 Spectre 和 Meltdown 这样的漏洞就利用了这一点。攻击者可以诱骗 CPU 推测性地执行访问机密的代码,尽管该访问最终会被回滚,但机密数据会短暂地加载到共享缓存中。然后,攻击者利用时序侧信道来检测这些幽灵般的踪迹并窃取机密。

这不仅仅是 CPU 的问题。随着我们探索像图形处理单元 (GPU) 这样的新架构,我们发现同样的基本原理在起作用,尽管它们的表现形式不同。虽然 GPU 可能没有与 CPU 相同类型的推测执行,但其处理分歧控制流的方式可能会为依赖于机密的内存访问在共享缓存中留下痕迹创造类似的机会。理解不同架构如何产生和暴露这些微妙的共享状态,是当今硬件安全研究的前沿。

从不起眼的剪贴板到 CPU 内部的幽灵计算,保护域的概念是贯穿始终的统一线索。它是在混乱、互联的比特和电子世界中划定界限、创造秩序和分离的艺术。它是一场在隔离与通信、安全与性能之间的持续谈判,正是这种深刻而优美的挑战,使现代计算成为可能。