通过滥用UI Access绕过管理员保护

访问原始链接 Google 翻译

在我上一篇博客文章中,我介绍了Windows的新功能——管理员保护(Administrator Protection),以及它如何旨在为UAC创建一个原本不存在的安全边界。我描述了在该功能发布前我能够绕过它的其中一种方法。在我的研究中,总共发现了9种绕过方法,现在这些方法都已被修复。

在这篇博客文章中,我想描述这9个问题中5个问题的根本原因,特别是UI Access的实现,以及这如何成为UAC中长期存在但未被充分重视的问题,以及现在是如何修复的。

可访问性问题

在Windows Vista之前,在用户桌面上运行的任何进程都可以控制另一个进程创建的任何窗口,例如通过发送窗口消息。如果特权用户(例如SYSTEM)在桌面上显示用户界面,这种行为可能会被滥用。受限用户可以控制该UI并可能提升权限。这被称为Shatter Attack,通常通过从特权代码中移除用户界面组件来修复。

由于UAC鼓励在同一桌面上以不同权限级别运行进程,微软引入了一个附加功能——用户界面隐私隔离(UIPI)。它利用UAC中的强制完整性控制(Mandatory Integrity Control)功能来限制进程可以与哪些窗口交互。如果一个进程的完整性级别低于创建窗口的进程,那么它将被阻止向该窗口发送消息等操作。作为额外的保护,Vista不再在"服务"桌面上运行用户进程,因此即使UIPI不足,服务进程暴露的用户界面也无法被受限进程访问。

举个例子,受限用户进程被分配的完整性级别为"中",而UAC管理员进程为"高"。在这种情况下,UIPI将阻止受限用户进程向管理员进程创建的任何窗口发送消息,除了一小部分明确允许的消息。它还会阻止其他UI功能,例如窗口钩子。

这对任何依赖辅助技术(例如屏幕阅读器)的用户来说都带来了问题。如果辅助进程以受限用户身份运行,它将无法再与桌面上创建的管理员进程交互。它既无法读取窗口内容,也无法执行点击按钮等操作。这是不可接受的妥协,因此Vista需要一种方法来允许这些应用程序继续工作。

微软选择的解决方案是为进程的访问令牌分配一个名为UI Access的标志。如果进程的访问令牌在初始化其与Win32子系统的连接时设置了此标志,则该进程将被授予特殊权限,以绕过UIPI施加的许多限制。通过调用NtSetInformationToken并指定TokenUIAccess信息类来启用此标志,需要检查是否拥有SE_TCB_NAME特权,因此受限用户无法执行此操作。因此,要创建具有UI Access能力的进程,需要一个系统服务来启用该标志并创建新进程。

UAC已经需要一个系统服务,因此创建UI Access进程被纳入与通过RAiLaunchAdminProcess RPC调用启动管理员进程相同的流程中。当通过此RPC调用创建UI Access进程时,它不会像管理员提升那样显示同意提示。这很重要,否则用户可能无法创建所需的辅助应用程序来点击提升的同意提示。

为了防止恶意软件声称自己是辅助应用程序,该服务对可执行文件施加了一些额外的检查,必须满足这些条件才能在新进程上启用UI Access标志:

  • 它必须嵌入一个清单,其中uiAccess属性设置为true。
  • 它必须由本地计算机根证书存储信任的代码签名证书签名。除了这一点,证书没有特殊要求,例如不需要特殊的EKU或由微软交叉签名。
  • 它必须存储在系统驱动器上仅限管理员访问的位置,例如:

Program Files目录
Windows目录(排除一些已知的可写位置)
System32目录(排除一些已知的可写位置)

  • Program Files目录
  • Windows目录(排除一些已知的可写位置)
  • System32目录(排除一些已知的可写位置)

如果满足所有条件,那么当通过RAiLaunchAdminProcess启动进程时,服务将复制调用者的访问令牌,启用UI Access标志,并根据调用者按如下方式提高完整性级别:

  • 如果调用者是UAC管理员的受限用户,则将完整性级别设置为高。
  • 如果调用者是管理员,则将完整性级别设置为高(通常是无操作)。
  • 如果调用者是普通用户,则将完整性级别设置为调用者的完整性级别加16,最高不超过高。

高完整性级别是允许设置的绝对最大值,尽管存在更高的"系统"级别,但该级别保留给服务进程。另请注意,如果调用者已经启用了UI Access标志,则令牌的完整性级别不会改变,这对于不会自动设置为高完整性的普通用户来说很重要。设置提升的完整性级别的一个好处是,较低完整性进程无法打开创建的进程进行读取或写入访问,从而防止受限用户将代码注入新进程并进而获取UI Access标志。

顺便提一下,你可以在没有TCB特权的情况下禁用令牌上的UI Access标志。以普通用户身份运行的有效UI Access进程可以通过清除其自身令牌上的标志,然后通过UAC服务重新生成自身的另一个副本来"逐步提升"到高完整性。由于中和高之间有4096个级别,这需要调用UAC服务255次,这有点嘈杂,但确实有效。

重要的是,UI Access标志仅允许绕过有限的操作,例如向其他更高完整性进程发送窗口消息。它不允许使用诸如窗口钩子之类允许将代码注入进程的功能。因此,对于以完整性级别低于高的普通用户身份运行的UI Access进程,它可以通过消息与生成的管理员进程交互,但不能执行更侵入性的操作,例如挂钩窗口消息队列。

但是,如果受限用户创建了一个UI Access进程,它将以高完整性级别运行,并且可以接管任何包含窗口的管理员进程。具有系统完整性级别的服务进程只能使用窗口消息进行交互。但管理员和系统服务之间没有安全边界,因此这在实践中没有意义。

最终结果是,拥有UI Access标志但没有高完整性级别不足以轻易地破坏管理员进程,你需要该进程暴露一个可以自动化的用户界面,以便让特权代码运行。例如,可以向管理员命令提示符发送击键来运行任意命令。

但是,如果你与目标进程具有相同的完整性级别,UI Access标志就变得无关紧要,你可以通过使用窗口钩子注入DLL来直接破坏至少有一个窗口的进程。这个窗口不需要呈现用户界面,事实上,像COM这样的技术在底层使用仅消息窗口,可以在不向用户显示任何内容的情况下破坏进程。

当然,这是UAC中的工作方式,但新的和改进的管理员保护呢?与现有的管理员批准UAC完全相同。UI Access进程将在调用者的令牌下运行,在这种情况下,调用者是受限用户,而不是影子管理员。该进程将启用UI Access标志,并将完整性级别设置为高。

这是一个问题,拥有以高完整性级别运行的进程可以破坏同一桌面上以该级别运行的任何其他进程,即使该进程以不同用户身份运行。由于UI Access进程以受限用户身份运行,没有配置文件分离,而这是管理员保护的关键改进之一。

提升到高完整性也是静默的,因此,假设存在可利用的合适管理员进程,至少可以在不提示用户的情况下破坏安全边界。我们现在需要的只是一种在高完整性UI Access进程中获取任意代码执行的方法。幸运的是,有很多方法可以做到这一点。

实现任意UI Access执行

多年来,有多种方法可以以高完整性级别UI Access进程的身份执行任意代码。虽然微软明确表示修复这些方法不是优先事项,但它们有时还是被修复了。让我们将其分解为几个类别,并提供一些历史细节以及我最近的研究。

绕过安全应用程序目录检查

获取任意代码执行的一种方法是绕过UAC服务中的安全目录位置检查。如果你可以绕过此检查,你可以放置自己签名的可执行文件,或者可以被劫持的现有可执行文件,例如通过DLL植入。

一种方法是在appinfo.dll中实现检查的AiCheckSecureApplicationDirectory方法中找到一个bug。该方法首先打开通过RAiLaunchAdminProcess提供的文件路径,然后在句柄上调用GetFinalPathNameByHandle以确保路径未重定向到不安全的位置。然后它对路径进行简单的字符串检查,查找包含和排除的目录。我在2017年发现了该检查的一个绕过方法并报告给了MSRC。该检查没有考虑到,如果受限用户对该目录具有写入权限,则可以向目录写入NTFS命名流。

例如,目录C:\Windows\tracing对受限用户可写,但tracing子目录在检查中被明确排除,因此C:\Windows\tracing\file.exe不会被视作安全。但是,使用相同的访问权限,你可以在目录上写入命名流,使得C:\Windows\tracing:file.exe被视为在C:\Windows目录内,因此是安全的。这个bug没有作为安全公告修复,但最终在Windows的后续版本中得到解决,并且不适用于Windows 11。

另一种方法是在安全位置找到未在检查中明确排除的可写文件或目录。如果你找到一个可写文件,那么你可以用可执行文件覆盖它,因为UAC服务使用的CreateProcessAsUser API不需要可执行文件具有特定的文件扩展名。如果你找到一个目录,那么你可以直接将可执行文件复制到该位置。

在默认安装中,似乎没有未覆盖的位置。我在研究中发现的一个位置是,有时在主要的Windows更新中,Tasks目录会作为备份复制到Tasks_Migrated目录。这个备份目录像原始的Tasks一样可写,并且未包含在排除目录列表中。但是,你没有已知的方法强制创建它,而且自从我指出这一点后,微软已将其添加到排除目录列表中。

注意:微软确实忘记为Tasks_Migrated添加命名流检查,但由于目录的访问控制,普通用户无法利用。

你可以使用我的PowerShell工具通过以下命令查找潜在的候选位置。为了获得最佳效果,请以管理员身份运行它,并将替换为受限用户进程的进程ID。它不会过滤掉排除的目录,因此你需要自己检查。

PS> $paths = "C:\Windows","C:\Program Files","C:\Program Files (x86)"
PS> Get-AccessibleFile -Win32Path $paths -Access Execute,WriteData `
       -DirectoryAccess AddFile -Recurse -ProcessId <PID>

最后一种方法是找到通过不绕过该目录访问控制的单独机制将文件写入现有安全位置的方法。我在最近的研究中发现了这样一个问题。Windows安装程序会将MSIX文件安装到C:\Program Files\WindowsApps目录中,该目录未被检查排除。Windows 11默认配置为允许安装签名的MSIX文件,而无需管理员权限。

因此,你可以将UI Access可执行文件打包到MSIX安装程序中,使用任意证书对安装程序进行签名,然后在安装时,可执行文件将位于安全位置。当然,要做到这一点,你需要一个代码签名证书,但这并不像看起来那么困难。如果你愿意,你甚至可以将签名的UI Access可执行文件偷偷放入商店应用程序中。但现在这已被修复,因为WindowsApps目录也被排除了。

有趣的是,在构建MSIX时,你可以向清单添加一个uiAccess受限能力,这将把打包的可执行文件提升为高完整性UI Access。但是,当你这样做时,安装包需要管理员权限,如下所示,因此这不是一个绕过方法。

Windows dialog showing the installation of a PoC application which has the uiAccess capability set. It's requiring running the installer as an administrator.

重新利用现有的安全UI Access可执行文件

第二类是在已位于安全位置的具有UI Access能力的可执行文件中找到可被滥用的功能。你可以完全控制UI Access进程的命令行,也许有一个选项可以加载任意DLL?

在找到可利用的行为之前,你需要找到可以进行逆向工程的候选可执行文件。你可以使用我的PowerShell工具查找将uiAccess清单选项设置为true的可执行文件。

PS> $paths = "C:\Windows","C:\Program Files","C:\Program Files (x86)"
PS> Get-ChildItem -Path $paths -Include *.exe -Recurse |
  % {
       Get-Win32ModuleManifest $_.FullName
  } | Where-Object UiAccess | Select-Object -ExpandProperty FullPath

有了候选列表,你需要进行一些逆向工程。我将把这留给你。

共享配置文件和环境

管理员保护的一个重大变化是分离了受限用户和管理员之间的共享配置文件。目的是防止通过修改磁盘上的用户配置文件或注册表来提升权限。不幸的是,由于UI Access进程是基于受限用户创建的,与UAC相同,这种分离不适用,你可以找到利用这种行为的方法。

检查潜在可利用行为的一种简单方法是运行Process Monitor并捕获访问受限用户注册表配置单元或配置文件目录的事件。还可以劫持诸如用户的C:驱动器映射之类的东西,因为登录会话在受限用户及其UI Access进程之间是相同的。

Screenshot of process monitor showing osk.exe accessing registry keys in the current user's registry hive.

这是UI Access和UAC的一个众所周知的问题,所以当我在管理员保护中发现它时,我并不真的需要报告它,但觉得我应该报告。为了确保它得到适当处理,我找到了一个特定的可利用条件并附上了概念证明。在这种情况下,我发现屏幕键盘(On-Screen Keyboard)从基于CommonProgramFiles环境变量的路径加载DLL。通过覆盖用户注册表配置单元中的此变量,我可以重定向DLL加载并在UI Access进程中获取任意代码执行。

在我的研究中,我偶然发现了一个公开的绕过方法,最初是针对UAC的,但它仍然适用于管理员保护。这个绕过方法在快速助手应用程序中,它似乎是一个可选组件,但默认安装在Windows 11上。它滥用了快速助手应用程序将加载WebView2 API来显示HTML内容的事实。WebView2会在用户的配置单元中查找可覆盖的安装位置以加载其库,通过将其覆盖到用户控制下的位置,可以强制将DLL加载到UI Access进程中。

这个绕过方法最有趣的一个方面是,它使用了一个我不知道存在的API,GetProcessHandleFromHwnd,来获取创建窗口的进程的内核句柄,以便在UI Access进程中获取任意代码执行。

利用RAiLaunchAdminProcess

要启动UI Access进程,shell会调用UAC服务中的RAiLaunchAdminProcess RPC方法。与所有未直接暴露或未记录的API一样,它们可能隐藏导致可利用行为的功能。

我报告了两个问题,它们允许我在UI Access进程中获取任意代码执行,一个是公开已知的绕过方法,另一个是处理可执行文件路径时的TOCTOU(检查时间与使用时间)问题。公开的绕过方法由我自己在一篇关于使用我的PowerShell工具调用本地RPC方法的博客文章中描述。我给出的例子是调用RAiLaunchAdminProcess并滥用服务不清理进程创建标志的事实。

你可以传递DEBUG_PROCESS标志,并从中获得对创建进程的完全控制。这篇博客文章在UAC绕过的背景下描述了这一点,但当然,它同样适用于UI Access进程,正如我在发送给MSRC的报告中详细说明的那样。这是那些我担心在管理员保护开发过程中未被修复的bug之一,但由于它仅仅是一个UAC绕过,它显然被遗漏了。

第二个问题在于处理可执行文件路径的方式,这允许我们破坏UI Access进程。RAiLaunchAdminProcess RPC方法具有与最终调用以创建新进程的CreateProcessAsUser API非常相似的一组参数。这包括有一个单独的字符串表示要创建的可执行文件的路径以及传递给新进程的命令行。

正如我已经在安全应用程序目录检查部分描述的那样,验证不是使用用户提供的不受信任的路径字符串完成的,而是打开文件并提取最终解析的路径进行比较。但是,这个解析的路径仅在检查期间使用,当创建进程时,原始的不受信任的路径被传递给CreateProcessAsUser的lpApplicationName参数。

例如,如果你传递路径Z:\osk.exe作为可执行文件,服务将尝试打开该路径然后解析最终名称。如果Z:驱动器映射到C:\Windows\system32目录,它将找到位于C:\Windows\system32\osk.exe的可执行文件,这将是一个允许的安全目录。但是,Z:\osk.exe随后将作为lpApplicationName传递给CreateProcessAsUser。

这有什么用?新进程需要一个基本目录,它将从中检查本地DLL加载,而CreateProcessAsUser API使用lpApplicationName参数(移除了可执行文件名)作为此基本目录。这意味着你可以使用Z:\osk.exe路径启动UI Access进程;它将首先尝试从Z:\目录加载未知DLL。如果你在进程创建和尝试加载DLL之间将Z:驱动器重新映射到不受信任的位置,你可以强制将不受信任的DLL加载到进程中并获取任意代码执行。这很容易做到,因为UI Access进程可以通过在调用RAiLaunchAdminProcess时传递CREATE_SUSPENDED来以挂起状态创建,重新映射驱动器,然后恢复进程。

访问令牌窃取

我要提到的最后一类是访问令牌窃取。这与其他类别有些不同,因为你通常无法从中获得高完整性级别,而是获得一个启用了UI Access标志的进程,该进程可用于控制更高完整性的UI,只是不能滥用窗口钩子之类的东西。

正如我在一篇旧博客文章中描述的那样,如果你创建了一个UI Access进程,你可以打开该进程的访问令牌,复制它,将完整性级别降低到中,最后使用该令牌创建一个新进程。该过程中的任何步骤都没有禁用令牌上的UI Access标志。

在那篇博客文章之后,微软做了一个更改。现在,如果通过NtSetInformationToken系统调用降低令牌的完整性级别,它也会禁用UI Access标志。如果你无法将完整性级别降低到中,就不可能模拟该令牌或将其用于新进程,从而缓解了该问题。

然而,我注意到内核中有一些地方会降低令牌的完整性级别,这些地方不经过NtSetInformationToken,因此最终不会禁用UI Access标志。一个选项是通过NtCreateLowBoxToken系统调用创建App Container令牌。这将把完整性级别设置为低,这将允许新令牌用于创建进程。即使该进程随后在App Container沙箱中运行,也足以向更高特权的进程发送任意窗口消息。

静默绕过Windows管理员保护

假设我们现在在高完整性级别UI Access进程中拥有代码执行。我们如何利用它来绕过管理员保护?我们需要一个进程以影子管理员身份静默创建,并在其执行期间创建一个窗口。由此,我们可以使用SetWindowsHookEx,强制将任意DLL加载到新进程中。

我发现的最佳向量是利用计划任务可以配置为在执行时以管理员权限运行的事实。当启用管理员保护时,这仍然有效,只是任务进程以影子管理员身份运行。我们需要一个已启用、可以由受限用户启动并以管理员权限运行的任务。我们可以使用我的PowerShell工具的Get-AccessibleScheduledTask命令来查找一个:

PS> Get-AccessibleScheduledTask -Access Execute |
   ? { $_.AllowDemandStart -and $_.Enabled -and ($_.RunLevel -eq "Highest") } |
   Select-Object Name

Name
----
\Microsoft\Windows\DiskCleanup\SilentCleanup
\Microsoft\Windows\Input\LocalUserSyncDataAvailable
\Microsoft\Windows\Input\MouseSyncDataAvailable
...

列表中的第一个任务SilentCleanup对我来说很熟悉。它已被多次用于绕过UAC,例如通过滥用它使用环境变量来查找可执行文件,而受限用户可以覆盖这些变量。出乎意料的是,在我测试的管理员保护版本中,环境变量的问题没有得到修复,因此我将其作为一个单独问题报告。

如果我们忽略处理环境变量的问题,我们可以滥用这个任务,因为它会在进程运行时创建一个窗口,所以我们只需设置一个钩子,启动任务,等待你的钩子DLL被cleanmgr.exe进程加载,你就绕过了管理员保护。你可以在此处找到使用此方法的完整PoC此处。

注意:如果你想使用GetProcessHandleFromHwnd API,就像QuickAssist公开绕过所做的那样,你可能需要在进程创建窗口和终止之间赢得一场竞赛。例如,QuickAssist使用文件oplock导致任务进程在打开特定文件时挂起。如果你使用窗口钩子方法,你不需要担心这一点。

结论

我报告的所有问题都已修复,但这并不意味着没有其他问题可发现。希望现在比以前更难一些。然而,如果你能在高完整性级别进程中获取代码执行(无论是否启用UI Access),你仍然可以利用它来绕过管理员保护。希望现在发现的任何允许在UI Access进程中执行代码的问题都是可修复的安全漏洞,并将得到修复。

与管理员保护原始设计相比,UI Access进程的一个重大变化是它们不再以受限用户身份运行。引入此更改是为了修复问题437868751。相反,它们是用影子管理员令牌的过滤副本创建的。这消除了共享配置文件问题,在管理员进程和受限用户之间引入了更清晰的分离。

时间将证明管理员保护作为安全边界是否成功。微软正在认真对待它,但在开发过程中进行更严格的测试本可以防止许多先前存在的UAC绕过方法被遗漏。我建议任何对此功能感兴趣的人现在看看它已经发布,并且先前已知的bug已被修复。