你不会相信这一行的改变对Chrome沙盒造成了什么影响

2020-05-12 15:04:26

Windows上的Chromium沙盒经受住了时间的考验。它被认为是不需要提升特权就可以大规模部署的更好的沙箱机制之一。从好的方面来说,它确实有它的弱点。主要的问题是沙箱的实现依赖于Windows操作系统的安全性。更改Windows的行为不在Chromium开发团队的控制范围之内。如果在Windows的安全实施机制中发现错误,则沙箱可能会被破坏。

这篇博客是关于Windows101903中引入的一个漏洞,该漏洞打破了Chromium用来保证沙箱安全的一些安全假设。我将介绍如何利用该bug开发一系列执行链,以避开Chrome/Edge上的GPU进程或Firefox中的默认内容沙箱所使用的沙箱。利用过程也是对Windows中的小弱点的一个有趣的洞察,这些小弱点本身并没有越过安全边界,但却导致了成功的沙盒逃脱。此漏洞已在2020年4月修复,名称为CVE-2020-0981。

在描述错误本身之前,让我们快速了解一下Chromium沙箱在Windows上是如何工作的。沙箱通过使用受限令牌来处理最小特权的概念。受限令牌是Windows 2000中添加的一项功能,它通过以下操作修改进程的访问令牌,从而减少授予进程的访问权限:

禁用组会删除访问令牌的成员身份,从而导致禁用对这些组保护的资源的访问。删除权限可防止进程执行任何不必要的特权操作。最后,添加受限SID会更改安全访问检查流程。要获得对资源的访问权限,我们需要匹配主列表和受限SID列表中的组的安全描述符条目。如果其中一个SID列表未授予对资源的访问权限,则访问将被拒绝。

Chrome还使用Vista中添加的完整性级别(IL)功能进一步限制资源访问。通过设置较低的IL,我们可以阻止对更高完整性资源的写访问,而不管访问检查的结果如何。

以这种方式将受限令牌与IL一起使用允许沙箱限制受危害的进程可以访问的资源,从而限制RCE可能产生的影响。阻止写访问尤其重要,因为这通常会允许攻击者通过写入文件或注册表项来危害系统的其他部分。

Windows上的任何进程都可以使用不同的令牌创建新进程,例如,通过调用CreateProcessAsUser。是什么阻止沙箱进程使用无限制令牌创建新进程?Windows和Chromium实施了一些安全缓解措施,使得在沙箱之外创建新进程变得困难:

内核限制非特权用户可以将哪些令牌分配给新进程。

沙箱限制限制了用于新进程的适当访问令牌的可用性。

Cr在Job对象内运行沙箱进程,该对象由任何硬进程配额限制为1的子进程继承。

在Windows 10中,Chromium使用子进程缓解策略来阻止创建子进程。这是在3中的Job对象之外应用的。

所有这些缓解措施最终都依赖于Windows的安全性。但是,到目前为止,最关键的是1。即使2到4失败,理论上我们也不能为新进程分配更有特权的访问令牌。当涉及到分配新令牌时,内核检查是什么?

假设调用进程没有SeAssignPrimaryTokenPrivilege(我们没有),那么新令牌必须满足内核函数SeIsTokenAssignableToProcess中检查的两个条件之一。这些条件基于内核的标记对象结构中的指定值,如下图所示。

当前进程令牌的子级。基于新令牌的父令牌ID等于进程令牌的ID。

当前进程令牌的同级。基于父令牌ID和身份验证ID字段都相等。

还需要进行额外的检查,以确保新的令牌不是标识级别的模拟令牌(由于我报告的这个错误),并且新令牌的IL必须小于或等于当前的进程令牌。这些都同样重要,但正如我们将看到的,在实践中用处较小。

令牌分配不会明显检查父令牌或子令牌是否受到限制。如果您在受限令牌沙箱中,是否可以获得通过所有检查并将其分配给有效逃离沙箱的子代的无限制令牌?不,您不能,系统确保在分配受限令牌时兄弟令牌检查失败,而是确保父/子检查是将强制执行的检查。如果您查看内核函数SepFilterToken,就会了解它是如何实现的。将现有属性从父令牌复制到新的受限令牌时,将执行以下代码。

通过设置新的受限令牌的父令牌ID,它确保只有创建受限令牌的进程才能为子进程使用它,因为令牌ID对于令牌对象的每个实例都是唯一的。同时,通过更改父令牌ID,兄弟检查被破坏。

然而,当我在做一些测试来验证Windows101909上的令牌分配行为时,我注意到一些奇怪的事情。无论我创建了什么受限令牌,我都无法使赋值失败。再次查看SepFilterToken,我发现代码已经更改。

内核代码现在只是从旧令牌直接复制父令牌ID。这完全打破了检查,因为新的沙箱进程有一个令牌,该令牌被认为是桌面上任何其他令牌的兄弟。

假设我可以绕过已经到位的其他3个子进程缓解,那么这一行更改就足以突破受限令牌沙箱。让我们来经历一下为做到这一点而承担的考验和磨难。

我想出的最终沙盒逃逸是相当复杂的,这也不一定是最优的方法。然而,Windows的复杂性意味着在我们的链中很难找到可供利用的替代原语。

让我们从尝试获取合适的访问令牌来分配给新进程开始。令牌需要满足一些条件:

令牌具有等于沙箱IL的IL,或者是可写的,从而可以降低IL级别。

访问令牌是可保护的对象,因此,如果您有足够的访问权限,则可以打开令牌的句柄。但是,访问令牌不是由名称引用的,相反,要打开令牌,您需要拥有对进程或模拟线程的访问权限。我们可以使用NtObjectManager PowerShell模块通过Get-AccessibleToken命令查找可访问的令牌。

该脚本获得在我的机器上运行的每个沙箱Chrome进程的句柄(显然是首先启动Chrome),然后使用每个进程的访问令牌来确定我们可以打开哪些其他令牌进行Token_Duplicate访问。检查TOKEN_DUPLICATE以用作新进程中的令牌的原因是,我们需要制作令牌的副本,因为两个进程不能使用相同的访问令牌对象。访问检查会考虑调用进程是否具有对目标进程的PROCESS_QUERY_LIMITED_INFORMATION访问权限,这是打开令牌的先决条件。我们已经有了相当数量的结果,超过100个条目。

然而,这个数字具有欺骗性,首先,我们可以访问的一些令牌几乎肯定会比当前令牌被沙箱更多地被沙箱。实际上,我们只需要无沙箱的可访问令牌。其次,虽然有很多可访问的令牌,但这很可能是少数进程能够访问大量令牌的产物。我们将把它过滤到只有Chrome进程的命令行,这些进程可以访问非沙箱令牌。

在所有可能的Chrome进程中,只有GPU进程和Audio实用程序进程可以访问非沙箱令牌。这应该不会让人大吃一惊。由于调用系统服务以使这些进程正常工作的限制,渲染器进程比GPU或音频沙箱的锁定程度要高得多。这确实意味着从RCE到沙箱转义的可能性大大降低,因为大多数RCE发生在呈现HTML/JS内容时。也就是说,GPU错误确实存在,例如,这个错误就是洛基哈特在2016年Pwn2Own大会上使用的一个错误。

让我们重点关注如何摆脱GPU进程沙箱。因为我没有GPU RCE可用,所以我将只向进程中注入一个DLL来运行转义。这并不像听起来那么简单,一旦GPU进程启动,该进程就被锁定为只加载Microsoft签名的DLL。我对KnownDlls使用了一个技巧,将DLL加载到内存中(有关详细信息,请参阅这篇博客文章)。

丢弃令牌的IL以匹配当前令牌(对于GPU,这是低IL)。

即使是在第一步,我们也有问题。获取无限制令牌的最简单方法是打开父进程的令牌,父进程是Chrome浏览器的主进程。但是,如果您查看GPU进程可以访问的令牌列表,您会发现不包括主Chrome浏览器进程。为什么会这样呢?这是故意的,因为在报告内核中的这个错误后,我意识到GPU进程沙箱可以打开浏览器进程的令牌。有了这个令牌,就可以创建一个新的受限令牌,该令牌将通过同级检查来创建一个具有更多访问权限的新进程,并退出沙箱。为了缓解这个问题,我修改了进程令牌的访问权限,以阻止较低的IL进程为TOKEN_DUPLICATE访问打开令牌。请参阅HardenTokenIntegerityLevelPolicy。在此修复之前,您不需要内核中的bug就可以逃离Chrome GPU沙箱,至少可以达到正常的低IL令牌。

因此,我们没有简单的路径可用,但是我们应该能够简单地枚举进程,并找到符合我们标准的进程。我们可以通过使用NtGetNextProcess系统调用来实现这一点,正如我在上一篇博客文章(关于稍后我们将讨论的主题)中所描述的那样。我们为PROCESS_QUERY_LIMITED_INFORMATION访问打开所有进程,然后为TOKEN_DUPLICATE和TOKEN_QUERY。然后,在继续执行步骤2之前,我们可以检查令牌以确保其不受限制。

为了复制令牌,我们调用DuplicateTokenEx并请求一个主令牌,将TOKEN_ALL_ACCESS作为所需的访问权限进行传递。但是有一个新问题,当我们尝试降低IL时,从SetTokenInformation得到ERROR_ACCESS_DENIED。这是因为微软在Windows10中添加了沙箱,并重新移植到所有支持的操作系统(包括Windows7)。以下代码是引入缓解的NtDuplicateToken中的一段代码。

当您复制令牌时,内核会检查调用方是否被沙箱保护。如果内核被沙箱保护,则检查要复制的令牌是否比调用方受到的限制更少。如果限制较少,则代码将所需的访问限制为TOKEN_READ和TOKEN_EXECUTE。这意味着,如果我们请求诸如TOKEN_ADJUST_DEFAULT这样的写访问,它将在从复制调用返回给我们的句柄上被删除。反过来,这将阻止我们减少IL,以便将其分配给新进程。

这似乎结束了我们的利用链。如果我们不能写入令牌,我们就不能减少令牌的IL,这会阻止我们分配它。但是实现有一个很小的缺陷,复制操作会继续完成,并返回一个访问权限有限的句柄。创建新的令牌对象时,默认安全性授予调用方对令牌对象的完全访问权限。这意味着一旦获得新令牌的句柄,就可以调用普通的DuplicateHandle API将其转换为完全可写的句柄。目前还不清楚这是否是故意的,不过应该注意,如果新令牌不受限制,CreateRestrictedToken中的类似检查将返回错误。无论在哪种情况下,我们都可以滥用这一错误特性来获得一个可写的无限制令牌,以使用正确的IL将其分配给新进程。

现在我们可以获得一个不受限制的令牌,我们可以调用CreateProcessAsUser来创建我们的新流程。但不会那么快,因为GPU进程仍在一个受限的Job对象中运行,该对象会阻止创建新进程。大约5年前,我在我的“In-Console-able”博客文章中详细介绍了Job对象如何阻止新进程的创建。我们可以不使用控制台驱动程序中的相同错误来转义作业对象吗?在Windows8.1上,您可能可以(尽管我承认我没有测试过),但是在Windows10上,有两件事阻碍了我们使用它:

Microsoft更改了作业对象以支持辅助进程计数器。如果您拥有SeTcbPrivilege,您可以向NtCreateUserProcess传递一个标志,以创建一个仍在Job内的新进程,该进程不计入进程计数。控制台驱动程序使用它来消除转义作业的要求。因为我们在沙箱中没有SeTcbPrivilege,所以我们不能使用此功能。

Microsoft向令牌添加了一个新标志,以防止它们被用于新进程。此标志由Chrome在所有沙箱进程上设置,以限制新的子进程。即使没有‘1’,该标志也会阻止滥用控制台驱动程序来生成新进程。

这两个功能的组合会阻止通过滥用控制台驱动程序在当前作业之外生成新进程。我们需要想出另一种方法来避开Job对象限制,同时绕过子进程限制标志。

Job对象是从父对象继承到子对象的,因此,如果我们可以找到GPU进程可以控制的Job对象之外的进程,我们就可以将该进程用作新的父进程并退出Job。不幸的是,至少在默认情况下,如果您检查GPU进程可以访问哪些进程,它只能打开自己。

开放本身不会有多大用处,我们不能指望幸运地处理一个恰好正在运行的进程,而这个进程既可以访问,又不能运行作业。我们需要自己创造运气。

我注意到的一件事是,有一个小的竞争条件设置了一个新的Chrome沙箱进程。首先创建进程,然后应用作业对象。如果我们可以让Chrome浏览器生成一个新的GPU进程,我们就可以在应用Job对象之前将其用作父进程。GPU进程的处理甚至支持在进程崩溃时重新生成进程。然而,我无法找到一种方法,让一个新的GPU进程在不终止当前进程的情况下产生,所以不可能让代码运行足够长的时间来利用竞争。

相反,我决定集中精力寻找一个RPC服务,该服务将在Job之外创建一个新流程。有相当多的RPC服务以创建进程为主要目标,而其他服务则以创建进程为副作用。例如,我已经在上一篇博客文章中记录了二级登录服务,其中RPC服务的全部目的是生成新进程。

不过,这个想法有一点小缺陷,特别是令牌中的子进程缓解标志是跨模拟边界继承的。由于使用模拟令牌作为新进程的基础是很常见的,因此任何新进程都将被阻塞。但是,我们有一个未设置标志的无限制令牌。我们可以使用无限制令牌来创建可以在RPC调用期间模拟的受限令牌,并且可以绕过子进程缓解标志。

我尝试列出了哪些已知的服务可以以这种方式使用,我将这些服务汇总在下表中:

该表并不详尽,可能还有其他允许创建进程的RPC服务。正如我们在表中看到的那样,产生二级登录、WMI和BITS等进程的众所周知的RPC服务不能从我们的沙箱级别访问。UAC服务是可访问的,正如我在上一篇博客中所描述的,存在一种通过滥用调试对象来滥用该服务来运行任意特权代码的方法。不幸的是,当创建新的UAC进程时,该服务将父进程设置为调用者进程。由于继承了Job对象,因此新进程将被阻止。

列表中的最后一个服务是DCOM激活器。这是负责启动进程外COM服务器的系统服务,可以从我们的沙箱级别进行访问。它还将所有COM服务器作为服务进程的子级启动,这意味着作业对象不会被继承。看起来很理想,但是有一个小问题,为了使DCOM Activator有用,我们需要沙箱可以创建的进程外COM服务器。此对象必须满足一组条件:

服务器不能作为交互式用户(将从沙箱中派生出来)或在服务进程内部运行。

我们不必担心标准3,GPU进程可以访问系统可执行文件,因此我们将坚持使用预先安装的COM服务器。如果我们在创建后不能访问COM服务器,也不要紧,我们所需要的就是在作业之外启动COM服务器进程的权限,然后我们就可以劫持它了。我们可以使用OleViewDotNet和Select-ComAccess命令查找可访问的COM服务器。

在默认安装的Windows 10上,我们有6个候选者。请注意,最后4个都在DLL中,但是这些类被注册为在DLL代理程序内运行,因此仍然可以在进程外使用。我决定使用COREDPUSSVR中的服务器,因为它是唯一的可执行文件,而不是通用的DLLHOST,因此更容易找到。此COM服务器的启动安全性授予Everyone和所有AppContainer包本地激活权限,如下所示:

另外,尽管有两个类注册了COREDPUSSVR,但实际上只有一个以417976b7开头的类是由可执行文件注册的。创建另一个类将启动服务器可执行文件,但是类创建将挂起,等待永远不会出现的类。

要启动服务器,您可以在模拟子进程缓解无标志受限令牌时调用CoCreateInstance。您还需要传递CLSCTX_ENABLE_COWAKING以使用模拟令牌激活服务器,默认情况下将使用进程令牌,该令牌设置子进程缓解标志,因此会阻止进程创建。这样做,您会发现COREDPUSSVR的一个实例在相同的沙箱级别上运行,但是在Job对象之外,并且没有子进程缓解。成功?

还没那么快。通常,新进程的缺省安全性基于用于创建它的访问令牌内的缺省DACL。不幸的是,由于某些不清楚的原因,DCOM激活器在进程上设置了显式DACL,该DACL仅授予用户、系统和当前登录SID访问权限。这不允许GPU进程打开新的COM服务器进程,即使它实际上运行在相同的安全级别。如此接近,却又如此遥远。我尝试了几种方法来让代码在COM服务器内执行,比如Windows Hooks,但是都没有明显的效果。

幸运的是,默认的DACL仍然用于进程启动后创建的任何线程。我们可以打开其中一个线程进行完全访问,并使用S将线程上下文更改为重定向执行。

..