概述
MSDT 全称是 Microsoft Support Diagnostic Tool,主要用于 Windows 故障诊断支持。
2022 年 5 月 27 日,@nao_sec 发推,称他在 VirusTotal 发现一个恶意的 Word 文档,当受害者打开这个文档后,它会通过使用 ms-msdt URI 协议来实现远程代码执行。随后微软发布安全更新对该漏洞进行了修复,漏洞编号为 CVE-2022-30190。CVE-2022-30190 漏洞又被命名为 “Follina”。
本文是对该漏洞的学习笔记。
漏洞分析
ms-msdt URI 协议
虽然漏洞利用的传播媒介是 Word 文档,不过本质上问题是出在 ms-msdt 这个 URI 协议处理上的,因此重点看这个协议。
ms-msdt URI 注入代码的 PoC 如下:
1 | ms-msdt:/id PCWDiagnostic /skip force /param "IT_RebrowseForFile=cal?c IT_LaunchMethod=ContextMenu IT_SelectProgram=NotListed IT_BrowseForFile=h$(Start-Process('calc'))i/../../../../../../../../../../../../../../Windows/SYstem32/mpsigstub.exe IT_AutoTroubleshoot=ts_AUTO" |
先看 ms-msdt URI 协议的关联程序,很直观,URI 直接作为命令行参数,传递给 msdt.exe 程序执行:
msdt!wWinMain
函数里,调用 Configuration::ParseArgs
方法解析命令行参数,调用 Mode::Run
方法进行主要的逻辑处理:
命令行参数解析,支持通过 ms-msdt:
URI 协议唤起:
空格作为分隔符分割出键值对,-
或者 /
开头的标识为键,另一个就是值:
因此,URI 基本的构造形如:
1 | ms-msdt:/key1 val1 /key2 val2 /key3 val3 |
注意到 PoC 里涉及到的3个参数分别是 id、skip,以及 param。
msdt.exe 是什么
MSDT 全称 Microsoft Support Diagnostic Tool,为微软的故障诊断支持工具。先直接执行 msdt.exe 感受下它大概是个什么东西:
由于故障的类型多种多样,想要进行区分,可以通过 id 参数指定具体的故障类型。比如 AppsDiagnostic 是和 Windows Store Apps 相关的故障:
SpeechDiagnosticCalibrate 是和麦克风相关的:
PoC 里所使用的 PCWDiagnostic 看起来是处理旧版本程序兼容性问题的:
这些具体的故障类型 id,其实是根据 C:\Windows\diagnostics\index
目录下的这些 xml 配置文件里 id 元素的值来匹配决定的:
通过 id 参数确定要调用哪个诊断包后,它就知道该诊断包相关配置文件在 Package Path 对应的路径下:
如图所示,PCWDiagnostic 诊断包路径在 C:\Windows\diagnostics\system\PCW
:
Package Path 对应的文件夹下,有个名为 DiagPackage.diagpkg 的文件,它其实也是 xml 文本文件,它进一步说明了该诊断包运行时需要哪些东西,后面会再讲到:
正常执行不加其他额外参数时,当 UI 界面弹出后,需要点击 Next 按钮才会继续进行:
但若指定了 skip 参数且值为 force 时,就会跳过这一步交互,直接进入到之后的处理流程:
诊断包确定后,msdt 会分两步走:诊断问题(Diagnose),解决问题(Resolve)
诊断问题(Diagnose)
为了诊断问题,msdt.exe 会通过 COM 调用获取 CScriptedDiag 对象,并调用 Diagnose 接口方法:
这个 COM 实现在 sdiageng.dll 中,它的全称我猜测是 “Script Diagnose Engine”,作用如其名。
sdiageng.dll 的功能实现又通过 COM 依赖于 sdiagnhost.exe:
注:LocalServer32 表示 COM 服务端实现在一个可独立运行的应用中(如果是 InprocServer32 则表明在动态链接库里),如果要调试 COM 服务端,等 COM 客户端这边 CoCreateInstance 执行后 Windows 就会启一个 COM 服务端对应程序的进程,这时候调试器再 attach 上去即可。
sdiagnhost.exe 的功能实现又通过 COM 依赖于 mscoree.dll:
但奇怪的是,在 mscoree.dll 里我并没有搜到对应的 CLSID,并且尝试在 DllGetClassObject 函数上下断点也没有断到。
注:后来发现对于 mscoree.dll 而言,获取 COM 接口对象时,调用的是
ShellShim_DllGetClassObject
而不是DllGetClassObject
。
查了下 mscoree.dll 的作用才知道,它是 .NET 程序中的核心 DLL,它可以提供 .NET 与 COM 组件的互操作支持,允许 .NET 程序调用原生的 COM 组件,或者让 COM 组件调用 .NET 代码。
既然 mscoree.dll 是起到 .NET 与 COM 连接桥梁的作用,那想必一个具体应用的 CLSID 实现不会在 mscoree.dll 里写出来,mscoree.dll 在接收到 COM 调用请求后,很可能将逻辑分发给了其他的 .NET DLL 里。因此通过观察 Procmon 捕获到的进程行为和注册表,发现它的 COM 接口实现位置体现在:
这个 Assembly 对应的值是对应的 .NET dll 名称,Class 就是实现的类名:
所以 sdiagnhost.exe 的功能实现通过 COM 依赖于 mscoree.dll,mscoree.dll 作为 COM 和 .NET 间的桥梁,又将调用分发给 Microsoft.Windows.Diagnosis.SDHost.dll 这个 .NET dll。
总结梳理下这个调用依赖关系:
库之间的功能调用传递弄清楚就好说了。回过头来继续看,诊断问题的过程中,会调用 sdiageng.dll Troubleshooter::Run
方法:
它会走到 COM 调用 IScriptedDiagnosticHost 接口 RunScript 方法:
经过如前所述的调用关系传递后,最终就变成了执行 .NET Microsoft.Windows.Diagnosis.ManagedHost 类 RunScript 方法:
能看出来,RunScript 里又去调用执行了 scriptPath 参数对应路径的 PowerShell 脚本,而它的值体现在 DiagPackage.diagpkg 配置文件里的 Troubleshooter XML 节点。
比如 PCWDiagnostic 诊断包 DiagPackage.diagpkg 配置如下:
那么 scriptPath 参数值就是 TS_ProgramCompatibilityWizard.ps1 文件路径,而由于 Parameters 没有子节点,所以 parameterNames 和 parameterValues 参数为空。
解决问题(Resolve)
类似地,为了解决问题,msdt.exe 会通过 COM 调用获取 CScriptedDiag 对象,并调用 Resolve 接口方法:
这个过程中,最终也会调用 .NET Microsoft.Windows.Diagnosis.ManagedHost 类 RunScript 方法,参数值体现在 DiagPackage.diagpkg 配置文件里的 Resolver 节点:
scriptPath 就是 Script Filename 值,parameterNames 有俩个:TargetPath 和 AppName,而至于它们的值 parameterValues,这个我们一会再说。
param 参数
程序的执行逻辑和架构现在大概有了个初步印象了,现在来讲最关键的 param 参数如何被解析和传递的。
解析 param 参数会用空格分割出键值对,对每个键值对用等号分割出键和值:
然后根据键值对,构造如下的 XML 节点对象:
1 | <Packages> |
并将这个 XML 节点对象赋值到 Answers::s_Instance
这个 Answers 全局结构体对象 +0x0 偏移处。
这里在做的一个事情其实是,通过命令行 param 参数来指定诊断过程中需要用到的参数,这些参数本来是通过 UI 交互由用户来输入指定的。比如仅执行:
1 | ms-msdt:/id PCWDiagnostic /skip force |
它会弹出对话框要求你选择哪个程序有问题:
如果选择的是 Not Listed
,还会让你输入具体的程序路径:
诸如此类,每个诊断包在诊断过程中都有各自需求的参数需要用户通过 UI 界面输入,如果不想通过 UI 交互指定,那就可以通过命令行 param 参数来设定。
这部分信息体现在 DiagPackage.diagpkg 配置文件里的 Interactions 节点:
每个 SingleResponseInteraction 都代表一个用户交互,ID 是它的标识。反应到命令行 param 参数中,ID 就是键,用户输入的结果就是值。
诊断问题所执行的 PowerShell 脚本里,如果想要获取到这样的用户交互输入,可以通过调用 Get-DiagInput
命令,比如:
Get-DiagInput
是在 .NET dll 里注册绑定的命令:
它的作用就是获取 id 对应的参数值,并调用 sdiageng.dll 里的 CScriptedDiagInteraction::Interact
方法,记录供后续使用:
比如上面那段,/param IT_LaunchMethod=test
,PowerShell 脚本里取到返回的值就是 test 并赋值给 $LaunchMethod
变量。这样实际上就是通过命令行 param 参数,来控制执行的 PowerShell 脚本里 Get-DiagInput
返回赋值的变量。
那重点就变成审计 PowerShell 脚本代码了。先看 TS_ProgramCompatibilityWizard.ps1
,这是诊断问题(Diagnose)的过程中会执行的脚本。
这里注意当到 IT_SelectProgram
参数值是 NotListed
时,IT_BrowseForFile
参数值赋值给 $selectedProgram
变量:
$selectedProgram
值会传递给 Test-Selection
函数检查:
这里需要想办法构造 $appPath
的值,使得 appValid 被设置为 true,而这需要满足两个条件:
test-path -literalpath $appPath
返回 true$appPath
文件路径后缀以 .exe 或者 .msi 结尾
Test-Path
本是用于检查对应路径的文件是否存在的,然而 Test-Path
存在一个 bug,当要测试的路径包含 ..\
跳转符时,并且 ..\
的层级跳转超出了系统盘之外,就会报异常,但仍然会返回 true:
注:这个问题现在仍然存在,不太理解开发者的想法。
这就导致其实只要 ..\
跳转层级够多,Test-Path
的检查就形同虚设,因此剩下仅需使得文件路径以 .exe 或 .msi 结尾即可。
然后从 $selectedProgram
里提取出文件名部分(不含后缀),赋值给 $appName:
在 TS_ProgramCompatibilityWizard.ps1
脚本末尾,将 $selectedProgram
和 $appName
分别作为 TARGETPATH
和 APPNAME
参数值,传递给 Update-DiagRootCause
命令:
这也是 .NET 中注册的命令,它的作用主要是更新 Rootcause Resolver 的参数值,先前在 DiagPackage.diagpkg 里已经看到过了:
诊断问题(Diagnose)的步骤结束后就是解决问题(Resolve),这个过程中通过调用 .NET ManagedHost 类 RunScript 方法来执行 RS_ProgramCompatibilityWizard.ps1
脚本,但这次就先暂停一下,先别着急再看 RS_ProgramCompatibilityWizard.ps1
脚本代码,回头来看看 .NET ManagedHost 类 RunScript 方法。不同于先前诊断问题(Diagnose),这次解决问题(Resolve)调用 RunScript 时 Parameters 是有内容的:
注意看 parameterValues 值里 TargetPath 那部分是可控的,也就是先前脚本里的 $selectedProgram
变量值,而它会被拼接到 text 字符串里,随后通过 PSCommand 类 AddScript 方法添加到 PowerShell 命令中并被执行。拼接的过程中没有对字符串里的 $
符号进行反引号转义,最终导致了 PowerShell 代码注入漏洞。
简化后的 PoC:
1 | ms-msdt:/id PCWDiagnostic /skip force /param "IT_LaunchMethod=Whatever IT_SelectProgram=NotListed IT_BrowseForFile=$(Start-Process('calc'))/../../../../../../../../../../../../../../x.exe" |
可以看到,整个程序的设计架构还是有点复杂的,从 C++ binary 到 .NET 再到 PowerShell,数据在各个组件之间通过 COM 相互流转,没点耐心和经验去逆向和分析,感觉真不太能挖得出来这种洞。
结合 Word 环境下的利用和补丁分析,还是以后有机会再补吧。
参考资料
- Microsoft Support Diagnostic Tool - Wikipedia
- CVE-2022-30190 - Security Update Guide - Microsoft - Microsoft Windows Support Diagnostic Tool (MSDT) Remote Code Execution Vulnerability
- https://github.com/onecloudemoji/CVE-2022-30190
- Detect the Follina MSDT Vulnerability (CVE-2022-30190) with Qualys Multi-Vector EDR & Context XDR | Qualys Security Blog
- https://x.com/nao_sec/status/1530196847679401984
- msdt | Microsoft Learn
- LocalServer32 - Win32 apps | Microsoft Learn
- DotNET中的幕后英雄:MSCOREE.DLL - 许海彪 - 博客园
- Test-Path -IsValid returns true for invalid path · Issue #8823 · PowerShell/PowerShell
- CVE-2022-30190 MSDT 代码注入漏洞分析