Windows上的Chromium沙箱经受了时间的考验。它被认为是大规模部署的最好的沙箱机制之一,不需要提升特权即可运行。尽管优点明显,但确实也存在一些缺点。沙箱的实现主要取决于Windows操作系统的安全性,更改Windows的行为不受Chromium开发团队的控制,如果在Windows的安全实施机制中发现漏洞,则沙箱就可能会被破坏。
这篇文章介绍了Windows 10 1903中引入的一个漏洞,此漏洞打破了Chromium可以使沙盒非常安全的假设。我将介绍如何使用该漏洞来开发执行链,以逃逸用于Chrome / Edge上的GPU进程或Firefox中默认内容的沙箱。利用进程也是对Windows中一些小缺陷的挖掘,这些小缺陷本身并没有越过安全边界,但却导致成功的沙箱逃逸。此漏洞于2020年4月修复为CVE-2020-0981。
https://bugs.chromium.org/p/project-zero/issues/detail?id=2006 https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/CVE-2020-0981
0x01 漏洞背景
在描述漏洞本身之前,快速看一下Chromium沙箱在Windows上是如何工作的,沙箱是指通过使用受限令牌来处理最小特权。
https://chromium.googlesource.com/chromium/src/+/master/docs/design/sandbox.md
受限令牌是Windows 2000中添加的一项功能,可通过以下操作来修改进程的访问令牌,从而减少授予进程的访问权限:
· 永久禁用群组。
· 删除特权。
· 添加受限制的SID。
禁用组将删除访问令牌的成员身份,从而导致无法访问由这些组保护的资源。删除特权可防止进程执行任何不必要的特权操作。最后,添加受限制的SID会更改安全访问检查进程,要被授予对资源的访问权限,我们需要匹配主列表以及“受限SID”列表中的组的安全描述符条目。如果SID列表之一未授予对资源的访问权限,则访问将被拒绝。
Chromium还使用Vista中添加的完整性级别(IL)功能来进一步限制资源访问。通过设置较低的IL,无论访问检查的结果如何,我们都可以阻止对更高完整性资源的写访问。
通过这种方式将受限令牌与IL结合使用,沙箱可以限制受威胁进程可以访问哪些资源,从而限制RCE可以产生的影响。阻止写访问尤为重要,因为通常这将使攻击者可以通过写文件或注册表项来破坏系统的其他部分。
Windows上的任何进程都可以使用其他令牌创建新进程,例如,通过调用CreateProcessAsUser。是什么阻止了沙盒流程使用不受限制的令牌创建新流程?Windows和Chromium实施了一些安全缓解措施,以使在沙箱外部创建新进程变得困难:
1. 内核限制了非特权用户可以向新进程分配的令牌。
2. 沙箱限制了用于新进程的合适访问令牌的可用性。
3. Chromium在Job对象内运行一个沙盒流程,该流程可被任何硬进程配额限制为1的子进程继承。
4. 在Windows 10中,Chromium使用子进程缓解策略来阻止子进程创建,除了来自3的Job对象之外,还应用了该对象。
所有这些缓解措施最终都依赖Windows来确保安全。但是,到目前为止,最关键的是1,即使2到4都失效了,从理论上讲,我们也不能为新进程分配更多特权的访问令牌。分配新令牌时内核要检查什么?
假设调用进程没有SeAssignPrimaryTokenPrivilege ,则新令牌必须满足两个标准之一,该条件已在内核函数SeIsTokenAssignableToProcess 中进行了检查,条件基于内核的TOKEN对象结构中的指定值,如下图所示:
总之,令牌必须是:
1. 当前流程令牌的子级。基于新令牌的父令牌ID等于进程令牌的ID。
2. 当前进程令牌的同级。基于父令牌ID和身份验证ID字段相等。
还进行了其他检查,以确保新令牌不是标识级别的模拟令牌,并且新令牌的IL必须小于或等于当前进程令牌,这些同样重要,但正如我们看到的那样,在实践中用处不大。
令牌分配显然不会检查父令牌或子令牌是否受到限制。如果你位于受限令牌沙箱中,能否获得通过所有检查并将其分配给有效逃离沙箱的不受限制的子令牌?答案是不能的,系统会在分配受限令牌时确保同级令牌检查失败,而是确保将执行父/子检查。如果检查内核函数SepFilterToken ,将了解如何实现此函数。将现有属性从父令牌复制到新的受限令牌时,将执行以下代码。
NewToken- > ParentTokenId = OldToken- > TokenId;
通过设置新的受限令牌的父令牌ID,它可以确保只有创建受限令牌的进程才能将其用作子对象,因为令牌ID对于TOKEN对象的每个实例都是唯一的。同时,通过更改父代令牌ID,同级检查将被破坏。
但是,当我做一些测试以验证Windows 10 1909上的令牌分配行为时,我发现有些奇怪。无论我创建了什么受限令牌,我都无法使分配失败,再次查看SepFilterToken ,我发现代码已更改。
NewToken- > ParentTokenId = OldToken- > ParentTokenId;
现在,内核代码只是直接从旧令牌中复制父令牌ID。这完全打破了检查,因为新的沙盒进程具有一个令牌,该令牌被视为桌面上任何其他令牌的同级。
假设我可以绕过已有的其他三个子进程缓解措施,那么这一行更改可能足以突破“受限令牌”沙箱。
0x02 沙箱逃逸
我想出的最后一个沙箱逃逸程序非常复杂,也不一定是最佳方法。但是,Windows的复杂性意味着很难在我们的链中找到可供利用的替代原语。
首先尝试获取合适的访问令牌以分配给新进程。令牌需要满足一些条件:
1. 令牌是主令牌或可转换为主令牌。
2. 令牌的IL等于沙箱IL,或者可以写,因此可以降低IL级别。
3. 令牌符合兄弟令牌标准,因此可以进行分配。
4. 令牌用于当前的控制台会话。
5. 令牌未沙盒化或沙盒化程度小于当前令牌。
访问令牌是可保护的对象,因此,如果你具有足够的访问权限,则可以打开令牌的句柄。但是,访问令牌不是用名称来引用的,而是要打开令牌,你需要有权访问进程或模拟线程。可以使用Get-AccessibleToken 命令使用NtObjectManager PowerShell模块查找可访问令牌。
PS> $ps = Get-NtProcess -Name "chrome.exe" ` -FilterScript { $_.IsSandboxToken } ` -IgnoreDeadProcess PS> $ts = Get-AccessibleToken -Processes $ps -CurrentSession ` -AccessRights Duplicate PS> $ts.Count 101
该脚本获取了我计算机上运行的每个沙盒Chrome进程的句柄(显然首先启动了Chrome),然后使用每个进程中的访问令牌来确定可以为TOKEN_DUPLICATE 访问打开哪些其他令牌。检查TOKEN_DUPLICATE 在新进程中用作令牌的原因是,由于两个进程不能使用同一访问令牌对象,因此需要复制令牌。访问检查考虑了调用进程是否对目标进程具有PROCESS_QUERY_LIMITED_INFORMATION 访问权限,这是打开令牌的先决条件。
一开始,我们可以访问的某些令牌在沙盒中的数量要比当前令牌在沙盒中的数量更多,我们只需要未沙盒化的可访问令牌。其次,尽管有许多可访问的令牌,但这很可能是少数进程能够访问大量令牌的产物。我们将其过滤为仅可以访问非沙盒标记的Chrome进程的命令行。
PS> $ts | ? Sandbox -ne $true | ` Sort {$_.TokenInfo.ProcessCommandLine} -Unique | ` Select {$_.TokenInfo.ProcessId},{$_.TokenInfo.ProcessCommandLine} ProcessId ProcessCommandLine --------- ---------------------------------- 6840 chrome.exe --type=gpu-process ... 13920 chrome.exe --type=utility --service-sandbox-type=audio ...
在所有可能的Chrome进程中,只有GPU进程和Audio Utility进程有权访问非沙盒令牌,这不应该让人感到意外,渲染器进程比GPU或“音频”沙箱具有更多的锁定功能,这是由于调用系统服务以使这些进程正常运行的限制。由于大多数RCE发生在HTML / JS内容中,因此大大降低了RCE发生沙箱逃逸的可能性。也就是说确实存在GPU漏洞,例如Lokihardt在Pwn2Own 2016上使用的一个漏洞。
https://bugs.chromium.org/p/chromium/issues/detail?id=595834
让我们集中讨论逃逸GPU进程沙箱。因为我没有GPU RCE,所以我将DLL注入到进程中以运行逃逸代码。这并不像听起来那么简单,一旦GPU进程启动,该进程就被锁定为仅加载Microsoft签名的DLL。我使用KnownDlls 的技巧将DLL加载到内存中。
https://www.tiraniddo.dev/2019/08/windows-code-injection-bypassing-cig.html
为了逃逸沙箱,我们需要执行以下操作:
1. 打开一个不受限制的令牌。
2. 复制令牌以创建新的主令牌并使令牌可写。
3. 删除令牌的IL以匹配当前令牌(对于GPU,这是低IL)
4. 使用新令牌调用CreateProcessAsUser 。
5. 逃逸低IL沙箱。
即使是执行第1步,我们也存在问题。获取无限制令牌的最简单方法是为父进程(即主要的Chrome浏览器进程)打开令牌。但是,如果你查看令牌列表,则GPU进程可以访问,你会发现不包括Chrome主浏览器进程。这是为什么?这是故意的,因为我在报告了内核中的此漏洞后才意识到,GPU进程沙箱可以打开浏览器进程的令牌。使用此令牌,可以创建一个新的受限令牌,该令牌将通过同级检查以创建具有更多访问权限并逃逸沙箱的新进程。为了减轻这种情况,我修改了对进程令牌的访问权限,以阻止较低IL的进程为TOKEN_DUPLICATE 访问打开令牌。看到HardenTokenIntegerityLevelPolicy。在此修复程序之前,你不需要内核中的任何漏洞即可逃逸Chrome GPU沙箱,至少不需要普通的Low IL令牌即可。
因此,我们无法使用简单的方法,但是我们应该能够简单地枚举进程并找到符合我们标准的进程。我们可以通过使用NtGetNextProcess 系统调用来做到这一点,正如我在上一篇博客文章中所描述的。我们打开所有进程以进行PROCESS_QUERY_LIMITED_INFORMATION 访问,然后打开令牌以进行TOKEN_DUPLICATE 和TOKEN_QUERY 访问。然后,我们可以在继续执行步骤2之前检查令牌以确保其不受限制。
https://googleprojectzero.blogspot.com/2015/05/in-console-able.html
要复制令牌,我们调用DuplicateTokenEx并请求传递TOKEN_ALL_ACCESS 作为所需访问的主令牌。但是有一个新问题,当我们尝试降低IL时,会从SetTokenInformation获得ERROR_ACCESS_DENIED 。这是由于Microsoft添加到Windows 10并向后移植到所有支持的操作系统(包括Windows 7)的沙箱缓解措施。以下代码是NtDuplicateToken的摘要,其中已引入缓解措施。
ObReferenceObjectByHandle(TokenHandle, TOKEN_DUPLICATE, SeTokenObjectType, &Token, &Info); DWORD RealDesiredAccess = 0; if (DesiredAccess) { SeCaptureSubjectContext(&Subject); if (RtlIsSandboxedToken(Subject.PrimaryToken) && RtlIsSandboxedToken(Subject.ClientToken)) { BOOLEAN IsRestricted; SepNewTokenAsRestrictedAsProcessToken(Token, Subject.PrimaryToken, &IsRestricted); if (Token == Subject.PrimaryToken || IsRestricted) RealDesiredAccess = DesiredAccess; else RealDesiredAccess = DesiredAccess & (Info.GrantedAccess | TOKEN_READ | TOKEN_EXECUTE); } } else { RealDesiredAccess = Info.GrantedAccess; } SepDuplicateToken(Token, &DuplicatedToken, ...) ObInsertObject(DuplicatedToken, RealDesiredAccess, &Handle);
当复制令牌时,内核会检查调用方是否沙箱保护。如果将其沙箱保护,则内核将检查要复制的令牌的限制是否小于调用者。如果限制较少,则代码会将所需的访问限制为TOKEN_READ 和TOKEN_EXECUTE 。这意味着,如果我们请求诸如TOKEN_ADJUST_DEFAULT 之类的写访问权限,它将被从复制调用返回给我们的句柄上删除。反过来,这将阻止我们减少IL,以便可以将其分配给新流程。
这似乎使我们的漏洞利用链无法继续写下去了。如果我们无法写入令牌,则无法降低令牌的IL,这会阻止我们对其进行分配。但是该实现有一个小缺陷,重复操作将继续完成,并仅返回具有有限访问权限的句柄。当你创建新的令牌对象时,默认安全性将授予调用者对令牌对象的完全访问权限。这意味着一旦你获得了新令牌的句柄,就可以调用普通的DuplicateHandle API将其转换为完全可写的句柄。尚不清楚这是否是有意的,尽管应注意,CreateRestrictedToken 中的类似检查如果新令牌不受限制,则返回错误。无论如何,我们都可以滥用此功能来获得可写的不受限制的令牌,以将其分配给具有正确IL的新进程。
现在我们可以获得一个不受限制的令牌,可以调用CreateProcessAsUser 来创建我们的新进程。但是速度并不快,因为GPU进程仍在限制性Job对象中运行,这阻止了创建新进程。我在将近5年前的“ In-Console-Able ”文章中详细介绍了Job对象如何阻止新流程的创建。
https://googleprojectzero.blogspot.com/2015/05/in-console-able.html
我们不能在控制台驱动程序中使用相同的漏洞来逃逸Job对象吗?在Windows 8.1上,你可能可以,但是在Windows 10上,有两点使我们无法使用它:
1. Microsoft更改了Job对象以支持辅助进程计数器。如果你拥有SeTcbPrivilege ,则可以将flag传递给NtCreateUserProcess 来创建仍在Job内的新进程,该进程不计入进程数。控制台驱动程序使用它来删除逃逸作业的要求。由于我们在沙箱中没有SeTcbPrivilege ,因此无法使用此功能。
2. Microsoft向令牌添加了一个新flag,以防止将其用于新进程。Chrome会在所有沙盒进程中设置此flag,以限制新的子进程。即使没有“ 1”,该flag也会阻止滥用控制台驱动程序以生成新进程。
这两个功能块的组合通过滥用控制台驱动程序在当前作业之外产生了一个新进程。我们需要想出一种既可以避免Job对象限制,又可以绕过子进程限制flag的方法。
Job对象是从父对象继承到子对象的,因此,如果我们可以在Job对象之外找到GPU进程可以控制的进程,则可以将该进程用作新的父对象并逃逸Job。不幸的是,至少在默认情况下,如果你检查GPU进程可以访问哪些进程,则它只能自行打开。
PS> Get-AccessibleProcess -ProcessIds 6804 -AccessRights GenericAll ` | Select-Object ProcessId, Name ProcessId Name --------- ---- 6804 chrome.exe
打开本身并不会很有用,而且我们不能依靠偶发的进程来运行它,而该进程恰好在运行时同时可以访问并且没有运行Job。
我注意到的一件事是,在设置新的Chrome沙箱进程的进程中,竞争很小。首先创建流程,然后应用Job对象。如果可以让Chrome浏览器生成新的GPU进程,则可以在应用Job对象之前将其用作父进程。GPU进程的处理甚至支持崩溃时重新生成该进程。但是,我找不到在不导致当前GPU进程终止的情况下启动新GPU进程的方法,因此不可能使代码运行足够长的时间来利用这种竞争。
相反,我决定专注于寻找一个RPC服务,该服务将在Job之外创建一个新进程。有很多RPC服务将流程创建作为主要目标,而其他服务则将流程创建作为副作用。例如,我已经在以前的博客文章中记录了Secondary Logon服务,其中RPC服务的全部目的是产生新进程。
https://googleprojectzero.blogspot.com/2016/03/exploiting-leaked-thread-handle.html
但是,此想法有一个小缺陷,特别是令牌中的子进程缓解flag是跨模拟边界继承的。由于通常使用模拟令牌作为新进程的基础,因此任何新进程都会被阻止。但是,我们有一个未设置flag的无限制令牌,可以使用非限制令牌创建一个可以在RPC调用期间模拟的限制令牌,并且可以绕过子进程缓解flag。
我列出可以通过这种方式使用的已知服务,这些服务汇总在下表中:
该表并不详尽,可能还会有其他RPC服务允许创建进程。正如我们在表中看到的那样,无法从沙盒级别访问生成了辅助登录,WMI和BITS之类的进程的众所周知的RPC服务。UAC服务是可访问的,存在一种通过滥用调试对象来滥用服务以运行任意特权代码的方法。不幸的是,当创建一个新的UAC进程时,该服务会将父进程设置为调用方进程。继承Job对象后,新进程将被阻止。
列表中的最后一个服务是DCOM激活器。这是负责启动进程外COM服务器的系统服务,可从我们的沙箱级别访问该服务。它还将所有COM服务器作为服务进程的子级启动,这意味着Job对象不会被继承。但是有一个小问题,为了使DCOM Activator有用,我们需要沙箱可以创建的进程外COM服务器。该对象必须满足一组条件:
1. 服务器的启动安全性将本地激活授予沙箱。
2. 服务器不得以交互用户身份运行(该用户会从沙箱中生成)或在服务进程中运行。
3. 服务器可执行文件必须可访问受限令牌。
我们不必担心3,GPU进程可以访问系统可执行文件,因此我们将坚持使用预安装的COM服务器。创建后是否无法访问COM服务器也无关紧要,我们所需要的只是在Job外部启动COM服务器进程的权限,然后可以劫持它。可以使用OleViewDotNet和Select-ComAccess 命令找到可访问的COM服务器。
PS> Get-ComDatabase -SetCurrent PS> Get-ComClass -ServerType LocalServer32 | ` Where-Object RunAs -eq "" | ` Where-Object {$_.AppIdEntry.ServiceName -eq ""} | ` Select-ComAccess -ProcessId 6804 ` -LaunchAccess ActivateLocal -Access 0 | ` Select-Object Clsid, DefaultServerName Clsid DefaultServerName ----- ----------------- 3d5fea35-6973-4d5b-9937-dd8e53482a56 coredpussvr.exe 417976b7-917d-4f1e-8f14-c18fccb0b3a8 coredpussvr.exe 46cb32fa-b5ca-8a3a-62ca-a7023c0496c5 ieframe.dll 4b360c3c-d284-4384-abcc-ef133e1445da ieframe.dll 5bbd58bb-993e-4c17-8af6-3af8e908fca8 ieproxy.dll d63c23c5-53e6-48d5-adda-a385b6bb9c7b ieframe.dll
在Windows 10的默认安装中,有6个候选软件。请注意,最后4个都在DLL中,但是这些类已注册为在DLL Surrogate中运行,因此仍可以在进程外使用。我选择COREDPUSSVR中的服务器,因为它是唯一的可执行文件,而不是通用的DLLHOST,因此更易于查找。此COM服务器的启动安全性授予所有AppContainer程序包本地激活权限,如下所示:
即使为COREDPUSSVR注册了两个类,该可执行文件实际上也只注册了一个以417976b7 开头的类。创建另一个类将启动服务器可执行文件,但是类创建将挂起,等待一个永远不会出现的类。
要启动服务器,请在模拟子进程缓解flag免费的受限令牌的同时调用CoCreateInstance。你还需要传递CLSCTX_ENABLE_CLOAKING 来使用模拟令牌激活服务器,默认值将使用已设置了子进程缓解flag的进程令牌,因此将阻止进程创建。
通常,新进程的默认安全性基于用于创建新进程的访问令牌中的默认DACL。不幸的是,由于某些不清楚的原因,DCOM激活器在进程上设置了一个明确的DACL,它仅授予对用户,SYSTEM和当前登录SID的访问权限,即使GPU进程实际上以相同的安全级别运行,这也不允许GPU进程打开新的COM服务器进程。我尝试了几种方法来在COM服务器内部执行代码,例如Windows Hooks,但是没有明显的作用。
幸运的是,进程启动后创建的所有线程仍将使用默认的DACL。我们可以打开其中一个线程进行完全访问,并使用SetThreadContext更改线程上下文以重定向执行。我们需要暴力地使用这些新线程的线程ID,因为进一步的沙箱缓解会阻止我们使用CreateToolhelp32Snapshot枚举无法直接打开的进程,并且NtGetNextThread API需要没有的父进程句柄。
滥用线程会很复杂,特别是因为无法直接将内容写入进程,但至少可以正常工作。将执行重定向到哪里?为了简便起见,我决定调用WinExec,它将生成一个新进程,并且只需要执行命令行即可。新进程将具有基于默认DACL的安全性,因此我们可以将其打开。我可以选择其他类似LoadLibrary的方式来加载DLL。但是,在与线程上下文打交道时,可能会导致进程崩溃。我觉得最好是通过尽快逃逸此进程来避免这种情况。
我们无法在COM服务器进程中直接写入或分配内存,但是我们可以轻松地重新利用二进制文件中现有的字符串来执行。为了避免寻找字符串地址或处理ASLR,我只是选择在DLL的开头使用PE签名,该签名为我们提供了字符串“ PE”。当传递给WinExec时,当前的PATH环境变量将用于查找要启动的可执行文件。我们可以将PATH设置为COM服务器中所需的任何内容,因为当以相同的安全级别启动进程时,DCOM激活器将使用调用者的环境。唯一需要做的就是找到一个我们可以写入的目录,这一次可以使用Get-AccessibleFile 找到一个候选对象,如图所示。
PS> Get-AccessibleFile -Win32Path "C:\" -Recurse -ProcessIds 6804 ` -DirectoryAccessRights AddFile -CheckMode DirectoriesOnly ` -FormatWin32Path | Select-Object Name Name ---- C:\ProgramData\Microsoft\DeviceSync
通过设置PATH Environment变量以包含DeviceSync 路径并将名为PE.exe的可执行文件复制到该目录,我们可以设置线程上下文并生成一个新进程,该进程不在Job对象中,并且可以由GPU进程打开。
现在,我们可以利用内核漏洞,并以低IL运行不受限制的令牌从新进程中调用CreateProcessAsUser 。这将删除所有“低IL”以外的沙箱。最后一步是突破低IL,同样有很多方法可以做到这一点,但我决定滥用UAC服务。我本可以滥用以前博客中记录的“调试对象”漏洞,但是我决定滥用UAC的另一个“函数”。通过滥用同一令牌访问,我们在链中滥用以打开不受限制的令牌,我们可以获得UI访问权限。这使我们能够自动化特权用户界面(例如Explorer运行对话框),以在Low IL沙箱外部执行任意代码。
最终利用链如下:
1. 打开一个不受限制的令牌
a.蛮力查找过程,直到找到合适的过程令牌。
2. 复制令牌以创建新的主令牌并使令牌可写。
a.重复令牌为只读
b复制句柄以取回写访问权限
3. 删除令牌的以匹配当前令牌
4. 使用新令牌调用 Create ProcessAsUser。
a.创建一个新的受限令牌以删除子进程缓解标志
b.将环境块的PATH设置为包含 Device Sync文件夹,然后删除 PE.exe文件。
c.模拟受限令牌并创建 OOP COM服务器。
d.COM服务器进程的暴力线程ID。
e.修改线程上下文以调用 WinExec,以在内存中传递已知PE签名的地址。
f.等待创建PE进程
5. 逃避低沙箱。
a.生成屏幕键盘的副本并打开其令牌。
b.根据打开的令牌创建具有U问权限的新进程。
c.自动运行对话框以退出低L沙箱。
流程图:
0x03 分析总结
我希望能学习Windows内核中的微小更改如何对沙盒环境的安全性产生影响,由于大量缓解措施,许多时候封堵了开发漏洞利用的便利之路。
本文翻译自:https://googleprojectzero.blogspot.com/2020/04/you-wont-believe-what-this-one-line.html如若转载,请注明原文地址: