绕过Windows管理员保护
Windows 11 最新版本 25H2 中引入的一项主打功能是管理员保护。该功能的目标是用一个更健壮且(更重要的是)安全的系统来替代用户账户控制(UAC),以便本地用户仅在必要时才能访问管理员权限。
这篇博客文章将简要概述这项新功能、其工作原理以及与 UAC 的区别。然后,我将描述在 Windows 11 内部预览版中该功能尚在测试阶段时,我所进行的一些安全研究。最后,我将详细说明我发现的九个独立漏洞之一,该漏洞可以绕过该功能,静默获取完整的管理员权限。我向微软报告的所有问题都已被修复,要么是在该功能正式发布之前(通过可选更新 KB5067036),要么是作为后续的安全公告。
注意:截至 2025 年 12 月 1 日,微软已禁用管理员保护功能,以处理一个应用程序兼容性问题。该问题很可能与本文中描述的任何内容无关,因此分析结论不变。
管理员保护试图解决的问题
UAC 在 Windows Vista 中引入,旨在方便用户临时获取管理员权限,同时用户的大多数进程以有限权限运行。不幸的是,由于其设计方式,很快人们就发现它并不代表一个严格的安全边界,微软将其降级为一项安全功能。这是一个重要的变化,因为它使得修复允许有限进程静默获取管理员权限的 UAC 绕过不再是优先事项。
UAC 设计的主要问题在于,受限用户和管理员用户是同一个账户,只是拥有不同的组和权限集。这意味着它们共享配置文件资源,例如用户目录和注册表配置单元。此外,还可以打开管理员进程的访问令牌并模拟它以授予管理员权限,因为最初的模拟权限检查并不考虑访问令牌是否“已提升”,它只考虑用户和完整性级别。
即便如此,在 Vista 上,静默获取管理员权限并不那么容易,因为大多数途径仍然会向用户显示提示。不幸的是,微软决定减少用户在修改系统配置时看到的提升提示数量,并在 Windows 7 中引入了“自动提升”功能。选定的微软二进制文件可以选择自动提升。然而,这也意味着在某些情况下,有可能重新利用这些二进制文件来静默获取管理员权限。虽然可以配置 UAC 以始终显示提示,但默认设置(很少有人更改)会允许自动提升。
一个已知绕过方法的良好存储库是 UACMe 工具,目前列出了 81 种获取管理员权限的独立技术。其中一部分已通过对操作系统的重大更新得到修复,尽管微软从未正式承认何时修复了 UAC 绕过。然而,仍然存在影响最新版 Windows 11 且尚未修复的静默绕过方法。
恶意软件经常使用已知的绕过方法来获取管理员权限,这一事实正是管理员保护旨在解决的问题。如果能够缓解 UAC 中的弱点,那么就可以使其成为一个安全边界,这不仅需要更多的工作来绕过,而且实现中的任何漏洞都可以作为安全问题得到修复。
事实上,UAC 已经可以使用一种更安全的机制,它不受许多所谓的“管理员批准”提升问题的影响。当用户不是管理员组的成员时,就会使用这种机制,它被称为“肩并肩”提升。这种机制要求用户知道本地管理员用户的凭据,这些凭据必须输入到 UAC 提升提示中。它比管理员批准提升更安全,原因如下:
- 配置文件数据不再共享,这可以防止受限用户修改可能被已提升的管理员进程使用的文件或注册表项。
- 不再可能获取管理员用户的访问令牌并进行模拟,因为受限用户无法模拟其他用户账户。
- 不支持微软二进制文件的自动提升,所有提升请求都需要通过提示进行确认。
不幸的是,这种机制在实践中难以安全使用,因为共享另一个本地管理员账户的凭据将是一个巨大的风险。因此,它主要用作技术支持的一种手段,系统管理员在用户身后输入凭据。
管理员保护通过使用由 UAC 服务自动配置的独立影子管理员账户,改进了“肩并肩”提升。它具有“肩并肩”提升的所有优点,外加以下优点:
- 用户不需要知道影子管理员的凭据,因为根本没有凭据。相反,可以配置 UAC 以提示输入受限用户的凭据,包括使用生物识别技术(如果需要)。
- 不需要单独的本地管理员账户,只需将用户配置为管理员组的成员,这使得部署更加容易。
虽然微软将管理员保护称为一个独立的功能,但它实际上可以被视为第三种 UAC 机制,因为它使用相同的基础设施和代码来执行提升,只是进行了一些调整。然而,该功能取代了管理员批准模式,因此您不能同时使用“传统”模式和管理员保护。如果您想启用它,目前没有用户界面可以操作,但您可以修改本地安全策略来实现。
最大的问题是,这会使 UAC 成为一个安全边界,从而使恶意软件不再能轻易得逞吗?我想我们最好仔细研究一下。
研究管理员保护
我通常避免在 Windows 新功能发布之前进行研究。过去这样做效果不佳,我在内部预览阶段发现新功能中的安全问题,结果发现该错误是由于临时代码引起的,随后被删除。此外,如果在内部预览阶段修复了安全问题,它们不会产生安全公告,使得跟踪何时修复变得更加困难。因此,在功能发布之前进行研究几乎没有动力,因为我可以确信发现的任何错误都是真正的安全问题,并且会得到及时修复。
这次情况略有不同,微软联系我,询问我是否愿意在内部预览阶段帮助他们发现实现中的问题。毫无疑问,他们联系我的部分原因是我有发现复杂逻辑性 UAC 绕过方法的历史。此外,我已经简要查看过,并注意到该功能仍然容易受到一些众所周知的公开绕过方法的影响,例如我对环回 Kerberos 的滥用。
我同意查看设计文档并提供反馈,而不进行完整的“渗透测试”。然而,如果我确实发现了问题,考虑到管理员保护的目标是成为一个安全边界,我得到保证,它们将通过安全公告得到修复,或者至少在该功能最终发布之前得到补救。
微软提供的文档给出了概述,但并非所有设计细节。例如,我确实有一个关于开发人员认为什么是安全边界的问题。与移除自动提升保持一致,我假设绕过边界需要满足以下一个或多个条件:
- 破坏影子管理员的配置文件,例如写入任意文件或注册表项。
- 劫持一个以影子管理员身份运行的现有进程。
- 在不显示提示的情况下获取以管理员身份执行的进程。
提示作为一个边界很重要,有许多 UAC 绕过方法,例如那些依赖于已提升的 COM 对象的方法,在管理员保护中仍然有效。然而,由于不再允许自动提升,它们将始终显示提示,因此这些不被视为绕过。当然,提示中显示的内容,例如要提升的可执行文件,并不一定与即将以管理员权限执行的操作相关。
在文档中,对一些相关的 UAC 功能(如 UI 访问进程(这将在本系列的第 2 部分中讨论))缺乏考虑,但即便如此,一些描述引起了我的注意。因此,我忍不住决定至少查看一下内部预览版金丝雀版本中的当前实现。这项研究混合了对 appinfo.dll 中 UAC 服务代码的逆向工程以及行为分析。
在研究结束时,我发现了 9 种独立的 绕过该功能并静默获取管理员权限的方法。一些绕过方法是长期存在的 UAC 问题,有公开可用的测试案例。其他则是由于该功能本身的实现缺陷。但最有趣的漏洞类别是那些根本没有漏洞的情况,直到操作系统的其他部分参与进来。
让我们深入探讨我在研究中发现的这个最有趣的绕过方法。如果您想跳过前面的内容,可以在问题跟踪器上阅读完整详情。这个问题很有趣,不仅因为它允许我绕过保护,还因为它是一个我已知多年的潜在 UAC 绕过方法,但由于引入了此功能才变得实际可利用。
登录会话
首先,了解该漏洞需要一些背景知识。当用户成功验证到 Windows 系统时,系统会为其分配一个唯一的登录会话。该会话用于控制用户的信息,例如它保存用户凭据的副本,以便用于网络身份验证。
登录会话作为引用添加到登录过程中创建的访问令牌中,以便在使用令牌的任何内核操作中可以轻松引用它。您可以通过使用 NtQueryInformationToken 系统调用查询令牌来找到该会话的唯一 64 位身份验证 ID。在 UAC 中,为受限令牌和链接的管理员访问令牌分配了单独的登录会话,如下面的脚本所示,您可以观察到受限令牌和链接令牌具有不同的身份验证 ID LUID 值:
# Get authentication ID of current token
PS> Get-NtTokenId -Authentication
LUID
----
00000000-11457F17
# Query linked administrator token and get its authentication ID.
PS> $t = Get-NtToken -Linked
PS> Get-NtTokenId -Authentication -Token $t
LUID
----
00000000-11457E9E
内核引用登录会话的一个重要地方是在查找 DOS 驱动器盘符时。从内核的角度来看,驱动器盘符存储在一个特殊的对象目录 ?? 中。当内核查找此路径时,它会首先检查是否存在特定于登录会话的目录,该目录存储在路径 \Sessions\0\DosDevices\X-Y 下,其中 X-Y 是登录会话身份验证 ID 的十六进制表示。如果在该目录中找不到驱动器盘符符号链接,内核将回退到检查 \GLOBAL?? 目录。您可以通过使用 NtOpenDirectoryObject 系统调用打开 ?? 对象目录来观察此行为,如下所示:
PS> $d = Get-NtDirectory "\??"
PS> $d.FullPath
\Sessions\0\DosDevices\00000000-11457f17
众所周知,如果您可以向 DOS 设备对象目录写入符号链接,就可以劫持在该登录会话中使用该访问令牌运行的任何进程的 C: 驱动器。即使 C: 驱动器是在全局对象目录中定义的,也会首先检查特定于登录会话的目录,因此可以被覆盖。
如果用户可以向另一个登录会话的 DOS 设备对象目录写入内容,就可以重定向任何文件访问到系统驱动器。例如,您可以重定向系统 DLL 加载,以强制任意代码在该登录会话中运行的进程上下文中执行。在 UAC 的情况下,这不是问题,因为单独的 DOS 设备对象目录具有不同的访问控制,因此受限用户无法劫持管理员进程的 C: 驱动器。管理员 DOS 设备对象目录的访问控制如下所示:
PS> Get-NtTokenSid
Name Sid
---- ---
DOMAIN\user S-1-5-21-5242245-89012345-3239842-1001
PS> $d = Get-NtDirectory "\??"
PS> Format-NtSecurityDescriptor $d -Summary
<Owner> : BUILTIN\Administrators
<Group> : DOMAIN\Domain Users
<DACL>
NT AUTHORITY\SYSTEM: (Allowed)(ObjectInherit, ContainerInherit)(Full Access)
BUILTIN\Administrators: (Allowed)(ObjectInherit, ContainerInherit)(Full Access)
BUILTIN\Administrators: (Allowed)(None)(Full Access)
CREATOR OWNER: (Allowed)(ObjectInherit, ContainerInherit, InheritOnly)(GenericAll)
创建 DOS 设备对象目录
您可能会问,谁创建了这个 DOS 设备对象目录?事实证明,内核在首次访问该目录时按需创建它。执行创建的代码在 SeGetTokenDeviceMap 中,大致如下所示:
NTSTATUS SeGetTokenDeviceMap(PTOKEN Token, PDEVICE_MAP *ppDeviceMap) {
*ppDeviceMap = Token->LogonSession->pDeviceMap;
if (*ppDeviceMap) {
return STATUS_SUCCESS;
}
WCHAR path[64];
swprintf_s(
path,
64,
L"\\Sessions\\0\\DosDevices\\%08x-%08x",
Token->AuthenticationId.HighPart,
Token->AuthenticationId.LowPart);
PUNICODE_STRING PathString;
RtlInitUnicodeString(&PathString, path);
OBJECT_ATTRIBUTES ObjectAttributes;
InitializeObjectAttributes(&ObjectAttributes,
&PathString,
OBJ_CASE_INSENSITIVE |
OBJ_OPENIF |
OBJ_KERNEL_HANDLE |
OBJ_PERMANENT, 0, NULL);
HANDLE Handle;
NTSTATUS status = ZwCreateDirectoryObject(&Handle,
0xF000F,
&ObjectAttributes);
if (NT_ERROR(status)) {
return status;
}
status = ObpSetDeviceMap(Token->LogonSession, Handle);
if (NT_ERROR(status)) {
return status;
}
*ppDeviceMap = Token->LogonSession->pDeviceMap;
return STATUS_SUCCESS;
}
您可能会注意到的一件事是,对象目录是使用 ZwCreateDirectoryObject 系统调用创建的。在内核中使用 Zw 系统调用的一个重要安全细节是,它会禁用安全访问检查,除非在 OBJECT_ATTRIBUTES 中设置了可选的 OBJ_FORCE_ACCESS_CHECK 标志,而这里并非如此。
绕过访问检查对于此代码正常运行是必要的;让我们看看 \Sessions\0\DosDevices 目录的访问控制。
PS> Format-NtSecurityDescriptor -Path \Sessions\0\DosDevices -Summary
<Owner> : BUILTIN\Administrators
<Group> : NT AUTHORITY\SYSTEM
<DACL>
NT AUTHORITY\SYSTEM: (Allowed)(ObjectInherit, ContainerInherit)(Full Access)
BUILTIN\Administrators: (Allowed)(ObjectInherit, ContainerInherit)(Full Access)
CREATOR OWNER: (Allowed)(ObjectInherit, ContainerInherit, InheritOnly)(GenericAll)
非管理员用户无法写入该目录,但由于此代码是在用户的安全上下文中调用的,它需要禁用访问检查来创建目录,因为它无法确定用户是否是管理员。重要的是,该目录的访问控制有一个针对特殊 CREATOR OWNER 组的可继承规则,授予完全访问权限。这会在对象创建期间自动替换为所使用的访问令牌的指定所有者。
因此,即使访问检查已被禁用,最终创建的目录也可以被调用者访问。这解释了 UAC 管理员 DOS 设备对象目录如何阻止受限用户的访问。管理员令牌创建时,本地管理员组被设置为其所有者,因此这就是 CREATOR OWNER 被替换的内容。然而,受限用户只能将自己的 SID 设置为所有者,因此它只授予该用户访问权限。
这有什么用呢?我很久以前就注意到,这种行为是一种潜在的 UAC 绕过方法,实际上是一种潜在的权限提升(EoP),但 UAC 绕过是最可能的结果。具体来说,可以通过使用 TokenLinkedToken 信息类调用 NtQueryInformationToken 来获取管理员用户的访问令牌句柄。出于安全原因,此令牌仅限于 SecurityIdentification 模拟级别,因此不能用于授予对任何资源的访问权限。
但是,如果您模拟该令牌并打开 ?? 目录,那么内核将使用标识令牌调用 SeGetTokenDeviceMap,如果它当前尚未创建,它将使用 ZwCreateDirectoryObject 来创建 DOS 设备对象目录。由于访问检查被禁用,创建仍会成功,但是一旦创建,内核将对目录本身进行访问检查,并且会由于模拟的是标识令牌而失败。
这似乎对我们帮助不大;虽然目录被创建,但它将使用标识令牌的所有者,即本地管理员组。但我们可以在模拟之前将令牌的所有者 SID 更改为用户的 SID,因为这是一个允许的操作。现在,最终的 DOS 设备对象目录将由用户拥有,并且可以写入。由于 UAC 的管理员端只使用单个登录会话,那么任何已提升的进程现在都可以被劫持其 C: 目录。
作为 UAC 绕过方法,这只有一个问题,我从未找到过受限用户在创建任何管理员进程之前获得代码运行的情况。一旦进程被创建并运行,几乎可以肯定某些代码会打开文件,从而访问 ?? 目录。当受限用户获得控制权时,DOS 设备对象目录已经被创建并分配了预期的访问控制。尽管如此,由于 UAC 不是一个安全边界,报告它没有意义,所以我将这个行为记录下来,以备将来可能相关。
绕过管理员保护
快进到今天,管理员保护出现了。出于兼容性原因,微软使得使用 TokenLinkedToken 信息类调用 NtQueryInformationToken 仍然返回管理员令牌的标识句柄。但在这种情况下,它是影子管理员令牌,而不是用户令牌的管理员版本。但一个关键的区别是,对于 UAC,此令牌每次都是相同的,而在管理员保护中,内核会调用 LSA 并验证影子管理员的新实例。这导致从 TokenLinkedToken 返回的每个令牌都具有唯一的登录会话,因此目前尚未创建 DOS 设备对象目录,如下所示:
PS> $t = Get-NtToken -Linked
PS> $auth_id = Get-NtTokenId -Authentication -Token $t
PS> $auth_id
LUID
----
00000000-01C23BB3
PS> Get-NtDirectory "\Sessions\0\DosDevices\$auth_id"
Get-NtDirectory : (0xC0000034) - Object Name not found.
虽然理论上我们现在可以强制创建 DOS 设备对象目录,但不幸的是,这对我们帮助不大。由于 UAC 服务也使用 TokenLinkedToken 来获取令牌以创建新进程,这意味着当前正在运行或将来将运行的每个管理员进程都不共享登录会话,因此不共享相同的 DOS 设备对象目录,我们无法使用在自己进程中查询到的令牌劫持它们的 C: 驱动器。
要利用这一点,我们需要为实际运行的进程使用令牌。这是可能的,因为在创建提升进程时,可以将其启动为挂起状态。有了这个挂起的进程,我们可以打开进程令牌进行读取,将其复制为标识令牌,然后在模拟它时创建 DOS 设备对象目录。然后,进程可以恢复运行,其 C: 驱动器已被劫持。
作为绕过方法,这只有两个问题:首先,创建挂起的提升进程需要点击通过提升提示。对于具有自动提升功能的 UAC,这不是问题,但对于管理员保护,它将始终提示,而显示提示不被视为跨越安全边界。有办法解决这个问题,例如,UAC 服务公开了 RAiProcessRunOnce API,它将静默运行提升的二进制文件。唯一的问题是进程不是挂起的,因此您必须在任何代码在该进程中运行之前赢得竞争条件,以打开进程并执行绕过。这应该是可行的,例如通过调整线程优先级来阻止新进程的主线程被调度。
第二个问题似乎更棘手。当设置访问令牌的所有者时,它只允许您设置令牌的用户 SID,或者设置了 SE_GROUP_OWNER 标志的成员组。唯一具有所有者标志的组是本地管理员组,当然,影子管理员的 SID 与受限用户的 SID 不同。因此,在创建后访问目录时,将其中任何一个 SID 设置为所有者对我们没有帮助。
事实证明,这不是问题,因为我没有完全说明所有者分配过程的真相。在为新对象构建访问控制时,如果模拟令牌处于标识级别,内核不会信任它。这是出于一个很好的安全原因,标识令牌不应该用于做出访问控制决策,因此在创建对象时分配其所有者没有意义。相反,内核使用进程的主令牌来做出该决策,因此指定的所有者是受限用户的 SID。实际上,为 UAC 绕过设置所有者 SID 从来都不是必需的,它从未被使用过。您可以通过创建一个没有名称的对象来验证此行为,以便在模拟标识令牌时可以创建它,并检查指定的所有者 SID:
PS> $t = Get-NtToken -Anonymous
# Impersonate anonymous token and create directory
PS> $d = Invoke-NtToken $t { New-NtDirectory }
PS> $d.SecurityDescriptor.Owner.Sid.Name
NT AUTHORITY\ANONYMOUS LOGON
# Impersonate at identification level
PS> $d = Invoke-NtToken $t -ImpersonationLevel Identification {
New-NtDirectory
}
PS> $d.SecurityDescriptor.Owner.Sid.Name
DOMAIN\user
您可能有的最后一个问题是,为什么使用影子管理员令牌创建进程不会以该用户身份访问某些 DOS 驱动器的文件资源,从而导致 DOS 设备对象目录被创建?CreateProcessAsUser API 的实现会在调用者的安全上下文中运行其所有代码,无论分配了什么访问令牌,因此默认情况下,它永远不会在新登录会话下打开文件。
但是,如果您知道如何在系统服务中安全地创建进程,您可能会期望应该在调用 CreateProcessAsUser 时模拟新令牌,以确保不允许用户为其无法访问的可执行文件创建进程。UAC 服务正确地执行了此操作,因此它肯定访问了驱动器来创建进程,并且 DOS 设备对象目录应该已经被创建,为什么没有呢?
具有讽刺意味的是,UAC 服务遇到了最近引入的安全缓解措施,该措施旨在防止在系统服务中模拟低权限用户时劫持 C: 驱动器。如果系统调用的调用者是 SYSTEM 用户,并且它试图访问 C: 驱动器,此缓解措施就会生效。这是微软为应对清单文件解析中的多个漏洞而添加的,如果您想了解概述,这里有一个视频,是我和 Maddie Stone 在 OffensiveCon 23 上做的演讲,描述了一些攻击面。
碰巧的是,UAC 服务以 SYSTEM 身份运行,并且只要提升的可执行文件在 C: 驱动器上(这非常可能),缓解措施就会完全忽略模拟令牌的 DOS 设备对象目录。因此,SeGetTokenDeviceMap 永远不会被调用,所以在登录会话下首次访问文件是在进程启动并运行之后。只要我们在新进程接触文件之前执行漏洞利用,我们就可以创建 DOS 设备对象目录并重定向进程的 C: 驱动器。
总而言之,利用此绕过方法的步骤如下:
- 通过 RAiProcessRunOnce 生成一个影子管理员进程,该进程将运行来自 C: 驱动器的 runonce.exe。
- 在新进程访问文件资源之前打开它,并查询主令牌。
- 将令牌复制为标识令牌。
- 在模拟影子管理员令牌时强制创建 DOS 设备对象目录。这可以通过调用 NtOpenDirectoryObject 打开 ?? 来实现。
- 在新的 DOS 设备目录中创建一个 C: 驱动器符号链接以劫持系统驱动器。
- 让进程恢复并等待加载被重定向的 DLL。
最终想法
这个绕过方法很有趣,因为很难指出导致它的具体错误。该漏洞是 5 种独立的操作系统行为的结果:
- 管理员保护功能对 TokenLinkedToken 查询的更改会为每个影子管理员令牌生成一个新的登录会话。
- 每个令牌的 DOS 设备目录为每个新登录会话延迟初始化,这意味着当链接令牌首次创建时,该目录当前不存在。
- 内核在访问 DOS 设备目录时使用 Zw 函数创建它,这会禁用访问检查。这允许受限用户在标识级别模拟影子管理员令牌,并通过打开 ?? 来创建目录。
- 如果线程在标识级别模拟令牌,任何安全描述符分配都从主令牌获取所有者 SID,而不是模拟令牌。这导致受限用户被授予对影子管理员令牌的 DOS 设备对象目录的完全访问权限。
- 一旦低权限用户获得对进程令牌的访问权限,DOS 设备对象目录尚未创建,这是因为安全缓解措施在 SYSTEM 进程中从 C: 驱动器打开文件时禁用了模拟的 DOS 设备对象目录。
我不一定责怪微软在测试期间没有发现这个问题。这是一个复杂的漏洞,涉及许多动态部分。很可能我只是因为知道创建 DOS 设备对象目录时的奇怪行为才发现了它。
微软实施的修复是防止在标识级别模拟影子管理员令牌时创建 DOS 设备对象目录。由于此修复作为可选更新 KB5067036 的一部分添加到最终发布版本中,因此没有与之关联的安全公告。我要感谢管理员保护团队和 MSRC 快速响应并修复了所有问题,并证明该功能将作为一个安全边界受到重视。我还要感谢他们提供额外信息,例如有助于研究的设计文档。
至于我对管理员保护功能的看法,我觉得微软本可以更大胆一些。对 UAC 进行小的调整导致了近 20 年来未修复的绕过方法,这些方法表现为该功能中的安全漏洞。我希望看到的是更具可配置性和可控性的东西,也许是 sudo 或 Linux 能力的适当版本,用户可以针对某些任务被授予特定的额外访问权限。
我想应用程序兼容性最终是这里的问题,Windows 并非为如此激进的改变而设计。我也希望这是一个单独的可配置模式,而不是完全取代管理员批准。这样,系统管理员可以选择何时让人们加入新模型,而不是要求每个人都使用它。
我认为,如果默认启用,它确实比管理员批准的 UAC 提高了安全性。它呈现了一个更重要的安全边界,除非发现更严重的设计问题,否则应该是可防御的。我预计恶意软件仍然能够获取管理员权限,即使这只是通过强迫用户接受提升提示来实现,但它们可能使用的任何静默绕过方法都应该得到修复,这将是当前情况的重大改进。无论如何,使用 Windows 最安全的方法是永远不要以管理员身份运行,无论使用哪个版本的 UAC。理想情况下,首先避免在您的机器上感染恶意软件。