这篇博文将讨论CVE-2024-30052,该漏洞允许在 Visual Studio 中调试转储文件时执行任意代码。我于 2023 年 8 月向 Microsoft 报告了此问题,他们于 2024 年 6 月提供了解决此问题的更新。下面我分享了有关该漏洞的一些详细信息,以及我的github上的 poc / 可利用的代码。
0.0 介绍
在我的日常工作中,我发现自己经常在 Visual Studio 中调试转储文件。这些对于调查我们想要防止发生的低重现崩溃或程序状态非常有用。通常,转储文件会来自不受信任的地方 - 大多数在 Windows 上部署本机应用程序的大公司都会有一个用于检测崩溃的自动化系统,在这种情况下,转储文件会作为遥测的一部分收集并上传到开发人员可以访问的门户,以便对崩溃进行分类。例如,Google 使用自定义版本的crashpad来捕获和报告 Google Chrome 中的崩溃。
这可能会使开发人员通过转储文件遭受攻击。如果 Visual Studio 中存在一个漏洞,可以通过打开特制的转储文件来触发,那么恶意用户可以将此转储文件插入崩溃报告系统,然后等待开发人员打开它。最终用户手动发送转储文件(例如支持凭单)也并不罕见,这可能会增加开发人员打开文件的机会。攻击的主要机会是通过 PDB 文件,该文件可以与转储文件(如果需要,可以使用任意扩展名)一起提供,VS 会在调试会话期间愉快地打开它。一般来说,PDB 相当不安全:
它们可以包含在 VS 中尝试可视化某些数据时可以执行任意代码的可视化工具。
它们可以包含在尝试获取源文件时执行任意命令的源服务器配置。
但是,通过 PDB 文件指定的可视化工具在转储调试期间被禁用,并且源服务器命令默认被禁用,需要用户手动启用它们。相反,在我之前的研究中,我专注于寻找不需要这两个组件的漏洞,并且发现许多漏洞 MS 在此期间已经修复。其中大部分位于 msdia140.dll 中,这是用于解析和查询 PDB 文件的库。所有问题都是内存损坏错误。尽管其中一些损坏很容易被利用,但我觉得它们不会在现实世界的攻击中被实际使用。
去年,我决定调查 Visual Studio 在调试会话期间使用的其他库,希望某个逻辑错误可能隐藏在某个地方,从而允许在不依赖内存损坏的情况下执行代码。最终,我找到了一种在调试托管转储文件时运行任意代码的方法。
总结
-
当调试包含带有嵌入 PDB 的可执行文件的堆内存已满的转储文件时,如果 PDB 具有嵌入的源文件,则如果嵌入的源文件对应于转储文件中主(崩溃)线程的调用堆栈上存在的源代码行,Visual Studio 将自动打开嵌入的源文件。
-
当尝试打开包含二进制(即可不可打印)字符的文件时,Visual Studio 将默认使用与操作系统上的扩展名关联的默认编辑器打开该文件。
-
利用这两种行为,攻击者可以制作一个特殊的转储文件,该文件内部包含一个“源文件”,Visual Studio 将使用外部程序打开该文件。通过使用特定扩展名(例如 .hta、.chm 或 .py),攻击者可以编写将在文件打开后执行的任意代码。
-
该漏洞被标记为CVE-2024-30052 ,并已于 2024 年 6 月 11 日在Visual Studio 2022 17.8.11中修复。
1.0 嵌入式 PDB、嵌入式源
几年前,微软推出了可移植 PDB 格式。该格式旨在替代传统的托管模块 MSF 格式,主要用于跨平台支持和对标准格式的优化。同时,他们还增加了在编译期间使用命令行开关将可移植 PDB 文件嵌入-debug:embedded可执行文件的可能性。据我所知,该过程实际上没有在任何地方记录,但不难找出如何通过例如逆向一些 C# 运行时库并遵循公共 MS 文档提供的提示来实现这一点。
-
首先生成一个普通的可移植 PDB 文件,然后使用Deflate流进行压缩。
-
创建一个数据“blob”,包含两个 32 位值(一个魔术数字(“MPDB”)+ PDB 的未压缩大小),后跟压缩的 PDB 数据。
-
然后将该 blob 插入到可执行文件的特殊部分下,并通过PE 调试目录中的 调试目录条目引用,如下所示:
-
DataPointer指向那个斑点。
-
DataSize等于斑点的大小。
-
IsPortableCodeView设置为 true。
-
Type设置为 17 (又名嵌入式便携式 PDB)
我们可以使用 PETools 查看使用嵌入式 PDB 编译的 .NET 核心 DLL,从而看到这一点:
最底部的调试目录条目类型为 17,我们可以看到它指向的数据格式为“MPDB”,后跟 0x2910(未压缩的 pdb 长度),然后是压缩的 PDB 数据。
人们还要求将源文件嵌入到 PDB 中。现在可以通过几种不同的方式实现这一点,例如在 vcxproj 文件中将EmbedAllSources-embed设置为 true,或者在编译器的命令行中指定。源文件嵌入在可移植 PDB 文件中的嵌入式源流下,可以在需要时由调试器轻松提取。
上述两个功能在对托管项目进行迭代并同时调试过时版本时非常有用。虽然这种情况并不经常发生,但如果没有备份 PDB 或与之关联的源文件,则调试较旧的转储文件或较旧的可执行文件版本通常非常令人沮丧(并且通常是不可能的)。对于嵌入式 PDB 和嵌入式源,信息直接存储在可执行文件中(因此,如果使用完整内存捕获,则存储在转储文件中),并允许完整的调试体验。
2.0 不可打印文件
在调试转储文件时,VS 完全信任其中包含的数据。这意味着 Visual Studio 将很乐意接受嵌入式 PDB 和这些 PDB 中的源。尽管首选磁盘上的数据,但如果找不到,则将使用转储文件中的数据。在集思广益寻找滥用 VS 对嵌入式源文件的信任的可能方法时,我想起了过去偶然发现的一些奇怪行为。众所周知,VS 支持打开图像文件,但仅限于某些格式,例如 JPG 或 PNG。过去有一次,我尝试打开一个 webp 文件,并收到以下消息:
按“OK”或“X”将导致在 Paint 中打开 webp 文件:
... SHELL32 methods ...
shell32.dll!ShellExecuteW
msenv.dll!CExternalEditorFactory::CreateEditorInstance
msenv.dll!CVsUIShellOpenDocument::LoadCreateEditorInstance
msenv.dll!CVsUIShellOpenDocument::CreateInitEditorInstance
msenv.dll!CVsUIShellOpenDocument::OpenStandardEditor
msenv.dll!CVsUIShellOpenDocument::OpenStandardEditorAsync
... CLR methods...
msenv.dll!CVsUIShellOpenDocument::OpenDocumentViaProject2
msenv.dll!CVsUIShellOpenDocument::OpenDocumentViaProject
经过简短的调查,我发现:
-
打开文件时,VS 将尝试查找与文件扩展名关联的内部编辑器/查看器。
-
如果没有关联的编辑器,它将尝试在其默认(文本)编辑器中打开该文件。
-
如果文件包含任何不可打印的字符,则执行将采用上面显示的代码路径。
调用堆栈中最有趣的条目是CExternalEditorFactory::CreateEditorInstance。这是它的反编译实现:
HRESULT CExternalEditorFactory::CreateEditorInstance(..., const wchar_t* filePath, ...)
{
const wchar_t* extensionPtr = wcsrchr(filePath, L'\');
if (extensionPtr && (CompareFilenames(extensionPtr, L".exe") == 0 || CompareFilenames(extensionPtr, L".com") == 0))
return 0x80041FEB;
bool useOpenAssoc = false;
wchar_t assocProgramPath[MAX_PATH + 4];
uint32_t assocProgramPathLen = MAX_PATH;
HRESULT assocRes = AssocQueryStringW(ASSOCF_NOTRUNCATE | ASSOCF_VERIFY, ASSOCSTR_EXECUTABLE, extensionPtr, L"edit", assocProgramPath, &assocProgramPathLen);
if (FAILED(assocRes))
{
useOpenAssoc = true;
assocProgramPathLen = MAX_PATH;
assocRes = AssocQueryStringW(ASSOCF_NOTRUNCATE | ASSOCF_VERIFY, ASSOCSTR_EXECUTABLE, extensionPtr, L"open", assocProgramPath, &assocProgramPathLen);
}
if (FAILED(assocRes))
return 0x80041FEB;
...
// checking if the name of the program is equal to devenv.exe or any of special names:
// VBExpress, VCSExpress, VJSExpress, VCExpress, VWDExpress, VPDExpress, VSWinExpress, WDExpress, VSLauncher, vsgd, vsga
// if so, abort
...
if (CompareFilenames(filePath, assocProgramPath) != 0)
{
wchar_t assocProgramShortPath[MAX_PATH+1];
GetShortPathNameW(a3, assocProgramShortPath, MAX_PATH);
if (CompareFilenames(assocProgramPath, assocProgramShortPath) != 0)
{
SHELLEXECUTEINFOW execInfo = {};
... // set some execInfo members
execInfo.lpVerb = useOpenAssoc ? "open" : "edit";
execInfo.lpFile = filePath;
execInfo.nShow = 1;
ShellExecuteW(&execInfo);
...
}
}
...
}
上述函数接收一组参数,其中包括需要打开的文件的完整路径。对AssocQueryStringW的调用旨在获取与文件名扩展名交互的默认程序。VS 首先查询与“编辑”操作关联的程序,如果不存在,则查询与“打开”操作关联的程序。如果找到其中任何一个,它将进行一些健全性检查,如果一切顺利,则调用ShellExecuteW以在其关联程序中打开文件。
上述行为表明了使用嵌入式源文件可能发起的攻击的轮廓:
-
如果我们可以让 VS 在调试转储文件时打开任意嵌入的源文件
-
如果我们可以让源文件具有任意扩展名
-
如果我们可以找到一个与程序关联的扩展,该程序会根据正在打开的文件中提供的数据执行任意代码
那么只需调试转储文件就可以执行任意代码。
3.0 制作 POC
为了测试攻击的可行性,我尝试编写一个简单的 poc,用示例 PDF 文件替换嵌入式 PDB 中的合法源文件,希望 VS 能够:
-
将其视为合法的嵌入源文件。
-
在调试会话(包括转储调试会话)期间通过外部编辑器打开它。
我选择 PDF 是因为我知道它肯定会包含不可打印的字符,并且系统上肯定有一个相关程序(在本例中是 Firefox)。制作 poc 需要几个步骤:
-
创建一个简单的.NET 项目,并将主文件从 Program.cs 重命名为 Program.pdf。
-
使用 编译项目-debug:portable。这将生成一个 exe 文件和一个具有磁盘上可移植 PDB 的 dll 文件。源文件嵌入在与 DLL 对应的 PDB 文件中。
-
修改便携式 PDB 文件,以便我们用我们希望植入的 PDF 文件的数据替换原始源文件的数据。
-
我使用此示例来找出嵌入源在文件中的序列化位置。其格式在格式规范中描述。
-
然后,我用植入的 PDF 文件的数据替换了数据。这里的实际文件数据是使用 deflate 压缩的。为了让事情变得更容易,我回去修改了原始源文件,使其相当大,这样就有足够的空间容纳新数据,而不必重新定位内容并冒着破坏格式的风险。
-
我还更新了文档表中源文件的哈希值,以便 Visual Studio 不会将其视为无效而拒绝。
-
将新生成的可移植 PDB 嵌入到可执行文件中。我为此使用了一个自定义程序,因为我不知道有任何工具可以做这样的事情。该程序只会扩展可执行文件的调试目录并插入一个新部分,其中包含新条目将链接到的 PDB 数据。
-
运行可执行文件,让它崩溃并创建全内存转储。为了自动捕获转储,我按照本指南操作,设置DumpType为 2,也就是捕获全内存的转储。
-
从磁盘中删除/重命名 exe、dll、pdb 和源文件。这将使 VS 回退到转储中嵌入的信息,而不是使用可用的磁盘信息。
-
在 VS 中打开转储文件。单击“使用托管调试”。
此时,我遇到了与上面相同的消息框,按下 OK 或 X 会导致示例 PDF 文件在 Firefox 中打开。这证实了在调试转储文件和打开嵌入源时可以到达有问题的代码路径。
4.0 猎捕王牌
假设得到证实后,剩下的就是找到一些可用于实现 ACE 的扩展名。VS 正在过滤掉一些扩展名,例如 .exe 和 .com,但我确信还会有其他扩展名可以通过。我最终编写了一个程序,它遍历所有可能的 2 个字母、3 个字母和 4 个字母的扩展名,并使用 打印与它们关联的程序AssocQueryStringW。
这总共只花了大约 20 分钟,我很快就得到了 PC 上的完整关联列表。尽管许多文件在通过其默认程序打开时可用于执行任意代码,但其中大多数文件与文本编辑器都有“编辑”关联,这对我们不利。经过彻底检查,我发现了三个看起来特别合适的扩展:
-
CHM,又称Microsoft Compiled HTML。用于打开这些文件的默认程序是 hh.exe。此格式最常用于 Windows 上的帮助文件,如果您在某些程序中不小心按了 F1,它会随机弹出。CHM 文件可以包含打开后将运行的任意 VB 代码。
-
HTA,又称HTML 应用程序,相关程序为 mshta.exe。这些也是扩展的 html 文件,可以包含在打开文件时运行的 VB 代码。
-
PY,又称 python 脚本。在干净的 Windows 安装中不存在此关联,但开发人员可能会安装 python,在这种情况下,扩展将与 python 关联。当然,打开 python 脚本可能会导致执行任意代码。
默认情况下,CHM 文件经过编译,将包含不可打印字符。另一方面,HTA 和 PY 文件是文本文件,需要注入不可打印字符,同时仍保持其功能。这不是什么大问题:
-
在 HTA 源中的 html 标记结束后添加不可打印字符并不能阻止 hh.exe 执行标记内指定的代码。
-
在 Python 脚本中添加注释和一些 NUL 字符不会阻止其余代码的执行。
5.0 漏洞
一切准备就绪后,是时候设计一个漏洞了。但是,我没有按照我之前概述的步骤进行操作,而是编写了一个 C# 程序来自动处理所有事情,并向其输入了三个不同的文件 (CHM/HTA/PY) 以生成三个不同的转储。一旦你在 VS 中开始调试其中任何一个,calc.exe 就会生成,演示 ACE。
基于输入源文件创建转储文件的程序可在github上找到。您可以在存储库的 readme 中找到运行它的说明。下面是 poc 的演示,使用生成 calc.exe 的 CHM 输入文件:
6.0 修复
我太懒了,没有对这个修复进行完全逆向工程。但我们可以看到一个新的变化CVsUIShellOpenDocument::OpenStandardEditor,它看起来像这样:
HRESULT CVsUIShellOpenDocument::OpenStandardEditor(..., uint64_t flags, ...)
{
// ++++++++
if (flags & 0xF0000000)
{
return 0x80042010;
}
// ++++++++
...
CVsUIShellOpenDocument::CreateInitEditorInstance(...)
}
现在,在调试会话期间打开嵌入式源时会设置传递给函数的标志参数的最高位,但如果将文件拖入空闲的 VS 则不会设置。如果设置了该参数,则函数会拒绝继续执行CreateInitEditorInstance,这稍后会导致我们上面记录的行为。
如果在调试转储时尝试在 VS 中手动打开源文件,我们现在会遇到以下消息,这意味着 VS 甚至不会让用户手动陷入陷阱。
7.0 时间线
2023 年 8 月 14 日——我向 Microsoft 报告此问题。
2023 年 9 月 15 日 - 微软回应称,他们认为该漏洞影响中等,即“纵深防御”,不会优先修复。他们还表示,他们已经与工程团队分享了详细信息,并将采取措施保护客户。我不同意这种评估,但不会进一步追究,因为我当时正在处理其他事情。
2024 年 1 月 10 日——我询问微软是否计划很快修复该问题,并重申这看起来像是一个合理的攻击媒介。
2024 年 1 月 16 日-微软做出回应并确认有解决该问题的计划。
2024 年 5 月 2 日——我再次联系微软并要求更新,因为问题尚未解决。
2024 年 5 月 13 日 - 微软回应称,他们重新开启了此案,并已评估该漏洞的重要性,这意味着他们将发布一份带有确认的公告。他们告诉我修复计划在 7 月进行。
2024 年 6 月 11 日 - Visual Studio 17.8.11 发布并修复。
原文始发于微信公众号(Ots安全):通过转储文件利用 Visual Studio - CVE-2024-30052
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论